Repository: JeffMony/MediaSDK Branch: master Commit: b9fc6524c5ec Files: 946 Total size: 6.6 MB Directory structure: gitextract_ez39w3qr/ ├── .gitignore ├── LICENSE ├── README.md ├── README_cn.md ├── androidasync/ │ ├── .gitignore │ ├── androidasync.iml │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── jeffmony/ │ └── async/ │ ├── AsyncDatagramSocket.java │ ├── AsyncNetworkSocket.java │ ├── AsyncSSLException.java │ ├── AsyncSSLServerSocket.java │ ├── AsyncSSLSocket.java │ ├── AsyncSSLSocketWrapper.java │ ├── AsyncSemaphore.java │ ├── AsyncServer.java │ ├── AsyncServerSocket.java │ ├── AsyncSocket.java │ ├── BufferedDataSink.java │ ├── ByteBufferList.java │ ├── ChannelWrapper.java │ ├── DataEmitter.java │ ├── DataEmitterBase.java │ ├── DataEmitterReader.java │ ├── DataSink.java │ ├── DataTrackingEmitter.java │ ├── DatagramChannelWrapper.java │ ├── FileDataEmitter.java │ ├── FilteredDataEmitter.java │ ├── FilteredDataSink.java │ ├── HostnameResolutionException.java │ ├── LineEmitter.java │ ├── PushParser.java │ ├── SelectorWrapper.java │ ├── ServerSocketChannelWrapper.java │ ├── SocketChannelWrapper.java │ ├── TapCallback.java │ ├── ThreadQueue.java │ ├── Util.java │ ├── ZipDataSink.java │ ├── callback/ │ │ ├── CompletedCallback.java │ │ ├── ConnectCallback.java │ │ ├── ContinuationCallback.java │ │ ├── DataCallback.java │ │ ├── ListenCallback.java │ │ ├── ResultCallback.java │ │ ├── SocketCreateCallback.java │ │ ├── ValueCallback.java │ │ ├── ValueFunction.java │ │ └── WritableCallback.java │ ├── dns/ │ │ ├── Dns.java │ │ └── DnsResponse.java │ ├── future/ │ │ ├── Cancellable.java │ │ ├── Continuation.java │ │ ├── Converter.java │ │ ├── DependentCancellable.java │ │ ├── DependentFuture.java │ │ ├── DoneCallback.java │ │ ├── FailCallback.java │ │ ├── FailConvertCallback.java │ │ ├── FailRecoverCallback.java │ │ ├── Future.java │ │ ├── FutureCallback.java │ │ ├── FutureRunnable.java │ │ ├── FutureThread.java │ │ ├── Futures.java │ │ ├── HandlerFuture.java │ │ ├── MultiFuture.java │ │ ├── MultiTransformFuture.java │ │ ├── SimpleCancellable.java │ │ ├── SimpleFuture.java │ │ ├── SuccessCallback.java │ │ ├── ThenCallback.java │ │ ├── ThenFutureCallback.java │ │ ├── TransformFuture.java │ │ └── TypeConverter.java │ ├── http/ │ │ ├── AsyncHttpClient.java │ │ ├── AsyncHttpClientMiddleware.java │ │ ├── AsyncHttpDelete.java │ │ ├── AsyncHttpGet.java │ │ ├── AsyncHttpHead.java │ │ ├── AsyncHttpPost.java │ │ ├── AsyncHttpPut.java │ │ ├── AsyncHttpRequest.java │ │ ├── AsyncHttpResponse.java │ │ ├── AsyncHttpResponseImpl.java │ │ ├── AsyncSSLEngineConfigurator.java │ │ ├── AsyncSSLSocketMiddleware.java │ │ ├── AsyncSocketMiddleware.java │ │ ├── BasicNameValuePair.java │ │ ├── BodyDecoderException.java │ │ ├── ConnectionClosedException.java │ │ ├── ConnectionFailedException.java │ │ ├── Headers.java │ │ ├── HttpDate.java │ │ ├── HttpTransportMiddleware.java │ │ ├── HttpUtil.java │ │ ├── HybiParser.java │ │ ├── Multimap.java │ │ ├── NameValuePair.java │ │ ├── Protocol.java │ │ ├── ProtocolVersion.java │ │ ├── RedirectLimitExceededException.java │ │ ├── RequestLine.java │ │ ├── SSLEngineSNIConfigurator.java │ │ ├── SimpleMiddleware.java │ │ ├── TaggedList.java │ │ ├── WebSocket.java │ │ ├── WebSocketHandshakeException.java │ │ ├── WebSocketImpl.java │ │ ├── body/ │ │ │ ├── AsyncHttpRequestBody.java │ │ │ ├── ByteBufferListRequestBody.java │ │ │ ├── DocumentBody.java │ │ │ ├── FileBody.java │ │ │ ├── FilePart.java │ │ │ ├── JSONArrayBody.java │ │ │ ├── JSONObjectBody.java │ │ │ ├── MultipartFormDataBody.java │ │ │ ├── Part.java │ │ │ ├── StreamBody.java │ │ │ ├── StreamPart.java │ │ │ ├── StringBody.java │ │ │ ├── StringPart.java │ │ │ └── UrlEncodedFormBody.java │ │ ├── cache/ │ │ │ ├── HeaderParser.java │ │ │ ├── Objects.java │ │ │ ├── RawHeaders.java │ │ │ ├── RequestHeaders.java │ │ │ ├── ResponseCacheMiddleware.java │ │ │ ├── ResponseHeaders.java │ │ │ ├── ResponseSource.java │ │ │ └── StrictLineReader.java │ │ ├── callback/ │ │ │ ├── HttpConnectCallback.java │ │ │ └── RequestCallback.java │ │ ├── filter/ │ │ │ ├── ChunkedDataException.java │ │ │ ├── ChunkedInputFilter.java │ │ │ ├── ChunkedOutputFilter.java │ │ │ ├── ContentLengthFilter.java │ │ │ ├── DataRemainingException.java │ │ │ ├── GZIPInputFilter.java │ │ │ ├── InflaterInputFilter.java │ │ │ └── PrematureDataEndException.java │ │ └── server/ │ │ ├── AsyncHttpRequestBodyProvider.java │ │ ├── AsyncHttpServer.java │ │ ├── AsyncHttpServerRequest.java │ │ ├── AsyncHttpServerRequestImpl.java │ │ ├── AsyncHttpServerResponse.java │ │ ├── AsyncHttpServerResponseImpl.java │ │ ├── AsyncHttpServerRouter.java │ │ ├── AsyncProxyServer.java │ │ ├── BoundaryEmitter.java │ │ ├── HttpServerRequestCallback.java │ │ ├── MalformedRangeException.java │ │ ├── MimeEncodingException.java │ │ ├── RouteMatcher.java │ │ ├── StreamSkipException.java │ │ └── UnknownRequestBody.java │ ├── parser/ │ │ ├── AsyncParser.java │ │ ├── ByteBufferListParser.java │ │ ├── DocumentParser.java │ │ ├── JSONArrayParser.java │ │ ├── JSONObjectParser.java │ │ └── StringParser.java │ ├── stream/ │ │ ├── ByteBufferListInputStream.java │ │ ├── FileDataSink.java │ │ ├── InputStreamDataEmitter.java │ │ ├── OutputStreamDataCallback.java │ │ └── OutputStreamDataSink.java │ ├── util/ │ │ ├── Allocator.java │ │ ├── ArrayDeque.java │ │ ├── Charsets.java │ │ ├── Deque.java │ │ ├── FileCache.java │ │ ├── FileUtility.java │ │ ├── HashList.java │ │ ├── IdleTimeout.java │ │ ├── LruCache.java │ │ ├── StreamUtility.java │ │ ├── TaggedList.java │ │ ├── ThrottleTimeout.java │ │ ├── TimeoutBase.java │ │ └── UntypedHashtable.java │ └── wrapper/ │ ├── AsyncSocketWrapper.java │ └── DataEmitterWrapper.java ├── app/ │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── assets/ │ │ └── list.json │ ├── java/ │ │ └── com/ │ │ └── android/ │ │ └── media/ │ │ ├── DownloadBaseListActivity.java │ │ ├── DownloadFeatureActivity.java │ │ ├── DownloadOrcodeActivity.java │ │ ├── DownloadPlayActivity.java │ │ ├── DownloadSettingsActivity.java │ │ ├── MainActivity.java │ │ ├── MediaScannerActivity.java │ │ ├── MyApplication.java │ │ ├── PlayFeatureActivity.java │ │ ├── PlayerActivity.java │ │ └── VideoListAdapter.java │ └── res/ │ ├── drawable/ │ │ ├── border.xml │ │ └── ic_launcher_background.xml │ ├── drawable-v24/ │ │ └── ic_launcher_foreground.xml │ ├── layout/ │ │ ├── activity_download_feature.xml │ │ ├── activity_download_list.xml │ │ ├── activity_download_play.xml │ │ ├── activity_download_settings.xml │ │ ├── activity_main.xml │ │ ├── activity_orcode.xml │ │ ├── activity_play_func.xml │ │ ├── activity_player.xml │ │ ├── activity_scanner.xml │ │ ├── download_item.xml │ │ └── video_item.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ └── values/ │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── base/ │ ├── .gitignore │ ├── base.iml │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── android/ │ │ └── baselib/ │ │ ├── MediaSDKReceiver.java │ │ ├── NetworkCallbackImpl.java │ │ ├── NetworkListener.java │ │ ├── WeakHandler.java │ │ └── utils/ │ │ ├── LogUtils.java │ │ ├── NetworkUtils.java │ │ ├── ScreenUtils.java │ │ └── Utility.java │ └── res/ │ └── values/ │ └── strings.xml ├── build.gradle ├── constants.gradle ├── exoplayer/ │ ├── .gitignore │ ├── build.gradle │ ├── exoplayer.iml │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── google/ │ │ └── android/ │ │ └── exoplayer2/ │ │ ├── AudioBecomingNoisyManager.java │ │ ├── AudioFocusManager.java │ │ ├── BasePlayer.java │ │ ├── BaseRenderer.java │ │ ├── C.java │ │ ├── ControlDispatcher.java │ │ ├── DefaultControlDispatcher.java │ │ ├── DefaultLoadControl.java │ │ ├── DefaultMediaClock.java │ │ ├── DefaultRenderersFactory.java │ │ ├── ExoPlaybackException.java │ │ ├── ExoPlayer.java │ │ ├── ExoPlayerFactory.java │ │ ├── ExoPlayerImpl.java │ │ ├── ExoPlayerImplInternal.java │ │ ├── ExoPlayerLibraryInfo.java │ │ ├── Format.java │ │ ├── FormatHolder.java │ │ ├── IllegalSeekPositionException.java │ │ ├── LoadControl.java │ │ ├── MediaPeriodHolder.java │ │ ├── MediaPeriodInfo.java │ │ ├── MediaPeriodQueue.java │ │ ├── NoSampleRenderer.java │ │ ├── ParserException.java │ │ ├── PlaybackInfo.java │ │ ├── PlaybackParameters.java │ │ ├── PlaybackPreparer.java │ │ ├── Player.java │ │ ├── PlayerMessage.java │ │ ├── Renderer.java │ │ ├── RendererCapabilities.java │ │ ├── RendererConfiguration.java │ │ ├── RenderersFactory.java │ │ ├── SeekParameters.java │ │ ├── SimpleExoPlayer.java │ │ ├── Timeline.java │ │ ├── WakeLockManager.java │ │ ├── analytics/ │ │ │ ├── AnalyticsCollector.java │ │ │ ├── AnalyticsListener.java │ │ │ ├── DefaultAnalyticsListener.java │ │ │ ├── DefaultPlaybackSessionManager.java │ │ │ ├── PlaybackSessionManager.java │ │ │ ├── PlaybackStats.java │ │ │ ├── PlaybackStatsListener.java │ │ │ └── package-info.java │ │ ├── audio/ │ │ │ ├── Ac3Util.java │ │ │ ├── Ac4Util.java │ │ │ ├── AudioAttributes.java │ │ │ ├── AudioCapabilities.java │ │ │ ├── AudioCapabilitiesReceiver.java │ │ │ ├── AudioDecoderException.java │ │ │ ├── AudioListener.java │ │ │ ├── AudioProcessor.java │ │ │ ├── AudioRendererEventListener.java │ │ │ ├── AudioSink.java │ │ │ ├── AudioTimestampPoller.java │ │ │ ├── AudioTrackPositionTracker.java │ │ │ ├── AuxEffectInfo.java │ │ │ ├── BaseAudioProcessor.java │ │ │ ├── ChannelMappingAudioProcessor.java │ │ │ ├── DefaultAudioSink.java │ │ │ ├── DtsUtil.java │ │ │ ├── FloatResamplingAudioProcessor.java │ │ │ ├── MediaCodecAudioRenderer.java │ │ │ ├── ResamplingAudioProcessor.java │ │ │ ├── SilenceSkippingAudioProcessor.java │ │ │ ├── SimpleDecoderAudioRenderer.java │ │ │ ├── Sonic.java │ │ │ ├── SonicAudioProcessor.java │ │ │ ├── TeeAudioProcessor.java │ │ │ ├── TrimmingAudioProcessor.java │ │ │ ├── WavUtil.java │ │ │ └── package-info.java │ │ ├── database/ │ │ │ ├── DatabaseIOException.java │ │ │ ├── DatabaseProvider.java │ │ │ ├── DefaultDatabaseProvider.java │ │ │ ├── ExoDatabaseProvider.java │ │ │ ├── VersionTable.java │ │ │ └── package-info.java │ │ ├── decoder/ │ │ │ ├── Buffer.java │ │ │ ├── CryptoInfo.java │ │ │ ├── Decoder.java │ │ │ ├── DecoderCounters.java │ │ │ ├── DecoderInputBuffer.java │ │ │ ├── OutputBuffer.java │ │ │ ├── SimpleDecoder.java │ │ │ ├── SimpleOutputBuffer.java │ │ │ └── package-info.java │ │ ├── drm/ │ │ │ ├── ClearKeyUtil.java │ │ │ ├── DecryptionException.java │ │ │ ├── DefaultDrmSession.java │ │ │ ├── DefaultDrmSessionEventListener.java │ │ │ ├── DefaultDrmSessionManager.java │ │ │ ├── DrmInitData.java │ │ │ ├── DrmSession.java │ │ │ ├── DrmSessionManager.java │ │ │ ├── DummyExoMediaDrm.java │ │ │ ├── ErrorStateDrmSession.java │ │ │ ├── ExoMediaCrypto.java │ │ │ ├── ExoMediaDrm.java │ │ │ ├── FrameworkMediaCrypto.java │ │ │ ├── FrameworkMediaDrm.java │ │ │ ├── HttpMediaDrmCallback.java │ │ │ ├── KeysExpiredException.java │ │ │ ├── LocalMediaDrmCallback.java │ │ │ ├── MediaDrmCallback.java │ │ │ ├── OfflineLicenseHelper.java │ │ │ ├── UnsupportedDrmException.java │ │ │ ├── WidevineUtil.java │ │ │ └── package-info.java │ │ ├── extractor/ │ │ │ ├── BinarySearchSeeker.java │ │ │ ├── ChunkIndex.java │ │ │ ├── ConstantBitrateSeekMap.java │ │ │ ├── DefaultExtractorInput.java │ │ │ ├── DefaultExtractorsFactory.java │ │ │ ├── DummyExtractorOutput.java │ │ │ ├── DummyTrackOutput.java │ │ │ ├── Extractor.java │ │ │ ├── ExtractorInput.java │ │ │ ├── ExtractorOutput.java │ │ │ ├── ExtractorsFactory.java │ │ │ ├── GaplessInfoHolder.java │ │ │ ├── Id3Peeker.java │ │ │ ├── MpegAudioHeader.java │ │ │ ├── PositionHolder.java │ │ │ ├── SeekMap.java │ │ │ ├── SeekPoint.java │ │ │ ├── TrackOutput.java │ │ │ ├── amr/ │ │ │ │ └── AmrExtractor.java │ │ │ ├── flv/ │ │ │ │ ├── AudioTagPayloadReader.java │ │ │ │ ├── FlvExtractor.java │ │ │ │ ├── ScriptTagPayloadReader.java │ │ │ │ ├── TagPayloadReader.java │ │ │ │ └── VideoTagPayloadReader.java │ │ │ ├── mkv/ │ │ │ │ ├── DefaultEbmlReader.java │ │ │ │ ├── EbmlProcessor.java │ │ │ │ ├── EbmlReader.java │ │ │ │ ├── MatroskaExtractor.java │ │ │ │ ├── Sniffer.java │ │ │ │ └── VarintReader.java │ │ │ ├── mp3/ │ │ │ │ ├── ConstantBitrateSeeker.java │ │ │ │ ├── MlltSeeker.java │ │ │ │ ├── Mp3Extractor.java │ │ │ │ ├── Seeker.java │ │ │ │ ├── VbriSeeker.java │ │ │ │ └── XingSeeker.java │ │ │ ├── mp4/ │ │ │ │ ├── Atom.java │ │ │ │ ├── AtomParsers.java │ │ │ │ ├── DefaultSampleValues.java │ │ │ │ ├── FixedSampleSizeRechunker.java │ │ │ │ ├── FragmentedMp4Extractor.java │ │ │ │ ├── MdtaMetadataEntry.java │ │ │ │ ├── MetadataUtil.java │ │ │ │ ├── Mp4Extractor.java │ │ │ │ ├── PsshAtomUtil.java │ │ │ │ ├── Sniffer.java │ │ │ │ ├── Track.java │ │ │ │ ├── TrackEncryptionBox.java │ │ │ │ ├── TrackFragment.java │ │ │ │ └── TrackSampleTable.java │ │ │ ├── ogg/ │ │ │ │ ├── DefaultOggSeeker.java │ │ │ │ ├── FlacReader.java │ │ │ │ ├── OggExtractor.java │ │ │ │ ├── OggPacket.java │ │ │ │ ├── OggPageHeader.java │ │ │ │ ├── OggSeeker.java │ │ │ │ ├── OpusReader.java │ │ │ │ ├── StreamReader.java │ │ │ │ ├── VorbisBitArray.java │ │ │ │ ├── VorbisReader.java │ │ │ │ └── VorbisUtil.java │ │ │ ├── rawcc/ │ │ │ │ └── RawCcExtractor.java │ │ │ ├── ts/ │ │ │ │ ├── Ac3Extractor.java │ │ │ │ ├── Ac3Reader.java │ │ │ │ ├── Ac4Extractor.java │ │ │ │ ├── Ac4Reader.java │ │ │ │ ├── AdtsExtractor.java │ │ │ │ ├── AdtsReader.java │ │ │ │ ├── DefaultTsPayloadReaderFactory.java │ │ │ │ ├── DtsReader.java │ │ │ │ ├── DvbSubtitleReader.java │ │ │ │ ├── ElementaryStreamReader.java │ │ │ │ ├── H262Reader.java │ │ │ │ ├── H264Reader.java │ │ │ │ ├── H265Reader.java │ │ │ │ ├── Id3Reader.java │ │ │ │ ├── LatmReader.java │ │ │ │ ├── MpegAudioReader.java │ │ │ │ ├── NalUnitTargetBuffer.java │ │ │ │ ├── PesReader.java │ │ │ │ ├── PsBinarySearchSeeker.java │ │ │ │ ├── PsDurationReader.java │ │ │ │ ├── PsExtractor.java │ │ │ │ ├── SectionPayloadReader.java │ │ │ │ ├── SectionReader.java │ │ │ │ ├── SeiReader.java │ │ │ │ ├── SpliceInfoSectionReader.java │ │ │ │ ├── TsBinarySearchSeeker.java │ │ │ │ ├── TsDurationReader.java │ │ │ │ ├── TsExtractor.java │ │ │ │ ├── TsPayloadReader.java │ │ │ │ ├── TsUtil.java │ │ │ │ └── UserDataReader.java │ │ │ └── wav/ │ │ │ ├── WavExtractor.java │ │ │ ├── WavHeader.java │ │ │ └── WavHeaderReader.java │ │ ├── mediacodec/ │ │ │ ├── MediaCodecInfo.java │ │ │ ├── MediaCodecRenderer.java │ │ │ ├── MediaCodecSelector.java │ │ │ ├── MediaCodecUtil.java │ │ │ ├── MediaFormatUtil.java │ │ │ └── package-info.java │ │ ├── metadata/ │ │ │ ├── Metadata.java │ │ │ ├── MetadataDecoder.java │ │ │ ├── MetadataDecoderFactory.java │ │ │ ├── MetadataInputBuffer.java │ │ │ ├── MetadataOutput.java │ │ │ ├── MetadataRenderer.java │ │ │ ├── emsg/ │ │ │ │ ├── EventMessage.java │ │ │ │ ├── EventMessageDecoder.java │ │ │ │ ├── EventMessageEncoder.java │ │ │ │ └── package-info.java │ │ │ ├── flac/ │ │ │ │ ├── PictureFrame.java │ │ │ │ ├── VorbisComment.java │ │ │ │ └── package-info.java │ │ │ ├── icy/ │ │ │ │ ├── IcyDecoder.java │ │ │ │ ├── IcyHeaders.java │ │ │ │ ├── IcyInfo.java │ │ │ │ └── package-info.java │ │ │ ├── id3/ │ │ │ │ ├── ApicFrame.java │ │ │ │ ├── BinaryFrame.java │ │ │ │ ├── ChapterFrame.java │ │ │ │ ├── ChapterTocFrame.java │ │ │ │ ├── CommentFrame.java │ │ │ │ ├── GeobFrame.java │ │ │ │ ├── Id3Decoder.java │ │ │ │ ├── Id3Frame.java │ │ │ │ ├── InternalFrame.java │ │ │ │ ├── MlltFrame.java │ │ │ │ ├── PrivFrame.java │ │ │ │ ├── TextInformationFrame.java │ │ │ │ ├── UrlLinkFrame.java │ │ │ │ └── package-info.java │ │ │ ├── package-info.java │ │ │ └── scte35/ │ │ │ ├── PrivateCommand.java │ │ │ ├── SpliceCommand.java │ │ │ ├── SpliceInfoDecoder.java │ │ │ ├── SpliceInsertCommand.java │ │ │ ├── SpliceNullCommand.java │ │ │ ├── SpliceScheduleCommand.java │ │ │ ├── TimeSignalCommand.java │ │ │ └── package-info.java │ │ ├── offline/ │ │ │ ├── ActionFile.java │ │ │ ├── ActionFileUpgradeUtil.java │ │ │ ├── DefaultDownloadIndex.java │ │ │ ├── DefaultDownloaderFactory.java │ │ │ ├── Download.java │ │ │ ├── DownloadCursor.java │ │ │ ├── DownloadException.java │ │ │ ├── DownloadHelper.java │ │ │ ├── DownloadIndex.java │ │ │ ├── DownloadManager.java │ │ │ ├── DownloadProgress.java │ │ │ ├── DownloadRequest.java │ │ │ ├── DownloadService.java │ │ │ ├── Downloader.java │ │ │ ├── DownloaderConstructorHelper.java │ │ │ ├── DownloaderFactory.java │ │ │ ├── FilterableManifest.java │ │ │ ├── FilteringManifestParser.java │ │ │ ├── ProgressiveDownloader.java │ │ │ ├── SegmentDownloader.java │ │ │ ├── StreamKey.java │ │ │ ├── WritableDownloadIndex.java │ │ │ └── package-info.java │ │ ├── package-info.java │ │ ├── scheduler/ │ │ │ ├── PlatformScheduler.java │ │ │ ├── Requirements.java │ │ │ ├── RequirementsWatcher.java │ │ │ ├── Scheduler.java │ │ │ └── package-info.java │ │ ├── source/ │ │ │ ├── AbstractConcatenatedTimeline.java │ │ │ ├── AdaptiveMediaSourceEventListener.java │ │ │ ├── BaseMediaSource.java │ │ │ ├── BehindLiveWindowException.java │ │ │ ├── ClippingMediaPeriod.java │ │ │ ├── ClippingMediaSource.java │ │ │ ├── CompositeMediaSource.java │ │ │ ├── CompositeSequenceableLoader.java │ │ │ ├── CompositeSequenceableLoaderFactory.java │ │ │ ├── ConcatenatingMediaSource.java │ │ │ ├── DefaultCompositeSequenceableLoaderFactory.java │ │ │ ├── DefaultMediaSourceEventListener.java │ │ │ ├── EmptySampleStream.java │ │ │ ├── ExtractorMediaSource.java │ │ │ ├── ForwardingTimeline.java │ │ │ ├── IcyDataSource.java │ │ │ ├── LoopingMediaSource.java │ │ │ ├── MaskingMediaPeriod.java │ │ │ ├── MaskingMediaSource.java │ │ │ ├── MediaPeriod.java │ │ │ ├── MediaSource.java │ │ │ ├── MediaSourceEventListener.java │ │ │ ├── MediaSourceFactory.java │ │ │ ├── MergingMediaPeriod.java │ │ │ ├── MergingMediaSource.java │ │ │ ├── ProgressiveMediaPeriod.java │ │ │ ├── ProgressiveMediaSource.java │ │ │ ├── SampleMetadataQueue.java │ │ │ ├── SampleQueue.java │ │ │ ├── SampleStream.java │ │ │ ├── SequenceableLoader.java │ │ │ ├── ShuffleOrder.java │ │ │ ├── SilenceMediaSource.java │ │ │ ├── SinglePeriodTimeline.java │ │ │ ├── SingleSampleMediaPeriod.java │ │ │ ├── SingleSampleMediaSource.java │ │ │ ├── TrackGroup.java │ │ │ ├── TrackGroupArray.java │ │ │ ├── UnrecognizedInputFormatException.java │ │ │ ├── ads/ │ │ │ │ ├── AdPlaybackState.java │ │ │ │ ├── AdsLoader.java │ │ │ │ ├── AdsMediaSource.java │ │ │ │ └── SinglePeriodAdTimeline.java │ │ │ ├── chunk/ │ │ │ │ ├── BaseMediaChunk.java │ │ │ │ ├── BaseMediaChunkIterator.java │ │ │ │ ├── BaseMediaChunkOutput.java │ │ │ │ ├── Chunk.java │ │ │ │ ├── ChunkExtractorWrapper.java │ │ │ │ ├── ChunkHolder.java │ │ │ │ ├── ChunkSampleStream.java │ │ │ │ ├── ChunkSource.java │ │ │ │ ├── ContainerMediaChunk.java │ │ │ │ ├── DataChunk.java │ │ │ │ ├── InitializationChunk.java │ │ │ │ ├── MediaChunk.java │ │ │ │ ├── MediaChunkIterator.java │ │ │ │ ├── MediaChunkListIterator.java │ │ │ │ └── SingleSampleMediaChunk.java │ │ │ ├── dash/ │ │ │ │ ├── DashChunkSource.java │ │ │ │ ├── DashManifestStaleException.java │ │ │ │ ├── DashMediaPeriod.java │ │ │ │ ├── DashMediaSource.java │ │ │ │ ├── DashSegmentIndex.java │ │ │ │ ├── DashUtil.java │ │ │ │ ├── DashWrappingSegmentIndex.java │ │ │ │ ├── DefaultDashChunkSource.java │ │ │ │ ├── EventSampleStream.java │ │ │ │ ├── PlayerEmsgHandler.java │ │ │ │ ├── manifest/ │ │ │ │ │ ├── AdaptationSet.java │ │ │ │ │ ├── DashManifest.java │ │ │ │ │ ├── DashManifestParser.java │ │ │ │ │ ├── Descriptor.java │ │ │ │ │ ├── EventStream.java │ │ │ │ │ ├── Period.java │ │ │ │ │ ├── ProgramInformation.java │ │ │ │ │ ├── RangedUri.java │ │ │ │ │ ├── Representation.java │ │ │ │ │ ├── SegmentBase.java │ │ │ │ │ ├── SingleSegmentIndex.java │ │ │ │ │ ├── UrlTemplate.java │ │ │ │ │ ├── UtcTimingElement.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── offline/ │ │ │ │ │ ├── DashDownloader.java │ │ │ │ │ └── package-info.java │ │ │ │ └── package-info.java │ │ │ ├── hls/ │ │ │ │ ├── Aes128DataSource.java │ │ │ │ ├── DefaultHlsDataSourceFactory.java │ │ │ │ ├── DefaultHlsExtractorFactory.java │ │ │ │ ├── FullSegmentEncryptionKeyCache.java │ │ │ │ ├── HlsChunkSource.java │ │ │ │ ├── HlsDataSourceFactory.java │ │ │ │ ├── HlsExtractorFactory.java │ │ │ │ ├── HlsManifest.java │ │ │ │ ├── HlsMediaChunk.java │ │ │ │ ├── HlsMediaPeriod.java │ │ │ │ ├── HlsMediaSource.java │ │ │ │ ├── HlsSampleStream.java │ │ │ │ ├── HlsSampleStreamWrapper.java │ │ │ │ ├── HlsTrackMetadataEntry.java │ │ │ │ ├── SampleQueueMappingException.java │ │ │ │ ├── TimestampAdjusterProvider.java │ │ │ │ ├── WebvttExtractor.java │ │ │ │ ├── offline/ │ │ │ │ │ ├── HlsDownloader.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── package-info.java │ │ │ │ └── playlist/ │ │ │ │ ├── DefaultHlsPlaylistParserFactory.java │ │ │ │ ├── DefaultHlsPlaylistTracker.java │ │ │ │ ├── FilteringHlsPlaylistParserFactory.java │ │ │ │ ├── HlsMasterPlaylist.java │ │ │ │ ├── HlsMediaPlaylist.java │ │ │ │ ├── HlsPlaylist.java │ │ │ │ ├── HlsPlaylistParser.java │ │ │ │ ├── HlsPlaylistParserFactory.java │ │ │ │ ├── HlsPlaylistTracker.java │ │ │ │ └── package-info.java │ │ │ └── smoothstreaming/ │ │ │ ├── DefaultSsChunkSource.java │ │ │ ├── SsChunkSource.java │ │ │ ├── SsMediaPeriod.java │ │ │ ├── SsMediaSource.java │ │ │ ├── manifest/ │ │ │ │ ├── SsManifest.java │ │ │ │ ├── SsManifestParser.java │ │ │ │ ├── SsUtil.java │ │ │ │ └── package-info.java │ │ │ ├── offline/ │ │ │ │ ├── SsDownloader.java │ │ │ │ └── package-info.java │ │ │ └── package-info.java │ │ ├── text/ │ │ │ ├── CaptionStyleCompat.java │ │ │ ├── Cue.java │ │ │ ├── SimpleSubtitleDecoder.java │ │ │ ├── SimpleSubtitleOutputBuffer.java │ │ │ ├── Subtitle.java │ │ │ ├── SubtitleDecoder.java │ │ │ ├── SubtitleDecoderException.java │ │ │ ├── SubtitleDecoderFactory.java │ │ │ ├── SubtitleInputBuffer.java │ │ │ ├── SubtitleOutputBuffer.java │ │ │ ├── TextOutput.java │ │ │ ├── TextRenderer.java │ │ │ ├── cea/ │ │ │ │ ├── Cea608Decoder.java │ │ │ │ ├── Cea708Cue.java │ │ │ │ ├── Cea708Decoder.java │ │ │ │ ├── Cea708InitializationData.java │ │ │ │ ├── CeaDecoder.java │ │ │ │ ├── CeaSubtitle.java │ │ │ │ ├── CeaUtil.java │ │ │ │ └── package-info.java │ │ │ ├── dvb/ │ │ │ │ ├── DvbDecoder.java │ │ │ │ ├── DvbParser.java │ │ │ │ ├── DvbSubtitle.java │ │ │ │ └── package-info.java │ │ │ ├── package-info.java │ │ │ ├── pgs/ │ │ │ │ ├── PgsDecoder.java │ │ │ │ ├── PgsSubtitle.java │ │ │ │ └── package-info.java │ │ │ ├── ssa/ │ │ │ │ ├── SsaDecoder.java │ │ │ │ ├── SsaDialogueFormat.java │ │ │ │ ├── SsaStyle.java │ │ │ │ ├── SsaSubtitle.java │ │ │ │ └── package-info.java │ │ │ ├── subrip/ │ │ │ │ ├── SubripDecoder.java │ │ │ │ ├── SubripSubtitle.java │ │ │ │ └── package-info.java │ │ │ ├── ttml/ │ │ │ │ ├── TtmlDecoder.java │ │ │ │ ├── TtmlNode.java │ │ │ │ ├── TtmlRegion.java │ │ │ │ ├── TtmlRenderUtil.java │ │ │ │ ├── TtmlStyle.java │ │ │ │ ├── TtmlSubtitle.java │ │ │ │ └── package-info.java │ │ │ ├── tx3g/ │ │ │ │ ├── Tx3gDecoder.java │ │ │ │ ├── Tx3gSubtitle.java │ │ │ │ └── package-info.java │ │ │ └── webvtt/ │ │ │ ├── CssParser.java │ │ │ ├── Mp4WebvttDecoder.java │ │ │ ├── Mp4WebvttSubtitle.java │ │ │ ├── WebvttCssStyle.java │ │ │ ├── WebvttCue.java │ │ │ ├── WebvttCueParser.java │ │ │ ├── WebvttDecoder.java │ │ │ ├── WebvttParserUtil.java │ │ │ ├── WebvttSubtitle.java │ │ │ └── package-info.java │ │ ├── trackselection/ │ │ │ ├── AdaptiveTrackSelection.java │ │ │ ├── BaseTrackSelection.java │ │ │ ├── BufferSizeAdaptationBuilder.java │ │ │ ├── DefaultTrackSelector.java │ │ │ ├── FixedTrackSelection.java │ │ │ ├── MappingTrackSelector.java │ │ │ ├── RandomTrackSelection.java │ │ │ ├── TrackSelection.java │ │ │ ├── TrackSelectionArray.java │ │ │ ├── TrackSelectionParameters.java │ │ │ ├── TrackSelectionUtil.java │ │ │ ├── TrackSelector.java │ │ │ ├── TrackSelectorResult.java │ │ │ └── package-info.java │ │ ├── upstream/ │ │ │ ├── Allocation.java │ │ │ ├── Allocator.java │ │ │ ├── AssetDataSource.java │ │ │ ├── BandwidthMeter.java │ │ │ ├── BaseDataSource.java │ │ │ ├── ByteArrayDataSink.java │ │ │ ├── ByteArrayDataSource.java │ │ │ ├── ContentDataSource.java │ │ │ ├── DataSchemeDataSource.java │ │ │ ├── DataSink.java │ │ │ ├── DataSource.java │ │ │ ├── DataSourceException.java │ │ │ ├── DataSourceInputStream.java │ │ │ ├── DataSpec.java │ │ │ ├── DefaultAllocator.java │ │ │ ├── DefaultBandwidthMeter.java │ │ │ ├── DefaultDataSource.java │ │ │ ├── DefaultDataSourceFactory.java │ │ │ ├── DefaultHttpDataSource.java │ │ │ ├── DefaultHttpDataSourceFactory.java │ │ │ ├── DefaultLoadErrorHandlingPolicy.java │ │ │ ├── DummyDataSource.java │ │ │ ├── FileDataSource.java │ │ │ ├── FileDataSourceFactory.java │ │ │ ├── HttpDataSource.java │ │ │ ├── LoadErrorHandlingPolicy.java │ │ │ ├── Loader.java │ │ │ ├── LoaderErrorThrower.java │ │ │ ├── ParsingLoadable.java │ │ │ ├── PriorityDataSource.java │ │ │ ├── PriorityDataSourceFactory.java │ │ │ ├── RawResourceDataSource.java │ │ │ ├── ResolvingDataSource.java │ │ │ ├── StatsDataSource.java │ │ │ ├── TeeDataSource.java │ │ │ ├── TransferListener.java │ │ │ ├── UdpDataSource.java │ │ │ ├── cache/ │ │ │ │ ├── Cache.java │ │ │ │ ├── CacheDataSink.java │ │ │ │ ├── CacheDataSinkFactory.java │ │ │ │ ├── CacheDataSource.java │ │ │ │ ├── CacheDataSourceFactory.java │ │ │ │ ├── CacheEvictor.java │ │ │ │ ├── CacheFileMetadata.java │ │ │ │ ├── CacheFileMetadataIndex.java │ │ │ │ ├── CacheKeyFactory.java │ │ │ │ ├── CacheSpan.java │ │ │ │ ├── CacheUtil.java │ │ │ │ ├── CachedContent.java │ │ │ │ ├── CachedContentIndex.java │ │ │ │ ├── CachedRegionTracker.java │ │ │ │ ├── ContentMetadata.java │ │ │ │ ├── ContentMetadataMutations.java │ │ │ │ ├── DefaultContentMetadata.java │ │ │ │ ├── LeastRecentlyUsedCacheEvictor.java │ │ │ │ ├── NoOpCacheEvictor.java │ │ │ │ ├── SimpleCache.java │ │ │ │ └── SimpleCacheSpan.java │ │ │ └── crypto/ │ │ │ ├── AesCipherDataSink.java │ │ │ ├── AesCipherDataSource.java │ │ │ ├── AesFlushingCipher.java │ │ │ └── CryptoUtil.java │ │ ├── util/ │ │ │ ├── Assertions.java │ │ │ ├── AtomicFile.java │ │ │ ├── Clock.java │ │ │ ├── CodecSpecificDataUtil.java │ │ │ ├── ColorParser.java │ │ │ ├── ConditionVariable.java │ │ │ ├── EGLSurfaceTexture.java │ │ │ ├── ErrorMessageProvider.java │ │ │ ├── EventDispatcher.java │ │ │ ├── EventLogger.java │ │ │ ├── FlacStreamMetadata.java │ │ │ ├── GlUtil.java │ │ │ ├── HandlerWrapper.java │ │ │ ├── LibraryLoader.java │ │ │ ├── Log.java │ │ │ ├── LongArray.java │ │ │ ├── MediaClock.java │ │ │ ├── MimeTypes.java │ │ │ ├── NalUnitUtil.java │ │ │ ├── NonNullApi.java │ │ │ ├── NotificationUtil.java │ │ │ ├── ParsableBitArray.java │ │ │ ├── ParsableByteArray.java │ │ │ ├── ParsableNalUnitBitArray.java │ │ │ ├── Predicate.java │ │ │ ├── PriorityTaskManager.java │ │ │ ├── RepeatModeUtil.java │ │ │ ├── ReusableBufferedOutputStream.java │ │ │ ├── SlidingPercentile.java │ │ │ ├── StandaloneMediaClock.java │ │ │ ├── SystemClock.java │ │ │ ├── SystemHandlerWrapper.java │ │ │ ├── TimedValueQueue.java │ │ │ ├── TimestampAdjuster.java │ │ │ ├── TraceUtil.java │ │ │ ├── UriUtil.java │ │ │ ├── Util.java │ │ │ ├── XmlPullParserUtil.java │ │ │ └── package-info.java │ │ └── video/ │ │ ├── AvcConfig.java │ │ ├── ColorInfo.java │ │ ├── DolbyVisionConfig.java │ │ ├── DummySurface.java │ │ ├── HevcConfig.java │ │ ├── MediaCodecVideoRenderer.java │ │ ├── SimpleDecoderVideoRenderer.java │ │ ├── VideoDecoderException.java │ │ ├── VideoDecoderGLSurfaceView.java │ │ ├── VideoDecoderInputBuffer.java │ │ ├── VideoDecoderOutputBuffer.java │ │ ├── VideoDecoderOutputBufferRenderer.java │ │ ├── VideoDecoderRenderer.java │ │ ├── VideoFrameMetadataListener.java │ │ ├── VideoFrameReleaseTimeHelper.java │ │ ├── VideoListener.java │ │ ├── VideoRendererEventListener.java │ │ ├── package-info.java │ │ └── spherical/ │ │ ├── CameraMotionListener.java │ │ ├── CameraMotionRenderer.java │ │ ├── FrameRotationQueue.java │ │ ├── Projection.java │ │ ├── ProjectionDecoder.java │ │ └── package-info.java │ └── res/ │ └── values/ │ └── strings.xml ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── ijkplayer/ │ ├── .gitignore │ ├── build.gradle │ ├── ijkplayer.iml │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── tv/ │ │ └── danmaku/ │ │ └── ijk/ │ │ └── media/ │ │ └── player/ │ │ ├── AbstractMediaPlayer.java │ │ ├── AndroidMediaPlayer.java │ │ ├── IMediaPlayer.java │ │ ├── ISurfaceTextureHolder.java │ │ ├── ISurfaceTextureHost.java │ │ ├── IjkLibLoader.java │ │ ├── IjkMediaCodecInfo.java │ │ ├── IjkMediaMeta.java │ │ ├── IjkMediaPlayer.java │ │ ├── IjkTimedText.java │ │ ├── MediaInfo.java │ │ ├── MediaPlayerProxy.java │ │ ├── TextureMediaPlayer.java │ │ ├── annotations/ │ │ │ ├── AccessedByNative.java │ │ │ └── CalledByNative.java │ │ ├── exceptions/ │ │ │ └── IjkMediaException.java │ │ ├── ffmpeg/ │ │ │ └── FFmpegApi.java │ │ ├── misc/ │ │ │ ├── AndroidMediaFormat.java │ │ │ ├── AndroidTrackInfo.java │ │ │ ├── IAndroidIO.java │ │ │ ├── IMediaDataSource.java │ │ │ ├── IMediaFormat.java │ │ │ ├── ITrackInfo.java │ │ │ ├── IjkMediaFormat.java │ │ │ └── IjkTrackInfo.java │ │ └── pragma/ │ │ ├── DebugLog.java │ │ └── Pragma.java │ └── res/ │ └── values/ │ └── strings.xml ├── mediaproxy/ │ ├── .gitignore │ ├── build.gradle │ ├── mediaproxy.iml │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── media/ │ │ └── cache/ │ │ ├── CacheManager.java │ │ ├── DownloadConstants.java │ │ ├── LocalProxyConfig.java │ │ ├── StorageManager.java │ │ ├── VideoCacheException.java │ │ ├── VideoDownloadManager.java │ │ ├── VideoDownloadQueue.java │ │ ├── VideoInfoParserManager.java │ │ ├── download/ │ │ │ ├── BaseVideoDownloadTask.java │ │ │ ├── M3U8VideoDownloadTask.java │ │ │ └── VideoDownloadTask.java │ │ ├── hls/ │ │ │ ├── M3U8.java │ │ │ ├── M3U8Constants.java │ │ │ ├── M3U8Ts.java │ │ │ └── M3U8Utils.java │ │ ├── http/ │ │ │ ├── ChunkedOutputStream.java │ │ │ ├── ContentType.java │ │ │ ├── HttpRequest.java │ │ │ ├── HttpResponse.java │ │ │ ├── IState.java │ │ │ ├── Method.java │ │ │ ├── ResponseState.java │ │ │ └── SocketProcessorTask.java │ │ ├── listener/ │ │ │ ├── IDownloadInfosCallback.java │ │ │ ├── IDownloadListener.java │ │ │ ├── IDownloadTaskListener.java │ │ │ ├── IVideoInfoCallback.java │ │ │ └── IVideoInfoParseCallback.java │ │ ├── model/ │ │ │ ├── Video.java │ │ │ ├── VideoCacheInfo.java │ │ │ ├── VideoTaskItem.java │ │ │ ├── VideoTaskMode.java │ │ │ └── VideoTaskState.java │ │ ├── proxy/ │ │ │ ├── AsyncProxyServer.java │ │ │ └── CustomProxyServer.java │ │ └── utils/ │ │ ├── DownloadExceptionUtils.java │ │ ├── HttpUtils.java │ │ ├── LocalProxyThreadUtils.java │ │ ├── LocalProxyUtils.java │ │ └── StorageUtils.java │ └── res/ │ └── values/ │ └── strings.xml ├── playersdk/ │ ├── .gitignore │ ├── build.gradle │ ├── playersdk.iml │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── android/ │ │ └── player/ │ │ ├── CommonPlayer.java │ │ ├── IPlayer.java │ │ ├── PlayerAttributes.java │ │ ├── PlayerType.java │ │ ├── impl/ │ │ │ ├── ExoPlayerImpl.java │ │ │ ├── IjkPlayerImpl.java │ │ │ ├── MediaPlayerImpl.java │ │ │ └── PlayerImpl.java │ │ └── proxy/ │ │ └── LocalProxyPlayerImpl.java │ └── res/ │ └── values/ │ └── strings.xml └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ build .gradle .idea local.properties app/app.iml playerlib/playerlib.iml exoplayerlib/exoplayerlib.iml ijkplayerlib/ijkplayerlib.iml mediaproxylib/mediaproxylib.iml mediaproxylib/build mediaproxylib/build/* /MediaSDK.iml ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2014-2016 Alexey Danilov Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # MediaSDK The library is working for downloading video while playing the video, the video contains M3U8/MP4
Developer documentation is here, [Click it](./README_cn.md)
You can refer to the technical documentation:https://www.jianshu.com/p/27085da32a35
Use the library:
``` allprojects { repositories { ... maven { url 'https://jitpack.io' } } } dependencies { implementation 'com.github.JeffMony:MediaSDK:2.0.0' } ``` #### The Core functions of the project > * Cache management, LRU > * Video downloading management > * Local proxy management > * Display downloading speed > * Display the video cache's size > * Support the video's downloading while playing the video, M3U8/MP4 video The project's architecture is as follows: ![](./files/LocalProxy.png) ### Developer documentation #### 1.Application->onCreate(...) ``` File file = LocalProxyUtils.getVideoCacheDir(this); if (!file.exists()) { file.mkdir(); } LocalProxyConfig config = new VideoDownloadManager.Build(this) .setCacheRoot(file) .setUrlRedirect(false) .setTimeOut(DownloadConstants.READ_TIMEOUT, DownloadConstants.CONN_TIMEOUT, DownloadConstants.SOCKET_TIMEOUT) .setConcurrentCount(DownloadConstants.CONCURRENT_COUNT) .setIgnoreAllCertErrors(true) .buildConfig(); VideoDownloadManager.getInstance().initConfig(config); ``` 1.setCacheRoot The cache path; 2.setUrlRedirect Support request's redirect; 3.setCacheSize Set cache size; 4.setTimeOut Set request's timeout; 5.setPort Set the local proxy server's port; 6.setIgnoreAllCertErrors Support the certificate; #### 2.The local proxy server's switch ``` PlayerAttributes attributes = new PlayerAttributes(); attributes.setUseLocalProxy(mUseLocalProxy); ``` #### 3.Set the listener ``` mPlayer.setOnLocalProxyCacheListener(mOnLocalProxyCacheListener); mPlayer.startLocalProxy(mUrl, null); private IPlayer.OnLocalProxyCacheListener mOnLocalProxyCacheListener = new IPlayer.OnLocalProxyCacheListener() { @Override public void onCacheReady(IPlayer mp, String proxyUrl) { LogUtils.w("onCacheReady proxyUrl = " + proxyUrl); Uri uri = Uri.parse(proxyUrl); try { mPlayer.setDataSource(PlayerActivity.this, uri); } catch (IOException e) { e.printStackTrace(); return; } mPlayer.setSurface(mSurface); mPlayer.setOnPreparedListener(mPreparedListener); mPlayer.setOnVideoSizeChangedListener(mVideoSizeChangeListener); mPlayer.prepareAsync(); } @Override public void onCacheProgressChanged(IPlayer mp, int percent, long cachedSize) { LogUtils.w("onCacheProgressChanged percent = " + percent); mPercent = percent; } @Override public void onCacheSpeedChanged(String url, float cacheSpeed) { if (mPlayer != null && mPlayer.get() != null) { mPlayer.get().notifyProxyCacheSpeed(cacheSpeed); } } @Override public void onCacheFinished(String url) { LogUtils.i("onCacheFinished url="+url + ", player="+this); mIsCompleteCached = true; } @Override public void onCacheForbidden(String url) { LogUtils.w("onCacheForbidden url="+url+", player="+this); mUseLocalProxy = false; if (mPlayer != null && mPlayer.get() != null) { mPlayer.get().notifyProxyCacheForbidden(url); } } @Override public void onCacheFailed(String url, Exception e) { LogUtils.w("onCacheFailed , player="+this); pauseProxyCacheTask(PROXY_CACHE_EXCEPTION); } }; ``` demo:
![](./files/test1_low.jpg)![](./files/test2_low.jpg) ![](./files/JeffMony.jpg) ![](./files/ErWeiMa.jpg) ================================================ FILE: README_cn.md ================================================ # MediaSDK 关注一下分析文章:https://www.jianshu.com/p/27085da32a35 ``` allprojects { repositories { ... maven { url 'https://jitpack.io' } } } dependencies { implementation 'com.github.JeffMony:MediaSDK:2.0.0' } ``` 最近这个项目有新的维护计划: > * 1.本地代理的控制逻辑移到server端 > * 2.增加mp4 moov端的识别规则 > * 3.将本地代理库和播放器解耦 ### 版本LOG 2.0.0 > * 1.使用androidasync替换proxyserver > * 2.优化MediaSDK接口 t1.5.0 > * 1.视频下载队列,可以设置视频并发下载的个数 > * 2.视频播放缓存和下载缓存的数据合并,但是逻辑分离 t1.4.0 > * 1.增加视频下载模块; > * 2.重构本地代理模块代码; > * 3.视频下载和本地代理模块代码复用; > * 4.还有一些bug待处理,很快更新 > * 5.后续版本更新计划: 下载队列;初始化本地已下载的视频;下载和播放缓存隔离; t1.3.0 > * 1.封装好边下边播模块 > * 2.可以直接商用 v1.1.0 > * 1.解决https 证书出错的视频url请求,信任证书; > * 2.解决播放过程中息屏的问题,保持屏幕常亮; > * 3.增加 isPlaying 接口,表示当前是否正在播放视频; > * 4.解决Cleartext HTTP traffic to 127.0.0.1 not permitted 问题,Android P版本不允许未加密请求; v1.0.0 > * 1.支持MediaPlayer/IjkPlayer/ExoPlayer 三种播放器播放视频; > * 2.支持M3U8/MP4视频的边下边播功能; > * 3.本地代理实现边下边播功能; > * 4.提供当前下载速度和下载进度的回调; #### 封装了一个播放器功能库 > * 实现ijkplayer exoplayer mediaplayer 3种播放器类型;可以任意切换; > * ijkplayer 是从 k0.8.8分支上拉出来的; > * exoplayer 是 2.11.1版本 #### 实现视频边下边播的功能 > * 缓存管理 > * 下载管理 > * 本地代理管理模块(使用androidasync管理本地代理) > * 回调播放下载实时速度 > * 显示缓存大小 本项目的架构如下: ![](./files/LocalProxy.png) 从上面的架构可以看出来,本项目的重点在本地代理层,这是实现边下边播的核心逻辑; ### 接入库须知 #### 1.在Application->onCreate(...) 中初始化 ``` File file = LocalProxyUtils.getVideoCacheDir(this); if (!file.exists()) { file.mkdir(); } LocalProxyConfig config = new VideoDownloadManager.Build(this) .setCacheRoot(file) .setUrlRedirect(false) .setTimeOut(DownloadConstants.READ_TIMEOUT, DownloadConstants.CONN_TIMEOUT, DownloadConstants.SOCKET_TIMEOUT) .setConcurrentCount(DownloadConstants.CONCURRENT_COUNT) .setIgnoreAllCertErrors(true) .buildConfig(); VideoDownloadManager.getInstance().initConfig(config); ``` 这儿可以设置一些属性: 1.setCacheRoot 设置缓存的路径; 2.setUrlRedirect 是否需要重定向请求; 3.setCacheSize 设置缓存的大小限制; 4.setTimeOut 设置连接和读超时时间; 5.setPort 设置本地代理的端口; 6.setIgnoreAllCertErrors 是否需要信任证书; #### 2.打开本地代理开关 ``` PlayerAttributes attributes = new PlayerAttributes(); attributes.setUseLocalProxy(mUseLocalProxy); ``` #### 3.设置本地代理模块监听 ``` mPlayer.setOnLocalProxyCacheListener(mOnLocalProxyCacheListener); mPlayer.startLocalProxy(mUrl, null); private IPlayer.OnLocalProxyCacheListener mOnLocalProxyCacheListener = new IPlayer.OnLocalProxyCacheListener() { @Override public void onCacheReady(IPlayer mp, String proxyUrl) { LogUtils.w("onCacheReady proxyUrl = " + proxyUrl); Uri uri = Uri.parse(proxyUrl); try { mPlayer.setDataSource(PlayerActivity.this, uri); } catch (IOException e) { e.printStackTrace(); return; } mPlayer.setSurface(mSurface); mPlayer.setOnPreparedListener(mPreparedListener); mPlayer.setOnVideoSizeChangedListener(mVideoSizeChangeListener); mPlayer.prepareAsync(); } @Override public void onCacheProgressChanged(IPlayer mp, int percent, long cachedSize) { LogUtils.w("onCacheProgressChanged percent = " + percent); mPercent = percent; } @Override public void onCacheSpeedChanged(String url, float cacheSpeed) { if (mPlayer != null && mPlayer.get() != null) { mPlayer.get().notifyProxyCacheSpeed(cacheSpeed); } } @Override public void onCacheFinished(String url) { LogUtils.i("onCacheFinished url="+url + ", player="+this); mIsCompleteCached = true; } @Override public void onCacheForbidden(String url) { LogUtils.w("onCacheForbidden url="+url+", player="+this); mUseLocalProxy = false; if (mPlayer != null && mPlayer.get() != null) { mPlayer.get().notifyProxyCacheForbidden(url); } } @Override public void onCacheFailed(String url, Exception e) { LogUtils.w("onCacheFailed , player="+this); pauseProxyCacheTask(PROXY_CACHE_EXCEPTION); } }; ``` #### 4.本地代理的生命周期跟着播放器的生命周期一起 #### 5.下载接入函数 ``` public interface IDownloadListener { void onDownloadPrepare(VideoTaskItem item); void onDownloadPending(VideoTaskItem item); void onDownloadStart(VideoTaskItem item); void onDownloadProxyReady(VideoTaskItem item); void onDownloadProgress(VideoTaskItem item); void onDownloadSpeed(VideoTaskItem item); void onDownloadPause(VideoTaskItem item); void onDownloadError(VideoTaskItem item); void onDownloadProxyForbidden(VideoTaskItem item); void onDownloadSuccess(VideoTaskItem item); } ``` ### 功能概要 #### 1.封装了一个player sdk层 > * 1.1 接入Android 原生的 MediaPlayer 播放器 > * 1.2 接入google的EXO player 播放器 > * 1.3 接入开源的 ijk player 播放器 #### 2.实现整视频的边下边播 > * 2.1 实现整视频的分片下载 > * 2.2 实现整视频的断点下载 #### 3.实现HLS分片视频的边下边播 > * 3.1 实现HLS视频源的解析工作 > * 3.2 实现HLS的边下边播 > * 3.3 实现HLS的断点下载功能 #### 4.线程池控制下载功能 #### 5.提供视频下载的额外功能 > * 5.1 可以提供播放视频或者下载视频的实时网速 > * 5.2 可以提供已缓存视频的大小 demo示意图:
![](./files/test1_low.jpg)![](./files/test2_low.jpg) 欢迎关注我的公众号JeffMony,我会持续为你带来音视频---算法---Android---python 方面的知识分享
![](./files/JeffMony.jpg) 如果你觉得这个库有用,请鼓励一下吧;
![](./files/ErWeiMa.jpg) ================================================ FILE: androidasync/.gitignore ================================================ /build ================================================ FILE: androidasync/androidasync.iml ================================================ ================================================ FILE: androidasync/build.gradle ================================================ apply plugin: 'com.android.library' android { compileSdkVersion 29 buildToolsVersion "29.0.3" defaultConfig { minSdkVersion 19 targetSdkVersion 29 versionCode 1 versionName "1.0" consumerProguardFiles 'consumer-rules.pro' } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) compileOnly group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.60' compileOnly group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.60' implementation 'androidx.appcompat:appcompat:1.1.0' } ================================================ FILE: androidasync/consumer-rules.pro ================================================ ================================================ FILE: androidasync/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: androidasync/src/main/AndroidManifest.xml ================================================ ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/AsyncDatagramSocket.java ================================================ package com.jeffmony.async; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; public class AsyncDatagramSocket extends AsyncNetworkSocket { public void disconnect() throws IOException { socketAddress = null; ((DatagramChannelWrapper)getChannel()).disconnect(); } @Override public InetSocketAddress getRemoteAddress() { if (isOpen()) return super.getRemoteAddress(); return ((DatagramChannelWrapper)getChannel()).getRemoteAddress(); } public void connect(InetSocketAddress address) throws IOException { socketAddress = address; ((DatagramChannelWrapper)getChannel()).mChannel.connect(address); } public void send(final String host, final int port, final ByteBuffer buffer) { if (getServer().getAffinity() != Thread.currentThread()) { getServer().run(new Runnable() { @Override public void run() { send(host, port, buffer); } }); return; } try { ((DatagramChannelWrapper)getChannel()).mChannel.send(buffer, new InetSocketAddress(host, port)); } catch (IOException e) { // close(); // reportEndPending(e); // reportClose(e); } } public void send(final InetSocketAddress address, final ByteBuffer buffer) { if (getServer().getAffinity() != Thread.currentThread()) { getServer().run(new Runnable() { @Override public void run() { send(address, buffer); } }); return; } try { int sent = ((DatagramChannelWrapper)getChannel()).mChannel.send(buffer, new InetSocketAddress(address.getHostName(), address.getPort())); } catch (IOException e) { // Log.e("SEND", "send error", e); // close(); // reportEndPending(e); // reportClose(e); } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/AsyncNetworkSocket.java ================================================ package com.jeffmony.async; import android.util.Log; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.callback.WritableCallback; import com.jeffmony.async.util.Allocator; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.CancelledKeyException; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; public class AsyncNetworkSocket implements AsyncSocket { AsyncNetworkSocket() { } @Override public void end() { mChannel.shutdownOutput(); } public boolean isChunked() { return mChannel.isChunked(); } InetSocketAddress socketAddress; void attach(SocketChannel channel, InetSocketAddress socketAddress) throws IOException { this.socketAddress = socketAddress; allocator = new Allocator(); mChannel = new SocketChannelWrapper(channel); } void attach(DatagramChannel channel) throws IOException { mChannel = new DatagramChannelWrapper(channel); // keep udp at roughly the mtu, which is 1540 or something // letting it grow freaks out nio apparently. allocator = new Allocator(8192); } ChannelWrapper getChannel() { return mChannel; } public void onDataWritable() { // assert mWriteableHandler != null; if (!mChannel.isChunked()) { // turn write off mKey.interestOps(~SelectionKey.OP_WRITE & mKey.interestOps()); } if (mWriteableHandler != null) mWriteableHandler.onWriteable(); } private ChannelWrapper mChannel; private SelectionKey mKey; private AsyncServer mServer; void setup(AsyncServer server, SelectionKey key) { mServer = server; mKey = key; } @Override public void write(final ByteBufferList list) { if (mServer.getAffinity() != Thread.currentThread()) { mServer.run(new Runnable() { @Override public void run() { write(list); } }); return; } if (!mChannel.isConnected()) { assert !mChannel.isChunked(); return; } try { int before = list.remaining(); ByteBuffer[] arr = list.getAllArray(); mChannel.write(arr); list.addAll(arr); handleRemaining(list.remaining()); mServer.onDataSent(before - list.remaining()); } catch (IOException e) { closeInternal(); reportEndPending(e); reportClose(e); } } private void handleRemaining(int remaining) throws IOException { if (!mKey.isValid()) throw new IOException(new CancelledKeyException()); if (remaining > 0) { // chunked channels should not fail assert !mChannel.isChunked(); // register for a write notification if a write fails // turn write on mKey.interestOps(SelectionKey.OP_WRITE | mKey.interestOps()); } else { // turn write off mKey.interestOps(~SelectionKey.OP_WRITE & mKey.interestOps()); } } private ByteBufferList pending = new ByteBufferList(); // private ByteBuffer[] buffers = new ByteBuffer[8]; Allocator allocator; int onReadable() { spitPending(); // even if the socket is paused, // it may end up getting a queued readable event if it is // already in the selector's ready queue. if (mPaused) return 0; int total = 0; boolean closed = false; // ByteBufferList.obtainArray(buffers, Math.min(Math.max(mToAlloc, 2 << 11), maxAlloc)); ByteBuffer b = allocator.allocate(); // keep track of the max mount read during this read cycle // so we can be quicker about allocations during the next // time this socket reads. long read; try { read = mChannel.read(b); } catch (Exception e) { read = -1; closeInternal(); reportEndPending(e); reportClose(e); } if (read < 0) { closeInternal(); closed = true; } else { total += read; } if (read > 0) { allocator.track(read); b.flip(); // for (int i = 0; i < buffers.length; i++) { // ByteBuffer b = buffers[i]; // buffers[i] = null; // b.flip(); // pending.add(b); // } pending.add(b); Util.emitAllData(this, pending); } else { ByteBufferList.reclaim(b); } if (closed) { reportEndPending(null); reportClose(null); } return total; } boolean closeReported; protected void reportClose(Exception e) { if (closeReported) return; closeReported = true; if (mClosedHander != null) { mClosedHander.onCompleted(e); mClosedHander = null; } } @Override public void close() { closeInternal(); reportClose(null); } private void closeInternal() { mKey.cancel(); try { mChannel.close(); } catch (IOException e) { } } WritableCallback mWriteableHandler; @Override public void setWriteableCallback(WritableCallback handler) { mWriteableHandler = handler; } DataCallback mDataHandler; @Override public void setDataCallback(DataCallback callback) { mDataHandler = callback; } @Override public DataCallback getDataCallback() { return mDataHandler; } CompletedCallback mClosedHander; @Override public void setClosedCallback(CompletedCallback handler) { mClosedHander = handler; } @Override public CompletedCallback getClosedCallback() { return mClosedHander; } @Override public WritableCallback getWriteableCallback() { return mWriteableHandler; } void reportEnd(Exception e) { if (mEndReported) return; mEndReported = true; if (mCompletedCallback != null) mCompletedCallback.onCompleted(e); else if (e != null) { Log.e("NIO", "Unhandled exception", e); } } boolean mEndReported; Exception mPendingEndException; void reportEndPending(Exception e) { if (pending.hasRemaining()) { mPendingEndException = e; return; } reportEnd(e); } private CompletedCallback mCompletedCallback; @Override public void setEndCallback(CompletedCallback callback) { mCompletedCallback = callback; } @Override public CompletedCallback getEndCallback() { return mCompletedCallback; } @Override public boolean isOpen() { return mChannel.isConnected() && mKey.isValid(); } boolean mPaused = false; @Override public void pause() { if (mServer.getAffinity() != Thread.currentThread()) { mServer.run(new Runnable() { @Override public void run() { pause(); } }); return; } if (mPaused) return; mPaused = true; try { mKey.interestOps(~SelectionKey.OP_READ & mKey.interestOps()); } catch (Exception ex) { } } private void spitPending() { if (pending.hasRemaining()) { Util.emitAllData(this, pending); } } @Override public void resume() { if (mServer.getAffinity() != Thread.currentThread()) { mServer.run(new Runnable() { @Override public void run() { resume(); } }); return; } if (!mPaused) return; mPaused = false; try { mKey.interestOps(SelectionKey.OP_READ | mKey.interestOps()); } catch (Exception ex) { } spitPending(); if (!isOpen()) reportEndPending(mPendingEndException); } @Override public boolean isPaused() { return mPaused; } @Override public AsyncServer getServer() { return mServer; } public InetSocketAddress getRemoteAddress() { return socketAddress; } public InetAddress getLocalAddress() { return mChannel.getLocalAddress(); } public int getLocalPort() { return mChannel.getLocalPort(); } public Object getSocket() { return getChannel().getSocket(); } @Override public String charset() { return null; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/AsyncSSLException.java ================================================ package com.jeffmony.async; public class AsyncSSLException extends Exception { public AsyncSSLException(Throwable cause) { super("Peer not trusted by any of the system trust managers.", cause); } private boolean mIgnore = false; public void setIgnore(boolean ignore) { mIgnore = ignore; } public boolean getIgnore() { return mIgnore; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/AsyncSSLServerSocket.java ================================================ package com.jeffmony.async; import java.security.PrivateKey; import java.security.cert.Certificate; public interface AsyncSSLServerSocket extends AsyncServerSocket { PrivateKey getPrivateKey(); Certificate getCertificate(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/AsyncSSLSocket.java ================================================ package com.jeffmony.async; import java.security.cert.X509Certificate; import javax.net.ssl.SSLEngine; public interface AsyncSSLSocket extends AsyncSocket { X509Certificate[] getPeerCertificates(); SSLEngine getSSLEngine(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/AsyncSSLSocketWrapper.java ================================================ package com.jeffmony.async; import android.content.Context; import android.os.Build; import android.util.Base64; import android.util.Pair; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.ConnectCallback; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.callback.ListenCallback; import com.jeffmony.async.callback.WritableCallback; import com.jeffmony.async.future.Cancellable; import com.jeffmony.async.future.SimpleCancellable; import com.jeffmony.async.http.SSLEngineSNIConfigurator; import com.jeffmony.async.util.Allocator; import com.jeffmony.async.util.StreamUtility; import com.jeffmony.async.wrapper.AsyncSocketWrapper; import org.apache.http.conn.ssl.StrictHostnameVerifier; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.BasicConstraints; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import java.io.ByteArrayInputStream; import java.io.File; import java.math.BigInteger; import java.net.InetAddress; import java.nio.ByteBuffer; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.PrivateKey; import java.security.Provider; import java.security.Security; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Calendar; import java.util.Date; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLEngineResult.HandshakeStatus; import javax.net.ssl.SSLEngineResult.Status; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; public class AsyncSSLSocketWrapper implements AsyncSocketWrapper, AsyncSSLSocket { private static final String LOGTAG = "AsyncSSLSocketWrapper"; public interface HandshakeCallback { public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket); } static SSLContext defaultSSLContext; static SSLContext trustAllSSLContext; static TrustManager[] trustAllManagers; static HostnameVerifier trustAllVerifier; AsyncSocket mSocket; BufferedDataSink mSink; boolean mUnwrapping; SSLEngine engine; boolean finishedHandshake; private int mPort; private String mHost; private boolean mWrapping; HostnameVerifier hostnameVerifier; HandshakeCallback handshakeCallback; X509Certificate[] peerCertificates; WritableCallback mWriteableCallback; DataCallback mDataCallback; TrustManager[] trustManagers; boolean clientMode; static { // following is the "trust the system" certs setup try { // critical extension 2.5.29.15 is implemented improperly prior to 4.0.3. // https://code.google.com/p/android/issues/detail?id=9307 // https://groups.google.com/forum/?fromgroups=#!topic/netty/UCfqPPk5O4s // certs that use this extension will throw in Cipher.java. // fallback is to use a custom SSLContext, and hack around the x509 extension. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) throw new Exception(); defaultSSLContext = SSLContext.getInstance("Default"); } catch (Exception ex) { try { defaultSSLContext = SSLContext.getInstance("TLS"); TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { } public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { for (X509Certificate cert : certs) { if (cert != null && cert.getCriticalExtensionOIDs() != null) cert.getCriticalExtensionOIDs().remove("2.5.29.15"); } } } }; defaultSSLContext.init(null, trustAllCerts, null); } catch (Exception ex2) { ex.printStackTrace(); ex2.printStackTrace(); } } try { trustAllSSLContext = SSLContext.getInstance("TLS"); trustAllManagers = new TrustManager[] { new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { } public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { } } }; trustAllSSLContext.init(null, trustAllManagers, null); trustAllVerifier = (hostname, session) -> true; } catch (Exception ex2) { ex2.printStackTrace(); } } public static SSLContext getDefaultSSLContext() { return defaultSSLContext; } public static void handshake(AsyncSocket socket, String host, int port, SSLEngine sslEngine, TrustManager[] trustManagers, HostnameVerifier verifier, boolean clientMode, final HandshakeCallback callback) { AsyncSSLSocketWrapper wrapper = new AsyncSSLSocketWrapper(socket, host, port, sslEngine, trustManagers, verifier, clientMode); wrapper.handshakeCallback = callback; socket.setClosedCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (ex != null) callback.onHandshakeCompleted(ex, null); else callback.onHandshakeCompleted(new SSLException("socket closed during handshake"), null); } }); try { wrapper.engine.beginHandshake(); wrapper.handleHandshakeStatus(wrapper.engine.getHandshakeStatus()); } catch (SSLException e) { wrapper.report(e); } } public static Cancellable connectSocket(AsyncServer server, String host, int port, ConnectCallback callback) { return connectSocket(server, host, port, false, callback); } public static Cancellable connectSocket(AsyncServer server, String host, int port, boolean trustAllCerts, ConnectCallback callback) { SimpleCancellable cancellable = new SimpleCancellable(); Cancellable connect = server.connectSocket(host, port, (ex, netSocket) -> { if (ex != null) { if (cancellable.setComplete()) callback.onConnectCompleted(ex, null); return; } handshake(netSocket, host, port, (trustAllCerts ? trustAllSSLContext : defaultSSLContext).createSSLEngine(host, port), trustAllCerts ? trustAllManagers : null, trustAllCerts ? trustAllVerifier : null, true, (e, socket) -> { if (!cancellable.setComplete()) { if (socket != null) socket.close(); return; } if (e != null) callback.onConnectCompleted(e, null); else callback.onConnectCompleted(null, socket); }); }); cancellable.setParent(connect); return cancellable; } boolean mEnded; Exception mEndException; final ByteBufferList pending = new ByteBufferList(); private AsyncSSLSocketWrapper(AsyncSocket socket, String host, int port, SSLEngine sslEngine, TrustManager[] trustManagers, HostnameVerifier verifier, boolean clientMode) { mSocket = socket; hostnameVerifier = verifier; this.clientMode = clientMode; this.trustManagers = trustManagers; this.engine = sslEngine; mHost = host; mPort = port; engine.setUseClientMode(clientMode); mSink = new BufferedDataSink(socket); mSink.setWriteableCallback(new WritableCallback() { @Override public void onWriteable() { if (mWriteableCallback != null) mWriteableCallback.onWriteable(); } }); // on pause, the emitter is paused to prevent the buffered // socket and itself from firing. // on resume, emitter is resumed, ssl buffer is flushed as well mSocket.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (mEnded) return; mEnded = true; mEndException = ex; if (!pending.hasRemaining() && mEndCallback != null) mEndCallback.onCompleted(ex); } }); mSocket.setDataCallback(dataCallback); } final DataCallback dataCallback = new DataCallback() { final Allocator allocator = new Allocator().setMinAlloc(8192); final ByteBufferList buffered = new ByteBufferList(); @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { if (mUnwrapping) return; try { mUnwrapping = true; bb.get(buffered); if (buffered.hasRemaining()) { ByteBuffer all = buffered.getAll(); buffered.add(all); } ByteBuffer b = ByteBufferList.EMPTY_BYTEBUFFER; while (true) { if (b.remaining() == 0 && buffered.size() > 0) { b = buffered.remove(); } int remaining = b.remaining(); int before = pending.remaining(); SSLEngineResult res; { // wrap to prevent access to the readBuf ByteBuffer readBuf = allocator.allocate(); res = engine.unwrap(b, readBuf); addToPending(pending, readBuf); allocator.track(pending.remaining() - before); } if (res.getStatus() == Status.BUFFER_OVERFLOW) { allocator.setMinAlloc(allocator.getMinAlloc() * 2); remaining = -1; } else if (res.getStatus() == Status.BUFFER_UNDERFLOW) { buffered.addFirst(b); if (buffered.size() <= 1) { break; } // pack it remaining = -1; b = buffered.getAll(); buffered.addFirst(b); b = ByteBufferList.EMPTY_BYTEBUFFER; } handleHandshakeStatus(res.getHandshakeStatus()); if (b.remaining() == remaining && before == pending.remaining()) { buffered.addFirst(b); break; } } AsyncSSLSocketWrapper.this.onDataAvailable(); } catch (SSLException ex) { // ex.printStackTrace(); report(ex); } finally { mUnwrapping = false; } } }; public void onDataAvailable() { Util.emitAllData(this, pending); if (mEnded && !pending.hasRemaining() && mEndCallback != null) mEndCallback.onCompleted(mEndException); } @Override public SSLEngine getSSLEngine() { return engine; } void addToPending(ByteBufferList out, ByteBuffer mReadTmp) { mReadTmp.flip(); if (mReadTmp.hasRemaining()) { out.add(mReadTmp); } else { ByteBufferList.reclaim(mReadTmp); } } @Override public void end() { mSocket.end(); } public String getHost() { return mHost; } public int getPort() { return mPort; } private void handleHandshakeStatus(HandshakeStatus status) { if (status == HandshakeStatus.NEED_TASK) { final Runnable task = engine.getDelegatedTask(); task.run(); } if (status == HandshakeStatus.NEED_WRAP) { write(writeList); } if (status == HandshakeStatus.NEED_UNWRAP) { dataCallback.onDataAvailable(this, new ByteBufferList()); } try { if (!finishedHandshake && (engine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING || engine.getHandshakeStatus() == HandshakeStatus.FINISHED)) { if (clientMode) { Exception peerUnverifiedCause = null; boolean trusted = false; try { peerCertificates = (X509Certificate[]) engine.getSession().getPeerCertificates(); if (mHost != null) { if (hostnameVerifier == null) { StrictHostnameVerifier verifier = new StrictHostnameVerifier(); verifier.verify(mHost, StrictHostnameVerifier.getCNs(peerCertificates[0]), StrictHostnameVerifier.getDNSSubjectAlts(peerCertificates[0])); } else { if (!hostnameVerifier.verify(mHost, engine.getSession())) { throw new SSLException("hostname <" + mHost + "> has been denied"); } } } trusted = true; } catch (SSLException ex) { peerUnverifiedCause = ex; } finishedHandshake = true; if (!trusted) { AsyncSSLException e = new AsyncSSLException(peerUnverifiedCause); report(e); if (!e.getIgnore()) throw e; } } else { finishedHandshake = true; } handshakeCallback.onHandshakeCompleted(null, this); handshakeCallback = null; mSocket.setClosedCallback(null); // handshake can complete during a wrap, so make sure that the call // stack and wrap flag is cleared before invoking writable getServer().post(new Runnable() { @Override public void run() { if (mWriteableCallback != null) mWriteableCallback.onWriteable(); } }); onDataAvailable(); } } catch (Exception ex) { report(ex); } } int calculateAlloc(int remaining) { // alloc 50% more than we need for writing int alloc = remaining * 3 / 2; if (alloc == 0) alloc = 8192; return alloc; } ByteBufferList writeList = new ByteBufferList(); @Override public void write(ByteBufferList bb) { if (mWrapping) return; if (mSink.remaining() > 0) return; mWrapping = true; int remaining; SSLEngineResult res = null; ByteBuffer writeBuf = ByteBufferList.obtain(calculateAlloc(bb.remaining())); do { // if the handshake is finished, don't send // 0 bytes of data, since that makes the ssl connection die. // it wraps a 0 byte package, and craps out. if (finishedHandshake && bb.remaining() == 0) break; remaining = bb.remaining(); try { ByteBuffer[] arr = bb.getAllArray(); res = engine.wrap(arr, writeBuf); bb.addAll(arr); writeBuf.flip(); writeList.add(writeBuf); assert !writeList.hasRemaining(); if (writeList.remaining() > 0) mSink.write(writeList); int previousCapacity = writeBuf.capacity(); writeBuf = null; if (res.getStatus() == Status.BUFFER_OVERFLOW) { writeBuf = ByteBufferList.obtain(previousCapacity * 2); remaining = -1; } else { writeBuf = ByteBufferList.obtain(calculateAlloc(bb.remaining())); handleHandshakeStatus(res.getHandshakeStatus()); } } catch (SSLException e) { report(e); } } while ((remaining != bb.remaining() || (res != null && res.getHandshakeStatus() == HandshakeStatus.NEED_WRAP)) && mSink.remaining() == 0); mWrapping = false; ByteBufferList.reclaim(writeBuf); } @Override public void setWriteableCallback(WritableCallback handler) { mWriteableCallback = handler; } @Override public WritableCallback getWriteableCallback() { return mWriteableCallback; } private void report(Exception e) { final HandshakeCallback hs = handshakeCallback; if (hs != null) { handshakeCallback = null; mSocket.setDataCallback(new DataCallback.NullDataCallback()); mSocket.end(); // handshake sets this callback. unset it. mSocket.setClosedCallback(null); mSocket.close(); hs.onHandshakeCompleted(e, null); return; } CompletedCallback cb = getEndCallback(); if (cb != null) cb.onCompleted(e); } @Override public void setDataCallback(DataCallback callback) { mDataCallback = callback; } @Override public DataCallback getDataCallback() { return mDataCallback; } @Override public boolean isChunked() { return mSocket.isChunked(); } @Override public boolean isOpen() { return mSocket.isOpen(); } @Override public void close() { mSocket.close(); } @Override public void setClosedCallback(CompletedCallback handler) { mSocket.setClosedCallback(handler); } @Override public CompletedCallback getClosedCallback() { return mSocket.getClosedCallback(); } CompletedCallback mEndCallback; @Override public void setEndCallback(CompletedCallback callback) { mEndCallback = callback; } @Override public CompletedCallback getEndCallback() { return mEndCallback; } @Override public void pause() { mSocket.pause(); } @Override public void resume() { mSocket.resume(); onDataAvailable(); } @Override public boolean isPaused() { return mSocket.isPaused(); } @Override public AsyncServer getServer() { return mSocket.getServer(); } @Override public AsyncSocket getSocket() { return mSocket; } @Override public DataEmitter getDataEmitter() { return mSocket; } @Override public X509Certificate[] getPeerCertificates() { return peerCertificates; } @Override public String charset() { return null; } private static Certificate selfSign(KeyPair keyPair, String subjectDN) throws Exception { Provider bcProvider = new BouncyCastleProvider(); Security.addProvider(bcProvider); long now = System.currentTimeMillis(); Date startDate = new Date(now); X500Name dnName = new X500Name("CN=" + subjectDN); BigInteger certSerialNumber = new BigInteger(Long.toString(now)); // <-- Using the current timestamp as the certificate serial number Calendar calendar = Calendar.getInstance(); calendar.setTime(startDate); calendar.add(Calendar.YEAR, 1); // <-- 1 Yr validity Date endDate = calendar.getTime(); String signatureAlgorithm = "SHA256WithRSA"; // <-- Use appropriate signature algorithm based on your keyPair algorithm. ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm).build(keyPair.getPrivate()); JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(dnName, certSerialNumber, startDate, endDate, dnName, keyPair.getPublic()); // Extensions -------------------------- // Basic Constraints BasicConstraints basicConstraints = new BasicConstraints(true); // <-- true for CA, false for EndEntity certBuilder.addExtension(new ASN1ObjectIdentifier("2.5.29.19"), true, basicConstraints); // Basic Constraints is usually marked as critical. // ------------------------------------- return new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(contentSigner)); } public static Pair selfSignCertificate(final Context context, String subjectName) throws Exception { File keyPath = context.getFileStreamPath(subjectName + "-key.txt"); KeyPair pair; Certificate cert; try { String[] keyParts = StreamUtility.readFile(keyPath).split("\n"); X509EncodedKeySpec pub = new X509EncodedKeySpec(Base64.decode(keyParts[0], 0)); PKCS8EncodedKeySpec priv = new PKCS8EncodedKeySpec(Base64.decode(keyParts[1], 0)); cert = CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(Base64.decode(keyParts[2], 0))); KeyFactory fact = KeyFactory.getInstance("RSA"); pair = new KeyPair(fact.generatePublic(pub), fact.generatePrivate(priv)); } catch (Exception e) { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); pair = keyGen.generateKeyPair(); cert = selfSign(pair, subjectName); StreamUtility.writeFile(keyPath, Base64.encodeToString(pair.getPublic().getEncoded(), Base64.NO_WRAP) + "\n" + Base64.encodeToString(pair.getPrivate().getEncoded(), Base64.NO_WRAP) + "\n" + Base64.encodeToString(cert.getEncoded(), Base64.NO_WRAP)); } return new Pair<>(pair, cert); } public static AsyncSSLServerSocket listenSecure(final Context context, final AsyncServer server, final String subjectName, final InetAddress host, final int port, final ListenCallback handler) { final ObjectHolder holder = new ObjectHolder<>(); server.run(() -> { try { Pair keyCert = selfSignCertificate(context, subjectName); KeyPair pair = keyCert.first; Certificate cert = keyCert.second; holder.held = listenSecure(server, pair.getPrivate(), cert, host, port, handler); } catch (Exception e) { handler.onCompleted(e); } }); return holder.held; } public static AsyncSSLServerSocket listenSecure(AsyncServer server, String keyDer, String certDer, final InetAddress host, final int port, final ListenCallback handler) { return listenSecure(server, Base64.decode(keyDer, Base64.DEFAULT), Base64.decode(certDer, Base64.DEFAULT), host, port, handler); } private static class ObjectHolder { T held; } public static AsyncSSLServerSocket listenSecure(final AsyncServer server, final byte[] keyDer, final byte[] certDer, final InetAddress host, final int port, final ListenCallback handler) { final ObjectHolder holder = new ObjectHolder<>(); server.run(() -> { try { PKCS8EncodedKeySpec key = new PKCS8EncodedKeySpec(keyDer); Certificate cert = CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(certDer)); PrivateKey pk = KeyFactory.getInstance("RSA").generatePrivate(key); holder.held = listenSecure(server, pk, cert, host, port, handler); } catch (Exception e) { handler.onCompleted(e); } }); return holder.held; } public static AsyncSSLServerSocket listenSecure(final AsyncServer server, final PrivateKey pk, final Certificate cert, final InetAddress host, final int port, final ListenCallback handler) { final ObjectHolder holder = new ObjectHolder<>(); server.run(() -> { try { KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(null); ks.setKeyEntry("key", pk, null, new Certificate[] { cert }); KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509"); kmf.init(ks, "".toCharArray()); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(ks); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); final AsyncServerSocket socket = listenSecure(server, sslContext, host, port, handler); holder.held = new AsyncSSLServerSocket() { @Override public PrivateKey getPrivateKey() { return pk; } @Override public Certificate getCertificate() { return cert; } @Override public void stop() { socket.stop(); } @Override public int getLocalPort() { return socket.getLocalPort(); } }; } catch (Exception e) { handler.onCompleted(e); } }); return holder.held; } public static AsyncServerSocket listenSecure(AsyncServer server, final SSLContext sslContext, final InetAddress host, final int port, final ListenCallback handler) { final SSLEngineSNIConfigurator conf = new SSLEngineSNIConfigurator() { @Override public SSLEngine createEngine(SSLContext sslContext, String peerHost, int peerPort) { SSLEngine engine = super.createEngine(sslContext, peerHost, peerPort); // String[] ciphers = engine.getEnabledCipherSuites(); // for (String cipher: ciphers) { // Log.i(LOGTAG, cipher); // } // todo: what's this for? some vestigal vysor code i think. required by audio mirroring? engine.setEnabledCipherSuites(new String[] { "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" }); return engine; } }; return server.listen(host, port, new ListenCallback() { @Override public void onAccepted(final AsyncSocket socket) { AsyncSSLSocketWrapper.handshake(socket, null, port, conf.createEngine(sslContext, null, port), null, null, false, (e, sslSocket) -> { if (e != null) { // chrome seems to do some sort of SSL probe and cancels handshakes. not sure why. // i suspect it is to pick an optimal strong cipher. // seeing a lot of the following in the log (but no actual connection errors) // javax.net.ssl.SSLHandshakeException: error:10000416:SSL routines:OPENSSL_internal:SSLV3_ALERT_CERTIFICATE_UNKNOWN // seen on Shield TV running API 26 // todo fix: conscrypt ssl context? // Log.e(LOGTAG, "Error while handshaking", e); socket.close(); return; } handler.onAccepted(sslSocket); }); } @Override public void onListening(AsyncServerSocket socket) { handler.onListening(socket); } @Override public void onCompleted(Exception ex) { handler.onCompleted(ex); } }); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/AsyncSemaphore.java ================================================ package com.jeffmony.async; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class AsyncSemaphore { Semaphore semaphore = new Semaphore(0); public void acquire() throws InterruptedException { ThreadQueue threadQueue = ThreadQueue.getOrCreateThreadQueue(Thread.currentThread()); AsyncSemaphore last = threadQueue.waiter; threadQueue.waiter = this; Semaphore queueSemaphore = threadQueue.queueSemaphore; try { if (semaphore.tryAcquire()) return; while (true) { // run the queue while (true) { Runnable run = threadQueue.remove(); if (run == null) break; // Log.i(LOGTAG, "Pumping for AsyncSemaphore"); run.run(); } int permits = Math.max(1, queueSemaphore.availablePermits()); queueSemaphore.acquire(permits); if (semaphore.tryAcquire()) break; } } finally { threadQueue.waiter = last; } } public boolean tryAcquire(long timeout, TimeUnit timeunit) throws InterruptedException { long timeoutMs = TimeUnit.MILLISECONDS.convert(timeout, timeunit); ThreadQueue threadQueue = ThreadQueue.getOrCreateThreadQueue(Thread.currentThread()); AsyncSemaphore last = threadQueue.waiter; threadQueue.waiter = this; Semaphore queueSemaphore = threadQueue.queueSemaphore; try { if (semaphore.tryAcquire()) return true; long start = System.currentTimeMillis(); do { // run the queue while (true) { Runnable run = threadQueue.remove(); if (run == null) break; // Log.i(LOGTAG, "Pumping for AsyncSemaphore"); run.run(); } int permits = Math.max(1, queueSemaphore.availablePermits()); if (!queueSemaphore.tryAcquire(permits, timeoutMs, TimeUnit.MILLISECONDS)) return false; if (semaphore.tryAcquire()) return true; } while (System.currentTimeMillis() - start < timeoutMs); return false; } finally { threadQueue.waiter = last; } } public void release() { semaphore.release(); ThreadQueue.release(this); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/AsyncServer.java ================================================ package com.jeffmony.async; import android.os.Build; import android.os.Handler; import android.os.SystemClock; import android.util.Log; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.ConnectCallback; import com.jeffmony.async.callback.ListenCallback; import com.jeffmony.async.callback.SocketCreateCallback; import com.jeffmony.async.callback.ValueFunction; import com.jeffmony.async.future.Cancellable; import com.jeffmony.async.future.Future; import com.jeffmony.async.future.FutureCallback; import com.jeffmony.async.future.SimpleCancellable; import com.jeffmony.async.future.SimpleFuture; import com.jeffmony.async.util.StreamUtility; import java.io.IOException; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedChannelException; import java.nio.channels.ClosedSelectorException; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.channels.spi.SelectorProvider; import java.util.Arrays; import java.util.Comparator; import java.util.PriorityQueue; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class AsyncServer { public static final String LOGTAG = "NIO"; private static class RunnableWrapper implements Runnable { boolean hasRun; Runnable runnable; ThreadQueue threadQueue; Handler handler; @Override public void run() { synchronized (this) { if (hasRun) return; hasRun = true; } try { runnable.run(); } finally { threadQueue.remove(this); handler.removeCallbacks(this); threadQueue = null; handler = null; runnable = null; } } } public static void post(Handler handler, Runnable runnable) { RunnableWrapper wrapper = new RunnableWrapper(); ThreadQueue threadQueue = ThreadQueue.getOrCreateThreadQueue(handler.getLooper().getThread()); wrapper.threadQueue = threadQueue; wrapper.handler = handler; wrapper.runnable = runnable; // run it in a blocking AsyncSemaphore or a Handler, whichever gets to it first. threadQueue.add(wrapper); handler.post(wrapper); // run the queue if the thread is blocking threadQueue.queueSemaphore.release(); } static { try { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.FROYO) { java.lang.System.setProperty("java.net.preferIPv4Stack", "true"); java.lang.System.setProperty("java.net.preferIPv6Addresses", "false"); } } catch (Throwable ex) { } } static AsyncServer mInstance = new AsyncServer(); public static AsyncServer getDefault() { return mInstance; } private SelectorWrapper mSelector; public boolean isRunning() { return mSelector != null; } String mName; public AsyncServer() { this(null); } public AsyncServer(String name) { if (name == null) name = "AsyncServer"; mName = name; } private static ExecutorService synchronousWorkers = newSynchronousWorkers("AsyncServer-worker-"); private static void wakeup(final SelectorWrapper selector) { synchronousWorkers.execute(() -> { try { selector.wakeupOnce(); } catch (Exception e) { } }); } boolean killed; public void kill() { synchronized (this) { killed = true; } stop(false); } int postCounter = 0; public Cancellable postDelayed(Runnable runnable, long delay) { Scheduled s; synchronized (this) { if (killed) return SimpleCancellable.CANCELLED; // Calculate when to run this queue item: // If there is a delay (non-zero), add it to the current time // When delay is zero, ensure that this follows all other // zero-delay queue items. This is done by setting the // "time" to the queue size. This will make sure it is before // all time-delayed queue items (for all real world scenarios) // as it will always be less than the current time and also remain // behind all other immediately run queue items. long time; if (delay > 0) time = SystemClock.elapsedRealtime() + delay; else if (delay == 0) time = postCounter++; else if (mQueue.size() > 0) time = Math.min(0, mQueue.peek().time - 1); else time = 0; mQueue.add(s = new Scheduled(this, runnable, time)); // start the server up if necessary if (mSelector == null) run(); if (!isAffinityThread()) { wakeup(mSelector); } } return s; } public Cancellable postImmediate(Runnable runnable) { if (Thread.currentThread() == getAffinity()) { runnable.run(); return null; } return postDelayed(runnable, -1); } public Cancellable post(Runnable runnable) { return postDelayed(runnable, 0); } public Cancellable post(final CompletedCallback callback, final Exception e) { return post(() -> callback.onCompleted(e)); } public void run(final Runnable runnable) { if (Thread.currentThread() == mAffinity) { post(runnable); lockAndRunQueue(this, mQueue); return; } final Semaphore semaphore; synchronized (this) { if (killed) return; semaphore = new Semaphore(0); post(() -> { runnable.run(); semaphore.release(); }); } try { semaphore.acquire(); } catch (InterruptedException e) { Log.e(LOGTAG, "run", e); } } private static class Scheduled implements Cancellable, Runnable { // this constructor is only called when the async execution should not be preserved // ie... AsyncServer.stop. public Scheduled(AsyncServer server, Runnable runnable, long time) { this.server = server; this.runnable = runnable; this.time = time; } public AsyncServer server; public Runnable runnable; public long time; @Override public void run() { this.runnable.run(); } @Override public boolean isDone() { synchronized (server) { return !cancelled && !server.mQueue.contains(this); } } boolean cancelled; @Override public boolean isCancelled() { return cancelled; } @Override public boolean cancel() { synchronized (server) { return cancelled = server.mQueue.remove(this); } } } PriorityQueue mQueue = new PriorityQueue(1, Scheduler.INSTANCE); static class Scheduler implements Comparator { public static Scheduler INSTANCE = new Scheduler(); private Scheduler() { } @Override public int compare(Scheduled s1, Scheduled s2) { // keep the smaller ones at the head, so they get tossed out quicker if (s1.time == s2.time) return 0; if (s1.time > s2.time) return 1; return -1; } } public void stop() { stop(false); } public void stop(boolean wait) { // Log.i(LOGTAG, "****AsyncServer is shutting down.****"); final SelectorWrapper currentSelector; final Semaphore semaphore; final boolean isAffinityThread; synchronized (this) { isAffinityThread = isAffinityThread(); currentSelector = mSelector; if (currentSelector == null) return; semaphore = new Semaphore(0); // post a shutdown and wait mQueue.add(new Scheduled(this, new Runnable() { @Override public void run() { shutdownEverything(currentSelector); semaphore.release(); } }, 0)); synchronousWorkers.execute(() -> { try { currentSelector.wakeupOnce(); } catch (Exception e) { } }); // force any existing connections to die shutdownKeys(currentSelector); mQueue = new PriorityQueue<>(1, Scheduler.INSTANCE); mSelector = null; mAffinity = null; } try { if (!isAffinityThread && wait) semaphore.acquire(); } catch (Exception e) { } } protected void onDataReceived(int transmitted) { } protected void onDataSent(int transmitted) { } private static class ObjectHolder { T held; } public AsyncServerSocket listen(final InetAddress host, final int port, final ListenCallback handler) { final ObjectHolder holder = new ObjectHolder<>(); run(new Runnable() { @Override public void run() { ServerSocketChannel closeableServer = null; ServerSocketChannelWrapper closeableWrapper = null; try { closeableServer = ServerSocketChannel.open(); closeableWrapper = new ServerSocketChannelWrapper( closeableServer); final ServerSocketChannel server = closeableServer; final ServerSocketChannelWrapper wrapper = closeableWrapper; InetSocketAddress isa; if (host == null) isa = new InetSocketAddress(port); else isa = new InetSocketAddress(host, port); server.socket().bind(isa); final SelectionKey key = wrapper.register(mSelector.getSelector()); key.attach(handler); handler.onListening(holder.held = new AsyncServerSocket() { @Override public int getLocalPort() { return server.socket().getLocalPort(); } @Override public void stop() { StreamUtility.closeQuietly(wrapper); try { key.cancel(); } catch (Exception e) { } } }); } catch (IOException e) { Log.e(LOGTAG, "wtf", e); StreamUtility.closeQuietly(closeableWrapper, closeableServer); handler.onCompleted(e); } } }); return holder.held; } private class ConnectFuture extends SimpleFuture { @Override protected void cancelCleanup() { super.cancelCleanup(); try { if (socket != null) socket.close(); } catch (IOException e) { } } SocketChannel socket; ConnectCallback callback; } public Cancellable connectResolvedInetSocketAddress(final InetSocketAddress address, final ConnectCallback callback) { return connectResolvedInetSocketAddress(address, callback, null); } public ConnectFuture connectResolvedInetSocketAddress(final InetSocketAddress address, final ConnectCallback callback, final SocketCreateCallback createCallback) { final ConnectFuture cancel = new ConnectFuture(); assert !address.isUnresolved(); post(new Runnable() { @Override public void run() { if (cancel.isCancelled()) return; cancel.callback = callback; SelectionKey ckey = null; SocketChannel socket = null; try { socket = cancel.socket = SocketChannel.open(); socket.configureBlocking(false); ckey = socket.register(mSelector.getSelector(), SelectionKey.OP_CONNECT); ckey.attach(cancel); if (createCallback != null) createCallback.onSocketCreated(socket.socket().getLocalPort()); socket.connect(address); } catch (Throwable e) { if (ckey != null) ckey.cancel(); StreamUtility.closeQuietly(socket); cancel.setComplete(new RuntimeException(e)); } } }); return cancel; } public Cancellable connectSocket(final InetSocketAddress remote, final ConnectCallback callback) { if (!remote.isUnresolved()) return connectResolvedInetSocketAddress(remote, callback); final SimpleFuture ret = new SimpleFuture(); Future lookup = getByName(remote.getHostName()); ret.setParent(lookup); lookup .setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, InetAddress result) { if (e != null) { callback.onConnectCompleted(e, null); ret.setComplete(e); return; } ret.setComplete((ConnectFuture)connectResolvedInetSocketAddress(new InetSocketAddress(result, remote.getPort()), callback)); } }); return ret; } public Cancellable connectSocket(final String host, final int port, final ConnectCallback callback) { return connectSocket(InetSocketAddress.createUnresolved(host, port), callback); } private static ExecutorService newSynchronousWorkers(String prefix) { ThreadFactory tf = new NamedThreadFactory(prefix); ThreadPoolExecutor tpe = new ThreadPoolExecutor(1, 4, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(), tf); return tpe; } private static final Comparator ipSorter = new Comparator() { @Override public int compare(InetAddress lhs, InetAddress rhs) { if (lhs instanceof Inet4Address && rhs instanceof Inet4Address) return 0; if (lhs instanceof Inet6Address && rhs instanceof Inet6Address) return 0; if (lhs instanceof Inet4Address && rhs instanceof Inet6Address) return -1; return 1; } }; private static ExecutorService synchronousResolverWorkers = newSynchronousWorkers("AsyncServer-resolver-"); public Future getAllByName(final String host) { final SimpleFuture ret = new SimpleFuture(); synchronousResolverWorkers.execute(new Runnable() { @Override public void run() { try { final InetAddress[] result = InetAddress.getAllByName(host); Arrays.sort(result, ipSorter); if (result == null || result.length == 0) throw new HostnameResolutionException("no addresses for host"); post(new Runnable() { @Override public void run() { ret.setComplete(null, result); } }); } catch (final Exception e) { post(new Runnable() { @Override public void run() { ret.setComplete(e, null); } }); } } }); return ret; } public Future getByName(String host) { return getAllByName(host).thenConvert(addresses -> addresses[0]); } private void handleSocket(final AsyncNetworkSocket handler) throws ClosedChannelException { final ChannelWrapper sc = handler.getChannel(); SelectionKey ckey = sc.register(mSelector.getSelector()); ckey.attach(handler); handler.setup(this, ckey); } public AsyncDatagramSocket connectDatagram(final String host, final int port) throws IOException { final DatagramChannel socket = DatagramChannel.open(); final AsyncDatagramSocket handler = new AsyncDatagramSocket(); handler.attach(socket); // ugh.. this should really be post to make it nonblocking... // but i want datagrams to be immediately writable. // they're not really used anyways. run(new Runnable() { @Override public void run() { try { final SocketAddress remote = new InetSocketAddress(host, port); handleSocket(handler); socket.connect(remote); } catch (IOException e) { Log.e(LOGTAG, "Datagram error", e); StreamUtility.closeQuietly(socket); } } }); return handler; } public AsyncDatagramSocket openDatagram() { return openDatagram(null, 0, false); } public Cancellable createDatagram(String address, int port, boolean reuseAddress, FutureCallback callback) { return createDatagram(() -> InetAddress.getByName(address), port, reuseAddress, callback); } public Cancellable createDatagram(InetAddress address, int port, boolean reuseAddress, FutureCallback callback) { return createDatagram(() -> address, port, reuseAddress, callback); } private Cancellable createDatagram(ValueFunction inetAddressValueFunction, final int port, final boolean reuseAddress, FutureCallback callback) { SimpleFuture ret = new SimpleFuture<>(); ret.setCallback(callback); post(() -> { DatagramChannel socket = null; try { socket = DatagramChannel.open(); final AsyncDatagramSocket handler = new AsyncDatagramSocket(); handler.attach(socket); InetSocketAddress address; if (inetAddressValueFunction == null) address = new InetSocketAddress(port); else address = new InetSocketAddress(inetAddressValueFunction.getValue(), port); if (reuseAddress) socket.socket().setReuseAddress(reuseAddress); socket.socket().bind(address); handleSocket(handler); if (!ret.setComplete(handler)) socket.close(); } catch (Exception e) { StreamUtility.closeQuietly(socket); ret.setComplete(e); } }); return ret; } public AsyncDatagramSocket openDatagram(final InetAddress host, final int port, final boolean reuseAddress) { final AsyncDatagramSocket handler = new AsyncDatagramSocket(); // ugh.. this should really be post to make it nonblocking... // but i want datagrams to be immediately writable. // they're not really used anyways. Runnable runnable = () -> { final DatagramChannel socket; try { socket = DatagramChannel.open(); } catch (Exception e) { return; } try { handler.attach(socket); InetSocketAddress address; if (host == null) address = new InetSocketAddress(port); else address = new InetSocketAddress(host, port); if (reuseAddress) socket.socket().setReuseAddress(reuseAddress); socket.socket().bind(address); handleSocket(handler); } catch (IOException e) { Log.e(LOGTAG, "Datagram error", e); StreamUtility.closeQuietly(socket); } }; if (getAffinity() != Thread.currentThread()) { run(runnable); return handler; } runnable.run(); return handler; } public AsyncDatagramSocket connectDatagram(final SocketAddress remote) throws IOException { final AsyncDatagramSocket handler = new AsyncDatagramSocket(); final DatagramChannel socket = DatagramChannel.open(); handler.attach(socket); // ugh.. this should really be post to make it nonblocking... // but i want datagrams to be immediately writable. // they're not really used anyways. Runnable runnable = () -> { try { handleSocket(handler); socket.connect(remote); } catch (IOException e) { StreamUtility.closeQuietly(socket); } }; if (getAffinity() != Thread.currentThread()) { run(runnable); return handler; } runnable.run(); return handler; } final private static ThreadLocal threadServer = new ThreadLocal<>(); public static AsyncServer getCurrentThreadServer() { return threadServer.get(); } Thread mAffinity; private void run() { final SelectorWrapper selector; final PriorityQueue queue; synchronized (this) { if (mSelector == null) { try { selector = mSelector = new SelectorWrapper(SelectorProvider.provider().openSelector()); queue = mQueue; } catch (IOException e) { throw new RuntimeException("unable to create selector?", e); } mAffinity = new Thread(mName) { public void run() { try { threadServer.set(AsyncServer.this); AsyncServer.run(AsyncServer.this, selector, queue); } finally { threadServer.remove(); } } }; mAffinity.start(); // kicked off the new thread, let's bail. return; } // this is a reentrant call selector = mSelector; queue = mQueue; // fall through to outside of the synchronization scope // to allow the thread to run without locking. } try { runLoop(this, selector, queue); } catch (AsyncSelectorException e) { Log.i(LOGTAG, "Selector closed", e); try { // StreamUtility.closeQuiety is throwing ArrayStoreException? selector.getSelector().close(); } catch (Exception ex) { } } } private static void run(final AsyncServer server, final SelectorWrapper selector, final PriorityQueue queue) { // Log.i(LOGTAG, "****AsyncServer is starting.****"); // at this point, this local queue and selector are owned // by this thread. // if a stop is called, the instance queue and selector // will be replaced and nulled respectively. // this will allow the old queue and selector to shut down // gracefully, while also allowing a new selector thread // to start up while the old one is still shutting down. while(true) { try { runLoop(server, selector, queue); } catch (AsyncSelectorException e) { if (!(e.getCause() instanceof ClosedSelectorException)) Log.i(LOGTAG, "Selector exception, shutting down", e); StreamUtility.closeQuietly(selector); } // see if we keep looping, this must be in a synchronized block since the queue is accessed. synchronized (server) { if (selector.isOpen() && (selector.keys().size() > 0 || queue.size() > 0)) continue; shutdownEverything(selector); if (server.mSelector == selector) { server.mQueue = new PriorityQueue(1, Scheduler.INSTANCE); server.mSelector = null; server.mAffinity = null; } break; } } // Log.i(LOGTAG, "****AsyncServer has shut down.****"); } private static void shutdownKeys(SelectorWrapper selector) { try { for (SelectionKey key: selector.keys()) { StreamUtility.closeQuietly(key.channel()); try { key.cancel(); } catch (Exception e) { } } } catch (Exception ex) { } } private static void shutdownEverything(SelectorWrapper selector) { shutdownKeys(selector); // SHUT. DOWN. EVERYTHING. StreamUtility.closeQuietly(selector); } private static final long QUEUE_EMPTY = Long.MAX_VALUE; private static long lockAndRunQueue(final AsyncServer server, final PriorityQueue queue) { long wait = QUEUE_EMPTY; // find the first item we can actually run while (true) { Scheduled run = null; synchronized (server) { long now = SystemClock.elapsedRealtime(); if (queue.size() > 0) { Scheduled s = queue.remove(); if (s.time <= now) { run = s; } else { wait = s.time - now; queue.add(s); } } } if (run == null) break; run.run(); } server.postCounter = 0; return wait; } private static class AsyncSelectorException extends IOException { public AsyncSelectorException(Exception e) { super(e); } } private static void runLoop(final AsyncServer server, final SelectorWrapper selector, final PriorityQueue queue) throws AsyncSelectorException { // Log.i(LOGTAG, "Keys: " + selector.keys().size()); boolean needsSelect = true; // run the queue to populate the selector with keys long wait = lockAndRunQueue(server, queue); try { synchronized (server) { // select now to see if anything is ready immediately. this // also clears the canceled key queue. int readyNow = selector.selectNow(); if (readyNow == 0) { // if there is nothing to select now, make sure we don't have an empty key set // which means it would be time to turn this thread off. if (selector.keys().size() == 0 && wait == QUEUE_EMPTY) { // Log.i(LOGTAG, "Shutting down. keys: " + selector.keys().size() + " keepRunning: " + keepRunning); return; } } else { needsSelect = false; } } if (needsSelect) { if (wait == QUEUE_EMPTY) { // wait until woken up selector.select(); } else { // nothing to select immediately but there's something pending so let's block that duration and wait. selector.select(wait); } } } catch (Exception e) { throw new AsyncSelectorException(e); } // process whatever keys are ready Set readyKeys = selector.selectedKeys(); for (SelectionKey key: readyKeys) { try { if (key.isAcceptable()) { ServerSocketChannel nextReady = (ServerSocketChannel) key.channel(); SocketChannel sc = null; SelectionKey ckey = null; try { sc = nextReady.accept(); if (sc == null) continue; sc.configureBlocking(false); ckey = sc.register(selector.getSelector(), SelectionKey.OP_READ); ListenCallback serverHandler = (ListenCallback) key.attachment(); AsyncNetworkSocket handler = new AsyncNetworkSocket(); handler.attach(sc, (InetSocketAddress)sc.socket().getRemoteSocketAddress()); handler.setup(server, ckey); ckey.attach(handler); serverHandler.onAccepted(handler); } catch (IOException e) { StreamUtility.closeQuietly(sc); if (ckey != null) ckey.cancel(); } } else if (key.isReadable()) { AsyncNetworkSocket handler = (AsyncNetworkSocket) key.attachment(); int transmitted = handler.onReadable(); server.onDataReceived(transmitted); } else if (key.isWritable()) { AsyncNetworkSocket handler = (AsyncNetworkSocket) key.attachment(); handler.onDataWritable(); } else if (key.isConnectable()) { ConnectFuture cancel = (ConnectFuture) key.attachment(); SocketChannel sc = (SocketChannel) key.channel(); key.interestOps(SelectionKey.OP_READ); AsyncNetworkSocket newHandler; try { sc.finishConnect(); newHandler = new AsyncNetworkSocket(); newHandler.setup(server, key); newHandler.attach(sc, (InetSocketAddress)sc.socket().getRemoteSocketAddress()); key.attach(newHandler); } catch (IOException ex) { key.cancel(); StreamUtility.closeQuietly(sc); if (cancel.setComplete(ex)) cancel.callback.onConnectCompleted(ex, null); continue; } if (cancel.setComplete(newHandler)) cancel.callback.onConnectCompleted(null, newHandler); } else { Log.i(LOGTAG, "wtf"); throw new RuntimeException("Unknown key state."); } } catch (CancelledKeyException ex) { } } readyKeys.clear(); } public void dump() { post(new Runnable() { @Override public void run() { if (mSelector == null) { Log.i(LOGTAG, "Server dump not possible. No selector?"); return; } Log.i(LOGTAG, "Key Count: " + mSelector.keys().size()); for (SelectionKey key: mSelector.keys()) { Log.i(LOGTAG, "Key: " + key); } } }); } public Thread getAffinity() { return mAffinity; } public boolean isAffinityThread() { return mAffinity == Thread.currentThread(); } public boolean isAffinityThreadOrStopped() { Thread affinity = mAffinity; return affinity == null || affinity == Thread.currentThread(); } private static class NamedThreadFactory implements ThreadFactory { private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; NamedThreadFactory(String namePrefix) { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); this.namePrefix = namePrefix; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) { t.setPriority(Thread.NORM_PRIORITY); } return t; } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/AsyncServerSocket.java ================================================ package com.jeffmony.async; public interface AsyncServerSocket { void stop(); int getLocalPort(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/AsyncSocket.java ================================================ package com.jeffmony.async; public interface AsyncSocket extends DataEmitter, DataSink { AsyncServer getServer(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/BufferedDataSink.java ================================================ package com.jeffmony.async; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.WritableCallback; public class BufferedDataSink implements DataSink { DataSink mDataSink; public BufferedDataSink(DataSink datasink) { setDataSink(datasink); } public boolean isBuffering() { return mPendingWrites.hasRemaining() || forceBuffering; } public boolean isWritable() { synchronized (mPendingWrites) { return mPendingWrites.remaining() < mMaxBuffer; } } public DataSink getDataSink() { return mDataSink; } boolean forceBuffering; public void forceBuffering(boolean forceBuffering) { this.forceBuffering = forceBuffering; if (!forceBuffering) writePending(); } public void setDataSink(DataSink datasink) { mDataSink = datasink; mDataSink.setWriteableCallback(this::writePending); } private void writePending() { if (forceBuffering) return; // Log.i("NIO", "Writing to buffer..."); boolean empty; synchronized (mPendingWrites) { mDataSink.write(mPendingWrites); empty = mPendingWrites.isEmpty(); } if (empty) { if (endPending) mDataSink.end(); } if (empty && mWritable != null) mWritable.onWriteable(); } final ByteBufferList mPendingWrites = new ByteBufferList(); // before the data is queued, let inheritors know. allows for filters, without // issues with having to filter before writing which may fail in the buffer. protected void onDataAccepted(ByteBufferList bb) { } @Override public void write(final ByteBufferList bb) { if (getServer().getAffinity() != Thread.currentThread()) { synchronized (mPendingWrites) { if (mPendingWrites.remaining() >= mMaxBuffer) return; onDataAccepted(bb); bb.get(mPendingWrites); } getServer().post(this::writePending); return; } onDataAccepted(bb); if (!isBuffering()) mDataSink.write(bb); synchronized (mPendingWrites) { bb.get(mPendingWrites); } } WritableCallback mWritable; @Override public void setWriteableCallback(WritableCallback handler) { mWritable = handler; } @Override public WritableCallback getWriteableCallback() { return mWritable; } public int remaining() { return mPendingWrites.remaining(); } int mMaxBuffer = Integer.MAX_VALUE; public int getMaxBuffer() { return mMaxBuffer; } public void setMaxBuffer(int maxBuffer) { assert maxBuffer >= 0; mMaxBuffer = maxBuffer; } @Override public boolean isOpen() { return mDataSink.isOpen(); } boolean endPending; @Override public void end() { if (getServer().getAffinity() != Thread.currentThread()) { getServer().post(this::end); return; } synchronized (mPendingWrites) { if (mPendingWrites.hasRemaining()) { endPending = true; return; } } mDataSink.end(); } @Override public void setClosedCallback(CompletedCallback handler) { mDataSink.setClosedCallback(handler); } @Override public CompletedCallback getClosedCallback() { return mDataSink.getClosedCallback(); } @Override public AsyncServer getServer() { return mDataSink.getServer(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/ByteBufferList.java ================================================ package com.jeffmony.async; import android.annotation.TargetApi; import android.os.Build; import android.os.Looper; import com.jeffmony.async.util.ArrayDeque; import com.jeffmony.async.util.Charsets; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.Charset; import java.util.Comparator; import java.util.PriorityQueue; @TargetApi(Build.VERSION_CODES.GINGERBREAD) public class ByteBufferList { ArrayDeque mBuffers = new ArrayDeque(); ByteOrder order = ByteOrder.BIG_ENDIAN; public ByteOrder order() { return order; } public ByteBufferList order(ByteOrder order) { this.order = order; return this; } public ByteBufferList() { } public ByteBufferList(ByteBuffer... b) { addAll(b); } public ByteBufferList(byte[] buf) { super(); ByteBuffer b = ByteBuffer.wrap(buf); add(b); } public ByteBufferList addAll(ByteBuffer... bb) { for (ByteBuffer b: bb) add(b); return this; } public ByteBufferList addAll(ByteBufferList... bb) { for (ByteBufferList b: bb) b.get(this); return this; } public byte[] getBytes(int length) { byte[] ret = new byte[length]; get(ret); return ret; } public byte[] getAllByteArray() { byte[] ret = new byte[remaining()]; get(ret); return ret; } public ByteBuffer[] getAllArray() { ByteBuffer[] ret = new ByteBuffer[mBuffers.size()]; ret = mBuffers.toArray(ret); mBuffers.clear(); remaining = 0; return ret; } public boolean isEmpty() { return remaining == 0; } private int remaining = 0; public int remaining() { return remaining; } public boolean hasRemaining() { return remaining() > 0; } public short peekShort() { return read(2).getShort(mBuffers.peekFirst().position()); } public byte peek() { return read(1).get(mBuffers.peekFirst().position()); } public int peekInt() { return read(4).getInt(mBuffers.peekFirst().position()); } public long peekLong() { return read(8).getLong(mBuffers.peekFirst().position()); } public byte[] peekBytes(int size) { byte[] ret = new byte[size]; read(size).get(ret, mBuffers.peekFirst().position(), ret.length); return ret; } public ByteBufferList skip(int length) { get(null, 0, length); return this; } public int getInt() { int ret = read(4).getInt(); remaining -= 4; return ret; } public char getByteChar() { char ret = (char)read(1).get(); remaining--; return ret; } public short getShort() { short ret = read(2).getShort(); remaining -= 2; return ret; } public byte get() { byte ret = read(1).get(); remaining--; return ret; } public long getLong() { long ret = read(8).getLong(); remaining -= 8; return ret; } public void get(byte[] bytes) { get(bytes, 0, bytes.length); } public void get(byte[] bytes, int offset, int length) { if (remaining() < length) throw new IllegalArgumentException("length"); int need = length; while (need > 0) { ByteBuffer b = mBuffers.peek(); int read = Math.min(b.remaining(), need); if (bytes != null){ b.get(bytes, offset, read); } else { //when bytes is null, just skip data. b.position(b.position() + read); } need -= read; offset += read; if (b.remaining() == 0) { ByteBuffer removed = mBuffers.remove(); assert b == removed; reclaim(b); } } remaining -= length; } public void get(ByteBufferList into, int length) { if (remaining() < length) throw new IllegalArgumentException("length"); int offset = 0; while (offset < length) { ByteBuffer b = mBuffers.remove(); int remaining = b.remaining(); if (remaining == 0) { reclaim(b); continue; } if (offset + remaining > length) { int need = length - offset; // this is shared between both ByteBuffer subset = obtain(need); subset.limit(need); b.get(subset.array(), 0, need); into.add(subset); mBuffers.addFirst(b); assert subset.capacity() >= need; assert subset.position() == 0; break; } else { // this belongs to the new list into.add(b); } offset += remaining; } remaining -= length; } public void get(ByteBufferList into) { get(into, remaining()); } public ByteBufferList get(int length) { ByteBufferList ret = new ByteBufferList(); get(ret, length); return ret.order(order); } public ByteBuffer getAll() { if (remaining() == 0) return EMPTY_BYTEBUFFER; read(remaining()); return remove(); } private ByteBuffer read(int count) { if (remaining() < count) throw new IllegalArgumentException("count : " + remaining() + "/" + count); ByteBuffer first = mBuffers.peek(); while (first != null && !first.hasRemaining()) { reclaim(mBuffers.remove()); first = mBuffers.peek(); } if (first == null) { return EMPTY_BYTEBUFFER; } if (first.remaining() >= count) { return first.order(order); } ByteBuffer ret = obtain(count); ret.limit(count); byte[] bytes = ret.array(); int offset = 0; ByteBuffer bb = null; while (offset < count) { bb = mBuffers.remove(); int toRead = Math.min(count - offset, bb.remaining()); bb.get(bytes, offset, toRead); offset += toRead; if (bb.remaining() == 0) { reclaim(bb); bb = null; } } // if there was still data left in the last buffer we popped // toss it back into the head if (bb != null && bb.remaining() > 0) mBuffers.addFirst(bb); mBuffers.addFirst(ret); return ret.order(order); } public void trim() { // this clears out buffers that are empty in the beginning of the list read(0); } public ByteBufferList add(ByteBufferList b) { b.get(this); return this; } public ByteBufferList add(ByteBuffer b) { if (b.remaining() <= 0) { // System.out.println("reclaiming remaining: " + b.remaining()); reclaim(b); return this; } addRemaining(b.remaining()); // see if we can fit the entirety of the buffer into the end // of the current last buffer if (mBuffers.size() > 0) { ByteBuffer last = mBuffers.getLast(); if (last.capacity() - last.limit() >= b.remaining()) { last.mark(); last.position(last.limit()); last.limit(last.capacity()); last.put(b); last.limit(last.position()); last.reset(); reclaim(b); trim(); return this; } } mBuffers.add(b); trim(); return this; } public void addFirst(ByteBuffer b) { if (b.remaining() <= 0) { reclaim(b); return; } addRemaining(b.remaining()); // see if we can fit the entirety of the buffer into the beginning // of the current first buffer if (mBuffers.size() > 0) { ByteBuffer first = mBuffers.getFirst(); if (first.position() >= b.remaining()) { first.position(first.position() - b.remaining()); first.mark(); first.put(b); first.reset(); reclaim(b); return; } } mBuffers.addFirst(b); } private void addRemaining(int remaining) { if (this.remaining() >= 0) this.remaining += remaining; } public void recycle() { while (mBuffers.size() > 0) { reclaim(mBuffers.remove()); } assert mBuffers.size() == 0; remaining = 0; } public ByteBuffer remove() { ByteBuffer ret = mBuffers.remove(); remaining -= ret.remaining(); return ret; } public int size() { return mBuffers.size(); } public void spewString() { System.out.println(peekString()); } public String peekString() { return peekString(null); } // not doing toString as this is really nasty in the debugger... public String peekString(Charset charset) { if (charset == null) charset = Charsets.UTF_8; StringBuilder builder = new StringBuilder(); for (ByteBuffer bb: mBuffers) { byte[] bytes; int offset; int length; if (bb.isDirect()) { bytes = new byte[bb.remaining()]; offset = 0; length = bb.remaining(); bb.get(bytes); } else { bytes = bb.array(); offset = bb.arrayOffset() + bb.position(); length = bb.remaining(); } builder.append(new String(bytes, offset, length, charset)); } return builder.toString(); } public String readString() { return readString(null); } public String readString(Charset charset) { String ret = peekString(charset); recycle(); return ret; } static class Reclaimer implements Comparator { @Override public int compare(ByteBuffer byteBuffer, ByteBuffer byteBuffer2) { // keep the smaller ones at the head, so they get tossed out quicker if (byteBuffer.capacity() == byteBuffer2.capacity()) return 0; if (byteBuffer.capacity() > byteBuffer2.capacity()) return 1; return -1; } } static PriorityQueue reclaimed = new PriorityQueue(8, new Reclaimer()); private static PriorityQueue getReclaimed() { Looper mainLooper = Looper.getMainLooper(); if (mainLooper != null) { if (Thread.currentThread() == mainLooper.getThread()) return null; } return reclaimed; } private static int MAX_SIZE = 1024 * 1024; public static int MAX_ITEM_SIZE = 1024 * 256; static int currentSize = 0; static int maxItem = 0; public static void setMaxPoolSize(int size) { MAX_SIZE = size; } public static void setMaxItemSize(int size) { MAX_ITEM_SIZE = size; } private static boolean reclaimedContains(ByteBuffer b) { for (ByteBuffer other: reclaimed) { if (other == b) return true; } return false; } public static void reclaim(ByteBuffer b) { if (b == null || b.isDirect()) return; if (b.arrayOffset() != 0 || b.array().length != b.capacity()) return; if (b.capacity() < 8192) return; if (b.capacity() > MAX_ITEM_SIZE) return; PriorityQueue r = getReclaimed(); if (r == null) return; synchronized (LOCK) { while (currentSize > MAX_SIZE && r.size() > 0 && r.peek().capacity() < b.capacity()) { // System.out.println("removing for better: " + b.capacity()); ByteBuffer head = r.remove(); currentSize -= head.capacity(); } if (currentSize > MAX_SIZE) { // System.out.println("too full: " + b.capacity()); return; } assert !reclaimedContains(b); b.position(0); b.limit(b.capacity()); currentSize += b.capacity(); r.add(b); assert r.size() != 0 ^ currentSize == 0; maxItem = Math.max(maxItem, b.capacity()); } } private static final Object LOCK = new Object(); public static ByteBuffer obtain(int size) { if (size <= maxItem) { PriorityQueue r = getReclaimed(); if (r != null) { synchronized (LOCK) { while (r.size() > 0) { ByteBuffer ret = r.remove(); if (r.size() == 0) maxItem = 0; currentSize -= ret.capacity(); assert r.size() != 0 ^ currentSize == 0; if (ret.capacity() >= size) { // System.out.println("using " + ret.capacity()); return ret; } // System.out.println("dumping " + ret.capacity()); } } } } // System.out.println("alloc for " + size); ByteBuffer ret = ByteBuffer.allocate(Math.max(8192, size)); return ret; } public static void obtainArray(ByteBuffer[] arr, int size) { PriorityQueue r = getReclaimed(); int index = 0; int total = 0; if (r != null) { synchronized (LOCK) { while (r.size() > 0 && total < size && index < arr.length - 1) { ByteBuffer b = r.remove(); currentSize -= b.capacity(); assert r.size() != 0 ^ currentSize == 0; int needed = Math.min(size - total, b.capacity()); total += needed; arr[index++] = b; } } } if (total < size) { ByteBuffer b = ByteBuffer.allocate(Math.max(8192, size - total)); arr[index++] = b; } for (int i = index; i < arr.length; i++) { arr[i] = EMPTY_BYTEBUFFER; } } public static ByteBuffer deepCopy(ByteBuffer copyOf) { if (copyOf == null) return null; return (ByteBuffer)obtain(copyOf.remaining()).put(copyOf.duplicate()).flip(); } public static final ByteBuffer EMPTY_BYTEBUFFER = ByteBuffer.allocate(0); public static void writeOutputStream(OutputStream out, ByteBuffer b) throws IOException { byte[] bytes; int offset; int length; if (b.isDirect()) { bytes = new byte[b.remaining()]; offset = 0; length = b.remaining(); b.get(bytes); } else { bytes = b.array(); offset = b.arrayOffset() + b.position(); length = b.remaining(); } out.write(bytes, offset, length); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/ChannelWrapper.java ================================================ package com.jeffmony.async; import java.io.IOException; import java.net.InetAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.ReadableByteChannel; import java.nio.channels.ScatteringByteChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.spi.AbstractSelectableChannel; abstract class ChannelWrapper implements ReadableByteChannel, ScatteringByteChannel { private AbstractSelectableChannel mChannel; ChannelWrapper(AbstractSelectableChannel channel) throws IOException { channel.configureBlocking(false); mChannel = channel; } public abstract void shutdownInput(); public abstract void shutdownOutput(); public abstract boolean isConnected(); public abstract int write(ByteBuffer src) throws IOException; public abstract int write(ByteBuffer[] src) throws IOException; // register for default events appropriate for this channel public abstract SelectionKey register(Selector sel) throws ClosedChannelException; public SelectionKey register(Selector sel, int ops) throws ClosedChannelException { return mChannel.register(sel, ops); } public boolean isChunked() { return false; } @Override public boolean isOpen() { return mChannel.isOpen(); } @Override public void close() throws IOException { mChannel.close(); } public abstract int getLocalPort(); public abstract InetAddress getLocalAddress(); public abstract Object getSocket(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/DataEmitter.java ================================================ package com.jeffmony.async; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.DataCallback; public interface DataEmitter { void setDataCallback(DataCallback callback); DataCallback getDataCallback(); boolean isChunked(); void pause(); void resume(); void close(); boolean isPaused(); void setEndCallback(CompletedCallback callback); CompletedCallback getEndCallback(); AsyncServer getServer(); String charset(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/DataEmitterBase.java ================================================ package com.jeffmony.async; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.DataCallback; /** * Created by koush on 5/27/13. */ public abstract class DataEmitterBase implements DataEmitter { private boolean ended; protected void report(Exception e) { if (ended) return; ended = true; if (getEndCallback() != null) getEndCallback().onCompleted(e); } @Override public final void setEndCallback(CompletedCallback callback) { endCallback = callback; } CompletedCallback endCallback; @Override public final CompletedCallback getEndCallback() { return endCallback; } DataCallback mDataCallback; @Override public void setDataCallback(DataCallback callback) { mDataCallback = callback; } @Override public DataCallback getDataCallback() { return mDataCallback; } @Override public String charset() { return null; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/DataEmitterReader.java ================================================ package com.jeffmony.async; import com.jeffmony.async.callback.DataCallback; public class DataEmitterReader implements DataCallback { DataCallback mPendingRead; int mPendingReadLength; ByteBufferList mPendingData = new ByteBufferList(); public void read(int count, DataCallback callback) { assert mPendingRead == null; mPendingReadLength = count; mPendingRead = callback; assert !mPendingData.hasRemaining(); mPendingData.recycle(); } private boolean handlePendingData(DataEmitter emitter) { if (mPendingReadLength > mPendingData.remaining()) return false; DataCallback pendingRead = mPendingRead; mPendingRead = null; pendingRead.onDataAvailable(emitter, mPendingData); assert !mPendingData.hasRemaining(); return true; } public DataEmitterReader() { } @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { // if we're registered for data, we must be waiting for a read assert mPendingRead != null; do { int need = Math.min(bb.remaining(), mPendingReadLength - mPendingData.remaining()); bb.get(mPendingData, need); bb.remaining(); } while (handlePendingData(emitter) && mPendingRead != null); bb.remaining(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/DataSink.java ================================================ package com.jeffmony.async; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.WritableCallback; public interface DataSink { public void write(ByteBufferList bb); public void setWriteableCallback(WritableCallback handler); public WritableCallback getWriteableCallback(); public boolean isOpen(); public void end(); public void setClosedCallback(CompletedCallback handler); public CompletedCallback getClosedCallback(); public AsyncServer getServer(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/DataTrackingEmitter.java ================================================ package com.jeffmony.async; /** * Created by koush on 5/28/13. */ public interface DataTrackingEmitter extends DataEmitter { interface DataTracker { void onData(int totalBytesRead); } void setDataTracker(DataTracker tracker); DataTracker getDataTracker(); int getBytesRead(); void setDataEmitter(DataEmitter emitter); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/DatagramChannelWrapper.java ================================================ package com.jeffmony.async; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; class DatagramChannelWrapper extends ChannelWrapper { DatagramChannel mChannel; @Override public InetAddress getLocalAddress() { return mChannel.socket().getLocalAddress(); } @Override public int getLocalPort() { return mChannel.socket().getLocalPort(); } InetSocketAddress address; public InetSocketAddress getRemoteAddress() { return address; } public void disconnect() throws IOException { mChannel.disconnect(); } DatagramChannelWrapper(DatagramChannel channel) throws IOException { super(channel); mChannel = channel; } @Override public int read(ByteBuffer buffer) throws IOException { if (!isConnected()) { int position = buffer.position(); address = (InetSocketAddress)mChannel.receive(buffer); if (address == null) return -1; return buffer.position() - position; } address = null; return mChannel.read(buffer); } @Override public boolean isConnected() { return mChannel.isConnected(); } @Override public int write(ByteBuffer src) throws IOException { return mChannel.write(src); } @Override public int write(ByteBuffer[] src) throws IOException { return (int)mChannel.write(src); } @Override public SelectionKey register(Selector sel, int ops) throws ClosedChannelException { return mChannel.register(sel, ops); } @Override public boolean isChunked() { return true; } @Override public SelectionKey register(Selector sel) throws ClosedChannelException { return register(sel, SelectionKey.OP_READ); } @Override public void shutdownOutput() { } @Override public void shutdownInput() { } @Override public long read(ByteBuffer[] byteBuffers) throws IOException { return mChannel.read(byteBuffers); } @Override public long read(ByteBuffer[] byteBuffers, int i, int i2) throws IOException { return mChannel.read(byteBuffers, i, i2); } @Override public Object getSocket() { return mChannel.socket(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/FileDataEmitter.java ================================================ package com.jeffmony.async; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.util.StreamUtility; import java.io.File; import java.io.FileInputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; /** * Created by koush on 5/22/13. */ public class FileDataEmitter extends DataEmitterBase { AsyncServer server; File file; public FileDataEmitter(AsyncServer server, File file) { this.server = server; this.file = file; paused = !server.isAffinityThread(); if (!paused) doResume(); } DataCallback callback; @Override public void setDataCallback(DataCallback callback) { this.callback = callback; } @Override public DataCallback getDataCallback() { return callback; } @Override public boolean isChunked() { return false; } boolean paused; @Override public void pause() { paused = true; } @Override public void resume() { paused = false; doResume(); } @Override protected void report(Exception e) { StreamUtility.closeQuietly(channel); super.report(e); } ByteBufferList pending = new ByteBufferList(); FileChannel channel; Runnable pumper = new Runnable() { @Override public void run() { try { if (channel == null) channel = new FileInputStream(file).getChannel(); if (!pending.isEmpty()) { Util.emitAllData(FileDataEmitter.this, pending); if (!pending.isEmpty()) return; } ByteBuffer b; do { b = ByteBufferList.obtain(8192); if (-1 == channel.read(b)) { report(null); return; } b.flip(); pending.add(b); Util.emitAllData(FileDataEmitter.this, pending); } while (pending.remaining() == 0 && !isPaused()); } catch (Exception e) { report(e); } } }; private void doResume() { server.post(pumper); } @Override public boolean isPaused() { return paused; } @Override public AsyncServer getServer() { return server; } @Override public void close() { try { channel.close(); } catch (Exception e) { } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/FilteredDataEmitter.java ================================================ package com.jeffmony.async; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.wrapper.DataEmitterWrapper; public class FilteredDataEmitter extends DataEmitterBase implements DataEmitter, DataCallback, DataEmitterWrapper, DataTrackingEmitter { private DataEmitter mEmitter; @Override public DataEmitter getDataEmitter() { return mEmitter; } @Override public void setDataEmitter(DataEmitter emitter) { if (mEmitter != null) { mEmitter.setDataCallback(null); } mEmitter = emitter; mEmitter.setDataCallback(this); mEmitter.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { report(ex); } }); } @Override public int getBytesRead() { return totalRead; } @Override public DataTracker getDataTracker() { return tracker; } @Override public void setDataTracker(DataTracker tracker) { this.tracker = tracker; } private DataTracker tracker; private int totalRead; @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { if (closed) { // this emitter was closed but for some reason data is still being spewed... // eat it, nom nom. bb.recycle(); return; } if (bb != null) totalRead += bb.remaining(); Util.emitAllData(this, bb); if (bb != null) totalRead -= bb.remaining(); if (tracker != null && bb != null) tracker.onData(totalRead); // if there's data after the emitting, and it is paused... the underlying implementation // is obligated to cache the byte buffer list. } @Override public boolean isChunked() { return mEmitter.isChunked(); } @Override public void pause() { mEmitter.pause(); } @Override public void resume() { mEmitter.resume(); } @Override public boolean isPaused() { return mEmitter.isPaused(); } @Override public AsyncServer getServer() { return mEmitter.getServer(); } boolean closed; @Override public void close() { closed = true; if (mEmitter != null) mEmitter.close(); } @Override public String charset() { if (mEmitter == null) return null; return mEmitter.charset(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/FilteredDataSink.java ================================================ package com.jeffmony.async; public class FilteredDataSink extends BufferedDataSink { public FilteredDataSink(DataSink sink) { super(sink); setMaxBuffer(0); } public ByteBufferList filter(ByteBufferList bb) { return bb; } @Override protected void onDataAccepted(ByteBufferList bb) { ByteBufferList filtered = filter(bb); // filtering may return the same byte buffer, so watch for that. if (filtered != bb) { bb.recycle(); filtered.get(bb); } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/HostnameResolutionException.java ================================================ package com.jeffmony.async; public class HostnameResolutionException extends Exception { public HostnameResolutionException(String message) { super(message); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/LineEmitter.java ================================================ package com.jeffmony.async; import com.jeffmony.async.callback.DataCallback; import java.nio.ByteBuffer; import java.nio.charset.Charset; public class LineEmitter implements DataCallback { public interface StringCallback { void onStringAvailable(String s); } public LineEmitter() { this(null); } public LineEmitter(Charset charset) { this.charset = charset; } Charset charset; ByteBufferList data = new ByteBufferList(); StringCallback mLineCallback; public void setLineCallback(StringCallback callback) { mLineCallback = callback; } public StringCallback getLineCallback() { return mLineCallback; } @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { ByteBuffer buffer = ByteBuffer.allocate(bb.remaining()); while (bb.remaining() > 0) { byte b = bb.get(); if (b == '\n') { assert mLineCallback != null; buffer.flip(); data.add(buffer); mLineCallback.onStringAvailable(data.readString(charset)); data = new ByteBufferList(); return; } else { buffer.put(b); } } buffer.flip(); data.add(buffer); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/PushParser.java ================================================ package com.jeffmony.async; import android.util.Log; import com.jeffmony.async.callback.DataCallback; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Hashtable; import java.util.LinkedList; public class PushParser implements DataCallback { public interface ParseCallback { public void parsed(T data); } static abstract class Waiter { int length; public Waiter(int length) { this.length = length; } /** * Consumes received data, and/or returns next waiter to continue reading instead of this waiter. * @param bb received data, bb.remaining >= length * @return - a waiter that should continue reading right away, or null if this waiter is finished */ public abstract Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb); } static class IntWaiter extends Waiter { ParseCallback callback; public IntWaiter(ParseCallback callback) { super(4); this.callback = callback; } @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { callback.parsed(bb.getInt()); return null; } } static class ByteArrayWaiter extends Waiter { ParseCallback callback; public ByteArrayWaiter(int length, ParseCallback callback) { super(length); if (length <= 0) throw new IllegalArgumentException("length should be > 0"); this.callback = callback; } @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { byte[] bytes = new byte[length]; bb.get(bytes); callback.parsed(bytes); return null; } } static class LenByteArrayWaiter extends Waiter { private final ParseCallback callback; public LenByteArrayWaiter(ParseCallback callback) { super(4); this.callback = callback; } @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { int length = bb.getInt(); if (length == 0) { callback.parsed(new byte[0]); return null; } return new ByteArrayWaiter(length, callback); } } static class ByteBufferListWaiter extends Waiter { ParseCallback callback; public ByteBufferListWaiter(int length, ParseCallback callback) { super(length); if (length <= 0) throw new IllegalArgumentException("length should be > 0"); this.callback = callback; } @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { callback.parsed(bb.get(length)); return null; } } static class LenByteBufferListWaiter extends Waiter { private final ParseCallback callback; public LenByteBufferListWaiter(ParseCallback callback) { super(4); this.callback = callback; } @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { int length = bb.getInt(); return new ByteBufferListWaiter(length, callback); } } static class UntilWaiter extends Waiter { byte value; DataCallback callback; public UntilWaiter(byte value, DataCallback callback) { super(1); this.value = value; this.callback = callback; } @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { boolean found = true; ByteBufferList cb = new ByteBufferList(); while (bb.size() > 0) { ByteBuffer b = bb.remove(); b.mark(); int index = 0; while (b.remaining() > 0 && !(found = (b.get() == value))) { index++; } b.reset(); if (found) { bb.addFirst(b); bb.get(cb, index); // eat the one we're waiting on bb.get(); break; } else { cb.add(b); } } callback.onDataAvailable(emitter, cb); if (found) { return null; } else { return this; } } } private class TapWaiter extends Waiter { private final TapCallback callback; public TapWaiter(TapCallback callback) { super(0); this.callback = callback; } @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { Method method = getTap(callback); method.setAccessible(true); try { method.invoke(callback, args.toArray()); } catch (Exception e) { Log.e("PushParser", "Error while invoking tap callback", e); } args.clear(); return null; } } private Waiter noopArgWaiter = new Waiter(0) { @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { args.add(null); return null; } }; private Waiter byteArgWaiter = new Waiter(1) { @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { args.add(bb.get()); return null; } }; private Waiter shortArgWaiter = new Waiter(2) { @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { args.add(bb.getShort()); return null; } }; private Waiter intArgWaiter = new Waiter(4) { @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { args.add(bb.getInt()); return null; } }; private Waiter longArgWaiter = new Waiter(8) { @Override public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) { args.add(bb.getLong()); return null; } }; private ParseCallback byteArrayArgCallback = new ParseCallback() { @Override public void parsed(byte[] data) { args.add(data); } }; private ParseCallback byteBufferListArgCallback = new ParseCallback() { @Override public void parsed(ByteBufferList data) { args.add(data); } }; private ParseCallback stringArgCallback = new ParseCallback() { @Override public void parsed(byte[] data) { args.add(new String(data)); } }; DataEmitter mEmitter; private LinkedList mWaiting = new LinkedList(); private ArrayList args = new ArrayList(); ByteOrder order = ByteOrder.BIG_ENDIAN; public PushParser setOrder(ByteOrder order) { this.order = order; return this; } public PushParser(DataEmitter s) { mEmitter = s; mEmitter.setDataCallback(this); } public PushParser readInt(ParseCallback callback) { mWaiting.add(new IntWaiter(callback)); return this; } public PushParser readByteArray(int length, ParseCallback callback) { mWaiting.add(new ByteArrayWaiter(length, callback)); return this; } public PushParser readByteBufferList(int length, ParseCallback callback) { mWaiting.add(new ByteBufferListWaiter(length, callback)); return this; } public PushParser until(byte b, DataCallback callback) { mWaiting.add(new UntilWaiter(b, callback)); return this; } public PushParser readByte() { mWaiting.add(byteArgWaiter); return this; } public PushParser readShort() { mWaiting.add(shortArgWaiter); return this; } public PushParser readInt() { mWaiting.add(intArgWaiter); return this; } public PushParser readLong() { mWaiting.add(longArgWaiter); return this; } public PushParser readByteArray(int length) { return (length == -1) ? readLenByteArray() : readByteArray(length, byteArrayArgCallback); } public PushParser readLenByteArray() { mWaiting.add(new LenByteArrayWaiter(byteArrayArgCallback)); return this; } public PushParser readByteBufferList(int length) { return (length == -1) ? readLenByteBufferList() : readByteBufferList(length, byteBufferListArgCallback); } public PushParser readLenByteBufferList() { return readLenByteBufferList(byteBufferListArgCallback); } public PushParser readLenByteBufferList(ParseCallback callback) { mWaiting.add(new LenByteBufferListWaiter(callback)); return this; } public PushParser readString() { mWaiting.add(new LenByteArrayWaiter(stringArgCallback)); return this; } public PushParser noop() { mWaiting.add(noopArgWaiter); return this; } ByteBufferList pending = new ByteBufferList(); @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { bb.get(pending); while (mWaiting.size() > 0 && pending.remaining() >= mWaiting.peek().length) { pending.order(order); Waiter next = mWaiting.poll().onDataAvailable(emitter, pending); if (next != null) mWaiting.addFirst(next); } if (mWaiting.size() == 0) pending.get(bb); } public void tap(TapCallback callback) { mWaiting.add(new TapWaiter(callback)); } static Hashtable mTable = new Hashtable(); static Method getTap(TapCallback callback) { Method found = mTable.get(callback.getClass()); if (found != null) return found; for (Method method : callback.getClass().getMethods()) { if ("tap".equals(method.getName())) { mTable.put(callback.getClass(), method); return method; } } // try the proguard friendly route, take the first/only method // in case "tap" has been renamed Method[] candidates = callback.getClass().getDeclaredMethods(); if (candidates.length == 1) return candidates[0]; String fail = "-keep class * extends com.koushikdutta.async.TapCallback {\n" + " *;\n" + "}\n"; //null != "AndroidAsync: tap callback could not be found. Proguard? Use this in your proguard config:\n" + fail; throw new AssertionError(fail); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/SelectorWrapper.java ================================================ package com.jeffmony.async; import java.io.Closeable; import java.io.IOException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.util.Set; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * Created by koush on 2/13/14. */ class SelectorWrapper implements Closeable { private Selector selector; public AtomicBoolean isWaking = new AtomicBoolean(false); Semaphore semaphore = new Semaphore(0); public Selector getSelector() { return selector; } public SelectorWrapper(Selector selector) { this.selector = selector; } public int selectNow() throws IOException { return selector.selectNow(); } public void select() throws IOException { select(0); } public void select(long timeout) throws IOException { try { semaphore.drainPermits(); selector.select(timeout); } finally { semaphore.release(Integer.MAX_VALUE); } } public Set keys() { return selector.keys(); } public Set selectedKeys() { return selector.selectedKeys(); } @Override public void close() throws IOException { selector.close(); } public boolean isOpen() { return selector.isOpen(); } public void wakeupOnce() { // see if it is selecting, ie, can't acquire a permit boolean selecting = !semaphore.tryAcquire(); selector.wakeup(); // if it was selecting, then the wakeup definitely worked. if (selecting) return; // now, we NEED to wait for the select to start to forcibly wake it. if (isWaking.getAndSet(true)) { selector.wakeup(); return; } try { waitForSelect(); selector.wakeup(); } finally { isWaking.set(false); } } public boolean waitForSelect() { // try to wake up 10 times for (int i = 0; i < 100; i++) { try { if (semaphore.tryAcquire(10, TimeUnit.MILLISECONDS)) { // successfully acquiring means the selector is NOT selecting, since select // will drain all permits. continue; } } catch (InterruptedException e) { // an InterruptedException means the acquire failed a select is in progress, // since it holds all permits return true; } } return false; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/ServerSocketChannelWrapper.java ================================================ package com.jeffmony.async; import java.io.IOException; import java.net.InetAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; class ServerSocketChannelWrapper extends ChannelWrapper { ServerSocketChannel mChannel; @Override public void shutdownOutput() { } @Override public void shutdownInput() { } @Override public InetAddress getLocalAddress() { return mChannel.socket().getInetAddress(); } @Override public int getLocalPort() { return mChannel.socket().getLocalPort(); } ServerSocketChannelWrapper(ServerSocketChannel channel) throws IOException { super(channel); mChannel = channel; } @Override public int read(ByteBuffer buffer) throws IOException { final String msg = "Can't read ServerSocketChannel"; assert false; throw new IOException(msg); } @Override public boolean isConnected() { assert false; return false; } @Override public int write(ByteBuffer src) throws IOException { final String msg = "Can't write ServerSocketChannel"; assert false; throw new IOException(msg); } @Override public SelectionKey register(Selector sel) throws ClosedChannelException { return mChannel.register(sel, SelectionKey.OP_ACCEPT); } @Override public int write(ByteBuffer[] src) throws IOException { final String msg = "Can't write ServerSocketChannel"; assert false; throw new IOException(msg); } @Override public long read(ByteBuffer[] byteBuffers) throws IOException { final String msg = "Can't read ServerSocketChannel"; assert false; throw new IOException(msg); } @Override public long read(ByteBuffer[] byteBuffers, int i, int i2) throws IOException { final String msg = "Can't read ServerSocketChannel"; assert false; throw new IOException(msg); } @Override public Object getSocket() { return mChannel.socket(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/SocketChannelWrapper.java ================================================ package com.jeffmony.async; import java.io.IOException; import java.net.InetAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; class SocketChannelWrapper extends ChannelWrapper { SocketChannel mChannel; @Override public InetAddress getLocalAddress() { return mChannel.socket().getLocalAddress(); } @Override public int getLocalPort() { return mChannel.socket().getLocalPort(); } SocketChannelWrapper(SocketChannel channel) throws IOException { super(channel); mChannel = channel; } @Override public int read(ByteBuffer buffer) throws IOException { return mChannel.read(buffer); } @Override public boolean isConnected() { return mChannel.isConnected(); } @Override public int write(ByteBuffer src) throws IOException { return mChannel.write(src); } @Override public int write(ByteBuffer[] src) throws IOException { return (int)mChannel.write(src); } @Override public SelectionKey register(Selector sel) throws ClosedChannelException { return register(sel, SelectionKey.OP_CONNECT); } @Override public void shutdownOutput() { try { mChannel.socket().shutdownOutput(); } catch (Exception e) { } } @Override public void shutdownInput() { try { mChannel.socket().shutdownInput(); } catch (Exception e) { } } @Override public long read(ByteBuffer[] byteBuffers) throws IOException { return mChannel.read(byteBuffers); } @Override public long read(ByteBuffer[] byteBuffers, int i, int i2) throws IOException { return mChannel.read(byteBuffers, i, i2); } @Override public Object getSocket() { return mChannel.socket(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/TapCallback.java ================================================ package com.jeffmony.async; public interface TapCallback { } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/ThreadQueue.java ================================================ package com.jeffmony.async; import java.util.LinkedList; import java.util.WeakHashMap; import java.util.concurrent.Semaphore; class ThreadQueue extends LinkedList { final private static WeakHashMap mThreadQueues = new WeakHashMap(); static ThreadQueue getOrCreateThreadQueue(Thread thread) { ThreadQueue queue; synchronized (mThreadQueues) { queue = mThreadQueues.get(thread); if (queue == null) { queue = new ThreadQueue(); mThreadQueues.put(thread, queue); } } return queue; } static void release(AsyncSemaphore semaphore) { synchronized (mThreadQueues) { for (ThreadQueue threadQueue: mThreadQueues.values()) { if (threadQueue.waiter == semaphore) threadQueue.queueSemaphore.release(); } } } AsyncSemaphore waiter; Semaphore queueSemaphore = new Semaphore(0); @Override public boolean add(Runnable object) { synchronized (this) { return super.add(object); } } @Override public boolean remove(Object object) { synchronized (this) { return super.remove(object); } } @Override public Runnable remove() { synchronized (this) { if (this.isEmpty()) return null; return super.remove(); } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/Util.java ================================================ package com.jeffmony.async; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.callback.WritableCallback; import com.jeffmony.async.util.Allocator; import com.jeffmony.async.util.StreamUtility; import com.jeffmony.async.wrapper.AsyncSocketWrapper; import com.jeffmony.async.wrapper.DataEmitterWrapper; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; public class Util { public static boolean SUPRESS_DEBUG_EXCEPTIONS = false; public static void emitAllData(DataEmitter emitter, ByteBufferList list) { int remaining; DataCallback handler = null; while (!emitter.isPaused() && (handler = emitter.getDataCallback()) != null && (remaining = list.remaining()) > 0) { handler.onDataAvailable(emitter, list); if (remaining == list.remaining() && handler == emitter.getDataCallback() && !emitter.isPaused()) { // this is generally indicative of failure... // 1) The data callback has not changed // 2) no data was consumed // 3) the data emitter was not paused // call byteBufferList.recycle() or read all the data to prevent this assertion. // this is nice to have, as it identifies protocol or parsing errors. // System.out.println("Data: " + list.peekString()); System.out.println("handler: " + handler); list.recycle(); if (SUPRESS_DEBUG_EXCEPTIONS) return; assert false; throw new RuntimeException("mDataHandler failed to consume data, yet remains the mDataHandler."); } } if (list.remaining() != 0 && !emitter.isPaused()) { // not all the data was consumed... // call byteBufferList.recycle() or read all the data to prevent this assertion. // this is nice to have, as it identifies protocol or parsing errors. // System.out.println("Data: " + list.peekString()); System.out.println("handler: " + handler); System.out.println("emitter: " + emitter); list.recycle(); if (SUPRESS_DEBUG_EXCEPTIONS) return; // assert false; // throw new AssertionError("Not all data was consumed by Util.emitAllData"); } } public static void pump(final InputStream is, final DataSink ds, final CompletedCallback callback) { pump(is, Integer.MAX_VALUE, ds, callback); } public static void pump(final InputStream is, final long max, final DataSink ds, final CompletedCallback callback) { final CompletedCallback wrapper = new CompletedCallback() { boolean reported; @Override public void onCompleted(Exception ex) { if (reported) return; reported = true; callback.onCompleted(ex); } }; final WritableCallback cb = new WritableCallback() { int totalRead = 0; private void cleanup() { ds.setClosedCallback(null); ds.setWriteableCallback(null); pending.recycle(); StreamUtility.closeQuietly(is); } ByteBufferList pending = new ByteBufferList(); Allocator allocator = new Allocator().setMinAlloc((int) Math.min(2 << 19, max)); @Override public void onWriteable() { try { do { if (!pending.hasRemaining()) { ByteBuffer b = allocator.allocate(); long toRead = Math.min(max - totalRead, b.capacity()); int read = is.read(b.array(), 0, (int)toRead); if (read == -1 || totalRead == max) { cleanup(); wrapper.onCompleted(null); return; } allocator.track(read); totalRead += read; b.position(0); b.limit(read); pending.add(b); } ds.write(pending); } while (!pending.hasRemaining()); } catch (Exception e) { cleanup(); wrapper.onCompleted(e); } } }; ds.setWriteableCallback(cb); ds.setClosedCallback(wrapper); cb.onWriteable(); } public static void pump(final DataEmitter emitter, final DataSink sink, final CompletedCallback callback) { final DataCallback dataCallback = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { sink.write(bb); if (bb.remaining() > 0) emitter.pause(); } }; emitter.setDataCallback(dataCallback); sink.setWriteableCallback(new WritableCallback() { @Override public void onWriteable() { emitter.resume(); } }); final CompletedCallback wrapper = new CompletedCallback() { boolean reported; @Override public void onCompleted(Exception ex) { if (reported) return; reported = true; emitter.setDataCallback(null); emitter.setEndCallback(null); sink.setClosedCallback(null); sink.setWriteableCallback(null); callback.onCompleted(ex); } }; emitter.setEndCallback(wrapper); sink.setClosedCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (ex == null) ex = new IOException("sink was closed before emitter ended"); wrapper.onCompleted(ex); } }); } public static void stream(AsyncSocket s1, AsyncSocket s2, CompletedCallback callback) { pump(s1, s2, callback); pump(s2, s1, callback); } public static void pump(final File file, final DataSink ds, final CompletedCallback callback) { try { if (file == null || ds == null) { callback.onCompleted(null); return; } final InputStream is = new FileInputStream(file); pump(is, ds, new CompletedCallback() { @Override public void onCompleted(Exception ex) { try { is.close(); callback.onCompleted(ex); } catch (IOException e) { callback.onCompleted(e); } } }); } catch (Exception e) { callback.onCompleted(e); } } public static void writeAll(final DataSink sink, final ByteBufferList bb, final CompletedCallback callback) { WritableCallback wc; sink.setWriteableCallback(wc = new WritableCallback() { @Override public void onWriteable() { sink.write(bb); if (bb.remaining() == 0 && callback != null) { sink.setWriteableCallback(null); callback.onCompleted(null); } } }); wc.onWriteable(); } public static void writeAll(DataSink sink, byte[] bytes, CompletedCallback callback) { ByteBuffer bb = ByteBufferList.obtain(bytes.length); bb.put(bytes); bb.flip(); ByteBufferList bbl = new ByteBufferList(); bbl.add(bb); writeAll(sink, bbl, callback); } public static T getWrappedSocket(AsyncSocket socket, Class wrappedClass) { if (wrappedClass.isInstance(socket)) return (T)socket; while (socket instanceof AsyncSocketWrapper) { socket = ((AsyncSocketWrapper)socket).getSocket(); if (wrappedClass.isInstance(socket)) return (T)socket; } return null; } public static DataEmitter getWrappedDataEmitter(DataEmitter emitter, Class wrappedClass) { if (wrappedClass.isInstance(emitter)) return emitter; while (emitter instanceof DataEmitterWrapper) { emitter = ((AsyncSocketWrapper)emitter).getSocket(); if (wrappedClass.isInstance(emitter)) return emitter; } return null; } public static void end(DataEmitter emitter, Exception e) { if (emitter == null) return; end(emitter.getEndCallback(), e); } public static void end(CompletedCallback end, Exception e) { if (end != null) end.onCompleted(e); } public static void writable(DataSink emitter) { if (emitter == null) return; writable(emitter.getWriteableCallback()); } public static void writable(WritableCallback writable) { if (writable != null) writable.onWriteable(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/ZipDataSink.java ================================================ package com.jeffmony.async; import com.jeffmony.async.callback.CompletedCallback; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; public class ZipDataSink extends FilteredDataSink { public ZipDataSink(DataSink sink) { super(sink); } ByteArrayOutputStream bout = new ByteArrayOutputStream(); ZipOutputStream zop = new ZipOutputStream(bout); public void putNextEntry(ZipEntry ze) throws IOException { zop.putNextEntry(ze); } public void closeEntry() throws IOException { zop.closeEntry(); } protected void report(Exception e) { CompletedCallback closed = getClosedCallback(); if (closed != null) closed.onCompleted(e); } @Override public void end() { try { zop.close(); } catch (IOException e) { report(e); return; } setMaxBuffer(Integer.MAX_VALUE); write(new ByteBufferList()); super.end(); } @Override public ByteBufferList filter(ByteBufferList bb) { try { if (bb != null) { while (bb.size() > 0) { ByteBuffer b = bb.remove(); ByteBufferList.writeOutputStream(zop, b); ByteBufferList.reclaim(b); } } ByteBufferList ret = new ByteBufferList(bout.toByteArray()); bout.reset(); return ret; } catch (IOException e) { report(e); return null; } finally { if (bb != null) bb.recycle(); } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/callback/CompletedCallback.java ================================================ package com.jeffmony.async.callback; public interface CompletedCallback { class NullCompletedCallback implements CompletedCallback { @Override public void onCompleted(Exception ex) { } } public void onCompleted(Exception ex); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/callback/ConnectCallback.java ================================================ package com.jeffmony.async.callback; import com.jeffmony.async.AsyncSocket; public interface ConnectCallback { void onConnectCompleted(Exception ex, AsyncSocket socket); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/callback/ContinuationCallback.java ================================================ package com.jeffmony.async.callback; import com.jeffmony.async.future.Continuation; public interface ContinuationCallback { void onContinue(Continuation continuation, CompletedCallback next) throws Exception; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/callback/DataCallback.java ================================================ package com.jeffmony.async.callback; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; public interface DataCallback { class NullDataCallback implements DataCallback { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { bb.recycle(); } } void onDataAvailable(DataEmitter emitter, ByteBufferList bb); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/callback/ListenCallback.java ================================================ package com.jeffmony.async.callback; import com.jeffmony.async.AsyncServerSocket; import com.jeffmony.async.AsyncSocket; public interface ListenCallback extends CompletedCallback { void onAccepted(AsyncSocket socket); void onListening(AsyncServerSocket socket); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/callback/ResultCallback.java ================================================ package com.jeffmony.async.callback; public interface ResultCallback { public void onCompleted(Exception e, S source, T result); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/callback/SocketCreateCallback.java ================================================ package com.jeffmony.async.callback; public interface SocketCreateCallback { void onSocketCreated(int localPort); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/callback/ValueCallback.java ================================================ package com.jeffmony.async.callback; /** * Created by koush on 7/5/16. */ public interface ValueCallback { void onResult(T value); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/callback/ValueFunction.java ================================================ package com.jeffmony.async.callback; public interface ValueFunction { T getValue() throws Exception; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/callback/WritableCallback.java ================================================ package com.jeffmony.async.callback; public interface WritableCallback { public void onWriteable(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/dns/Dns.java ================================================ package com.jeffmony.async.dns; import com.jeffmony.async.AsyncDatagramSocket; import com.jeffmony.async.AsyncServer; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.future.Cancellable; import com.jeffmony.async.future.Future; import com.jeffmony.async.future.FutureCallback; import com.jeffmony.async.future.SimpleFuture; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Random; /** * Created by koush on 10/20/13. */ public class Dns { public static Future lookup(String host) { return lookup(AsyncServer.getDefault(), host, false, null); } private static int setFlag(int flags, int value, int offset) { return flags | (value << offset); } private static int setQuery(int flags) { return setFlag(flags, 0, 0); } private static int setRecursion(int flags) { return setFlag(flags, 1, 8); } private static void addName(ByteBuffer bb, String name) { String[] parts = name.split("\\."); for (String part: parts) { bb.put((byte)part.length()); bb.put(part.getBytes()); } bb.put((byte)0); } public static Future lookup(AsyncServer server, String host) { return lookup(server, host, false, null); } public static Cancellable multicastLookup(AsyncServer server, String host, FutureCallback callback) { return lookup(server, host, true, callback); } public static Cancellable multicastLookup(String host, FutureCallback callback) { return multicastLookup(AsyncServer.getDefault(), host, callback); } public static Future lookup(AsyncServer server, String host, final boolean multicast, final FutureCallback callback) { if (!server.isAffinityThread()) { SimpleFuture ret = new SimpleFuture<>(); server.post(() -> ret.setComplete(lookup(server, host, multicast, callback))); return ret; } ByteBuffer packet = ByteBufferList.obtain(1024).order(ByteOrder.BIG_ENDIAN); short id = (short)new Random().nextInt(); short flags = (short)setQuery(0); if (!multicast) flags = (short)setRecursion(flags); packet.putShort(id); packet.putShort(flags); // number questions packet.putShort(multicast ? (short)1 : (short)2); // number answer rr packet.putShort((short)0); // number authority rr packet.putShort((short)0); // number additional rr packet.putShort((short)0); addName(packet, host); // query packet.putShort(multicast ? (short)12 : (short)1); // request internet address packet.putShort((short)1); if (!multicast) { addName(packet, host); // AAAA query packet.putShort((short) 28); // request internet address packet.putShort((short)1); } packet.flip(); try { final AsyncDatagramSocket dgram; // todo, use the dns server... if (!multicast) { dgram = server.connectDatagram(new InetSocketAddress("8.8.8.8", 53)); } else { // System.out.println("multicast dns..."); dgram = AsyncServer.getDefault().openDatagram(null, 0, true); Field field = DatagramSocket.class.getDeclaredField("impl"); field.setAccessible(true); Object impl = field.get(dgram.getSocket()); Method method = impl.getClass().getDeclaredMethod("join", InetAddress.class); method.setAccessible(true); method.invoke(impl, InetAddress.getByName("224.0.0.251")); ((DatagramSocket)dgram.getSocket()).setBroadcast(true); } final SimpleFuture ret = new SimpleFuture() { @Override protected void cleanup() { super.cleanup(); // System.out.println("multicast dns cleanup..."); dgram.close(); } }; dgram.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { try { // System.out.println(dgram.getRemoteAddress()); DnsResponse response = DnsResponse.parse(bb); // System.out.println(response); response.source = dgram.getRemoteAddress(); if (!multicast) { dgram.close(); ret.setComplete(response); } else { callback.onCompleted(null, response); } } catch (Exception e) { } bb.recycle(); } }); if (!multicast) dgram.write(new ByteBufferList(packet)); else dgram.send(new InetSocketAddress("224.0.0.251", 5353), packet); return ret; } catch (Exception e) { SimpleFuture ret = new SimpleFuture(); ret.setComplete(e); if (multicast) callback.onCompleted(e, null); return ret; } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/dns/DnsResponse.java ================================================ package com.jeffmony.async.dns; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.http.Multimap; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; /** * Created by koush on 10/20/13. */ public class DnsResponse { public ArrayList addresses = new ArrayList(); public ArrayList names = new ArrayList(); public Multimap txt = new Multimap(); public InetSocketAddress source; private static String parseName(ByteBufferList bb, ByteBuffer backReference) { bb.order(ByteOrder.BIG_ENDIAN); String ret = ""; int len; while (0 != (len = bb.get() & 0x00FF)) { // compressed if ((len & 0x00c0) == 0x00c0) { int offset = ((len & ~0xFFFFFFc0) << 8) | (bb.get() & 0x00FF); if (ret.length() > 0) ret += "."; ByteBufferList sub = new ByteBufferList(); ByteBuffer duplicate = backReference.duplicate(); duplicate.get(new byte[offset]); sub.add(duplicate); return ret + parseName(sub, backReference); } byte[] bytes = new byte[len]; bb.get(bytes); if (ret.length() > 0) ret += "."; ret += new String(bytes); } return ret; } public static DnsResponse parse(ByteBufferList bb) { ByteBuffer b = bb.getAll(); bb.add(b.duplicate()); // naive parsing... bb.order(ByteOrder.BIG_ENDIAN); // id bb.getShort(); // flags bb.getShort(); // number questions int questions = bb.getShort(); // number answer rr int answers = bb.getShort(); // number authority rr int authorities = bb.getShort(); // number additional rr int additionals = bb.getShort(); for (int i = 0; i < questions; i++) { parseName(bb, b); // type bb.getShort(); // class bb.getShort(); } DnsResponse response = new DnsResponse(); for (int i = 0; i < answers; i++) { String name = parseName(bb, b); // type int type = bb.getShort(); // class int clazz = bb.getShort(); // ttl int ttl = bb.getInt(); // length of address int length = bb.getShort(); try { if (type == 1) { // data byte[] data = new byte[length]; bb.get(data); response.addresses.add(InetAddress.getByAddress(data)); } else if (type == 0x000c) { response.names.add(parseName(bb, b)); } else if (type == 16) { ByteBufferList txt = new ByteBufferList(); bb.get(txt, length); response.parseTxt(txt); } else { bb.get(new byte[length]); } } catch (Exception e) { // e.printStackTrace(); } } // authorities for (int i = 0; i < authorities; i++) { String name = parseName(bb, b); // type int type = bb.getShort(); // class int clazz = bb.getShort(); // ttl int ttl = bb.getInt(); // length of address int length = bb.getShort(); try { bb.get(new byte[length]); } catch (Exception e) { // e.printStackTrace(); } } // additionals for (int i = 0; i < additionals; i++) { String name = parseName(bb, b); // type int type = bb.getShort(); // class int clazz = bb.getShort(); // ttl int ttl = bb.getInt(); // length of address int length = bb.getShort(); try { if (type == 16) { ByteBufferList txt = new ByteBufferList(); bb.get(txt, length); response.parseTxt(txt); } else { bb.get(new byte[length]); } } catch (Exception e) { // e.printStackTrace(); } } return response; } void parseTxt(ByteBufferList bb) { while (bb.hasRemaining()) { int length = (int)bb.get() & 0x00FF; byte [] bytes = new byte[length]; bb.get(bytes); String string = new String(bytes); String[] pair = string.split("="); txt.add(pair[0], pair[1]); } } @Override public String toString() { String ret = "addresses:\n"; for (InetAddress address: addresses) ret += address.toString() + "\n"; ret += "names:\n"; for (String name: names) ret += name + "\n"; return ret; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/Cancellable.java ================================================ package com.jeffmony.async.future; public interface Cancellable { /** * Check whether this asynchronous operation completed successfully. * @return */ boolean isDone(); /** * Check whether this asynchronous operation has been cancelled. * @return */ boolean isCancelled(); /** * Attempt to cancel this asynchronous operation. * @return The return value is whether the operation cancelled successfully. */ boolean cancel(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/Continuation.java ================================================ package com.jeffmony.async.future; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.ContinuationCallback; import java.util.LinkedList; public class Continuation extends SimpleCancellable implements ContinuationCallback, Runnable, Cancellable { CompletedCallback callback; Runnable cancelCallback; public CompletedCallback getCallback() { return callback; } public void setCallback(CompletedCallback callback) { this.callback = callback; } public Runnable getCancelCallback() { return cancelCallback; } public void setCancelCallback(Runnable cancelCallback) { this.cancelCallback = cancelCallback; } public void setCancelCallback(final Cancellable cancel) { if (cancel == null) { this.cancelCallback = null; return; } this.cancelCallback = new Runnable() { @Override public void run() { cancel.cancel(); } }; } public Continuation() { this(null); } public Continuation(CompletedCallback callback) { this(callback, null); } public Continuation(CompletedCallback callback, Runnable cancelCallback) { this.cancelCallback = cancelCallback; this.callback = callback; } private CompletedCallback wrap() { return new CompletedCallback() { boolean mThisCompleted; @Override public void onCompleted(Exception ex) { // onCompleted may be called more than once... buggy code. // only accept the first (timeouts, etc) if (mThisCompleted) return; mThisCompleted = true; assert waiting; waiting = false; if (ex == null) { next(); return; } reportCompleted(ex); } }; } void reportCompleted(Exception ex) { if (!setComplete()) return; if (callback != null) callback.onCompleted(ex); } LinkedList mCallbacks = new LinkedList(); private ContinuationCallback hook(ContinuationCallback callback) { if (callback instanceof DependentCancellable) { DependentCancellable child = (DependentCancellable)callback; child.setParent(this); } return callback; } public Continuation add(ContinuationCallback callback) { mCallbacks.add(hook(callback)); return this; } public Continuation insert(ContinuationCallback callback) { mCallbacks.add(0, hook(callback)); return this; } public Continuation add(final DependentFuture future) { future.setParent(this); add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { future.get(); next.onCompleted(null); } }); return this; } private boolean inNext; private boolean waiting; private void next() { if (inNext) return; while (mCallbacks.size() > 0 && !waiting && !isDone() && !isCancelled()) { ContinuationCallback cb = mCallbacks.remove(); try { inNext = true; waiting = true; cb.onContinue(this, wrap()); } catch (Exception e) { reportCompleted(e); } finally { inNext = false; } } if (waiting) return; if (isDone()) return; if (isCancelled()) return; reportCompleted(null); } @Override public boolean cancel() { if (!super.cancel()) return false; if (cancelCallback != null) cancelCallback.run(); return true; } boolean started; public Continuation start() { if (started) throw new IllegalStateException("already started"); started = true; next(); return this; } @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { setCallback(next); start(); } @Override public void run() { start(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/Converter.java ================================================ package com.jeffmony.async.future; import android.text.TextUtils; import com.jeffmony.async.ByteBufferList; import org.json.JSONObject; import java.io.InvalidObjectException; import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; public class Converter { public static Converter convert(Future future, String mime) { return new Converter<>(future, mime); } public static Converter convert(Future future) { return convert(future, null); } static class MimedData { public MimedData(T data, String mime) { this.data = data; this.mime = mime; } T data; String mime; } static class MultiTransformer extends MultiTransformFuture>, MimedData>> { TypeConverter converter; String converterMime; int distance; public MultiTransformer(TypeConverter converter, String converterMime, int distance) { this.converter = converter; this.converterMime = converterMime; this.distance = distance; } @Override protected void transform(MimedData> converting) { // transform will only ever be called once, and is called immediately, // the transform is on the future itself, and not a pending value. // so there's no risk of running the converter twice. final String mime = converting.mime; // this future will receive the eventual actual value. final MultiFuture converted = new MultiFuture<>(); // this marks the conversion as "complete". the conversion will start // as soon as the value is ready. setComplete(new MimedData<>(converted, mimeReplace(mime, converterMime))); // wait on the incoming value and convert it converting.data.thenConvert(data -> converter.convert(data, mime)). setCallback((e, result1) -> { if (e != null) converted.setComplete(e); else converted.setComplete(result1); }); } } static abstract class EnsureHashMap extends LinkedHashMap { synchronized V ensure(K k) { if (!containsKey(k)) { put(k, makeDefault()); } return get(k); } protected abstract V makeDefault(); } static class MimedType { MimedType(Class type, String mime) { this.type = type; this.mime = mime; } Class type; String mime; @Override public int hashCode() { return type.hashCode() ^ mime.hashCode(); } @Override public boolean equals(Object obj) { MimedType other = (MimedType)obj; return type.equals(other.type) && mime.equals(other.mime); } // check if this mimed type is the same or more specific than this mimed type public boolean isTypeOf(MimedType other) { // check the type, this type must be less specific than the other type if (!this.type.isAssignableFrom(other.type)) return false; return isTypeOf(other.mime); } public String primary() { return mime.split("/")[0]; } public String secondary() { return mime.split("/")[1]; } // check if this mimed type is convertible to another mimed type public boolean isTypeOf(String mime) { String[] otherParts = mime.split("/"); String[] myParts = this.mime.split("/"); // ensure the other type is the same OR this type is fine with a wildcard if (!"*".equals(myParts[0]) && !otherParts[0].equals(myParts[0])) return false; if (!"*".equals(myParts[1]) && !otherParts[1].equals(myParts[1])) return false; return true; } @Override public String toString() { return type.getSimpleName() + " " + mime; } } static class ConverterTransformers extends LinkedHashMap, MultiTransformer> { } static class Converters extends EnsureHashMap, ConverterTransformers> { @Override protected ConverterTransformers makeDefault() { return new ConverterTransformers(); } private static void add(ConverterTransformers set, ConverterTransformers more) { if (more == null) return; set.putAll(more); } public ConverterTransformers getAll(MimedType mimedType) { ConverterTransformers ret = new ConverterTransformers<>(); for (MimedType candidate: keySet()) { if (candidate.isTypeOf(mimedType)) add(ret, get(candidate)); } return ret; } } Converters outputs; protected ConverterEntries getConverters() { return new ConverterEntries(Converters); } MultiFuture future = new MultiFuture<>(); String futureMime; protected Converter(Future future, String mime) { if (TextUtils.isEmpty(mime)) mime = MIME_ALL; this.futureMime = mime; this.future.setComplete(future); } synchronized private final Future to(Object value, Class clazz, String mime) { if (clazz.isInstance(value)) return new SimpleFuture<>((T) value); return to(value.getClass(), clazz, mime); } synchronized private final Future to(Class fromClass, Class clazz, String mime) { if (TextUtils.isEmpty(mime)) mime = MIME_ALL; if (outputs == null) { outputs = new Converters<>(); ConverterEntries converters = getConverters(); for (ConverterEntry entry: converters.list) { outputs.ensure(entry.from).put(entry.to, new MultiTransformer<>(entry.typeConverter, entry.to.mime, entry.distance)); } } MimedType target = new MimedType<>(clazz, mime); ArrayDeque bestMatch = new ArrayDeque<>(); ArrayDeque currentPath = new ArrayDeque<>(); if (search(target, bestMatch, currentPath, new MimedType(fromClass, futureMime), new HashSet())) { PathInfo current = bestMatch.removeFirst(); new SimpleFuture<>(new MimedData<>((Future)future, futureMime)).setCallback(current.transformer); while (!bestMatch.isEmpty()) { PathInfo next = bestMatch.removeFirst(); current.transformer.setCallback(next.transformer); current = next; } return ((MultiTransformer)current.transformer).then(from -> from.data); } return new SimpleFuture<>(new InvalidObjectException("unable to find converter")); } static class PathInfo { MultiTransformer transformer; String mime; MimedType candidate; static int distance(ArrayDeque path) { int distance = 0; for (PathInfo entry: path) { distance += entry.transformer.distance; } return distance; } } static String mimeReplace(String mime1, String mime2) { String[] parts = mime2.split("/"); String[] myParts = mime1.split("/"); // a wildcard mime converter adopts the mime of the converted type String primary = !"*".equals(parts[0]) ? parts[0] : myParts[0]; String secondary = !"*".equals(parts[1]) ? parts[1] : myParts[1]; return primary + "/" + secondary; } public final Future to(Class clazz) { return to(clazz, null); } private boolean search(MimedType target, ArrayDeque bestMatch, ArrayDeque currentPath, MimedType currentSearch, HashSet searched) { if (target.isTypeOf(currentSearch)) { bestMatch.clear(); bestMatch.addAll(currentPath); return true; } // the current path must have potential to be better than the best match if (!bestMatch.isEmpty() && PathInfo.distance(currentPath) >= PathInfo.distance(bestMatch)) return false; // prevent reentrancy if (searched.contains(currentSearch)) return false; boolean found = false; searched.add(currentSearch); ConverterTransformers converterTransformers = outputs.getAll(currentSearch); for (MimedType candidate: converterTransformers.keySet()) { // this simulates the mime results of a transform MimedType newSearch = new MimedType(candidate.type, mimeReplace(currentSearch.mime, candidate.mime)); PathInfo path = new PathInfo(); path.transformer = converterTransformers.get(candidate); path.mime = newSearch.mime; path.candidate = candidate; currentPath.addLast(path); try { found |= search(target, bestMatch, currentPath, newSearch, searched); } finally { currentPath.removeLast(); } } if (found) { // if this resulted in a success, // clear this from the currentSearch list, because we know this leads // to a potential solution. maybe we can arrive here faster. searched.remove(currentSearch); } return found; } private static final String MIME_ALL = "*/*"; public Future to(Class clazz, String mime) { return future.then(from -> to(from, clazz, mime)); } static class ConverterEntry { ConverterEntry(Class from, String fromMime, Class to, String toMime, int distance, TypeConverter typeConverter) { this.from = new MimedType<>(from, fromMime); this.to = new MimedType<>(to, toMime); this.distance = distance; this.typeConverter = typeConverter; } MimedType from; MimedType to; int distance; TypeConverter typeConverter; @Override public int hashCode() { return from.hashCode() ^ to.hashCode(); } @Override public boolean equals(Object obj) { ConverterEntry other = (ConverterEntry)obj; return from.equals(other.from) && to.equals(other.to); } } public static class ConverterEntries { public ArrayList list = new ArrayList<>(); public ConverterEntries() { } public ConverterEntries(ConverterEntries other) { list.addAll(other.list); } public synchronized void addConverter(Class from, String fromMime, Class to, String toMime, TypeConverter typeConverter) { addConverter(from, fromMime, to, toMime, 1, typeConverter); } public synchronized void addConverter(Class from, String fromMime, Class to, String toMime, int distance, TypeConverter typeConverter) { if (TextUtils.isEmpty(fromMime)) fromMime = MIME_ALL; if (TextUtils.isEmpty(toMime)) toMime = MIME_ALL; list.add(new ConverterEntry<>(from, fromMime, to, toMime, distance, typeConverter)); } public synchronized boolean removeConverter(TypeConverter typeConverter) { for (ConverterEntry entry: list) { if (entry.typeConverter == typeConverter) return list.remove(entry); } return false; } } public final static ConverterEntries Converters = new ConverterEntries(); static { // ensure byte buffer operations are idempotent. do deep copies. final TypeConverter ByteArrayToByteBufferList = (from, fromMime) -> new SimpleFuture<>(new ByteBufferList(ByteBufferList.deepCopy(ByteBuffer.wrap(from)))); final TypeConverter ByteBufferListToByteArray = (from, fromMime) -> new SimpleFuture<>(from.getAllByteArray()); final TypeConverter ByteBufferListToByteBuffer = (from, fromMime) -> new SimpleFuture<>(from.getAll()); final TypeConverter ByteBufferListToString = (from, fromMime) -> new SimpleFuture<>(from.peekString()); final TypeConverter ByteArrayToByteBuffer = (from, fromMime) -> new SimpleFuture<>(ByteBufferList.deepCopy(ByteBuffer.wrap(from))); final TypeConverter ByteBufferToByteBufferList = (from, fromMime) -> new SimpleFuture<>(new ByteBufferList(ByteBufferList.deepCopy(from))); final TypeConverter StringToByteArray = (from, fromMime) -> new SimpleFuture<>(from.getBytes()); final TypeConverter StringToJSONObject = (from, fromMime) -> new SimpleFuture<>(from).thenConvert(JSONObject::new); final TypeConverter JSONObjectToString = (from, fromMime) -> new SimpleFuture<>(from).thenConvert(JSONObject::toString); final TypeConverter ByteArrayToString = (from, fromMime) -> new SimpleFuture<>(new String(from)); Converters.addConverter(ByteBuffer.class, null, ByteBufferList.class, null, ByteBufferToByteBufferList); Converters.addConverter(String.class, null, byte[].class, "text/plain", StringToByteArray); Converters.addConverter(byte[].class, null, ByteBufferList.class, null, ByteArrayToByteBufferList); Converters.addConverter(ByteBufferList.class, null, byte[].class, null, ByteBufferListToByteArray); Converters.addConverter(ByteBufferList.class, null, ByteBuffer.class, null, ByteBufferListToByteBuffer); Converters.addConverter(ByteBufferList.class, "text/plain", String.class, null, ByteBufferListToString); Converters.addConverter(byte[].class, null, ByteBuffer.class, null, ByteArrayToByteBuffer); Converters.addConverter(String.class, "application/json", JSONObject.class, null, StringToJSONObject); Converters.addConverter(JSONObject.class, null, String.class, "application/json", JSONObjectToString); Converters.addConverter(byte[].class, "text/plain", String.class, null, ByteArrayToString); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/DependentCancellable.java ================================================ package com.jeffmony.async.future; public interface DependentCancellable extends Cancellable { boolean setParent(Cancellable parent); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/DependentFuture.java ================================================ package com.jeffmony.async.future; public interface DependentFuture extends Future, DependentCancellable { } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/DoneCallback.java ================================================ package com.jeffmony.async.future; public interface DoneCallback { void done(Exception e, T result) throws Exception; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/FailCallback.java ================================================ package com.jeffmony.async.future; public interface FailCallback { /** * Callback that is invoked when a future completes with an error. * The error should be rethrown to pass it along. * @param e * @throws Exception */ void fail(Exception e) throws Exception; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/FailConvertCallback.java ================================================ package com.jeffmony.async.future; public interface FailConvertCallback { /** * Callback that is invoked when a future completes with an error. * The error should be rethrown, or a new value should be returned. * @param e * @return * @throws Exception */ T fail(Exception e) throws Exception; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/FailRecoverCallback.java ================================================ package com.jeffmony.async.future; public interface FailRecoverCallback { /** * Callback that is invoked when a future completes with an error. * The error should be rethrown, or a new future value should be returned. * @param e * @return * @throws Exception */ Future fail(Exception e) throws Exception; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/Future.java ================================================ package com.jeffmony.async.future; import android.os.Build; import androidx.annotation.RequiresApi; import java.util.concurrent.Executor; public interface Future extends Cancellable, java.util.concurrent.Future { /** * Set a callback to be invoked when this Future completes. * @param callback * @return */ void setCallback(FutureCallback callback); /** * Set a callback to be invoked when the Future completes * with an error or a result. * The existing error or result will be passed down the chain, or a new error * may be thrown. * @param done * @return */ Future done(DoneCallback done); /** * Set a callback to be invoked when this Future completes successfully. * @param callback * @return A future that will resolve once the success callback completes, * which may contain any errors thrown by the success callback. */ Future success(SuccessCallback callback); /** * Set a callback to be invoked when this Future completes successfully. * @param then * @param * @return A future containing all exceptions that happened prior or during * the callback, or the successful result. */ Future then(ThenFutureCallback then); /** * Set a callback to be invoked when this Future completes successfully. * @param then * @param * @return A future containing all exceptions that happened prior or during * the callback, or the successful result. */ Future thenConvert(ThenCallback then); /** * Set a callback to be invoked when this future completes with a failure. * The failure can be observered and rethrown, otherwise it is considered handled. * The exception will be nulled for subsequent callbacks in the chain. * @param fail * @return */ Future fail(FailCallback fail); /** * Set a callback to be invoked when this future completes with a failure. * The failure can be observered and rethrown, or handled by returning * a new fallback value of the same type. * @param fail * @return */ Future failConvert(FailConvertCallback fail); /** * Set a callback to be invoked when this future completes with a failure. * The failure should be observered and rethrown, or handled by returning * a new future of the same type. * @param fail * @return */ Future failRecover(FailRecoverCallback fail); /** * Get the result, if any. Returns null if still in progress. * @return */ T tryGet(); /** * Get the exception, if any. Returns null if still in progress. * @return */ Exception tryGetException(); /** * Get the result on the executor thread. * @param executor * @return */ @RequiresApi(api = Build.VERSION_CODES.N) default Future executorThread(Executor executor) { SimpleFuture ret = new SimpleFuture<>(); executor.execute(() -> ret.setComplete(Future.this)); return ret; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/FutureCallback.java ================================================ package com.jeffmony.async.future; /** * Created by koush on 5/20/13. */ public interface FutureCallback { /** * onCompleted is called by the Future with the result or exception of the asynchronous operation. * @param e Exception encountered by the operation * @param result Result returned from the operation */ void onCompleted(Exception e, T result); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/FutureRunnable.java ================================================ package com.jeffmony.async.future; /** * Created by koush on 12/22/13. */ public interface FutureRunnable { T run() throws Exception; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/FutureThread.java ================================================ package com.jeffmony.async.future; import java.util.concurrent.ExecutorService; /** * Created by koush on 12/22/13. */ public class FutureThread extends SimpleFuture { public FutureThread(final FutureRunnable runnable) { this(runnable, "FutureThread"); } public FutureThread(final ExecutorService pool, final FutureRunnable runnable) { pool.submit(new Runnable() { @Override public void run() { try { setComplete(runnable.run()); } catch (Exception e) { setComplete(e); } } }); } public FutureThread(final FutureRunnable runnable, String name) { new Thread(new Runnable() { @Override public void run() { try { setComplete(runnable.run()); } catch (Exception e) { setComplete(e); } } }, name).start(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/Futures.java ================================================ package com.jeffmony.async.future; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; public class Futures { public static Future> waitAll(final List> futures) { final ArrayList results = new ArrayList<>(); final SimpleFuture> ret = new SimpleFuture<>(); if (futures.isEmpty()) { ret.setComplete(results); return ret; } FutureCallback cb = new FutureCallback() { int count = 0; @Override public void onCompleted(Exception e, T result) { results.add(result); count++; if (count < futures.size()) futures.get(count).setCallback(this); else ret.setComplete(results); } }; futures.get(0).setCallback(cb); return ret; } public static Future> waitAll(final Future... futures) { return waitAll(Arrays.asList(futures)); } private static void loopUntil(final Iterator values, ThenFutureCallback callback, SimpleFuture ret, Exception lastException) { while (values.hasNext()) { try { callback.then(values.next()) .success(ret::setComplete) .fail(e -> loopUntil(values, callback, ret, e)); return; } catch (Exception e) { lastException = e; } } if (lastException == null) ret.setComplete(new Exception("empty list")); else ret.setComplete(lastException); } public static Future loopUntil(final Iterable values, ThenFutureCallback callback) { SimpleFuture ret = new SimpleFuture<>(); loopUntil(values.iterator(), callback, ret, null); return ret; } public static Future loopUntil(final F[] values, ThenFutureCallback callback) { return loopUntil(Arrays.asList(values), callback); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/HandlerFuture.java ================================================ package com.jeffmony.async.future; import android.os.Handler; import android.os.Looper; /** * Created by koush on 12/25/13. */ public class HandlerFuture extends SimpleFuture { Handler handler; public HandlerFuture() { Looper looper = Looper.myLooper(); if (looper == null) looper = Looper.getMainLooper(); handler = new Handler(looper); } @Override public void setCallback(final FutureCallback callback) { FutureCallback wrapped = new FutureCallback() { @Override public void onCompleted(final Exception e, final T result) { if (Looper.myLooper() == handler.getLooper()) { callback.onCompleted(e, result); return; } handler.post(new Runnable() { @Override public void run() { onCompleted(e, result); } }); } }; super.setCallback(wrapped); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/MultiFuture.java ================================================ package com.jeffmony.async.future; import java.util.ArrayList; /** * Created by koush on 2/25/14. */ public class MultiFuture extends SimpleFuture { private ArrayList> internalCallbacks; public MultiFuture() { } public MultiFuture(T value) { super(value); } public MultiFuture(Exception e) { super(e); } public MultiFuture(Future future) { super(future); } private final FutureCallbackInternal internalCallback = (e, result, callsite) -> { ArrayList> callbacks; synchronized (MultiFuture.this) { callbacks = MultiFuture.this.internalCallbacks; MultiFuture.this.internalCallbacks = null; } if (callbacks == null) return; for (FutureCallbackInternal cb : callbacks) { cb.onCompleted(e, result, callsite); } }; @Override protected void setCallbackInternal(FutureCallsite callsite, FutureCallbackInternal internalCallback) { synchronized (this) { if (internalCallback != null) { if (internalCallbacks == null) internalCallbacks = new ArrayList<>(); internalCallbacks.add(internalCallback); } } // so, there is a race condition where this internal callback could get // executed twice, if two callbacks are added at the same time. // however, it doesn't matter, as the actual retrieval and nulling // of the callback list is done in another sync block. // one of the invocations will actually invoke all the callbacks, // while the other will not get a list back. // race: // 1-ADD // 2-ADD // 1-INVOKE LIST // 2-INVOKE NULL super.setCallbackInternal(callsite, this.internalCallback); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/MultiTransformFuture.java ================================================ package com.jeffmony.async.future; public abstract class MultiTransformFuture extends MultiFuture implements FutureCallback { @Override public void onCompleted(Exception e, F result) { if (isCancelled()) return; if (e != null) { error(e); return; } try { transform(result); } catch (Exception ex) { error(ex); } } protected void error(Exception e) { setComplete(e); } protected abstract void transform(F result) throws Exception; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/SimpleCancellable.java ================================================ package com.jeffmony.async.future; public class SimpleCancellable implements DependentCancellable { boolean complete; @Override public boolean isDone() { return complete; } protected void cancelCleanup() { } protected void cleanup() { } protected void completeCleanup() { } public boolean setComplete() { synchronized (this) { if (cancelled) return false; if (complete) { // don't allow a Cancellable to complete twice... return false; } complete = true; parent = null; } completeCleanup(); cleanup(); return true; } @Override public boolean cancel() { Cancellable parent; synchronized (this) { if (complete) return false; if (cancelled) return true; cancelled = true; parent = this.parent; // null out the parent to allow garbage collection this.parent = null; } if (parent != null) parent.cancel(); cancelCleanup(); cleanup(); return true; } boolean cancelled; private Cancellable parent; @Override public boolean setParent(Cancellable parent) { synchronized (this) { if (isDone()) return false; this.parent = parent; return true; } } @Override public boolean isCancelled() { synchronized (this) { return cancelled || (parent != null && parent.isCancelled()); } } public static final Cancellable COMPLETED = new SimpleCancellable() { { setComplete(); } }; public static final Cancellable CANCELLED = new SimpleCancellable() { { cancel(); } }; public Cancellable reset() { cancel(); complete = false; cancelled = false; return this; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/SimpleFuture.java ================================================ package com.jeffmony.async.future; import com.jeffmony.async.AsyncSemaphore; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class SimpleFuture extends SimpleCancellable implements DependentFuture { private AsyncSemaphore waiter; private Exception exception; private T result; private boolean silent; private FutureCallbackInternal internalCallback; protected interface FutureCallbackInternal { void onCompleted(Exception e, T result, FutureCallsite next); } public SimpleFuture() { } public SimpleFuture(T value) { setComplete(value); } public SimpleFuture(Exception e) { setComplete(e); } public SimpleFuture(Future future) { setComplete(future); } @Override public boolean cancel(boolean mayInterruptIfRunning) { return cancel(); } private boolean cancelInternal(boolean silent) { if (!super.cancel()) return false; // still need to release any pending waiters FutureCallbackInternal internalCallback; synchronized (this) { exception = new CancellationException(); releaseWaiterLocked(); internalCallback = handleInternalCompleteLocked(); this.silent = silent; } handleCallbackUnlocked(null, internalCallback); return true; } public boolean cancelSilently() { return cancelInternal(true); } @Override public boolean cancel() { return cancelInternal(silent); } @Override public T get() throws InterruptedException, ExecutionException { AsyncSemaphore waiter; synchronized (this) { if (isCancelled() || isDone()) return getResultOrThrow(); waiter = ensureWaiterLocked(); } waiter.acquire(); return getResultOrThrow(); } private T getResultOrThrow() throws ExecutionException { if (exception != null) throw new ExecutionException(exception); return result; } @Override public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { AsyncSemaphore waiter; synchronized (this) { if (isCancelled() || isDone()) return getResultOrThrow(); waiter = ensureWaiterLocked(); } if (!waiter.tryAcquire(timeout, unit)) throw new TimeoutException(); return getResultOrThrow(); } @Override public boolean setComplete() { return setComplete((T)null); } private FutureCallbackInternal handleInternalCompleteLocked() { // don't execute the callback inside the sync block... possible hangup // read the callback value, and then call it outside the block. // can't simply call this.callback.onCompleted directly outside the block, // because that may result in a race condition where the callback changes once leaving // the block. FutureCallbackInternal callback = this.internalCallback; // null out members to allow garbage collection this.internalCallback = null; return callback; } static class FutureCallsite { Exception e; Object result; FutureCallbackInternal callback; void loop() { while (callback != null) { // these values always start non null. FutureCallbackInternal callback = this.callback; Exception e = this.e; Object result = this.result; // null them out for reentrancy this.callback = null; this.e = null; this.result = null; callback.onCompleted(e, result, this); } } } private void handleCallbackUnlocked(FutureCallsite callsite, FutureCallbackInternal internalCallback) { if (silent) return; if (internalCallback == null) return; boolean needsLoop = false; if (callsite == null) { needsLoop = true; callsite = new FutureCallsite(); } callsite.callback = internalCallback; callsite.e = exception; callsite.result = result; if (needsLoop) callsite.loop(); } void releaseWaiterLocked() { if (waiter != null) { waiter.release(); waiter = null; } } AsyncSemaphore ensureWaiterLocked() { if (waiter == null) waiter = new AsyncSemaphore(); return waiter; } public boolean setComplete(Exception e) { return setComplete(e, null, null); } public boolean setCompleteException(Exception e) { return setComplete(e, null, null); } public boolean setComplete(T value) { return setComplete(null, value, null); } public boolean setCompleteValue(T value) { return setComplete(null, value, null); } public boolean setComplete(Exception e, T value) { return setComplete(e, value, null); } private boolean setComplete(Exception e, T value, FutureCallsite callsite) { FutureCallbackInternal internalCallback; synchronized (this) { if (!super.setComplete()) return false; result = value; exception = e; releaseWaiterLocked(); internalCallback = handleInternalCompleteLocked(); } handleCallbackUnlocked(callsite, internalCallback); return true; } void setCallbackInternal(FutureCallsite callsite, FutureCallbackInternal internalCallback) { // callback can only be changed or read/used inside a sync block synchronized (this) { this.internalCallback = internalCallback; if (!isDone() && !isCancelled()) return; internalCallback = handleInternalCompleteLocked(); } handleCallbackUnlocked(callsite, internalCallback); } @Override public void setCallback(FutureCallback callback) { if (callback == null) setCallbackInternal(null, null); else setCallbackInternal(null, (e, result, next) -> callback.onCompleted(e, result)); } private Future setComplete(Future future, FutureCallsite callsite) { setParent(future); SimpleFuture ret = new SimpleFuture<>(); if (future instanceof SimpleFuture) { ((SimpleFuture)future).setCallbackInternal(callsite, (e, result, next) -> ret.setComplete(SimpleFuture.this.setComplete(e, result, next) ? null : new CancellationException(), result, next)); } else { future.setCallback((e, result) -> ret.setComplete(SimpleFuture.this.setComplete(e, result, null) ? null : new CancellationException())); } return ret; } /** * Complete a future with another future. Returns a future that reports whether the completion * was successful. If the future was not completed due to cancellation, the callback * will be called with a CancellationException, and the original future result, if one was provided. * @param future * @return */ public Future setComplete(Future future) { return setComplete(future, null); } public Future setCompleteFuture(Future future) { return setComplete(future, null); } /** * THIS METHOD IS FOR TEST USE ONLY * @return */ @Deprecated public Object getCallback() { return internalCallback; } @Override public Future done(DoneCallback done) { final SimpleFuture ret = new SimpleFuture<>(); ret.setParent(this); setCallbackInternal(null, (e, result, next) -> { if (e == null) { try { done.done(e, result); } catch (Exception callbackException) { e = callbackException; // note that the result is not nulled out. this is useful for managed resources, like sockets. // for example: a successful socket connection was made, but the request can be cancelled. // so, returning an error along with a socket object allows for failure cleanup. } } ret.setComplete(e, result, next); }); return ret; } @Override public Future success(SuccessCallback callback) { final SimpleFuture ret = new SimpleFuture<>(); ret.setParent(this); setCallbackInternal(null, (e, result, next) -> { if (e == null) { try { callback.success(result); } catch (Exception callbackException) { e = callbackException; // note that the result is not nulled out. this is useful for managed resources, like sockets. // for example: a successful socket connection was made, but the request can be cancelled. // so, returning an error along with a socket object allows for failure cleanup. } } ret.setComplete(e, result, next); }); return ret; } @Override public Future then(ThenFutureCallback then) { final SimpleFuture ret = new SimpleFuture<>(); ret.setParent(this); setCallbackInternal(null, (e, result, next) -> { if (e != null) { ret.setComplete(e, null, next); return; } Future out; try { out = then.then(result); } catch (Exception callbackException) { ret.setComplete(callbackException, null, next); return; } ret.setComplete(out, next); }); return ret; } @Override public Future thenConvert(final ThenCallback callback) { return then(from -> new SimpleFuture<>(callback.then(from))); } @Override public Future fail(FailCallback fail) { return failRecover(e -> { fail.fail(e); return new SimpleFuture<>((T)null); }); } @Override public Future failRecover(FailRecoverCallback fail) { SimpleFuture ret = new SimpleFuture<>(); ret.setParent(this); setCallbackInternal(null, (e, result, next) -> { if (e == null) { ret.setComplete(e, result, next); return; } Future out; try { out = fail.fail(e); } catch (Exception callbackException) { ret.setComplete(callbackException, null, next); return; } ret.setComplete(out, next); }); return ret; } @Override public Future failConvert(FailConvertCallback fail) { return failRecover(e -> new SimpleFuture<>(fail.fail(e))); } @Override public boolean setParent(Cancellable parent) { return super.setParent(parent); } /** * Reset the future for reuse. * @return */ public SimpleFuture reset() { super.reset(); result = null; exception = null; waiter = null; internalCallback = null; silent = false; return this; } @Override public Exception tryGetException() { return exception; } @Override public T tryGet() { return result; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/SuccessCallback.java ================================================ package com.jeffmony.async.future; public interface SuccessCallback { void success(T value) throws Exception; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/ThenCallback.java ================================================ package com.jeffmony.async.future; public interface ThenCallback { /** * Callback that is invoked when Future.then completes, * and converts a value F to value T. * @param from * @return * @throws Exception */ T then(F from) throws Exception; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/ThenFutureCallback.java ================================================ package com.jeffmony.async.future; public interface ThenFutureCallback { /** * Callback that is invoked when Future.then completes, * and converts a value F to a Future. * @param from * @return * @throws Exception */ Future then(F from) throws Exception; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/TransformFuture.java ================================================ package com.jeffmony.async.future; public abstract class TransformFuture extends SimpleFuture implements FutureCallback { public TransformFuture(F from) { onCompleted(null, from); } public TransformFuture() { } @Override public void onCompleted(Exception e, F result) { if (isCancelled()) return; if (e != null) { error(e); return; } try { transform(result); } catch (Exception ex) { error(ex); } } protected void error(Exception e) { setComplete(e); } protected abstract void transform(F result) throws Exception; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/future/TypeConverter.java ================================================ package com.jeffmony.async.future; public interface TypeConverter { Future convert(F from, String fromMime) throws Exception; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpClient.java ================================================ package com.jeffmony.async.http; import android.annotation.SuppressLint; import android.net.Uri; import android.os.Build; import android.text.TextUtils; import com.jeffmony.async.AsyncSSLException; import com.jeffmony.async.AsyncServer; import com.jeffmony.async.AsyncSocket; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.ConnectCallback; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.future.Cancellable; import com.jeffmony.async.future.Future; import com.jeffmony.async.future.SimpleFuture; import com.jeffmony.async.http.callback.HttpConnectCallback; import com.jeffmony.async.http.callback.RequestCallback; import com.jeffmony.async.parser.AsyncParser; import com.jeffmony.async.parser.ByteBufferListParser; import com.jeffmony.async.parser.JSONArrayParser; import com.jeffmony.async.parser.JSONObjectParser; import com.jeffmony.async.parser.StringParser; import com.jeffmony.async.stream.OutputStreamDataCallback; import org.json.JSONArray; import org.json.JSONObject; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.ProxySelector; import java.net.URI; import java.net.URL; import java.util.Collection; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeoutException; public class AsyncHttpClient { private static AsyncHttpClient mDefaultInstance; public static AsyncHttpClient getDefaultInstance() { if (mDefaultInstance == null) mDefaultInstance = new AsyncHttpClient(AsyncServer.getDefault()); return mDefaultInstance; } final List mMiddleware = new CopyOnWriteArrayList<>(); public Collection getMiddleware() { return mMiddleware; } public void insertMiddleware(AsyncHttpClientMiddleware middleware) { mMiddleware.add(0, middleware); } AsyncSSLSocketMiddleware sslSocketMiddleware; AsyncSocketMiddleware socketMiddleware; HttpTransportMiddleware httpTransportMiddleware; AsyncServer mServer; public AsyncHttpClient(AsyncServer server) { mServer = server; insertMiddleware(socketMiddleware = new AsyncSocketMiddleware(this)); insertMiddleware(sslSocketMiddleware = new AsyncSSLSocketMiddleware(this)); insertMiddleware(httpTransportMiddleware = new HttpTransportMiddleware()); sslSocketMiddleware.addEngineConfigurator(new SSLEngineSNIConfigurator()); } @SuppressLint("NewApi") private static void setupAndroidProxy(AsyncHttpRequest request) { // using a explicit proxy? if (request.proxyHost != null) return; List proxies; try { proxies = ProxySelector.getDefault().select(URI.create(request.getUri().toString())); } catch (Exception e) { // uri parsing craps itself sometimes. return; } if (proxies.isEmpty()) return; Proxy proxy = proxies.get(0); if (proxy.type() != Proxy.Type.HTTP) return; if (!(proxy.address() instanceof InetSocketAddress)) return; InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address(); String proxyHost; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { proxyHost = proxyAddress.getHostString(); } else { InetAddress address = proxyAddress.getAddress(); if (address!=null) proxyHost = address.getHostAddress(); else proxyHost = proxyAddress.getHostName(); } request.enableProxy(proxyHost, proxyAddress.getPort()); } public AsyncSocketMiddleware getSocketMiddleware() { return socketMiddleware; } public AsyncSSLSocketMiddleware getSSLSocketMiddleware() { return sslSocketMiddleware; } public Future execute(final AsyncHttpRequest request, final HttpConnectCallback callback) { FutureAsyncHttpResponse ret; execute(request, 0, ret = new FutureAsyncHttpResponse(), callback); return ret; } public Future execute(String uri, final HttpConnectCallback callback) { return execute(new AsyncHttpGet(uri), callback); } private static final String LOGTAG = "AsyncHttp"; private class FutureAsyncHttpResponse extends SimpleFuture { public AsyncSocket socket; public Cancellable scheduled; public Runnable timeoutRunnable; @Override public boolean cancel() { if (!super.cancel()) return false; if (socket != null) { socket.setDataCallback(new DataCallback.NullDataCallback()); socket.close(); } if (scheduled != null) scheduled.cancel(); return true; } } private void reportConnectedCompleted(FutureAsyncHttpResponse cancel, Exception ex, AsyncHttpResponseImpl response, AsyncHttpRequest request, final HttpConnectCallback callback) { assert callback != null; cancel.scheduled.cancel(); boolean complete; if (ex != null) { request.loge("Connection error", ex); complete = cancel.setComplete(ex); } else { request.logd("Connection successful"); complete = cancel.setComplete(response); } if (complete) { callback.onConnectCompleted(ex, response); assert ex != null || response.socket() == null || response.getDataCallback() != null || response.isPaused(); return; } if (response != null) { // the request was cancelled, so close up shop, and eat any pending data response.setDataCallback(new DataCallback.NullDataCallback()); response.close(); } } private void execute(final AsyncHttpRequest request, final int redirectCount, final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback) { if (mServer.isAffinityThread()) { executeAffinity(request, redirectCount, cancel, callback); } else { mServer.post(new Runnable() { @Override public void run() { executeAffinity(request, redirectCount, cancel, callback); } }); } } private static long getTimeoutRemaining(AsyncHttpRequest request) { // need a better way to calculate this. // a timer of sorts that stops/resumes. return request.getTimeout(); } private static void copyHeader(AsyncHttpRequest from, AsyncHttpRequest to, String header) { String value = from.getHeaders().get(header); if (!TextUtils.isEmpty(value)) to.getHeaders().set(header, value); } private void executeAffinity(final AsyncHttpRequest request, final int redirectCount, final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback) { assert mServer.isAffinityThread(); if (redirectCount > 15) { reportConnectedCompleted(cancel, new RedirectLimitExceededException("too many redirects"), null, request, callback); return; } final Uri uri = request.getUri(); final AsyncHttpClientMiddleware.OnResponseCompleteData data = new AsyncHttpClientMiddleware.OnResponseCompleteData(); request.executionTime = System.currentTimeMillis(); data.request = request; request.logd("Executing request."); for (AsyncHttpClientMiddleware middleware: mMiddleware) { middleware.onRequest(data); } // flow: // 1) set a connect timeout // 2) wait for connect // 3) on connect, cancel timeout // 4) wait for request to be sent fully // 5) after request is sent, set a header timeout // 6) wait for headers // 7) on headers, cancel timeout // 8) TODO: response can take as long as it wants to arrive? if (request.getTimeout() > 0) { // set connect timeout cancel.timeoutRunnable = new Runnable() { @Override public void run() { // we've timed out, kill the connections if (data.socketCancellable != null) { data.socketCancellable.cancel(); if (data.socket != null) data.socket.close(); } reportConnectedCompleted(cancel, new TimeoutException(), null, request, callback); } }; cancel.scheduled = mServer.postDelayed(cancel.timeoutRunnable, getTimeoutRemaining(request)); } // 2) wait for a connect data.connectCallback = new ConnectCallback() { boolean reported; @Override public void onConnectCompleted(Exception ex, AsyncSocket socket) { if (reported) { if (socket != null) { socket.setDataCallback(new DataCallback.NullDataCallback()); socket.setEndCallback(new CompletedCallback.NullCompletedCallback()); socket.close(); throw new AssertionError("double connect callback"); } } reported = true; request.logv("socket connected"); if (cancel.isCancelled()) { if (socket != null) socket.close(); return; } // 3) on connect, cancel timeout if (cancel.timeoutRunnable != null) cancel.scheduled.cancel(); if (ex != null) { reportConnectedCompleted(cancel, ex, null, request, callback); return; } data.socket = socket; cancel.socket = socket; executeSocket(request, redirectCount, cancel, callback, data); } }; // set up the system default proxy and connect setupAndroidProxy(request); // set the implicit content type if (request.getBody() != null) { if (request.getHeaders().get("Content-Type") == null) request.getHeaders().set("Content-Type", request.getBody().getContentType()); } final Exception unsupportedURI; for (AsyncHttpClientMiddleware middleware: mMiddleware) { Cancellable socketCancellable = middleware.getSocket(data); if (socketCancellable != null) { data.socketCancellable = socketCancellable; cancel.setParent(socketCancellable); return; } } unsupportedURI = new IllegalArgumentException("invalid uri="+request.getUri()+" middlewares="+mMiddleware); reportConnectedCompleted(cancel, unsupportedURI, null, request, callback); } private void executeSocket(final AsyncHttpRequest request, final int redirectCount, final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback, final AsyncHttpClientMiddleware.OnResponseCompleteData data) { // 4) wait for request to be sent fully // and // 6) wait for headers final AsyncHttpResponseImpl ret = new AsyncHttpResponseImpl(request) { @Override protected void onRequestCompleted(Exception ex) { if (ex != null) { reportConnectedCompleted(cancel, ex, null, request, callback); return; } request.logv("request completed"); if (cancel.isCancelled()) return; // 5) after request is sent, set a header timeout if (cancel.timeoutRunnable != null && mHeaders == null) { cancel.scheduled.cancel(); cancel.scheduled = mServer.postDelayed(cancel.timeoutRunnable, getTimeoutRemaining(request)); } for (AsyncHttpClientMiddleware middleware: mMiddleware) { middleware.onRequestSent(data); } } @Override public void setDataEmitter(DataEmitter emitter) { data.bodyEmitter = emitter; for (AsyncHttpClientMiddleware middleware: mMiddleware) { middleware.onBodyDecoder(data); } super.setDataEmitter(data.bodyEmitter); for (AsyncHttpClientMiddleware middleware: mMiddleware) { AsyncHttpRequest newReq = middleware.onResponseReady(data); if (newReq != null) { newReq.executionTime = request.executionTime; newReq.logLevel = request.logLevel; newReq.LOGTAG = request.LOGTAG; newReq.proxyHost = request.proxyHost; newReq.proxyPort = request.proxyPort; setupAndroidProxy(newReq); request.logi("Response intercepted by middleware"); newReq.logi("Request initiated by middleware intercept by middleware"); // post to allow reuse of socket. mServer.post(() -> execute(newReq, redirectCount, cancel, callback)); setDataCallback(new NullDataCallback()); return; } } Headers headers = mHeaders; int responseCode = code(); if ((responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == 307) && request.getFollowRedirect()) { String location = headers.get("Location"); Uri redirect; try { redirect = Uri.parse(location); if (redirect.getScheme() == null) { redirect = Uri.parse(new URL(new URL(request.getUri().toString()), location).toString()); } } catch (Exception e) { reportConnectedCompleted(cancel, e, this, request, callback); return; } final String method = request.getMethod().equals(AsyncHttpHead.METHOD) ? AsyncHttpHead.METHOD : AsyncHttpGet.METHOD; AsyncHttpRequest newReq = new AsyncHttpRequest(redirect, method); newReq.executionTime = request.executionTime; newReq.logLevel = request.logLevel; newReq.LOGTAG = request.LOGTAG; newReq.proxyHost = request.proxyHost; newReq.proxyPort = request.proxyPort; setupAndroidProxy(newReq); copyHeader(request, newReq, "User-Agent"); copyHeader(request, newReq, "Range"); request.logi("Redirecting"); newReq.logi("Redirected"); mServer.post(() -> execute(newReq, redirectCount + 1, cancel, callback)); setDataCallback(new NullDataCallback()); return; } request.logv("Final (post cache response) headers:\n" + toString()); // at this point the headers are done being modified reportConnectedCompleted(cancel, null, this, request, callback); } protected void onHeadersReceived() { super.onHeadersReceived(); if (cancel.isCancelled()) return; // 7) on headers, cancel timeout if (cancel.timeoutRunnable != null) cancel.scheduled.cancel(); // allow the middleware to massage the headers before the body is decoded request.logv("Received headers:\n" + toString()); for (AsyncHttpClientMiddleware middleware: mMiddleware) { middleware.onHeadersReceived(data); } // drop through, and setDataEmitter will be called for the body decoder. // headers will be further massaged in there. } @Override protected void report(Exception ex) { if (ex != null) request.loge("exception during response", ex); if (cancel.isCancelled()) return; if (ex instanceof AsyncSSLException) { request.loge("SSL Exception", ex); AsyncSSLException ase = (AsyncSSLException)ex; request.onHandshakeException(ase); if (ase.getIgnore()) return; } final AsyncSocket socket = socket(); if (socket == null) return; super.report(ex); if (!socket.isOpen() || ex != null) { if (headers() == null && ex != null) reportConnectedCompleted(cancel, ex, null, request, callback); } data.exception = ex; for (AsyncHttpClientMiddleware middleware: mMiddleware) { middleware.onResponseComplete(data); } } @Override public AsyncSocket detachSocket() { request.logd("Detaching socket"); AsyncSocket socket = socket(); if (socket == null) return null; socket.setWriteableCallback(null); socket.setClosedCallback(null); socket.setEndCallback(null); socket.setDataCallback(null); setSocket(null); return socket; } }; data.sendHeadersCallback = new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (ex != null) ret.report(ex); else ret.onHeadersSent(); } }; data.receiveHeadersCallback = new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (ex != null) ret.report(ex); else ret.onHeadersReceived(); } }; data.response = ret; ret.setSocket(data.socket); for (AsyncHttpClientMiddleware middleware : mMiddleware) { if (middleware.exchangeHeaders(data)) break; } } public static abstract class RequestCallbackBase implements RequestCallback { @Override public void onProgress(AsyncHttpResponse response, long downloaded, long total) { } @Override public void onConnect(AsyncHttpResponse response) { } } public static abstract class DownloadCallback extends RequestCallbackBase { } public static abstract class StringCallback extends RequestCallbackBase { } public static abstract class JSONObjectCallback extends RequestCallbackBase { } public static abstract class JSONArrayCallback extends RequestCallbackBase { } public static abstract class FileCallback extends RequestCallbackBase { } public Future executeByteBufferList(AsyncHttpRequest request, DownloadCallback callback) { return execute(request, new ByteBufferListParser(), callback); } public Future executeString(AsyncHttpRequest req, final StringCallback callback) { return execute(req, new StringParser(), callback); } public Future executeJSONObject(AsyncHttpRequest req, final JSONObjectCallback callback) { return execute(req, new JSONObjectParser(), callback); } public Future executeJSONArray(AsyncHttpRequest req, final JSONArrayCallback callback) { return execute(req, new JSONArrayParser(), callback); } private void invokeWithAffinity(final RequestCallback callback, SimpleFuture future, final AsyncHttpResponse response, final Exception e, final T result) { boolean complete; if (e != null) complete = future.setComplete(e); else complete = future.setComplete(result); if (!complete) return; if (callback != null) callback.onCompleted(e, response, result); } private void invoke(final RequestCallback callback, final SimpleFuture future, final AsyncHttpResponse response, final Exception e, final T result) { Runnable runnable = new Runnable() { @Override public void run() { invokeWithAffinity(callback, future, response, e, result); } }; mServer.post(runnable); } private void invokeProgress(final RequestCallback callback, final AsyncHttpResponse response, final long downloaded, final long total) { if (callback != null) callback.onProgress(response, downloaded, total); } private void invokeConnect(final RequestCallback callback, final AsyncHttpResponse response) { if (callback != null) callback.onConnect(response); } public Future executeFile(AsyncHttpRequest req, final String filename, final FileCallback callback) { final File file = new File(filename); file.getParentFile().mkdirs(); final OutputStream fout; try { fout = new BufferedOutputStream(new FileOutputStream(file), 8192); } catch (FileNotFoundException e) { SimpleFuture ret = new SimpleFuture(); ret.setComplete(e); return ret; } final FutureAsyncHttpResponse cancel = new FutureAsyncHttpResponse(); final SimpleFuture ret = new SimpleFuture() { @Override public void cancelCleanup() { try { cancel.get().setDataCallback(new DataCallback.NullDataCallback()); cancel.get().close(); } catch (Exception e) { } try { fout.close(); } catch (Exception e) { } file.delete(); } }; ret.setParent(cancel); execute(req, 0, cancel, new HttpConnectCallback() { long mDownloaded = 0; @Override public void onConnectCompleted(Exception ex, final AsyncHttpResponse response) { if (ex != null) { try { fout.close(); } catch (IOException e) { } file.delete(); invoke(callback, ret, response, ex, null); return; } invokeConnect(callback, response); final long contentLength = HttpUtil.contentLength(response.headers()); response.setDataCallback(new OutputStreamDataCallback(fout) { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { mDownloaded += bb.remaining(); super.onDataAvailable(emitter, bb); invokeProgress(callback, response, mDownloaded, contentLength); } }); response.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { try { fout.close(); } catch (IOException e) { ex = e; } if (ex != null) { file.delete(); invoke(callback, ret, response, ex, null); } else { invoke(callback, ret, response, null, file); } } }); } }); return ret; } public SimpleFuture execute(AsyncHttpRequest req, final AsyncParser parser, final RequestCallback callback) { final FutureAsyncHttpResponse cancel = new FutureAsyncHttpResponse(); final SimpleFuture ret = new SimpleFuture(); execute(req, 0, cancel, (ex, response) -> { if (ex != null) { invoke(callback, ret, response, ex, null); return; } invokeConnect(callback, response); Future parsed = parser.parse(response); parsed.setCallback((e, result) -> invoke(callback, ret, response, e, result)); // reparent to the new parser future ret.setParent(parsed); }); ret.setParent(cancel); return ret; } public interface WebSocketConnectCallback { void onCompleted(Exception ex, WebSocket webSocket); } public Future websocket(final AsyncHttpRequest req, String protocol, final WebSocketConnectCallback callback) { return websocket(req, protocol != null ? new String[] { protocol } : null, callback); } public Future websocket(final AsyncHttpRequest req, String[] protocols, final WebSocketConnectCallback callback) { WebSocketImpl.addWebSocketUpgradeHeaders(req, protocols); final SimpleFuture ret = new SimpleFuture<>(); Cancellable connect = execute(req, (ex, response) -> { if (ex != null) { if (ret.setComplete(ex)) { if (callback != null) callback.onCompleted(ex, null); } return; } WebSocket ws = WebSocketImpl.finishHandshake(req.getHeaders(), response); if (ws == null) { ex = new WebSocketHandshakeException("Unable to complete websocket handshake"); response.close(); if (!ret.setComplete(ex)) return; } else { if (!ret.setComplete(ws)) return; } if (callback != null) callback.onCompleted(ex, ws); }); ret.setParent(connect); return ret; } public Future websocket(String uri, String protocol, final WebSocketConnectCallback callback) { // assert callback != null; final AsyncHttpGet get = new AsyncHttpGet(uri.replace("ws://", "http://").replace("wss://", "https://")); return websocket(get, protocol, callback); } public Future websocket(String uri, String[] protocols, final WebSocketConnectCallback callback) { // assert callback != null; final AsyncHttpGet get = new AsyncHttpGet(uri.replace("ws://", "http://").replace("wss://", "https://")); return websocket(get, protocols, callback); } public AsyncServer getServer() { return mServer; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpClientMiddleware.java ================================================ package com.jeffmony.async.http; import com.jeffmony.async.AsyncSocket; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.ConnectCallback; import com.jeffmony.async.future.Cancellable; import com.jeffmony.async.util.UntypedHashtable; /** * AsyncHttpClientMiddleware is used by AsyncHttpClient to * inspect, manipulate, and handle http requests. */ public interface AsyncHttpClientMiddleware { interface ResponseHead { AsyncSocket socket(); String protocol(); String message(); int code(); ResponseHead protocol(String protocol); ResponseHead message(String message); ResponseHead code(int code); Headers headers(); ResponseHead headers(Headers headers); DataSink sink(); ResponseHead sink(DataSink sink); DataEmitter emitter(); ResponseHead emitter(DataEmitter emitter); } class OnRequestData { public UntypedHashtable state = new UntypedHashtable(); public AsyncHttpRequest request; } class GetSocketData extends OnRequestData { public ConnectCallback connectCallback; public Cancellable socketCancellable; public String protocol; } class OnExchangeHeaderData extends GetSocketData { public AsyncSocket socket; public ResponseHead response; public CompletedCallback sendHeadersCallback; public CompletedCallback receiveHeadersCallback; } class OnRequestSentData extends OnExchangeHeaderData { } class OnHeadersReceivedData extends OnRequestSentData { } class OnBodyDecoderData extends OnHeadersReceivedData { public DataEmitter bodyEmitter; } class OnResponseReadyData extends OnBodyDecoderData { } class OnResponseCompleteData extends OnResponseReadyData { public Exception exception; } /** * Called immediately upon request execution * @param data */ void onRequest(OnRequestData data); /** * Called to retrieve the socket that will fulfill this request * @param data * @return */ Cancellable getSocket(GetSocketData data); /** * Called before when the headers are sent and received via the socket. * Implementers return true to denote they will manage header exchange. * @param data * @return */ boolean exchangeHeaders(OnExchangeHeaderData data); /** * Called once the headers and any optional request body has * been sent * @param data */ void onRequestSent(OnRequestSentData data); /** * Called once the headers have been received via the socket * @param data */ void onHeadersReceived(OnHeadersReceivedData data); /** * Called before the body is decoded * @param data */ void onBodyDecoder(OnBodyDecoderData data); /** * Called before the response is returned to the client. Return a new AsyncHttpRequest * to end the current request and start a new one. Can be used to implement redirect strategies * or multileg authentication, such as digest. * @param data * @return */ AsyncHttpRequest onResponseReady(OnResponseReadyData data); /** * Called once the request is complete and response has been received, * or if an error occurred * @param data */ void onResponseComplete(OnResponseCompleteData data); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpDelete.java ================================================ package com.jeffmony.async.http; import android.net.Uri; public class AsyncHttpDelete extends AsyncHttpRequest { public static final String METHOD = "DELETE"; public AsyncHttpDelete(String uri) { this(Uri.parse(uri)); } public AsyncHttpDelete(Uri uri) { super(uri, METHOD); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpGet.java ================================================ package com.jeffmony.async.http; import android.net.Uri; public class AsyncHttpGet extends AsyncHttpRequest { public static final String METHOD = "GET"; public AsyncHttpGet(String uri) { super(Uri.parse(uri), METHOD); } public AsyncHttpGet(Uri uri) { super(uri, METHOD); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpHead.java ================================================ package com.jeffmony.async.http; import android.net.Uri; /** * Created by koush on 8/25/13. */ public class AsyncHttpHead extends AsyncHttpRequest { public AsyncHttpHead(Uri uri) { super(uri, METHOD); } @Override public boolean hasBody() { return false; } public static final String METHOD = "HEAD"; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpPost.java ================================================ package com.jeffmony.async.http; import android.net.Uri; public class AsyncHttpPost extends AsyncHttpRequest { public static final String METHOD = "POST"; public AsyncHttpPost(String uri) { this(Uri.parse(uri)); } public AsyncHttpPost(Uri uri) { super(uri, METHOD); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpPut.java ================================================ package com.jeffmony.async.http; import android.net.Uri; public class AsyncHttpPut extends AsyncHttpRequest { public static final String METHOD = "PUT"; public AsyncHttpPut(String uri) { this(Uri.parse(uri)); } public AsyncHttpPut(Uri uri) { super(uri, METHOD); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpRequest.java ================================================ package com.jeffmony.async.http; import android.net.Uri; import android.util.Log; import com.jeffmony.async.AsyncSSLException; import com.jeffmony.async.http.body.AsyncHttpRequestBody; import java.util.Locale; public class AsyncHttpRequest { public RequestLine getRequestLine() { return new RequestLine() { @Override public String getUri() { return AsyncHttpRequest.this.getUri().toString(); } @Override public ProtocolVersion getProtocolVersion() { return new ProtocolVersion("HTTP", 1, 1); } @Override public String getMethod() { return mMethod; } @Override public String toString() { if (proxyHost != null) return String.format(Locale.ENGLISH, "%s %s %s", mMethod, AsyncHttpRequest.this.getUri(), requestLineProtocol); String path = getPath(); if (path == null || path.length() == 0) path = "/"; String query = AsyncHttpRequest.this.getUri().getEncodedQuery(); if (query != null && query.length() != 0) { path += "?" + query; } return String.format(Locale.ENGLISH, "%s %s %s", mMethod, path, requestLineProtocol); } }; } public boolean hasBody() { return true; } public String getPath() { return AsyncHttpRequest.this.getUri().getEncodedPath(); } protected static String getDefaultUserAgent() { String agent = System.getProperty("http.agent"); return agent != null ? agent : ("Java" + System.getProperty("java.version")); } private String requestLineProtocol = "HTTP/1.1"; private String mMethod; public String getMethod() { return mMethod; } public void setRequestLineProtocol(String scheme) { this.requestLineProtocol = scheme; } public String getRequestLineProtocol() { return requestLineProtocol; } public AsyncHttpRequest setMethod(String method) { if (getClass() != AsyncHttpRequest.class) throw new UnsupportedOperationException("can't change method on a subclass of AsyncHttpRequest"); mMethod = method; return this; } public AsyncHttpRequest(Uri uri, String method) { this(uri, method, null); } public static void setDefaultHeaders(Headers ret, Uri uri) { if (uri != null) { String host = uri.getHost(); if (uri.getPort() != -1) host = host + ":" + uri.getPort(); if (host != null) ret.set("Host", host); } ret.set("User-Agent", getDefaultUserAgent()); ret.set("Accept-Encoding", "gzip, deflate"); ret.set("Connection", "keep-alive"); ret.set("Accept", HEADER_ACCEPT_ALL); } public static final String HEADER_ACCEPT_ALL = "*/*"; public AsyncHttpRequest(Uri uri, String method, Headers headers) { assert uri != null; mMethod = method; this.uri = uri; if (headers == null) mRawHeaders = new Headers(); else mRawHeaders = headers; if (headers == null) setDefaultHeaders(mRawHeaders, uri); } Uri uri; public Uri getUri() { return uri; } private Headers mRawHeaders = new Headers(); public Headers getHeaders() { return mRawHeaders; } private boolean mFollowRedirect = true; public boolean getFollowRedirect() { return mFollowRedirect; } public AsyncHttpRequest setFollowRedirect(boolean follow) { mFollowRedirect = follow; return this; } private AsyncHttpRequestBody mBody; public void setBody(AsyncHttpRequestBody body) { mBody = body; } public AsyncHttpRequestBody getBody() { return mBody; } public void onHandshakeException(AsyncSSLException e) { } public static final int DEFAULT_TIMEOUT = 30000; int mTimeout = DEFAULT_TIMEOUT; public int getTimeout() { return mTimeout; } public AsyncHttpRequest setTimeout(int timeout) { mTimeout = timeout; return this; } public AsyncHttpRequest setHeader(String name, String value) { getHeaders().set(name, value); return this; } public AsyncHttpRequest addHeader(String name, String value) { getHeaders().add(name, value); return this; } String proxyHost; int proxyPort = -1; public void enableProxy(String host, int port) { proxyHost = host; proxyPort = port; } public void disableProxy() { proxyHost = null; proxyPort = -1; } public String getProxyHost() { return proxyHost; } public int getProxyPort() { return proxyPort; } @Override public String toString() { if (mRawHeaders == null) return super.toString(); return mRawHeaders.toPrefixString(uri.toString()); } public void setLogging(String tag, int level) { LOGTAG = tag; logLevel = level; } // request level logging String LOGTAG; int logLevel; public int getLogLevel() { return logLevel; } public String getLogTag() { return LOGTAG; } long executionTime; private String getLogMessage(String message) { long elapsed; if (executionTime != 0) elapsed = System.currentTimeMillis() - executionTime; else elapsed = 0; return String.format(Locale.ENGLISH, "(%d ms) %s: %s", elapsed, getUri(), message); } public void logi(String message) { if (LOGTAG == null) return; if (logLevel > Log.INFO) return; Log.i(LOGTAG, getLogMessage(message)); } public void logv(String message) { if (LOGTAG == null) return; if (logLevel > Log.VERBOSE) return; Log.v(LOGTAG, getLogMessage(message)); } public void logw(String message) { if (LOGTAG == null) return; if (logLevel > Log.WARN) return; Log.w(LOGTAG, getLogMessage(message)); } public void logd(String message) { if (LOGTAG == null) return; if (logLevel > Log.DEBUG) return; Log.d(LOGTAG, getLogMessage(message)); } public void logd(String message, Exception e) { if (LOGTAG == null) return; if (logLevel > Log.DEBUG) return; Log.d(LOGTAG, getLogMessage(message)); Log.d(LOGTAG, e.getMessage(), e); } public void loge(String message) { if (LOGTAG == null) return; if (logLevel > Log.ERROR) return; Log.e(LOGTAG, getLogMessage(message)); } public void loge(String message, Exception e) { if (LOGTAG == null) return; if (logLevel > Log.ERROR) return; Log.e(LOGTAG, getLogMessage(message)); Log.e(LOGTAG, e.getMessage(), e); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpResponse.java ================================================ package com.jeffmony.async.http; import com.jeffmony.async.AsyncSocket; import com.jeffmony.async.DataEmitter; public interface AsyncHttpResponse extends DataEmitter { String protocol(); String message(); int code(); Headers headers(); AsyncSocket detachSocket(); AsyncHttpRequest getRequest(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpResponseImpl.java ================================================ package com.jeffmony.async.http; import com.jeffmony.async.AsyncServer; import com.jeffmony.async.AsyncSocket; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.FilteredDataEmitter; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.http.body.AsyncHttpRequestBody; import java.nio.charset.Charset; abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements DataEmitter, AsyncHttpResponse, AsyncHttpClientMiddleware.ResponseHead { public AsyncSocket socket() { return mSocket; } @Override public AsyncHttpRequest getRequest() { return mRequest; } void setSocket(AsyncSocket exchange) { mSocket = exchange; if (mSocket == null) return; mSocket.setEndCallback(mReporter); } protected void onHeadersSent() { AsyncHttpRequestBody requestBody = mRequest.getBody(); if (requestBody != null) { requestBody.write(mRequest, mSink, new CompletedCallback() { @Override public void onCompleted(Exception ex) { onRequestCompleted(ex); } }); } else { onRequestCompleted(null); } } protected void onRequestCompleted(Exception ex) { } private CompletedCallback mReporter = new CompletedCallback() { @Override public void onCompleted(Exception error) { if (headers() == null) { report(new ConnectionClosedException("connection closed before headers received.", error)); } else if (error != null && !mCompleted) { report(new ConnectionClosedException("connection closed before response completed.", error)); } else { report(error); } } }; protected void onHeadersReceived() { } @Override public DataEmitter emitter() { return getDataEmitter(); } @Override public AsyncHttpClientMiddleware.ResponseHead emitter(DataEmitter emitter) { setDataEmitter(emitter); return this; } private void terminate() { // DISCONNECT. EVERYTHING. // should not get any data after this point... // if so, eat it and disconnect. mSocket.setDataCallback(new NullDataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { super.onDataAvailable(emitter, bb); mSocket.close(); } }); } @Override protected void report(Exception e) { super.report(e); terminate(); mSocket.setWriteableCallback(null); mSocket.setClosedCallback(null); mSocket.setEndCallback(null); mCompleted = true; } @Override public void close() { super.close(); terminate(); } private AsyncHttpRequest mRequest; private AsyncSocket mSocket; protected Headers mHeaders; public AsyncHttpResponseImpl(AsyncHttpRequest request) { mRequest = request; } boolean mCompleted = false; @Override public Headers headers() { return mHeaders; } @Override public AsyncHttpClientMiddleware.ResponseHead headers(Headers headers) { mHeaders = headers; return this; } int code; @Override public int code() { return code; } @Override public AsyncHttpClientMiddleware.ResponseHead code(int code) { this.code = code; return this; } @Override public AsyncHttpClientMiddleware.ResponseHead protocol(String protocol) { this.protocol = protocol; return this; } @Override public AsyncHttpClientMiddleware.ResponseHead message(String message) { this.message = message; return this; } String protocol; @Override public String protocol() { return protocol; } String message; @Override public String message() { return message; } @Override public String toString() { if (mHeaders == null) return super.toString(); return mHeaders.toPrefixString(protocol + " " + code + " " + message); } private boolean mFirstWrite = true; private void assertContent() { if (!mFirstWrite) return; mFirstWrite = false; assert null != mRequest.getHeaders().get("Content-Type"); assert mRequest.getHeaders().get("Transfer-Encoding") != null || HttpUtil.contentLength(mRequest.getHeaders()) != -1; } DataSink mSink; @Override public DataSink sink() { return mSink; } @Override public AsyncHttpClientMiddleware.ResponseHead sink(DataSink sink) { mSink = sink; return this; } @Override public AsyncServer getServer() { return mSocket.getServer(); } @Override public String charset() { Multimap mm = Multimap.parseSemicolonDelimited(headers().get("Content-Type")); String cs; if (mm != null && null != (cs = mm.getString("charset")) && Charset.isSupported(cs)) { return cs; } return null; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/AsyncSSLEngineConfigurator.java ================================================ package com.jeffmony.async.http; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; public interface AsyncSSLEngineConfigurator { SSLEngine createEngine(SSLContext sslContext, String peerHost, int peerPort); void configureEngine(SSLEngine engine, AsyncHttpClientMiddleware.GetSocketData data, String host, int port); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/AsyncSSLSocketMiddleware.java ================================================ package com.jeffmony.async.http; import android.net.Uri; import android.text.TextUtils; import com.jeffmony.async.AsyncSSLSocket; import com.jeffmony.async.AsyncSSLSocketWrapper; import com.jeffmony.async.AsyncSocket; import com.jeffmony.async.LineEmitter; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.ConnectCallback; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Locale; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManager; public class AsyncSSLSocketMiddleware extends AsyncSocketMiddleware { public AsyncSSLSocketMiddleware(AsyncHttpClient client) { super(client, "https", 443); } protected SSLContext sslContext; public void setSSLContext(SSLContext sslContext) { this.sslContext = sslContext; } public SSLContext getSSLContext() { return sslContext != null ? sslContext : AsyncSSLSocketWrapper.getDefaultSSLContext(); } protected TrustManager[] trustManagers; public void setTrustManagers(TrustManager[] trustManagers) { this.trustManagers = trustManagers; } protected HostnameVerifier hostnameVerifier; public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { this.hostnameVerifier = hostnameVerifier; } protected List engineConfigurators = new ArrayList(); public void addEngineConfigurator(AsyncSSLEngineConfigurator engineConfigurator) { engineConfigurators.add(engineConfigurator); } public void clearEngineConfigurators() { engineConfigurators.clear(); } protected SSLEngine createConfiguredSSLEngine(GetSocketData data, String host, int port) { SSLContext sslContext = getSSLContext(); SSLEngine sslEngine = null; for (AsyncSSLEngineConfigurator configurator : engineConfigurators) { sslEngine = configurator.createEngine(sslContext, host, port); if (sslEngine != null) break; } for (AsyncSSLEngineConfigurator configurator : engineConfigurators) { configurator.configureEngine(sslEngine, data, host, port); } return sslEngine; } protected AsyncSSLSocketWrapper.HandshakeCallback createHandshakeCallback(final GetSocketData data, final ConnectCallback callback) { return new AsyncSSLSocketWrapper.HandshakeCallback() { @Override public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { callback.onConnectCompleted(e, socket); } }; } protected void tryHandshake(AsyncSocket socket, GetSocketData data, final Uri uri, final int port, final ConnectCallback callback) { AsyncSSLSocketWrapper.handshake(socket, uri.getHost(), port, createConfiguredSSLEngine(data, uri.getHost(), port), trustManagers, hostnameVerifier, true, createHandshakeCallback(data, callback)); } @Override protected ConnectCallback wrapCallback(final GetSocketData data, final Uri uri, final int port, final boolean proxied, final ConnectCallback callback) { return new ConnectCallback() { @Override public void onConnectCompleted(Exception ex, final AsyncSocket socket) { if (ex != null) { callback.onConnectCompleted(ex, socket); return; } if (!proxied) { tryHandshake(socket, data, uri, port, callback); return; } // this SSL connection is proxied, must issue a CONNECT request to the proxy server // http://stackoverflow.com/a/6594880/704837 // some proxies also require 'Host' header, it should be safe to provide it every time String connect = String.format(Locale.ENGLISH, "CONNECT %s:%s HTTP/1.1\r\nHost: %s\r\n\r\n", uri.getHost(), port, uri.getHost()); data.request.logv("Proxying: " + connect); Util.writeAll(socket, connect.getBytes(), new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (ex != null) { callback.onConnectCompleted(ex, socket); return; } LineEmitter liner = new LineEmitter(); liner.setLineCallback(new LineEmitter.StringCallback() { String statusLine; @Override public void onStringAvailable(String s) { data.request.logv(s); if (statusLine == null) { statusLine = s.trim(); if (!statusLine.matches("HTTP/1.\\d 2\\d\\d .*")) { // connect response is allowed to have any 2xx status code socket.setDataCallback(null); socket.setEndCallback(null); callback.onConnectCompleted(new IOException("non 2xx status line: " + statusLine), socket); } } else if (TextUtils.isEmpty(s.trim())) { // skip all headers, complete handshake once empty line is received socket.setDataCallback(null); socket.setEndCallback(null); tryHandshake(socket, data, uri, port, callback); } } }); socket.setDataCallback(liner); socket.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (!socket.isOpen() && ex == null) ex = new IOException("socket closed before proxy connect response"); callback.onConnectCompleted(ex, socket); } }); } }); } }; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/AsyncSocketMiddleware.java ================================================ package com.jeffmony.async.http; import android.net.Uri; import com.jeffmony.async.AsyncSocket; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.ConnectCallback; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.future.Cancellable; import com.jeffmony.async.future.Future; import com.jeffmony.async.future.Futures; import com.jeffmony.async.future.SimpleCancellable; import com.jeffmony.async.future.SimpleFuture; import com.jeffmony.async.util.ArrayDeque; import java.net.InetSocketAddress; import java.util.Hashtable; import java.util.Locale; public class AsyncSocketMiddleware extends SimpleMiddleware { String scheme; int port; // 5 min idle timeout int idleTimeoutMs = 300 * 1000; public AsyncSocketMiddleware(AsyncHttpClient client, String scheme, int port) { mClient = client; this.scheme = scheme; this.port = port; } public void setIdleTimeoutMs(int idleTimeoutMs) { this.idleTimeoutMs = idleTimeoutMs; } public int getSchemePort(Uri uri) { if (uri.getScheme() == null || !uri.getScheme().equals(scheme)) return -1; if (uri.getPort() == -1) { return port; } else { return uri.getPort(); } } public AsyncSocketMiddleware(AsyncHttpClient client) { this(client, "http", 80); } protected AsyncHttpClient mClient; protected ConnectCallback wrapCallback(GetSocketData data, Uri uri, int port, boolean proxied, ConnectCallback callback) { return callback; } boolean connectAllAddresses; public boolean getConnectAllAddresses() { return connectAllAddresses; } public void setConnectAllAddresses(boolean connectAllAddresses) { this.connectAllAddresses = connectAllAddresses; } String proxyHost; int proxyPort; InetSocketAddress proxyAddress; public void disableProxy() { proxyPort = -1; proxyHost = null; proxyAddress = null; } public void enableProxy(String host, int port) { proxyHost = host; proxyPort = port; proxyAddress = null; } String computeLookup(Uri uri, int port, String proxyHost, int proxyPort) { String proxy; if (proxyHost != null) proxy = proxyHost + ":" + proxyPort; else proxy = ""; if (proxyHost != null) proxy = proxyHost + ":" + proxyPort; return uri.getScheme() + "//" + uri.getHost() + ":" + port + "?proxy=" + proxy; } class IdleSocketHolder { public IdleSocketHolder(AsyncSocket socket) { this.socket = socket; } AsyncSocket socket; long idleTime = System.currentTimeMillis(); } static class ConnectionInfo { int openCount; ArrayDeque queue = new ArrayDeque(); ArrayDeque sockets = new ArrayDeque(); } Hashtable connectionInfo = new Hashtable(); int maxConnectionCount = Integer.MAX_VALUE; public int getMaxConnectionCount() { return maxConnectionCount; } public void setMaxConnectionCount(int maxConnectionCount) { this.maxConnectionCount = maxConnectionCount; } @Override public Cancellable getSocket(final GetSocketData data) { final Uri uri = data.request.getUri(); final int port = getSchemePort(data.request.getUri()); if (port == -1) { return null; } data.state.put("socket-owner", this); final String lookup = computeLookup(uri, port, data.request.getProxyHost(), data.request.getProxyPort()); ConnectionInfo info = getOrCreateConnectionInfo(lookup); synchronized (AsyncSocketMiddleware.this) { if (info.openCount >= maxConnectionCount) { // wait for a connection queue to free up SimpleCancellable queueCancel = new SimpleCancellable(); info.queue.add(data); return queueCancel; } info.openCount++; while (!info.sockets.isEmpty()) { IdleSocketHolder idleSocketHolder = info.sockets.pop(); final AsyncSocket socket = idleSocketHolder.socket; if (idleSocketHolder.idleTime + idleTimeoutMs < System.currentTimeMillis()) { socket.setClosedCallback(null); socket.close(); continue; } if (!socket.isOpen()) continue; data.request.logd("Reusing keep-alive socket"); data.connectCallback.onConnectCompleted(null, socket); // just a noop/dummy, as this can't actually be cancelled. SimpleCancellable ret = new SimpleCancellable(); ret.setComplete(); return ret; } } if (!connectAllAddresses || proxyHost != null || data.request.getProxyHost() != null) { // just default to connecting to a single address data.request.logd("Connecting socket"); String unresolvedHost; int unresolvedPort; boolean proxied = false; if (data.request.getProxyHost() == null && proxyHost != null) data.request.enableProxy(proxyHost, proxyPort); if (data.request.getProxyHost() != null) { unresolvedHost = data.request.getProxyHost(); unresolvedPort = data.request.getProxyPort(); proxied = true; } else { unresolvedHost = uri.getHost(); unresolvedPort = port; } if (proxied) { data.request.logv("Using proxy: " + unresolvedHost + ":" + unresolvedPort); } return mClient.getServer().connectSocket(unresolvedHost, unresolvedPort, wrapCallback(data, uri, port, proxied, data.connectCallback)); } // try to connect to everything... data.request.logv("Resolving domain and connecting to all available addresses"); final SimpleFuture checkedReturnValue = new SimpleFuture<>(); Future socket = mClient.getServer().getAllByName(uri.getHost()) .then(addresses -> Futures.loopUntil(addresses, address -> { SimpleFuture loopResult = new SimpleFuture<>(); final String inetSockAddress = String.format(Locale.ENGLISH, "%s:%s", address, port); data.request.logv("attempting connection to " + inetSockAddress); mClient.getServer().connectSocket(new InetSocketAddress(address, port), loopResult::setComplete); return loopResult; })) // handle failures here (wrap the callback) .fail(e -> wrapCallback(data, uri, port, false, data.connectCallback).onConnectCompleted(e, null)); checkedReturnValue.setComplete(socket) .setCallback((e, successfulSocket) -> { if (successfulSocket == null) return; // SimpleFuture.setComplete(Future) returns a future as to whether // the completion was successful, or the future has been cancelled, // thus the completion failed. // The exception value will only ever be a CancellationException. if (e == null) { // handle successes here (wrap the callback) wrapCallback(data, uri, port, false, data.connectCallback).onConnectCompleted(null, successfulSocket); return; } data.request.logd("Recycling extra socket leftover from cancelled operation"); idleSocket(successfulSocket); recycleSocket(successfulSocket, data.request); }); return checkedReturnValue; } private ConnectionInfo getOrCreateConnectionInfo(String lookup) { ConnectionInfo info = connectionInfo.get(lookup); if (info == null) { info = new ConnectionInfo(); connectionInfo.put(lookup, info); } return info; } private void maybeCleanupConnectionInfo(String lookup) { ConnectionInfo info = connectionInfo.get(lookup); if (info == null) return; while (!info.sockets.isEmpty()) { IdleSocketHolder idleSocketHolder = info.sockets.peekLast(); AsyncSocket socket = idleSocketHolder.socket; if (idleSocketHolder.idleTime + idleTimeoutMs > System.currentTimeMillis()) break; info.sockets.pop(); // remove the callback, prevent reentrancy. socket.setClosedCallback(null); socket.close(); } if (info.openCount == 0 && info.queue.isEmpty() && info.sockets.isEmpty()) connectionInfo.remove(lookup); } private void recycleSocket(final AsyncSocket socket, AsyncHttpRequest request) { if (socket == null) return; Uri uri = request.getUri(); int port = getSchemePort(uri); final String lookup = computeLookup(uri, port, request.getProxyHost(), request.getProxyPort()); final ArrayDeque sockets; final IdleSocketHolder idleSocketHolder = new IdleSocketHolder(socket); synchronized (AsyncSocketMiddleware.this) { ConnectionInfo info = getOrCreateConnectionInfo(lookup); sockets = info.sockets; sockets.push(idleSocketHolder); } socket.setClosedCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { synchronized (AsyncSocketMiddleware.this) { sockets.remove(idleSocketHolder); maybeCleanupConnectionInfo(lookup); } } }); } private void idleSocket(final AsyncSocket socket) { // must listen for socket close, otherwise log will get spammed. socket.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { socket.setClosedCallback(null); socket.close(); } }); socket.setWriteableCallback(null); // should not get any data after this point... // if so, eat it and disconnect. socket.setDataCallback(new DataCallback.NullDataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { super.onDataAvailable(emitter, bb); bb.recycle(); socket.setClosedCallback(null); socket.close(); } }); } private void nextConnection(AsyncHttpRequest request) { Uri uri = request.getUri(); final int port = getSchemePort(uri); String key = computeLookup(uri, port, request.getProxyHost(), request.getProxyPort()); synchronized (AsyncSocketMiddleware.this) { ConnectionInfo info = connectionInfo.get(key); if (info == null) return; --info.openCount; while (info.openCount < maxConnectionCount && info.queue.size() > 0) { GetSocketData gsd = info.queue.remove(); SimpleCancellable socketCancellable = (SimpleCancellable)gsd.socketCancellable; if (socketCancellable.isCancelled()) continue; Cancellable connect = getSocket(gsd); socketCancellable.setParent(connect); } maybeCleanupConnectionInfo(key); } } protected boolean isKeepAlive(OnResponseCompleteData data) { return HttpUtil.isKeepAlive(data.response.protocol(), data.response.headers()) && HttpUtil.isKeepAlive(Protocol.HTTP_1_1, data.request.getHeaders()); } @Override public void onResponseComplete(final OnResponseCompleteData data) { if (data.state.get("socket-owner") != this) return; try { idleSocket(data.socket); if (data.exception != null || !data.socket.isOpen()) { data.request.logv("closing out socket (exception)"); data.socket.setClosedCallback(null); data.socket.close(); return; } if (!isKeepAlive(data)) { data.request.logv("closing out socket (not keep alive)"); data.socket.setClosedCallback(null); data.socket.close(); return; } data.request.logd("Recycling keep-alive socket"); recycleSocket(data.socket, data.request); } finally { nextConnection(data.request); } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/BasicNameValuePair.java ================================================ /* * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/message/BasicNameValuePair.java $ * $Revision: 604625 $ * $Date: 2007-12-16 06:11:11 -0800 (Sun, 16 Dec 2007) $ * * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * . * */ package com.jeffmony.async.http; import android.text.TextUtils; /** * A simple class encapsulating an attribute/value pair. *

* This class comforms to the generic grammar and formatting rules outlined in the * Section 2.2 * and * Section 3.6 * of RFC 2616 *

* 2.2 Basic Rules *

* The following rules are used throughout this specification to describe basic parsing constructs. * The US-ASCII coded character set is defined by ANSI X3.4-1986. *

*
 *     OCTET          = 
 *     CHAR           = 
 *     UPALPHA        = 
 *     LOALPHA        = 
 *     ALPHA          = UPALPHA | LOALPHA
 *     DIGIT          = 
 *     CTL            = 
 *     CR             = 
 *     LF             = 
 *     SP             = 
 *     HT             = 
 *     <">            = 
 * 
*

* Many HTTP/1.1 header field values consist of words separated by LWS or special * characters. These special characters MUST be in a quoted string to be used within * a parameter value (as defined in section 3.6). *

*

 * token          = 1*
 * separators     = "(" | ")" | "<" | ">" | "@"
 *                | "," | ";" | ":" | "\" | <">
 *                | "/" | "[" | "]" | "?" | "="
 *                | "{" | "}" | SP | HT
 * 
*

* A string of text is parsed as a single word if it is quoted using double-quote marks. *

*
 * quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
 * qdtext         = >
 * 
*

* The backslash character ("\") MAY be used as a single-character quoting mechanism only * within quoted-string and comment constructs. *

*
 * quoted-pair    = "\" CHAR
 * 
* 3.6 Transfer Codings *

* Parameters are in the form of attribute/value pairs. *

*
 * parameter               = attribute "=" value
 * attribute               = token
 * value                   = token | quoted-string
 * 
* * @author Oleg Kalnichevski * */ public class BasicNameValuePair implements NameValuePair, Cloneable { private final String name; private final String value; /** * Default Constructor taking a name and a value. The value may be null. * * @param name The name. * @param value The value. */ public BasicNameValuePair(final String name, final String value) { super(); if (name == null) { throw new IllegalArgumentException("Name may not be null"); } this.name = name; this.value = value; } /** * Returns the name. * * @return String name The name */ public String getName() { return this.name; } /** * Returns the value. * * @return String value The current value. */ public String getValue() { return this.value; } /** * Get a string representation of this pair. * * @return A string representation. */ public String toString() { return name + "=" + value; } public boolean equals(final Object object) { if (object == null) return false; if (this == object) return true; if (object instanceof NameValuePair) { BasicNameValuePair that = (BasicNameValuePair) object; return this.name.equals(that.name) && TextUtils.equals(this.value, that.value); } else { return false; } } public int hashCode() { return name.hashCode() ^ value.hashCode(); } public Object clone() throws CloneNotSupportedException { return super.clone(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/BodyDecoderException.java ================================================ package com.jeffmony.async.http; public class BodyDecoderException extends Exception { public BodyDecoderException(String message) { super(message); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/ConnectionClosedException.java ================================================ package com.jeffmony.async.http; public class ConnectionClosedException extends Exception { public ConnectionClosedException(String message) { super(message); } public ConnectionClosedException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/ConnectionFailedException.java ================================================ package com.jeffmony.async.http; public class ConnectionFailedException extends Exception { public ConnectionFailedException(String message) { super(message); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/Headers.java ================================================ package com.jeffmony.async.http; import android.text.TextUtils; import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Map; /** * Created by koush on 7/21/14. */ public class Headers { public Headers() { } public Headers(Map> mm) { for (String key: mm.keySet()) { addAll(key, mm.get(key)); } } final Multimap map = new Multimap() { @Override protected List newList() { return new TaggedList(); } }; public Multimap getMultiMap() { return map; } public List getAll(String header) { return map.get(header.toLowerCase(Locale.US)); } public String get(String header) { return map.getString(header.toLowerCase(Locale.US)); } public Headers set(String header, String value) { if (value != null && (value.contains("\n") || value.contains("\r"))) throw new IllegalArgumentException("value must not contain a new line or line feed"); String lc = header.toLowerCase(Locale.US); map.put(lc, value); TaggedList list = (TaggedList)map.get(lc); list.tagNull(header); return this; } public Headers add(String header, String value) { String lc = header.toLowerCase(Locale.US); map.add(lc, value); TaggedList list = (TaggedList)map.get(lc); list.tagNull(header); return this; } public Headers addLine(String line) { if (line != null) { line = line.trim(); String[] parts = line.split(":", 2); if (parts.length == 2) add(parts[0].trim(), parts[1].trim()); else add(parts[0].trim(), ""); } return this; } public Headers addAll(String header, List values) { for (String v: values) { add(header, v); } return this; } public Headers addAll(Map> m) { for (String key: m.keySet()) { for (String value: m.get(key)) { add(key, value); } } return this; } public Headers addAllMap(Map m) { for (String key: m.keySet()) { add(key, m.get(key)); } return this; } public Headers addAll(Headers headers) { // safe to addall since this is another Headers object map.putAll(headers.map); return this; } public List removeAll(String header) { return map.remove(header.toLowerCase(Locale.US)); } public String remove(String header) { List r = removeAll(header.toLowerCase(Locale.US)); if (r == null || r.size() == 0) return null; return r.get(0); } public Headers removeAll(Collection headers) { for (String header: headers) { remove(header); } return this; } public StringBuilder toStringBuilder() { StringBuilder result = new StringBuilder(256); for (String key: map.keySet()) { TaggedList list = (TaggedList)map.get(key); for (String v: list) { result.append((String)list.tag()) .append(": ") .append(v) .append("\r\n"); } } result.append("\r\n"); return result; } @Override public String toString() { return toStringBuilder().toString(); } public String toPrefixString(String prefix) { return toStringBuilder() .insert(0, prefix + "\r\n") .toString(); } public static Headers parse(String payload) { String[] lines = payload.split("\n"); Headers headers = new Headers(); for (String line: lines) { line = line.trim(); if (TextUtils.isEmpty(line)) continue; headers.addLine(line); } return headers; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/HttpDate.java ================================================ /* * 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 com.jeffmony.async.http; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.TimeZone; /** * Best-effort parser for HTTP dates. */ public final class HttpDate { /** * Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such * cookies are on the fast path. */ private static final ThreadLocal STANDARD_DATE_FORMAT = new ThreadLocal() { @Override protected DateFormat initialValue() { DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); rfc1123.setTimeZone(TimeZone.getTimeZone("UTC")); return rfc1123; } }; /** * If we fail to parse a date in a non-standard format, try each of these formats in sequence. */ private static final String[] BROWSER_COMPATIBLE_DATE_FORMATS = new String[] { /* This list comes from {@code org.apache.http.impl.cookie.BrowserCompatSpec}. */ "EEEE, dd-MMM-yy HH:mm:ss zzz", // RFC 1036 "EEE MMM d HH:mm:ss yyyy", // ANSI C asctime() "EEE, dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MMM-yyyy HH-mm-ss z", "EEE, dd MMM yy HH:mm:ss z", "EEE dd-MMM-yyyy HH:mm:ss z", "EEE dd MMM yyyy HH:mm:ss z", "EEE dd-MMM-yyyy HH-mm-ss z", "EEE dd-MMM-yy HH:mm:ss z", "EEE dd MMM yy HH:mm:ss z", "EEE,dd-MMM-yy HH:mm:ss z", "EEE,dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MM-yyyy HH:mm:ss z", /* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */ "EEE MMM d yyyy HH:mm:ss z", }; /** * Returns the date for {@code value}. Returns null if the value couldn't be * parsed. */ public static Date parse(String value) { if (value == null) return null; try { return STANDARD_DATE_FORMAT.get().parse(value); } catch (ParseException ignore) { } for (String formatString : BROWSER_COMPATIBLE_DATE_FORMATS) { try { return new SimpleDateFormat(formatString, Locale.US).parse(value); } catch (ParseException ignore) { } } return null; } /** * Returns the string for {@code value}. */ public static String format(Date value) { return STANDARD_DATE_FORMAT.get().format(value); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/HttpTransportMiddleware.java ================================================ package com.jeffmony.async.http; import android.text.TextUtils; import com.jeffmony.async.AsyncSocket; import com.jeffmony.async.BufferedDataSink; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.LineEmitter; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.http.body.AsyncHttpRequestBody; import com.jeffmony.async.http.filter.ChunkedOutputFilter; import java.io.IOException; /** * Created by koush on 7/24/14. */ public class HttpTransportMiddleware extends SimpleMiddleware { @Override public boolean exchangeHeaders(final OnExchangeHeaderData data) { Protocol p = Protocol.get(data.protocol); if (p != null && p != Protocol.HTTP_1_0 && p != Protocol.HTTP_1_1) return super.exchangeHeaders(data); AsyncHttpRequest request = data.request; AsyncHttpRequestBody requestBody = data.request.getBody(); if (requestBody != null) { if (requestBody.length() >= 0) { request.getHeaders().set("Content-Length", String.valueOf(requestBody.length())); data.response.sink(data.socket); } else if ("close".equals(request.getHeaders().get("Connection"))) { data.response.sink(data.socket); } else { request.getHeaders().set("Transfer-Encoding", "Chunked"); data.response.sink(new ChunkedOutputFilter(data.socket)); } } String rl = request.getRequestLine().toString(); String rs = request.getHeaders().toPrefixString(rl); byte[] rsBytes = rs.getBytes(); // try to get the request body in the same packet as the request headers... if it will fit // in the max MTU (1540 or whatever). final boolean waitForBody = requestBody != null && requestBody.length() >= 0 && requestBody.length() + rsBytes.length < 1024; final BufferedDataSink bsink; final DataSink headerSink; if (waitForBody) { // force buffering of headers bsink = new BufferedDataSink(data.response.sink()); bsink.forceBuffering(true); data.response.sink(bsink); headerSink = bsink; } else { bsink = null; headerSink = data.socket; } request.logv("\n" + rs); final CompletedCallback sentCallback = data.sendHeadersCallback; Util.writeAll(headerSink, rsBytes, new CompletedCallback() { @Override public void onCompleted(Exception ex) { Util.end(sentCallback, ex); // flush headers and any request body that was written by the callback if (bsink != null) { bsink.forceBuffering(false); bsink.setMaxBuffer(0); } } }); LineEmitter.StringCallback headerCallback = new LineEmitter.StringCallback() { Headers mRawHeaders = new Headers(); String statusLine; @Override public void onStringAvailable(String s) { try { s = s.trim(); if (statusLine == null) { statusLine = s; } else if (!TextUtils.isEmpty(s)) { mRawHeaders.addLine(s); } else { String[] parts = statusLine.split(" ", 3); if (parts.length < 2) throw new Exception(new IOException("Not HTTP")); data.response.headers(mRawHeaders); String protocol = parts[0]; data.response.protocol(protocol); data.response.code(Integer.parseInt(parts[1])); data.response.message(parts.length == 3 ? parts[2] : ""); data.receiveHeadersCallback.onCompleted(null); // socket may get detached after headers (websocket) AsyncSocket socket = data.response.socket(); if (socket == null) return; DataEmitter emitter; // HEAD requests must not return any data. They still may // return content length, etc, which will confuse the body decoder if (!data.request.hasBody()) { emitter = HttpUtil.EndEmitter.create(socket.getServer(), null); } else if (responseIsEmpty(data.response.code())) { emitter = HttpUtil.EndEmitter.create(socket.getServer(), null); } else { emitter = HttpUtil.getBodyDecoder(socket, Protocol.get(protocol), mRawHeaders, false); } data.response.emitter(emitter); } } catch (Exception ex) { data.receiveHeadersCallback.onCompleted(ex); } } }; LineEmitter liner = new LineEmitter(); data.socket.setDataCallback(liner); liner.setLineCallback(headerCallback); return true; } static boolean responseIsEmpty(int code) { return (code >= 100 && code <= 199) || code == 204 || code == 304; } @Override public void onRequestSent(OnRequestSentData data) { Protocol p = Protocol.get(data.protocol); if (p != null && p != Protocol.HTTP_1_0 && p != Protocol.HTTP_1_1) return; if (data.response.sink() instanceof ChunkedOutputFilter) data.response.sink().end(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/HttpUtil.java ================================================ package com.jeffmony.async.http; import com.jeffmony.async.AsyncServer; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.FilteredDataEmitter; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.http.body.AsyncHttpRequestBody; import com.jeffmony.async.http.body.JSONObjectBody; import com.jeffmony.async.http.body.MultipartFormDataBody; import com.jeffmony.async.http.body.StringBody; import com.jeffmony.async.http.body.UrlEncodedFormBody; import com.jeffmony.async.http.filter.ChunkedInputFilter; import com.jeffmony.async.http.filter.ContentLengthFilter; import com.jeffmony.async.http.filter.GZIPInputFilter; import com.jeffmony.async.http.filter.InflaterInputFilter; public class HttpUtil { public static AsyncHttpRequestBody getBody(DataEmitter emitter, CompletedCallback reporter, Headers headers) { String contentType = headers.get("Content-Type"); if (contentType != null) { String[] values = contentType.split(";"); for (int i = 0; i < values.length; i++) { values[i] = values[i].trim(); } for (String ct: values) { if (UrlEncodedFormBody.CONTENT_TYPE.equals(ct)) { return new UrlEncodedFormBody(); } if (JSONObjectBody.CONTENT_TYPE.equals(ct)) { return new JSONObjectBody(); } if (StringBody.CONTENT_TYPE.equals(ct)) { return new StringBody(); } if (ct != null && ct.startsWith(MultipartFormDataBody.PRIMARY_TYPE)) { return new MultipartFormDataBody(contentType); } } } return null; } static class EndEmitter extends FilteredDataEmitter { private EndEmitter() { } public static EndEmitter create(AsyncServer server, final Exception e) { final EndEmitter ret = new EndEmitter(); // don't need to worry about any race conditions with post and this return value // since we are in the server thread. server.post(new Runnable() { @Override public void run() { ret.report(e); } }); return ret; } } public static DataEmitter getBodyDecoder(DataEmitter emitter, Protocol protocol, Headers headers, boolean server) { long _contentLength = -1; try { String header = headers.get("Content-Length"); if (header != null) _contentLength = Long.parseLong(header); } catch (NumberFormatException ex) { } final long contentLength = _contentLength; if (-1 != contentLength) { if (contentLength < 0) { EndEmitter ender = EndEmitter.create(emitter.getServer(), new BodyDecoderException("not using chunked encoding, and no content-length found.")); ender.setDataEmitter(emitter); emitter = ender; return emitter; } if (contentLength == 0) { EndEmitter ender = EndEmitter.create(emitter.getServer(), null); ender.setDataEmitter(emitter); emitter = ender; return emitter; } ContentLengthFilter contentLengthWatcher = new ContentLengthFilter(contentLength); contentLengthWatcher.setDataEmitter(emitter); emitter = contentLengthWatcher; } else if ("chunked".equalsIgnoreCase(headers.get("Transfer-Encoding"))) { ChunkedInputFilter chunker = new ChunkedInputFilter(); chunker.setDataEmitter(emitter); emitter = chunker; } else if (server) { // if this is the server, and the client has not indicated a request body, the client is done EndEmitter ender = EndEmitter.create(emitter.getServer(), null); ender.setDataEmitter(emitter); emitter = ender; return emitter; } if ("gzip".equals(headers.get("Content-Encoding"))) { GZIPInputFilter gunzipper = new GZIPInputFilter(); gunzipper.setDataEmitter(emitter); emitter = gunzipper; } else if ("deflate".equals(headers.get("Content-Encoding"))) { InflaterInputFilter inflater = new InflaterInputFilter(); inflater.setDataEmitter(emitter); emitter = inflater; } // conversely, if this is the client (http 1.0), and the server has not indicated a request body, we do not report // the close/end event until the server actually closes the connection. return emitter; } public static boolean isKeepAlive(Protocol protocol, Headers headers) { // connection is always keep alive as this is an http/1.1 client String connection = headers.get("Connection"); if (connection == null) return protocol == Protocol.HTTP_1_1; return "keep-alive".equalsIgnoreCase(connection); } public static boolean isKeepAlive(String protocol, Headers headers) { // connection is always keep alive as this is an http/1.1 client String connection = headers.get("Connection"); if (connection == null) return Protocol.get(protocol) == Protocol.HTTP_1_1; return "keep-alive".equalsIgnoreCase(connection); } public static long contentLength(Headers headers) { String cl = headers.get("Content-Length"); if (cl == null) return -1; try { return Long.parseLong(cl); } catch (NumberFormatException e) { return -1; } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/HybiParser.java ================================================ // // HybiParser.java: draft-ietf-hybi-thewebsocketprotocol-13 parser // // Based on code from the faye project. // https://github.com/faye/faye-websocket-node // Copyright (c) 2009-2012 James Coglan // // Ported from Javascript to Java by Eric Butler // // (The MIT License) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.jeffmony.async.http; import android.util.Log; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataEmitterReader; import com.jeffmony.async.callback.DataCallback; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.List; import java.util.zip.DataFormatException; import java.util.zip.Inflater; abstract class HybiParser { private static final String TAG = "HybiParser"; private boolean mMasking = true; private boolean mDeflate = false; private int mStage; private boolean mFinal; private boolean mMasked; private boolean mDeflated; private int mOpcode; private int mLengthSize; private int mLength; private int mMode; private byte[] mMask = new byte[0]; private byte[] mPayload = new byte[0]; private boolean mClosed = false; private ByteArrayOutputStream mBuffer = new ByteArrayOutputStream(); private Inflater mInflater = new Inflater(true); private byte[] mInflateBuffer = new byte[4096]; private static final int BYTE = 255; private static final int FIN = 128; private static final int MASK = 128; private static final int RSV1 = 64; private static final int RSV2 = 32; private static final int RSV3 = 16; private static final int OPCODE = 15; private static final int LENGTH = 127; private static final int MODE_TEXT = 1; private static final int MODE_BINARY = 2; private static final int OP_CONTINUATION = 0; private static final int OP_TEXT = 1; private static final int OP_BINARY = 2; private static final int OP_CLOSE = 8; private static final int OP_PING = 9; private static final int OP_PONG = 10; private static final List OPCODES = Arrays.asList( OP_CONTINUATION, OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG ); private static final List FRAGMENTED_OPCODES = Arrays.asList( OP_CONTINUATION, OP_TEXT, OP_BINARY ); // // public HybiParser(WebSocketClient client) { // mClient = client; // } private static byte[] mask(byte[] payload, byte[] mask, int offset) { if (mask.length == 0) return payload; for (int i = 0; i < payload.length - offset; i++) { payload[offset + i] = (byte) (payload[offset + i] ^ mask[i % 4]); } return payload; } private byte[] inflate(byte[] payload) throws DataFormatException { ByteArrayOutputStream inflated = new ByteArrayOutputStream(); mInflater.setInput(payload); while (!mInflater.needsInput()) { int chunkSize = mInflater.inflate(mInflateBuffer); inflated.write(mInflateBuffer, 0, chunkSize); } mInflater.setInput(new byte[] { 0, 0, -1, -1 }); while (!mInflater.needsInput()) { int chunkSize = mInflater.inflate(mInflateBuffer); inflated.write(mInflateBuffer, 0, chunkSize); } return inflated.toByteArray(); } public void setMasking(boolean masking) { mMasking = masking; } public void setDeflate(boolean deflate) { mDeflate = deflate; } DataCallback mStage0 = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { try { parseOpcode(bb.get()); } catch (ProtocolError e) { report(e); e.printStackTrace(); } parse(); } }; DataCallback mStage1 = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { parseLength(bb.get()); parse(); } }; DataCallback mStage2 = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { byte[] bytes = new byte[mLengthSize]; bb.get(bytes); try { parseExtendedLength(bytes); } catch (ProtocolError e) { report(e); e.printStackTrace(); } parse(); } }; DataCallback mStage3 = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { mMask = new byte[4]; bb.get(mMask); mStage = 4; parse(); } }; DataCallback mStage4 = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { assert bb.remaining() == mLength; mPayload = new byte[mLength]; bb.get(mPayload); try { emitFrame(); } catch (IOException e) { report(e); e.printStackTrace(); } mStage = 0; parse(); } }; void parse() { switch (mStage) { case 0: mReader.read(1, mStage0); break; case 1: mReader.read(1, mStage1); break; case 2: mReader.read(mLengthSize, mStage2); break; case 3: mReader.read(4, mStage3); break; case 4: mReader.read(mLength, mStage4); break; } } private DataEmitterReader mReader = new DataEmitterReader(); private static final long BASE = 2; private static final long _2_TO_8_ = BASE << 7; private static final long _2_TO_16_ = BASE << 15; private static final long _2_TO_24 = BASE << 23; private static final long _2_TO_32_ = BASE << 31; private static final long _2_TO_40_ = BASE << 39; private static final long _2_TO_48_ = BASE << 47; private static final long _2_TO_56_ = BASE << 55; public HybiParser(DataEmitter socket) { socket.setDataCallback(mReader); parse(); } private void parseOpcode(byte data) throws ProtocolError { boolean rsv1 = (data & RSV1) == RSV1; boolean rsv2 = (data & RSV2) == RSV2; boolean rsv3 = (data & RSV3) == RSV3; if ((!mDeflate && rsv1) || rsv2 || rsv3) { throw new ProtocolError("RSV not zero"); } mFinal = (data & FIN) == FIN; mOpcode = (data & OPCODE); mDeflated = rsv1; mMask = new byte[0]; mPayload = new byte[0]; if (!OPCODES.contains(mOpcode)) { throw new ProtocolError("Bad opcode"); } if (!FRAGMENTED_OPCODES.contains(mOpcode) && !mFinal) { throw new ProtocolError("Expected non-final packet"); } mStage = 1; } private void parseLength(byte data) { mMasked = (data & MASK) == MASK; mLength = (data & LENGTH); if (mLength >= 0 && mLength <= 125) { mStage = mMasked ? 3 : 4; } else { mLengthSize = (mLength == 126) ? 2 : 8; mStage = 2; } } private void parseExtendedLength(byte[] buffer) throws ProtocolError { mLength = getInteger(buffer); mStage = mMasked ? 3 : 4; } public byte[] frame(String data) { return frame(OP_TEXT, data, -1); } public byte[] frame(byte[] data) { return frame(OP_BINARY, data, -1); } public byte[] frame(byte[] data, int offset, int length) { return frame(OP_BINARY, data, -1, offset, length); } public byte[] pingFrame(String data) { return frame(OP_PING, data, -1); } public byte[] pongFrame(String data) { return frame(OP_PONG, data, -1); } /** * Flip the opcode so to avoid the name collision with the public method * * @param opcode * @param data * @param errorCode * @return */ private byte[] frame(int opcode, byte[] data, int errorCode) { return frame(opcode, data, errorCode, 0, data.length); } /** * Don't actually need the flipped method signature, trying to keep it in line with the byte[] version * * @param opcode * @param data * @param errorCode * @return */ private byte[] frame(int opcode, String data, int errorCode) { return frame(opcode, decode(data), errorCode); } private byte[] frame(int opcode, byte [] data, int errorCode, int dataOffset, int dataLength) { if (mClosed) return null; // Log.d(TAG, "Creating frame for: " + data + " op: " + opcode + " err: " + errorCode); byte[] buffer = data; int insert = (errorCode > 0) ? 2 : 0; int length = dataLength + insert - dataOffset; int header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10); int offset = header + (mMasking ? 4 : 0); int masked = mMasking ? MASK : 0; byte[] frame = new byte[length + offset]; frame[0] = (byte) ((byte)FIN | (byte)opcode); if (length <= 125) { frame[1] = (byte) (masked | length); } else if (length <= 65535) { frame[1] = (byte) (masked | 126); frame[2] = (byte) (length / 256); frame[3] = (byte) (length & BYTE); } else { frame[1] = (byte) (masked | 127); frame[2] = (byte) (( length / _2_TO_56_) & BYTE); frame[3] = (byte) (( length / _2_TO_48_) & BYTE); frame[4] = (byte) (( length / _2_TO_40_) & BYTE); frame[5] = (byte) (( length / _2_TO_32_) & BYTE); frame[6] = (byte) (( length / _2_TO_24) & BYTE); frame[7] = (byte) (( length / _2_TO_16_) & BYTE); frame[8] = (byte) (( length / _2_TO_8_) & BYTE); frame[9] = (byte) (length & BYTE); } if (errorCode > 0) { frame[offset] = (byte) ((errorCode / 256) & BYTE); frame[offset+1] = (byte) (errorCode & BYTE); } System.arraycopy(buffer, dataOffset, frame, offset + insert, dataLength - dataOffset); if (mMasking) { byte[] mask = { (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256) }; System.arraycopy(mask, 0, frame, header, mask.length); mask(frame, mask, offset); } return frame; } public void close(int code, String reason) { if (mClosed) return; sendFrame(frame(OP_CLOSE, reason, code)); mClosed = true; } private void emitFrame() throws IOException { byte[] payload = mask(mPayload, mMask, 0); if (mDeflated) { try { payload = inflate(payload); } catch (DataFormatException e) { throw new IOException("Invalid deflated data"); } } int opcode = mOpcode; if (opcode == OP_CONTINUATION) { if (mMode == 0) { throw new ProtocolError("Mode was not set."); } mBuffer.write(payload); if (mFinal) { byte[] message = mBuffer.toByteArray(); if (mMode == MODE_TEXT) { onMessage(encode(message)); } else { onMessage(message); } reset(); } } else if (opcode == OP_TEXT) { if (mFinal) { String messageText = encode(payload); onMessage(messageText); } else { mMode = MODE_TEXT; mBuffer.write(payload); } } else if (opcode == OP_BINARY) { if (mFinal) { onMessage(payload); } else { mMode = MODE_BINARY; mBuffer.write(payload); } } else if (opcode == OP_CLOSE) { int code = (payload.length >= 2) ? 256 * (payload[0] & 0xFF) + (payload[1] & 0xFF) : 0; String reason = (payload.length > 2) ? encode(slice(payload, 2)) : null; // Log.d(TAG, "Got close op! " + code + " " + reason); onDisconnect(code, reason); } else if (opcode == OP_PING) { if (payload.length > 125) { throw new ProtocolError("Ping payload too large"); } // Log.d(TAG, "Sending pong!!"); String message = encode(payload); sendFrame(frame(OP_PONG, payload, -1)); onPing(message); } else if (opcode == OP_PONG) { String message = encode(payload); onPong(message); // Log.d(TAG, "Got pong! " + message); } } protected abstract void onMessage(byte[] payload); protected abstract void onMessage(String payload); protected abstract void onPong(String payload); protected abstract void onPing(String payload); protected abstract void onDisconnect(int code, String reason); protected abstract void report(Exception ex); protected abstract void sendFrame(byte[] frame); private void reset() { mMode = 0; mBuffer.reset(); } private String encode(byte[] buffer) { try { return new String(buffer, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } private byte[] decode(String string) { try { return (string).getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } private int getInteger(byte[] bytes) throws ProtocolError { long i = byteArrayToLong(bytes, 0, bytes.length); if (i < 0 || i > Integer.MAX_VALUE) { throw new ProtocolError("Bad integer: " + i); } return (int) i; } private byte[] slice(byte[] array, int start) { byte[] copy = new byte[array.length - start]; System.arraycopy(array, start, copy, 0, array.length - start); return copy; } @Override protected void finalize() throws Throwable { Inflater inflater = mInflater; if (inflater != null) { try { inflater.end(); } catch (Exception e) { Log.e(TAG, "inflater.end failed", e); } } super.finalize(); } public static class ProtocolError extends IOException { public ProtocolError(String detailMessage) { super(detailMessage); } } private static long byteArrayToLong(byte[] b, int offset, int length) { if (b.length < length) throw new IllegalArgumentException("length must be less than or equal to b.length"); long value = 0; for (int i = 0; i < length; i++) { int shift = (length - 1 - i) * 8; value += (b[i + offset] & 0x000000FF) << shift; } return value; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/Multimap.java ================================================ package com.jeffmony.async.http; import android.net.Uri; import android.text.TextUtils; import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Created by koush on 5/27/13. */ public class Multimap extends LinkedHashMap> implements Iterable { public Multimap() { } protected List newList() { return new ArrayList(); } public String getString(String name) { List ret = get(name); if (ret == null || ret.size() == 0) return null; return ret.get(0); } public String getAllString(String name, String delimiter) { List ret = get(name); if (ret == null || ret.size() == 0) return null; StringBuilder builder = new StringBuilder(); boolean first = true; for (String value: ret) { if (!first) builder.append(delimiter); builder.append(value); first = false; } return builder.toString(); } public List ensure(String name) { List ret = get(name); if (ret == null) { ret = newList(); put(name, ret); } return ret; } public void add(String name, String value) { ensure(name).add(value); } public void put(String name, String value) { List ret = newList(); ret.add(value); put(name, ret); } public Multimap(List pairs) { for (NameValuePair pair: pairs) add(pair.getName(), pair.getValue()); } public Multimap(Multimap m) { putAll(m); } public interface StringDecoder { public String decode(String s); } public static Multimap parse(String value, String delimiter, boolean unquote, StringDecoder decoder) { return parse(value, delimiter, "=", unquote, decoder); } public static Multimap parse(String value, String delimiter, String assigner, boolean unquote, StringDecoder decoder) { Multimap map = new Multimap(); if (value == null) return map; String[] parts = value.split(delimiter); for (String part: parts) { String[] pair = part.split(assigner, 2); String key = pair[0].trim(); // watch for empty string or trailing delimiter if (TextUtils.isEmpty(key)) continue; String v = null; if (pair.length > 1) v = pair[1]; if (v != null && unquote && v.endsWith("\"") && v.startsWith("\"")) v = v.substring(1, v.length() - 1); if (v != null && decoder != null) { key = decoder.decode(key); v = decoder.decode(v); } map.add(key, v); } return map; } public static Multimap parseSemicolonDelimited(String header) { return parse(header, ";", true, null); } public static Multimap parseCommaDelimited(String header) { return parse(header, ",", true, null); } public static final StringDecoder QUERY_DECODER = new StringDecoder() { @Override public String decode(String s) { return Uri.decode(s); } }; public static Multimap parseQuery(String query) { return parse(query, "&", false, QUERY_DECODER); } public static final StringDecoder URL_DECODER = new StringDecoder() { @Override public String decode(String s) { return URLDecoder.decode(s); } }; public static Multimap parseUrlEncoded(String query) { return parse(query, "&", false, URL_DECODER); } @Override public Iterator iterator() { ArrayList ret = new ArrayList(); for (String name: keySet()) { List values = get(name); for (String value: values) { ret.add(new BasicNameValuePair(name, value)); } } return ret.iterator(); } public Map toSingleMap() { HashMap ret = new HashMap<>(); for (String key: keySet()) { ret.put(key, getString(key)); } return ret; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/NameValuePair.java ================================================ /* * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/NameValuePair.java $ * $Revision: 496070 $ * $Date: 2007-01-14 04:18:34 -0800 (Sun, 14 Jan 2007) $ * * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * . * */ package com.jeffmony.async.http; /** * A simple class encapsulating an attribute/value pair. *

* This class comforms to the generic grammar and formatting rules outlined in the * Section 2.2 * and * Section 3.6 * of RFC 2616 *

* 2.2 Basic Rules *

* The following rules are used throughout this specification to describe basic parsing constructs. * The US-ASCII coded character set is defined by ANSI X3.4-1986. *

*
 *     OCTET          = 
 *     CHAR           = 
 *     UPALPHA        = 
 *     LOALPHA        = 
 *     ALPHA          = UPALPHA | LOALPHA
 *     DIGIT          = 
 *     CTL            = 
 *     CR             = 
 *     LF             = 
 *     SP             = 
 *     HT             = 
 *     <">            = 
 * 
*

* Many HTTP/1.1 header field values consist of words separated by LWS or special * characters. These special characters MUST be in a quoted string to be used within * a parameter value (as defined in section 3.6). *

*

 * token          = 1*
 * separators     = "(" | ")" | "<" | ">" | "@"
 *                | "," | ";" | ":" | "\" | <">
 *                | "/" | "[" | "]" | "?" | "="
 *                | "{" | "}" | SP | HT
 * 
*

* A string of text is parsed as a single word if it is quoted using double-quote marks. *

*
 * quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
 * qdtext         = >
 * 
*

* The backslash character ("\") MAY be used as a single-character quoting mechanism only * within quoted-string and comment constructs. *

*
 * quoted-pair    = "\" CHAR
 * 
* 3.6 Transfer Codings *

* Parameters are in the form of attribute/value pairs. *

*
 * parameter               = attribute "=" value
 * attribute               = token
 * value                   = token | quoted-string
 * 
* * @author Oleg Kalnichevski * */ public interface NameValuePair { String getName(); String getValue(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/Protocol.java ================================================ package com.jeffmony.async.http; import java.util.Hashtable; import java.util.Locale; /** * Protocols that OkHttp implements for NPN and * ALPN * selection. *

*

Protocol vs Scheme

* Despite its name, {@link java.net.URL#getProtocol()} returns the * {@linkplain java.net.URI#getScheme() scheme} (http, https, etc.) of the URL, not * the protocol (http/1.1, spdy/3.1, etc.). OkHttp uses the word protocol * to identify how HTTP messages are framed. */ public enum Protocol { /** * An obsolete plaintext framing that does not use persistent sockets by * default. */ HTTP_1_0("http/1.0"), /** * A plaintext framing that includes persistent connections. *

*

This version of OkHttp implements RFC 2616, and tracks * revisions to that spec. */ HTTP_1_1("http/1.1"), /** * Chromium's binary-framed protocol that includes header compression, * multiplexing multiple requests on the same socket, and server-push. * HTTP/1.1 semantics are layered on SPDY/3. *

*

This version of OkHttp implements SPDY 3 draft * 3.1. Future releases of OkHttp may use this identifier for a newer draft * of the SPDY spec. */ SPDY_3("spdy/3.1") { @Override public boolean needsSpdyConnection() { return true; } }, /** * The IETF's binary-framed protocol that includes header compression, * multiplexing multiple requests on the same socket, and server-push. * HTTP/1.1 semantics are layered on HTTP/2. *

*

This version of OkHttp implements HTTP/2 draft 12 * with HPACK draft * 6. Future releases of OkHttp may use this identifier for a newer draft * of these specs. */ HTTP_2("h2-13") { @Override public boolean needsSpdyConnection() { return true; } }; private final String protocol; private static final Hashtable protocols = new Hashtable(); static { protocols.put(HTTP_1_0.toString(), HTTP_1_0); protocols.put(HTTP_1_1.toString(), HTTP_1_1); protocols.put(SPDY_3.toString(), SPDY_3); protocols.put(HTTP_2.toString(), HTTP_2); } Protocol(String protocol) { this.protocol = protocol; } /** * Returns the protocol identified by {@code protocol}. */ public static Protocol get(String protocol) { if (protocol == null) return null; return protocols.get(protocol.toLowerCase(Locale.US)); } /** * Returns the string used to identify this protocol for ALPN and NPN, like * "http/1.1", "spdy/3.1" or "h2-13". */ @Override public String toString() { return protocol; } public boolean needsSpdyConnection() { return false; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/ProtocolVersion.java ================================================ /* * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/ProtocolVersion.java $ * $Revision: 609106 $ * $Date: 2008-01-05 01:15:42 -0800 (Sat, 05 Jan 2008) $ * * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * . * */ package com.jeffmony.async.http; import java.io.Serializable; /** * Represents a protocol version, as specified in RFC 2616. * RFC 2616 specifies only HTTP versions, like "HTTP/1.1" and "HTTP/1.0". * RFC 3261 specifies a message format that is identical to HTTP except * for the protocol name. It defines a protocol version "SIP/2.0". * There are some nitty-gritty differences between the interpretation * of versions in HTTP and SIP. In those cases, HTTP takes precedence. *

* This class defines a protocol version as a combination of * protocol name, major version number, and minor version number. * Note that {@link #equals} and {@link #hashCode} are defined as * final here, they cannot be overridden in derived classes. * * @author Oleg Kalnichevski * @author Roland Weber * * @version $Revision: 609106 $ */ public class ProtocolVersion implements Serializable, Cloneable { private static final long serialVersionUID = 8950662842175091068L; /** Name of the protocol. */ protected final String protocol; /** Major version number of the protocol */ protected final int major; /** Minor version number of the protocol */ protected final int minor; /** * Create a protocol version designator. * * @param protocol the name of the protocol, for example "HTTP" * @param major the major version number of the protocol * @param minor the minor version number of the protocol */ public ProtocolVersion(String protocol, int major, int minor) { if (protocol == null) { throw new IllegalArgumentException ("Protocol name must not be null."); } if (major < 0) { throw new IllegalArgumentException ("Protocol major version number must not be negative."); } if (minor < 0) { throw new IllegalArgumentException ("Protocol minor version number may not be negative"); } this.protocol = protocol; this.major = major; this.minor = minor; } /** * Returns the name of the protocol. * * @return the protocol name */ public final String getProtocol() { return protocol; } /** * Returns the major version number of the protocol. * * @return the major version number. */ public final int getMajor() { return major; } /** * Returns the minor version number of the HTTP protocol. * * @return the minor version number. */ public final int getMinor() { return minor; } /** * Obtains a specific version of this protocol. * This can be used by derived classes to instantiate themselves instead * of the base class, and to define constants for commonly used versions. *
* The default implementation in this class returns this * if the version matches, and creates a new {@link ProtocolVersion} * otherwise. * * @param major the major version * @param minor the minor version * * @return a protocol version with the same protocol name * and the argument version */ public ProtocolVersion forVersion(int major, int minor) { if ((major == this.major) && (minor == this.minor)) { return this; } // argument checking is done in the constructor return new ProtocolVersion(this.protocol, major, minor); } /** * Obtains a hash code consistent with {@link #equals}. * * @return the hashcode of this protocol version */ public final int hashCode() { return this.protocol.hashCode() ^ (this.major * 100000) ^ this.minor; } /** * Checks equality of this protocol version with an object. * The object is equal if it is a protocl version with the same * protocol name, major version number, and minor version number. * The specific class of the object is not relevant, * instances of derived classes with identical attributes are * equal to instances of the base class and vice versa. * * @param obj the object to compare with * * @return true if the argument is the same protocol version, * false otherwise */ public final boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof ProtocolVersion)) { return false; } ProtocolVersion that = (ProtocolVersion) obj; return ((this.protocol.equals(that.protocol)) && (this.major == that.major) && (this.minor == that.minor)); } /** * Checks whether this protocol can be compared to another one. * Only protocol versions with the same protocol name can be * {@link #compareToVersion compared}. * * @param that the protocol version to consider * * @return true if {@link #compareToVersion compareToVersion} * can be called with the argument, false otherwise */ public boolean isComparable(ProtocolVersion that) { return (that != null) && this.protocol.equals(that.protocol); } /** * Compares this protocol version with another one. * Only protocol versions with the same protocol name can be compared. * This method does not define a total ordering, as it would be * required for {@link java.lang.Comparable}. * * @param that the protocl version to compare with * * @return a negative integer, zero, or a positive integer * as this version is less than, equal to, or greater than * the argument version. * * @throws IllegalArgumentException * if the argument has a different protocol name than this object, * or if the argument is null */ public int compareToVersion(ProtocolVersion that) { if (that == null) { throw new IllegalArgumentException ("Protocol version must not be null."); } if (!this.protocol.equals(that.protocol)) { throw new IllegalArgumentException ("Versions for different protocols cannot be compared. " + this + " " + that); } int delta = getMajor() - that.getMajor(); if (delta == 0) { delta = getMinor() - that.getMinor(); } return delta; } /** * Tests if this protocol version is greater or equal to the given one. * * @param version the version against which to check this version * * @return true if this protocol version is * {@link #isComparable comparable} to the argument * and {@link #compareToVersion compares} as greater or equal, * false otherwise */ public final boolean greaterEquals(ProtocolVersion version) { return isComparable(version) && (compareToVersion(version) >= 0); } /** * Tests if this protocol version is less or equal to the given one. * * @param version the version against which to check this version * * @return true if this protocol version is * {@link #isComparable comparable} to the argument * and {@link #compareToVersion compares} as less or equal, * false otherwise */ public final boolean lessEquals(ProtocolVersion version) { return isComparable(version) && (compareToVersion(version) <= 0); } /** * Converts this protocol version to a string. * * @return a protocol version string, like "HTTP/1.1" */ public String toString() { StringBuilder buffer = new StringBuilder(); buffer.append(this.protocol); buffer.append('/'); buffer.append(Integer.toString(this.major)); buffer.append('.'); buffer.append(Integer.toString(this.minor)); return buffer.toString(); } public Object clone() throws CloneNotSupportedException { return super.clone(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/RedirectLimitExceededException.java ================================================ package com.jeffmony.async.http; public class RedirectLimitExceededException extends Exception { public RedirectLimitExceededException(String message) { super(message); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/RequestLine.java ================================================ /* * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/RequestLine.java $ * $Revision: 573864 $ * $Date: 2007-09-08 08:53:25 -0700 (Sat, 08 Sep 2007) $ * * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * . * */ package com.jeffmony.async.http; /** * The first line of an * It contains the method, URI, and HTTP version of the request. * For details, see RFC 2616. * * @author Oleg Kalnichevski * * @version $Revision: 573864 $ * * @since 4.0 */ public interface RequestLine { String getMethod(); ProtocolVersion getProtocolVersion(); String getUri(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/SSLEngineSNIConfigurator.java ================================================ package com.jeffmony.async.http; import android.os.Build; import java.lang.reflect.Field; import java.util.Hashtable; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; /** * Created by koush on 12/8/14. */ public class SSLEngineSNIConfigurator implements AsyncSSLEngineConfigurator { private static class EngineHolder implements AsyncSSLEngineConfigurator { Field peerHost; Field peerPort; Field sslParameters; Field useSni; boolean skipReflection; @Override public SSLEngine createEngine(SSLContext sslContext, String peerHost, int peerPort) { return null; } public EngineHolder(Class engineClass) { try { peerHost = engineClass.getSuperclass().getDeclaredField("peerHost"); peerHost.setAccessible(true); peerPort = engineClass.getSuperclass().getDeclaredField("peerPort"); peerPort.setAccessible(true); sslParameters = engineClass.getDeclaredField("sslParameters"); sslParameters.setAccessible(true); useSni = sslParameters.getType().getDeclaredField("useSni"); useSni.setAccessible(true); } catch (NoSuchFieldException e) { } } @Override public void configureEngine(SSLEngine engine, AsyncHttpClientMiddleware.GetSocketData data, String host, int port) { if (useSni == null || skipReflection) return; try { peerHost.set(engine, host); peerPort.set(engine, port); Object sslp = sslParameters.get(engine); useSni.set(sslp, true); } catch (IllegalAccessException e) { } } } Hashtable holders = new Hashtable(); @Override public SSLEngine createEngine(SSLContext sslContext, String peerHost, int peerPort) { // pre M, must use reflection to enable SNI, otherwise createSSLEngine(peerHost, peerPort) works. SSLEngine engine; boolean skipReflection = "GmsCore_OpenSSL".equals(sslContext.getProvider().getName()) || Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; if (skipReflection) engine = sslContext.createSSLEngine(peerHost, peerPort); else engine = sslContext.createSSLEngine(); // ensureHolder(engine).skipReflection = skipReflection; return engine; } EngineHolder ensureHolder(SSLEngine engine) { String name = engine.getClass().getCanonicalName(); EngineHolder holder = holders.get(name); if (holder == null) { holder = new EngineHolder(engine.getClass()); holders.put(name, holder); } return holder; } @Override public void configureEngine(SSLEngine engine, AsyncHttpClientMiddleware.GetSocketData data, String host, int port) { EngineHolder holder = ensureHolder(engine); holder.configureEngine(engine, data, host, port); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/SimpleMiddleware.java ================================================ package com.jeffmony.async.http; import com.jeffmony.async.future.Cancellable; public class SimpleMiddleware implements AsyncHttpClientMiddleware { @Override public void onRequest(OnRequestData data) { } @Override public Cancellable getSocket(GetSocketData data) { return null; } @Override public boolean exchangeHeaders(OnExchangeHeaderData data) { return false; } @Override public void onRequestSent(OnRequestSentData data) { } @Override public void onHeadersReceived(OnHeadersReceivedData data) { } @Override public void onBodyDecoder(OnBodyDecoderData data) { } @Override public AsyncHttpRequest onResponseReady(OnResponseReadyData data) { return null; } @Override public void onResponseComplete(OnResponseCompleteData data) { } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/TaggedList.java ================================================ package com.jeffmony.async.http; import java.util.ArrayList; public class TaggedList extends ArrayList { private Object tag; public synchronized V tag() { return (V)tag; } public synchronized void tag(V tag) { this.tag = tag; } public synchronized void tagNull(V tag) { if (this.tag == null) this.tag = tag; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/WebSocket.java ================================================ package com.jeffmony.async.http; import com.jeffmony.async.AsyncSocket; public interface WebSocket extends AsyncSocket { interface StringCallback { void onStringAvailable(String s); } interface PingCallback { void onPingReceived(String s); } interface PongCallback { void onPongReceived(String s); } void send(byte[] bytes); void send(String string); void send(byte[] bytes, int offset, int len); void ping(String message); void pong(String message); void setStringCallback(StringCallback callback); StringCallback getStringCallback(); void setPingCallback(PingCallback callback); void setPongCallback(PongCallback callback); PongCallback getPongCallback(); boolean isBuffering(); String getProtocol(); AsyncSocket getSocket(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/WebSocketHandshakeException.java ================================================ package com.jeffmony.async.http; public class WebSocketHandshakeException extends Exception { public WebSocketHandshakeException(String message) { super(message); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/WebSocketImpl.java ================================================ package com.jeffmony.async.http; import android.text.TextUtils; import android.util.Base64; import com.jeffmony.async.AsyncServer; import com.jeffmony.async.AsyncSocket; import com.jeffmony.async.BufferedDataSink; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.callback.WritableCallback; import com.jeffmony.async.http.server.AsyncHttpServerRequest; import com.jeffmony.async.http.server.AsyncHttpServerResponse; import java.nio.ByteBuffer; import java.nio.LongBuffer; import java.security.MessageDigest; import java.util.LinkedList; import java.util.UUID; public class WebSocketImpl implements WebSocket { @Override public void end() { mSocket.end(); } private static byte[] toByteArray(UUID uuid) { byte[] byteArray = new byte[(Long.SIZE / Byte.SIZE) * 2]; ByteBuffer buffer = ByteBuffer.wrap(byteArray); LongBuffer longBuffer = buffer.asLongBuffer(); longBuffer.put(new long[] { uuid.getMostSignificantBits(),uuid.getLeastSignificantBits() }); return byteArray; } private static String SHA1(String text) { try { MessageDigest md = MessageDigest.getInstance("SHA-1"); md.update(text.getBytes("iso-8859-1"), 0, text.length()); byte[] sha1hash = md.digest(); return Base64.encodeToString(sha1hash, Base64.NO_WRAP); } catch (Exception ex) { return null; } } final static String MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; private LinkedList pending; private void addAndEmit(ByteBufferList bb) { if (pending == null) { Util.emitAllData(this, bb); if (bb.remaining() > 0) { pending = new LinkedList(); pending.add(bb); } return; } while (!isPaused()) { bb = pending.remove(); Util.emitAllData(this, bb); if (bb.remaining() > 0) pending.add(0, bb); } if (pending.size() == 0) pending = null; } private void setupParser(boolean masking, boolean deflate) { mParser = new HybiParser(mSocket) { @Override protected void report(Exception ex) { if (WebSocketImpl.this.mExceptionCallback != null) WebSocketImpl.this.mExceptionCallback.onCompleted(ex); } @Override protected void onMessage(byte[] payload) { addAndEmit(new ByteBufferList(payload)); } @Override protected void onMessage(String payload) { if (WebSocketImpl.this.mStringCallback != null) WebSocketImpl.this.mStringCallback.onStringAvailable(payload); } @Override protected void onDisconnect(int code, String reason) { mSocket.close(); // if (WebSocketImpl.this.mClosedCallback != null) // WebSocketImpl.this.mClosedCallback.onCompleted(null); } @Override protected void sendFrame(byte[] frame) { mSink.write(new ByteBufferList(frame)); } @Override protected void onPing(String payload) { if (WebSocketImpl.this.mPingCallback != null) WebSocketImpl.this.mPingCallback.onPingReceived(payload); } @Override protected void onPong(String payload) { if (WebSocketImpl.this.mPongCallback != null) WebSocketImpl.this.mPongCallback.onPongReceived(payload); } }; mParser.setMasking(masking); mParser.setDeflate(deflate); if (mSocket.isPaused()) mSocket.resume(); } private AsyncSocket mSocket; BufferedDataSink mSink; public WebSocketImpl(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { this(request.getSocket()); String key = request.getHeaders().get("Sec-WebSocket-Key"); String concat = key + MAGIC; String sha1 = SHA1(concat); String origin = request.getHeaders().get("Origin"); response.code(101); response.getHeaders().set("Upgrade", "WebSocket"); response.getHeaders().set("Connection", "Upgrade"); response.getHeaders().set("Sec-WebSocket-Accept", sha1); String protocol = request.getHeaders().get("Sec-WebSocket-Protocol"); // match the protocol (sanity checking and enforcement is done in the caller) if (!TextUtils.isEmpty(protocol)) response.getHeaders().set("Sec-WebSocket-Protocol", protocol); // if (origin != null) // response.getHeaders().getHeaders().set("Access-Control-Allow-Origin", "http://" + origin); response.writeHead(); setupParser(false, false); } String protocol; @Override public String getProtocol() { return protocol; } public static void addWebSocketUpgradeHeaders(AsyncHttpRequest req, String... protocols) { Headers headers = req.getHeaders(); final String key = Base64.encodeToString(toByteArray(UUID.randomUUID()), Base64.NO_WRAP); headers.set("Sec-WebSocket-Version", "13"); headers.set("Sec-WebSocket-Key", key); headers.set("Sec-WebSocket-Extensions", "x-webkit-deflate-frame"); headers.set("Connection", "Upgrade"); headers.set("Upgrade", "websocket"); if (protocols != null) { for (String protocol: protocols) { headers.add("Sec-WebSocket-Protocol", protocol); } } headers.set("Pragma", "no-cache"); headers.set("Cache-Control", "no-cache"); if (TextUtils.isEmpty(req.getHeaders().get("User-Agent"))) req.getHeaders().set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.15 Safari/537.36"); } public WebSocketImpl(AsyncSocket socket) { mSocket = socket; mSink = new BufferedDataSink(mSocket); } public static WebSocket finishHandshake(Headers requestHeaders, AsyncHttpResponse response) { if (response == null) return null; if (response.code() != 101) return null; if (!"websocket".equalsIgnoreCase(response.headers().get("Upgrade"))) return null; String sha1 = response.headers().get("Sec-WebSocket-Accept"); if (sha1 == null) return null; String key = requestHeaders.get("Sec-WebSocket-Key"); if (key == null) return null; String concat = key + MAGIC; String expected = SHA1(concat).trim(); if (!sha1.equalsIgnoreCase(expected)) return null; String extensions = requestHeaders.get("Sec-WebSocket-Extensions"); boolean deflate = false; if (extensions != null) { if (extensions.equals("x-webkit-deflate-frame")) deflate = true; // is this right? do we want to crap out here? Commenting out // as I suspect this caused a regression. // else // return null; } WebSocketImpl ret = new WebSocketImpl(response.detachSocket()); ret.protocol = response.headers().get("Sec-WebSocket-Protocol"); ret.setupParser(true, deflate); return ret; } HybiParser mParser; @Override public void close() { mSocket.close(); } @Override public void setClosedCallback(CompletedCallback handler) { mSocket.setClosedCallback(handler); } @Override public CompletedCallback getClosedCallback() { return mSocket.getClosedCallback(); } CompletedCallback mExceptionCallback; @Override public void setEndCallback(CompletedCallback callback) { mExceptionCallback = callback; } @Override public CompletedCallback getEndCallback() { return mExceptionCallback; } @Override public void send(byte[] bytes) { getServer().post(() -> mSink.write(new ByteBufferList((mParser.frame(bytes))))); } @Override public void send(byte[] bytes, int offset, int len) { getServer().post(() -> mSink.write(new ByteBufferList(mParser.frame(bytes, offset, len)))); } @Override public void send(String string) { getServer().post(() -> mSink.write(new ByteBufferList((mParser.frame(string))))); } @Override public void ping(String string) { getServer().post(() -> mSink.write(new ByteBufferList(ByteBuffer.wrap(mParser.pingFrame(string))))); } @Override public void pong(String string) { getServer().post(() -> mSink.write(new ByteBufferList(ByteBuffer.wrap(mParser.pongFrame(string))))); } private StringCallback mStringCallback; @Override public void setStringCallback(StringCallback callback) { mStringCallback = callback; } private DataCallback mDataCallback; @Override public void setDataCallback(DataCallback callback) { mDataCallback = callback; } @Override public StringCallback getStringCallback() { return mStringCallback; } private PingCallback mPingCallback; @Override public void setPingCallback(PingCallback callback) { mPingCallback = callback; } private PongCallback mPongCallback; @Override public void setPongCallback(PongCallback callback) { mPongCallback = callback; } @Override public PongCallback getPongCallback() { return mPongCallback; } @Override public DataCallback getDataCallback() { return mDataCallback; } @Override public boolean isOpen() { return mSocket.isOpen(); } @Override public boolean isBuffering() { return mSink.remaining() > 0; } @Override public void write(ByteBufferList bb) { byte[] buf = bb.getAllByteArray(); send(buf); } @Override public void setWriteableCallback(WritableCallback handler) { mSink.setWriteableCallback(handler); } @Override public WritableCallback getWriteableCallback() { return mSink.getWriteableCallback(); } @Override public AsyncSocket getSocket() { return mSocket; } @Override public AsyncServer getServer() { return mSocket.getServer(); } @Override public boolean isChunked() { return false; } @Override public void pause() { mSocket.pause(); } @Override public void resume() { mSocket.resume(); } @Override public boolean isPaused() { return mSocket.isPaused(); } @Override public String charset() { return null; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/body/AsyncHttpRequestBody.java ================================================ package com.jeffmony.async.http.body; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.http.AsyncHttpRequest; public interface AsyncHttpRequestBody { void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed); void parse(DataEmitter emitter, CompletedCallback completed); String getContentType(); boolean readFullyOnRequest(); int length(); T get(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/body/ByteBufferListRequestBody.java ================================================ package com.jeffmony.async.http.body; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.http.AsyncHttpRequest; import com.jeffmony.async.parser.ByteBufferListParser; public class ByteBufferListRequestBody implements AsyncHttpRequestBody { public ByteBufferListRequestBody() { } ByteBufferList bb; public ByteBufferListRequestBody(ByteBufferList bb) { this.bb = bb; } @Override public void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed) { Util.writeAll(sink, bb, completed); } @Override public void parse(DataEmitter emitter, CompletedCallback completed) { new ByteBufferListParser().parse(emitter).setCallback((e, result) -> { bb = result; completed.onCompleted(e); }); } public static String CONTENT_TYPE = "application/binary"; @Override public String getContentType() { return CONTENT_TYPE; } @Override public boolean readFullyOnRequest() { return true; } @Override public int length() { return bb.remaining(); } @Override public ByteBufferList get() { return bb; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/body/DocumentBody.java ================================================ package com.jeffmony.async.http.body; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.future.FutureCallback; import com.jeffmony.async.http.AsyncHttpRequest; import com.jeffmony.async.parser.DocumentParser; import com.jeffmony.async.util.Charsets; import org.w3c.dom.Document; import java.io.ByteArrayOutputStream; import java.io.OutputStreamWriter; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; /** * Created by koush on 8/30/13. */ public class DocumentBody implements AsyncHttpRequestBody { public DocumentBody() { this(null); } public DocumentBody(Document document) { this.document = document; } ByteArrayOutputStream bout; private void prepare() { if (bout != null) return; try { DOMSource source = new DOMSource(document); TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); bout = new ByteArrayOutputStream(); OutputStreamWriter writer = new OutputStreamWriter(bout, Charsets.UTF_8); StreamResult result = new StreamResult(writer); transformer.transform(source, result); writer.flush(); } catch (Exception e) { } } @Override public void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed) { prepare(); byte[] bytes = bout.toByteArray(); Util.writeAll(sink, bytes, completed); } @Override public void parse(DataEmitter emitter, final CompletedCallback completed) { new DocumentParser().parse(emitter).setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, Document result) { document = result; completed.onCompleted(e); } }); } public static final String CONTENT_TYPE = "application/xml"; @Override public String getContentType() { return CONTENT_TYPE; } @Override public boolean readFullyOnRequest() { return true; } @Override public int length() { prepare(); return bout.size(); } Document document; @Override public Document get() { return document; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/body/FileBody.java ================================================ package com.jeffmony.async.http.body; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.http.AsyncHttpRequest; import java.io.File; /** * Created by koush on 10/14/13. */ public class FileBody implements AsyncHttpRequestBody { public static final String CONTENT_TYPE = "application/binary"; File file; String contentType = CONTENT_TYPE; public FileBody(File file) { this.file = file; } public FileBody(File file, String contentType) { this.file = file; this.contentType = contentType; } @Override public void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed) { Util.pump(file, sink, completed); } @Override public void parse(DataEmitter emitter, CompletedCallback completed) { throw new AssertionError("not implemented"); } @Override public String getContentType() { return contentType; } public void setContentType(String contentType) { this.contentType = contentType; } @Override public boolean readFullyOnRequest() { throw new AssertionError("not implemented"); } @Override public int length() { return (int)file.length(); } @Override public File get() { return file; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/body/FilePart.java ================================================ package com.jeffmony.async.http.body; import com.jeffmony.async.http.BasicNameValuePair; import com.jeffmony.async.http.NameValuePair; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; public class FilePart extends StreamPart { File file; public FilePart(String name, final File file) { super(name, (int)file.length(), new ArrayList() { { add(new BasicNameValuePair("filename", file.getName())); } }); // getRawHeaders().set("Content-Type", "application/xml"); this.file = file; } @Override protected InputStream getInputStream() throws IOException { return new FileInputStream(file); } @Override public String toString() { return getName(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/body/JSONArrayBody.java ================================================ package com.jeffmony.async.http.body; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.future.FutureCallback; import com.jeffmony.async.http.AsyncHttpRequest; import com.jeffmony.async.parser.JSONArrayParser; import org.json.JSONArray; public class JSONArrayBody implements AsyncHttpRequestBody { public JSONArrayBody() { } byte[] mBodyBytes; JSONArray json; public JSONArrayBody(JSONArray json) { this(); this.json = json; } @Override public void parse(DataEmitter emitter, final CompletedCallback completed) { new JSONArrayParser().parse(emitter).setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, JSONArray result) { json = result; completed.onCompleted(e); } }); } @Override public void write(AsyncHttpRequest request, DataSink sink, final CompletedCallback completed) { Util.writeAll(sink, mBodyBytes, completed); } @Override public String getContentType() { return "application/json"; } @Override public boolean readFullyOnRequest() { return true; } @Override public int length() { mBodyBytes = json.toString().getBytes(); return mBodyBytes.length; } public static final String CONTENT_TYPE = "application/json"; @Override public JSONArray get() { return json; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/body/JSONObjectBody.java ================================================ package com.jeffmony.async.http.body; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.future.FutureCallback; import com.jeffmony.async.http.AsyncHttpRequest; import com.jeffmony.async.parser.JSONObjectParser; import org.json.JSONObject; public class JSONObjectBody implements AsyncHttpRequestBody { public JSONObjectBody() { } byte[] mBodyBytes; JSONObject json; public JSONObjectBody(JSONObject json) { this(); this.json = json; } @Override public void parse(DataEmitter emitter, final CompletedCallback completed) { new JSONObjectParser().parse(emitter).setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, JSONObject result) { json = result; completed.onCompleted(e); } }); } @Override public void write(AsyncHttpRequest request, DataSink sink, final CompletedCallback completed) { Util.writeAll(sink, mBodyBytes, completed); } @Override public String getContentType() { return CONTENT_TYPE; } @Override public boolean readFullyOnRequest() { return true; } @Override public int length() { mBodyBytes = json.toString().getBytes(); return mBodyBytes.length; } public static final String CONTENT_TYPE = "application/json"; @Override public JSONObject get() { return json; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/body/MultipartFormDataBody.java ================================================ package com.jeffmony.async.http.body; import android.text.TextUtils; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.LineEmitter; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.ContinuationCallback; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.future.Continuation; import com.jeffmony.async.http.AsyncHttpRequest; import com.jeffmony.async.http.Headers; import com.jeffmony.async.http.Multimap; import com.jeffmony.async.http.server.BoundaryEmitter; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.UUID; public class MultipartFormDataBody extends BoundaryEmitter implements AsyncHttpRequestBody { LineEmitter liner; Headers formData; ByteBufferList lastData; Part lastPart; public interface MultipartCallback { public void onPart(Part part); } @Override public void parse(DataEmitter emitter, final CompletedCallback completed) { setDataEmitter(emitter); setEndCallback(completed); } void handleLast() { if (lastData == null) return; if (formData == null) formData = new Headers(); String value = lastData.peekString(); String name = TextUtils.isEmpty(lastPart.getName()) ? "unnamed" : lastPart.getName(); StringPart part = new StringPart(name, value); part.mHeaders = lastPart.mHeaders; addPart(part); formData.add(name, value); lastPart = null; lastData = null; } public String getField(String name) { if (formData == null) return null; return formData.get(name); } @Override protected void onBoundaryEnd() { super.onBoundaryEnd(); handleLast(); } @Override protected void onBoundaryStart() { final Headers headers = new Headers(); liner = new LineEmitter(); liner.setLineCallback(new LineEmitter.StringCallback() { @Override public void onStringAvailable(String s) { if (!"\r".equals(s)){ headers.addLine(s); } else { handleLast(); liner = null; setDataCallback(null); Part part = new Part(headers); if (mCallback != null) mCallback.onPart(part); if (getDataCallback() == null) { // if (part.isFile()) { // setDataCallback(new NullDataCallback()); // return; // } lastPart = part; lastData = new ByteBufferList(); setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { bb.get(lastData); } }); } } } }); setDataCallback(liner); } public static final String PRIMARY_TYPE = "multipart/"; public static final String CONTENT_TYPE = PRIMARY_TYPE + "form-data"; String contentType = CONTENT_TYPE; public MultipartFormDataBody(String contentType) { Multimap map = Multimap.parseSemicolonDelimited(contentType); String boundary = map.getString("boundary"); if (boundary == null) report(new Exception("No boundary found for multipart/form-data")); else setBoundary(boundary); } MultipartCallback mCallback; public void setMultipartCallback(MultipartCallback callback) { mCallback = callback; } public MultipartCallback getMultipartCallback() { return mCallback; } int written; @Override public void write(AsyncHttpRequest request, final DataSink sink, final CompletedCallback completed) { if (mParts == null) return; Continuation c = new Continuation(new CompletedCallback() { @Override public void onCompleted(Exception ex) { completed.onCompleted(ex); // if (ex == null) // sink.end(); // else // sink.close(); } }); for (final Part part: mParts) { c.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { byte[] bytes = part.getRawHeaders().toPrefixString(getBoundaryStart()).getBytes(); Util.writeAll(sink, bytes, next); written += bytes.length; } }) .add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { long partLength = part.length(); if (partLength >= 0) written += partLength; part.write(sink, next); } }) .add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { byte[] bytes = "\r\n".getBytes(); Util.writeAll(sink, bytes, next); written += bytes.length; } }); } c.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { byte[] bytes = (getBoundaryEnd()).getBytes(); Util.writeAll(sink, bytes, next); written += bytes.length; assert written == totalToWrite; } }); c.start(); } @Override public String getContentType() { if (getBoundary() == null) { setBoundary("----------------------------" + UUID.randomUUID().toString().replace("-", "")); } return contentType + "; boundary=" + getBoundary(); } @Override public boolean readFullyOnRequest() { return false; } int totalToWrite; @Override public int length() { if (getBoundary() == null) { setBoundary("----------------------------" + UUID.randomUUID().toString().replace("-", "")); } int length = 0; for (final Part part: mParts) { String partHeader = part.getRawHeaders().toPrefixString(getBoundaryStart()); if (part.length() == -1) return -1; length += part.length() + partHeader.getBytes().length + "\r\n".length(); } length += (getBoundaryEnd()).getBytes().length; return totalToWrite = length; } public MultipartFormDataBody() { } public void setContentType(String contentType) { this.contentType = contentType; } public List getParts() { if (mParts == null) return null; return new ArrayList<>(mParts); } public void addFilePart(String name, File file) { addPart(new FilePart(name, file)); } public void addStringPart(String name, String value) { addPart(new StringPart(name, value)); } private ArrayList mParts; public void addPart(Part part) { if (mParts == null) mParts = new ArrayList(); mParts.add(part); } @Override public Multimap get() { return new Multimap(formData.getMultiMap()); } @Override public String toString() { for (Part part: getParts()) { return part.toString(); } return "multipart content is empty"; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/body/Part.java ================================================ package com.jeffmony.async.http.body; import com.jeffmony.async.DataSink; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.http.Headers; import com.jeffmony.async.http.Multimap; import com.jeffmony.async.http.NameValuePair; import java.io.File; import java.util.List; import java.util.Locale; public class Part { public static final String CONTENT_DISPOSITION = "Content-Disposition"; Headers mHeaders; Multimap mContentDisposition; public Part(Headers headers) { mHeaders = headers; mContentDisposition = Multimap.parseSemicolonDelimited(mHeaders.get(CONTENT_DISPOSITION)); } public String getName() { return mContentDisposition.getString("name"); } private long length = -1; public Part(String name, long length, List contentDisposition) { this.length = length; mHeaders = new Headers(); StringBuilder builder = new StringBuilder(String.format(Locale.ENGLISH, "form-data; name=\"%s\"", name)); if (contentDisposition != null) { for (NameValuePair pair: contentDisposition) { builder.append(String.format(Locale.ENGLISH, "; %s=\"%s\"", pair.getName(), pair.getValue())); } } mHeaders.set(CONTENT_DISPOSITION, builder.toString()); mContentDisposition = Multimap.parseSemicolonDelimited(mHeaders.get(CONTENT_DISPOSITION)); } public Headers getRawHeaders() { return mHeaders; } public String getContentType() { return mHeaders.get("Content-Type"); } public void setContentType(String contentType) { mHeaders.set("Content-Type", contentType); } public String getFilename() { String file = mContentDisposition.getString("filename"); if (file == null) return null; return new File(file).getName(); } public boolean isFile() { return mContentDisposition.containsKey("filename"); } public long length() { return length; } public void write(DataSink sink, CompletedCallback callback) { assert false; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/body/StreamBody.java ================================================ package com.jeffmony.async.http.body; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.http.AsyncHttpRequest; import java.io.InputStream; public class StreamBody implements AsyncHttpRequestBody { InputStream stream; int length; String contentType = CONTENT_TYPE; /** * Construct an http body from a stream * @param stream * @param length Length of stream to read, or value < 0 to read to end */ public StreamBody(InputStream stream, int length) { this.stream = stream; this.length = length; } @Override public void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed) { Util.pump(stream, length < 0 ? Integer.MAX_VALUE : length, sink, completed); } @Override public void parse(DataEmitter emitter, CompletedCallback completed) { throw new AssertionError("not implemented"); } public static final String CONTENT_TYPE = "application/binary"; @Override public String getContentType() { return contentType; } public StreamBody setContentType(String contentType) { this.contentType = contentType; return this; } @Override public boolean readFullyOnRequest() { throw new AssertionError("not implemented"); } @Override public int length() { return length; } @Override public InputStream get() { return stream; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/body/StreamPart.java ================================================ package com.jeffmony.async.http.body; import com.jeffmony.async.DataSink; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.http.NameValuePair; import java.io.IOException; import java.io.InputStream; import java.util.List; public abstract class StreamPart extends Part { public StreamPart(String name, long length, List contentDisposition) { super(name, length, contentDisposition); } @Override public void write(DataSink sink, CompletedCallback callback) { try { InputStream is = getInputStream(); Util.pump(is, sink, callback); } catch (Exception e) { callback.onCompleted(e); } } protected abstract InputStream getInputStream() throws IOException; } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/body/StringBody.java ================================================ package com.jeffmony.async.http.body; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.future.FutureCallback; import com.jeffmony.async.http.AsyncHttpRequest; import com.jeffmony.async.parser.StringParser; public class StringBody implements AsyncHttpRequestBody { public StringBody() { } byte[] mBodyBytes; String string; public StringBody(String string) { this(); this.string = string; } @Override public void parse(DataEmitter emitter, final CompletedCallback completed) { new StringParser().parse(emitter).setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, String result) { string = result; completed.onCompleted(e); } }); } public static final String CONTENT_TYPE = "text/plain"; @Override public void write(AsyncHttpRequest request, DataSink sink, final CompletedCallback completed) { if (mBodyBytes == null) mBodyBytes = string.getBytes(); Util.writeAll(sink, mBodyBytes, completed); } @Override public String getContentType() { return "text/plain"; } @Override public boolean readFullyOnRequest() { return true; } @Override public int length() { if (mBodyBytes == null) mBodyBytes = string.getBytes(); return mBodyBytes.length; } @Override public String toString() { return string; } @Override public String get() { return toString(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/body/StringPart.java ================================================ package com.jeffmony.async.http.body; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; public class StringPart extends StreamPart { String value; public StringPart(String name, String value) { super(name, value.getBytes().length, null); this.value = value; } @Override protected InputStream getInputStream() throws IOException { return new ByteArrayInputStream(value.getBytes()); } public String getValue() { return value; } @Override public String toString() { return value; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/body/UrlEncodedFormBody.java ================================================ package com.jeffmony.async.http.body; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.http.AsyncHttpRequest; import com.jeffmony.async.http.Multimap; import com.jeffmony.async.http.NameValuePair; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.List; public class UrlEncodedFormBody implements AsyncHttpRequestBody { private Multimap mParameters; private byte[] mBodyBytes; public UrlEncodedFormBody(Multimap parameters) { mParameters = parameters; } public UrlEncodedFormBody(List parameters) { mParameters = new Multimap(parameters); } private void buildData() { boolean first = true; StringBuilder b = new StringBuilder(); try { for (NameValuePair pair: mParameters) { if (pair.getValue() == null) continue; if (!first) b.append('&'); first = false; b.append(URLEncoder.encode(pair.getName(), "UTF-8")); b.append('='); b.append(URLEncoder.encode(pair.getValue(), "UTF-8")); } mBodyBytes = b.toString().getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new AssertionError(e); } } @Override public void write(AsyncHttpRequest request, final DataSink response, final CompletedCallback completed) { if (mBodyBytes == null) buildData(); Util.writeAll(response, mBodyBytes, completed); } public static final String CONTENT_TYPE = "application/x-www-form-urlencoded"; @Override public String getContentType() { return CONTENT_TYPE + "; charset=utf-8"; } @Override public void parse(DataEmitter emitter, final CompletedCallback completed) { final ByteBufferList data = new ByteBufferList(); emitter.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { bb.get(data); } }); emitter.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { try { if (ex != null) throw ex; mParameters = Multimap.parseUrlEncoded(data.readString()); } catch (Exception e) { completed.onCompleted(e); return; } completed.onCompleted(null); } }); } public UrlEncodedFormBody() { } @Override public boolean readFullyOnRequest() { return true; } @Override public int length() { if (mBodyBytes == null) buildData(); return mBodyBytes.length; } @Override public Multimap get() { return mParameters; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/cache/HeaderParser.java ================================================ /* * 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 com.jeffmony.async.http.cache; final class HeaderParser { public interface CacheControlHandler { void handle(String directive, String parameter); } /** * Parse a comma-separated list of cache control header values. */ public static void parseCacheControl(String value, CacheControlHandler handler) { if (value == null) return; int pos = 0; while (pos < value.length()) { int tokenStart = pos; pos = skipUntil(value, pos, "=,"); String directive = value.substring(tokenStart, pos).trim(); if (pos == value.length() || value.charAt(pos) == ',') { pos++; // consume ',' (if necessary) handler.handle(directive, null); continue; } pos++; // consume '=' pos = skipWhitespace(value, pos); String parameter; // quoted string if (pos < value.length() && value.charAt(pos) == '\"') { pos++; // consume '"' open quote int parameterStart = pos; pos = skipUntil(value, pos, "\""); parameter = value.substring(parameterStart, pos); pos++; // consume '"' close quote (if necessary) // unquoted string } else { int parameterStart = pos; pos = skipUntil(value, pos, ","); parameter = value.substring(parameterStart, pos).trim(); } handler.handle(directive, parameter); } } /** * Returns the next index in {@code input} at or after {@code pos} that * contains a character from {@code characters}. Returns the input length if * none of the requested characters can be found. */ private static int skipUntil(String input, int pos, String characters) { for (; pos < input.length(); pos++) { if (characters.indexOf(input.charAt(pos)) != -1) { break; } } return pos; } /** * Returns the next non-whitespace character in {@code input} that is white * space. Result is undefined if input contains newline characters. */ private static int skipWhitespace(String input, int pos) { for (; pos < input.length(); pos++) { char c = input.charAt(pos); if (c != ' ' && c != '\t') { break; } } return pos; } /** * Returns {@code value} as a positive integer, or 0 if it is negative, or * -1 if it cannot be parsed. */ public static int parseSeconds(String value) { try { long seconds = Long.parseLong(value); if (seconds > Integer.MAX_VALUE) { return Integer.MAX_VALUE; } else if (seconds < 0) { return 0; } else { return (int) seconds; } } catch (NumberFormatException e) { return -1; } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/cache/Objects.java ================================================ /* * 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 com.jeffmony.async.http.cache; final class Objects { private Objects() {} /** * Returns true if two possibly-null objects are equal. */ public static boolean equal(Object a, Object b) { return a == b || (a != null && a.equals(b)); } public static int hashCode(Object o) { return (o == null) ? 0 : o.hashCode(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/cache/RawHeaders.java ================================================ package com.jeffmony.async.http.cache; /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT 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 android.text.TextUtils; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; /** * The HTTP status and unparsed header fields of a single HTTP message. Values * are represented as uninterpreted strings; use {@link RequestHeaders} and * {@link ResponseHeaders} for interpreted headers. This class maintains the * order of the header fields within the HTTP message. * *

This class tracks fields line-by-line. A field with multiple comma- * separated values on the same line will be treated as a field with a single * value by this class. It is the caller's responsibility to detect and split * on commas if their field permits multiple values. This simplifies use of * single-valued fields whose values routinely contain commas, such as cookies * or dates. * *

This class trims whitespace from values. It never returns values with * leading or trailing whitespace. */ final class RawHeaders { private static final Comparator FIELD_NAME_COMPARATOR = new Comparator() { @Override public int compare(String a, String b) { if (a == b) { return 0; } else if (a == null) { return -1; } else if (b == null) { return 1; } else { return String.CASE_INSENSITIVE_ORDER.compare(a, b); } } }; private final List namesAndValues = new ArrayList(20); private String statusLine; private int httpMinorVersion = 1; private int responseCode = -1; private String responseMessage; public RawHeaders() {} public RawHeaders(RawHeaders copyFrom) { copy(copyFrom); } public void copy(RawHeaders copyFrom) { namesAndValues.addAll(copyFrom.namesAndValues); statusLine = copyFrom.statusLine; httpMinorVersion = copyFrom.httpMinorVersion; responseCode = copyFrom.responseCode; responseMessage = copyFrom.responseMessage; } /** * Sets the response status line (like "HTTP/1.0 200 OK") or request line * (like "GET / HTTP/1.1"). */ public void setStatusLine(String statusLine) { statusLine = statusLine.trim(); this.statusLine = statusLine; if (statusLine == null || !statusLine.startsWith("HTTP/")) { return; } statusLine = statusLine.trim(); int mark = statusLine.indexOf(" ") + 1; if (mark == 0) { return; } if (statusLine.charAt(mark - 2) != '1') { this.httpMinorVersion = 0; } int last = mark + 3; if (last > statusLine.length()) { last = statusLine.length(); } this.responseCode = Integer.parseInt(statusLine.substring(mark, last)); if (last + 1 <= statusLine.length()) { this.responseMessage = statusLine.substring(last + 1); } } public String getStatusLine() { return statusLine; } /** * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0 * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown. */ public int getHttpMinorVersion() { return httpMinorVersion != -1 ? httpMinorVersion : 1; } /** * Returns the HTTP status code or -1 if it is unknown. */ public int getResponseCode() { return responseCode; } /** * Returns the HTTP status message or null if it is unknown. */ public String getResponseMessage() { return responseMessage; } /** * Add an HTTP header line containing a field name, a literal colon, and a * value. */ public void addLine(String line) { int index = line.indexOf(":"); if (index == -1) { add("", line); } else { add(line.substring(0, index), line.substring(index + 1)); } } /** * Add a field with the specified value. */ public void add(String fieldName, String value) { if (fieldName == null) { throw new IllegalArgumentException("fieldName == null"); } if (value == null) { /* * Given null values, the RI sends a malformed field line like * "Accept\r\n". For platform compatibility and HTTP compliance, we * print a warning and ignore null values. */ System.err.println("Ignoring HTTP header field '" + fieldName + "' because its value is null"); return; } namesAndValues.add(fieldName); namesAndValues.add(value.trim()); } public void removeAll(String fieldName) { for (int i = 0; i < namesAndValues.size(); i += 2) { if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) { namesAndValues.remove(i); // field name namesAndValues.remove(i); // value } } } public void addAll(String fieldName, List headerFields) { for (String value : headerFields) { add(fieldName, value); } } /** * Set a field with the specified value. If the field is not found, it is * added. If the field is found, the existing values are replaced. */ public void set(String fieldName, String value) { removeAll(fieldName); add(fieldName, value); } /** * Returns the number of field values. */ public int length() { return namesAndValues.size() / 2; } /** * Returns the field at {@code position} or null if that is out of range. */ public String getFieldName(int index) { int fieldNameIndex = index * 2; if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) { return null; } return namesAndValues.get(fieldNameIndex); } /** * Returns the value at {@code index} or null if that is out of range. */ public String getValue(int index) { int valueIndex = index * 2 + 1; if (valueIndex < 0 || valueIndex >= namesAndValues.size()) { return null; } return namesAndValues.get(valueIndex); } /** * Returns the last value corresponding to the specified field, or null. */ public String get(String fieldName) { for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) { if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) { return namesAndValues.get(i + 1); } } return null; } /** * @param fieldNames a case-insensitive set of HTTP header field names. */ public RawHeaders getAll(Set fieldNames) { RawHeaders result = new RawHeaders(); for (int i = 0; i < namesAndValues.size(); i += 2) { String fieldName = namesAndValues.get(i); if (fieldNames.contains(fieldName)) { result.add(fieldName, namesAndValues.get(i + 1)); } } return result; } public String toHeaderString() { StringBuilder result = new StringBuilder(256); result.append(statusLine).append("\r\n"); for (int i = 0; i < namesAndValues.size(); i += 2) { result.append(namesAndValues.get(i)).append(": ") .append(namesAndValues.get(i + 1)).append("\r\n"); } result.append("\r\n"); return result.toString(); } /** * Returns an immutable map containing each field to its list of values. The * status line is mapped to null. */ public Map> toMultimap() { Map> result = new TreeMap>(FIELD_NAME_COMPARATOR); for (int i = 0; i < namesAndValues.size(); i += 2) { String fieldName = namesAndValues.get(i); String value = namesAndValues.get(i + 1); List allValues = new ArrayList(); List otherValues = result.get(fieldName); if (otherValues != null) { allValues.addAll(otherValues); } allValues.add(value); result.put(fieldName, Collections.unmodifiableList(allValues)); } if (statusLine != null) { result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine))); } return Collections.unmodifiableMap(result); } /** * Creates a new instance from the given map of fields to values. If * present, the null field's last element will be used to set the status * line. */ public static RawHeaders fromMultimap(Map> map) { RawHeaders result = new RawHeaders(); for (Entry> entry : map.entrySet()) { String fieldName = entry.getKey(); List values = entry.getValue(); if (fieldName != null) { result.addAll(fieldName, values); } else if (!values.isEmpty()) { result.setStatusLine(values.get(values.size() - 1)); } } return result; } public static RawHeaders parse(String payload) { String[] lines = payload.split("\n"); RawHeaders headers = new RawHeaders(); for (String line: lines) { line = line.trim(); if (TextUtils.isEmpty(line)) continue; if (headers.getStatusLine() == null) headers.setStatusLine(line); else headers.addLine(line); } return headers; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/cache/RequestHeaders.java ================================================ /* * 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 com.jeffmony.async.http.cache; import android.net.Uri; import com.jeffmony.async.http.HttpDate; import java.util.Date; import java.util.List; import java.util.Map; /** * Parsed HTTP request headers. */ final class RequestHeaders { private final Uri uri; private final RawHeaders headers; /** Don't use a cache to satisfy this request. */ private boolean noCache; private int maxAgeSeconds = -1; private int maxStaleSeconds = -1; private int minFreshSeconds = -1; /** * This field's name "only-if-cached" is misleading. It actually means "do * not use the network". It is set by a client who only wants to make a * request if it can be fully satisfied by the cache. Cached responses that * would require validation (ie. conditional gets) are not permitted if this * header is set. */ private boolean onlyIfCached; /** * True if the request contains an authorization field. Although this isn't * necessarily a shared cache, it follows the spec's strict requirements for * shared caches. */ private boolean hasAuthorization; private int contentLength = -1; private String transferEncoding; private String userAgent; private String host; private String connection; private String acceptEncoding; private String contentType; private String ifModifiedSince; private String ifNoneMatch; private String proxyAuthorization; public RequestHeaders(Uri uri, RawHeaders headers) { this.uri = uri; this.headers = headers; HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() { @Override public void handle(String directive, String parameter) { if (directive.equalsIgnoreCase("no-cache")) { noCache = true; } else if (directive.equalsIgnoreCase("max-age")) { maxAgeSeconds = HeaderParser.parseSeconds(parameter); } else if (directive.equalsIgnoreCase("max-stale")) { maxStaleSeconds = HeaderParser.parseSeconds(parameter); } else if (directive.equalsIgnoreCase("min-fresh")) { minFreshSeconds = HeaderParser.parseSeconds(parameter); } else if (directive.equalsIgnoreCase("only-if-cached")) { onlyIfCached = true; } } }; for (int i = 0; i < headers.length(); i++) { String fieldName = headers.getFieldName(i); String value = headers.getValue(i); if ("Cache-Control".equalsIgnoreCase(fieldName)) { HeaderParser.parseCacheControl(value, handler); } else if ("Pragma".equalsIgnoreCase(fieldName)) { if (value.equalsIgnoreCase("no-cache")) { noCache = true; } } else if ("If-None-Match".equalsIgnoreCase(fieldName)) { ifNoneMatch = value; } else if ("If-Modified-Since".equalsIgnoreCase(fieldName)) { ifModifiedSince = value; } else if ("Authorization".equalsIgnoreCase(fieldName)) { hasAuthorization = true; } else if ("Content-Length".equalsIgnoreCase(fieldName)) { try { contentLength = Integer.parseInt(value); } catch (NumberFormatException ignored) { } } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) { transferEncoding = value; } else if ("User-Agent".equalsIgnoreCase(fieldName)) { userAgent = value; } else if ("Host".equalsIgnoreCase(fieldName)) { host = value; } else if ("Connection".equalsIgnoreCase(fieldName)) { connection = value; } else if ("Accept-Encoding".equalsIgnoreCase(fieldName)) { acceptEncoding = value; } else if ("Content-Type".equalsIgnoreCase(fieldName)) { contentType = value; } else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) { proxyAuthorization = value; } } } public boolean isChunked() { return "chunked".equalsIgnoreCase(transferEncoding); } public boolean hasConnectionClose() { return "close".equalsIgnoreCase(connection); } public Uri getUri() { return uri; } public RawHeaders getHeaders() { return headers; } public boolean isNoCache() { return noCache; } public int getMaxAgeSeconds() { return maxAgeSeconds; } public int getMaxStaleSeconds() { return maxStaleSeconds; } public int getMinFreshSeconds() { return minFreshSeconds; } public boolean isOnlyIfCached() { return onlyIfCached; } public boolean hasAuthorization() { return hasAuthorization; } public int getContentLength() { return contentLength; } public String getTransferEncoding() { return transferEncoding; } public String getUserAgent() { return userAgent; } public String getHost() { return host; } public String getConnection() { return connection; } public String getAcceptEncoding() { return acceptEncoding; } public String getContentType() { return contentType; } public String getIfModifiedSince() { return ifModifiedSince; } public String getIfNoneMatch() { return ifNoneMatch; } public String getProxyAuthorization() { return proxyAuthorization; } public void setChunked() { if (this.transferEncoding != null) { headers.removeAll("Transfer-Encoding"); } headers.add("Transfer-Encoding", "chunked"); this.transferEncoding = "chunked"; } public void setContentLength(int contentLength) { if (this.contentLength != -1) { headers.removeAll("Content-Length"); } if (contentLength != -1) { headers.add("Content-Length", Integer.toString(contentLength)); } this.contentLength = contentLength; } public void setUserAgent(String userAgent) { if (this.userAgent != null) { headers.removeAll("User-Agent"); } headers.add("User-Agent", userAgent); this.userAgent = userAgent; } public void setHost(String host) { if (this.host != null) { headers.removeAll("Host"); } headers.add("Host", host); this.host = host; } public void setConnection(String connection) { if (this.connection != null) { headers.removeAll("Connection"); } headers.add("Connection", connection); this.connection = connection; } public void setAcceptEncoding(String acceptEncoding) { if (this.acceptEncoding != null) { headers.removeAll("Accept-Encoding"); } headers.add("Accept-Encoding", acceptEncoding); this.acceptEncoding = acceptEncoding; } public void setContentType(String contentType) { if (this.contentType != null) { headers.removeAll("Content-Type"); } headers.add("Content-Type", contentType); this.contentType = contentType; } public void setIfModifiedSince(Date date) { if (ifModifiedSince != null) { headers.removeAll("If-Modified-Since"); } String formattedDate = HttpDate.format(date); headers.add("If-Modified-Since", formattedDate); ifModifiedSince = formattedDate; } public void setIfNoneMatch(String ifNoneMatch) { if (this.ifNoneMatch != null) { headers.removeAll("If-None-Match"); } headers.add("If-None-Match", ifNoneMatch); this.ifNoneMatch = ifNoneMatch; } /** * Returns true if the request contains conditions that save the server from * sending a response that the client has locally. When the caller adds * conditions, this cache won't participate in the request. */ public boolean hasConditions() { return ifModifiedSince != null || ifNoneMatch != null; } public void addCookies(Map> allCookieHeaders) { for (Map.Entry> entry : allCookieHeaders.entrySet()) { String key = entry.getKey(); if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) { headers.addAll(key, entry.getValue()); } } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/cache/ResponseCacheMiddleware.java ================================================ package com.jeffmony.async.http.cache; import android.net.Uri; import android.util.Base64; import com.jeffmony.async.AsyncSSLSocket; import com.jeffmony.async.AsyncServer; import com.jeffmony.async.AsyncSocket; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.FilteredDataEmitter; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.WritableCallback; import com.jeffmony.async.future.Cancellable; import com.jeffmony.async.future.SimpleCancellable; import com.jeffmony.async.http.AsyncHttpClient; import com.jeffmony.async.http.AsyncHttpClientMiddleware; import com.jeffmony.async.http.AsyncHttpGet; import com.jeffmony.async.http.AsyncHttpRequest; import com.jeffmony.async.http.Headers; import com.jeffmony.async.http.SimpleMiddleware; import com.jeffmony.async.util.Allocator; import com.jeffmony.async.util.Charsets; import com.jeffmony.async.util.FileCache; import com.jeffmony.async.util.StreamUtility; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.net.CacheResponse; import java.nio.ByteBuffer; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.List; import java.util.Locale; import java.util.Map; import javax.net.ssl.SSLEngine; public class ResponseCacheMiddleware extends SimpleMiddleware { public static final int ENTRY_METADATA = 0; public static final int ENTRY_BODY = 1; public static final int ENTRY_COUNT = 2; public static final String SERVED_FROM = "X-Served-From"; public static final String CONDITIONAL_CACHE = "conditional-cache"; public static final String CACHE = "cache"; private static final String LOGTAG = "AsyncHttpCache"; private boolean caching = true; private int writeSuccessCount; private int writeAbortCount; private FileCache cache; private AsyncServer server; private int conditionalCacheHitCount; private int cacheHitCount; private int networkCount; private int cacheStoreCount; private ResponseCacheMiddleware() { } public static ResponseCacheMiddleware addCache(AsyncHttpClient client, File cacheDir, long size) throws IOException { for (AsyncHttpClientMiddleware middleware: client.getMiddleware()) { if (middleware instanceof ResponseCacheMiddleware) throw new IOException("Response cache already added to http client"); } ResponseCacheMiddleware ret = new ResponseCacheMiddleware(); ret.server = client.getServer(); ret.cache = new FileCache(cacheDir, size, false); client.insertMiddleware(ret); return ret; } public FileCache getFileCache() { return cache; } public boolean getCaching() { return caching; } public void setCaching(boolean caching) { this.caching = caching; } public void removeFromCache(Uri uri) { String key = FileCache.toKeyString(uri); getFileCache().remove(key); } // step 1) see if we can serve request from the cache directly. // also see if this can be turned into a conditional cache request. @Override public Cancellable getSocket(final GetSocketData data) { RequestHeaders requestHeaders = new RequestHeaders(data.request.getUri(), RawHeaders.fromMultimap(data.request.getHeaders().getMultiMap())); data.state.put("request-headers", requestHeaders); if (cache == null || !caching || requestHeaders.isNoCache()) { networkCount++; return null; } String key = FileCache.toKeyString(data.request.getUri()); FileInputStream[] snapshot = null; long contentLength; Entry entry; try { snapshot = cache.get(key, ENTRY_COUNT); if (snapshot == null) { networkCount++; return null; } contentLength = snapshot[ENTRY_BODY].available(); entry = new Entry(snapshot[ENTRY_METADATA]); } catch (IOException e) { // Give up because the cache cannot be read. networkCount++; StreamUtility.closeQuietly(snapshot); return null; } // verify the entry matches if (!entry.matches(data.request.getUri(), data.request.getMethod(), data.request.getHeaders().getMultiMap())) { networkCount++; StreamUtility.closeQuietly(snapshot); return null; } EntryCacheResponse candidate = new EntryCacheResponse(entry, snapshot[ENTRY_BODY]); Map> responseHeadersMap; FileInputStream cachedResponseBody; try { responseHeadersMap = candidate.getHeaders(); cachedResponseBody = candidate.getBody(); } catch (Exception e) { networkCount++; StreamUtility.closeQuietly(snapshot); return null; } if (responseHeadersMap == null || cachedResponseBody == null) { networkCount++; StreamUtility.closeQuietly(snapshot); return null; } RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap); ResponseHeaders cachedResponseHeaders = new ResponseHeaders(data.request.getUri(), rawResponseHeaders); rawResponseHeaders.set("Content-Length", String.valueOf(contentLength)); rawResponseHeaders.removeAll("Content-Encoding"); rawResponseHeaders.removeAll("Transfer-Encoding"); cachedResponseHeaders.setLocalTimestamps(System.currentTimeMillis(), System.currentTimeMillis()); long now = System.currentTimeMillis(); ResponseSource responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders); if (responseSource == ResponseSource.CACHE) { data.request.logi("Response retrieved from cache"); final CachedSocket socket = entry.isHttps() ? new CachedSSLSocket(candidate, contentLength) : new CachedSocket(candidate, contentLength); socket.pending.add(ByteBuffer.wrap(rawResponseHeaders.toHeaderString().getBytes())); server.post(new Runnable() { @Override public void run() { data.connectCallback.onConnectCompleted(null, socket); socket.sendCachedDataOnNetworkThread(); } }); cacheHitCount++; data.state.put("socket-owner", this); SimpleCancellable ret = new SimpleCancellable(); ret.setComplete(); return ret; } else if (responseSource == ResponseSource.CONDITIONAL_CACHE) { data.request.logi("Response may be served from conditional cache"); CacheData cacheData = new CacheData(); cacheData.snapshot = snapshot; cacheData.contentLength = contentLength; cacheData.cachedResponseHeaders = cachedResponseHeaders; cacheData.candidate = candidate; data.state.put("cache-data", cacheData); return null; } else { data.request.logd("Response can not be served from cache"); // NETWORK or other networkCount++; StreamUtility.closeQuietly(snapshot); return null; } } public int getConditionalCacheHitCount() { return conditionalCacheHitCount; } public int getCacheHitCount() { return cacheHitCount; } public int getNetworkCount() { return networkCount; } public int getCacheStoreCount() { return cacheStoreCount; } // step 2) if this is a conditional cache request, serve it from the cache if necessary // otherwise, see if it is cacheable @Override public void onBodyDecoder(OnBodyDecoderData data) { CachedSocket cached = Util.getWrappedSocket(data.socket, CachedSocket.class); if (cached != null) { data.response.headers().set(SERVED_FROM, CACHE); return; } CacheData cacheData = data.state.get("cache-data"); RawHeaders rh = RawHeaders.fromMultimap(data.response.headers().getMultiMap()); rh.removeAll("Content-Length"); rh.setStatusLine(String.format(Locale.ENGLISH, "%s %s %s", data.response.protocol(), data.response.code(), data.response.message())); ResponseHeaders networkResponse = new ResponseHeaders(data.request.getUri(), rh); data.state.put("response-headers", networkResponse); if (cacheData != null) { if (cacheData.cachedResponseHeaders.validate(networkResponse)) { data.request.logi("Serving response from conditional cache"); ResponseHeaders combined = cacheData.cachedResponseHeaders.combine(networkResponse); data.response.headers(new Headers(combined.getHeaders().toMultimap())); data.response.code(combined.getHeaders().getResponseCode()); data.response.message(combined.getHeaders().getResponseMessage()); data.response.headers().set(SERVED_FROM, CONDITIONAL_CACHE); conditionalCacheHitCount++; CachedBodyEmitter bodySpewer = new CachedBodyEmitter(cacheData.candidate, cacheData.contentLength); bodySpewer.setDataEmitter(data.bodyEmitter); data.bodyEmitter = bodySpewer; bodySpewer.sendCachedData(); return; } // did not validate, so fall through and cache the response data.state.remove("cache-data"); StreamUtility.closeQuietly(cacheData.snapshot); } if (!caching) return; RequestHeaders requestHeaders = data.state.get("request-headers"); if (requestHeaders == null || !networkResponse.isCacheable(requestHeaders) || !data.request.getMethod().equals(AsyncHttpGet.METHOD)) { /* * Don't cache non-GET responses. We're technically allowed to cache * HEAD requests and some POST requests, but the complexity of doing * so is high and the benefit is low. */ networkCount++; data.request.logd("Response is not cacheable"); return; } String key = FileCache.toKeyString(data.request.getUri()); RawHeaders varyHeaders = requestHeaders.getHeaders().getAll(networkResponse.getVaryFields()); Entry entry = new Entry(data.request.getUri(), varyHeaders, data.request, networkResponse.getHeaders()); BodyCacher cacher = new BodyCacher(); EntryEditor editor = new EntryEditor(key); try { entry.writeTo(editor); // create the file editor.newOutputStream(ENTRY_BODY); } catch (Exception e) { // Log.e(LOGTAG, "error", e); editor.abort(); networkCount++; return; } cacher.editor = editor; cacher.setDataEmitter(data.bodyEmitter); data.bodyEmitter = cacher; data.state.put("body-cacher", cacher); data.request.logd("Caching response"); cacheStoreCount++; } // step 3: close up shop @Override public void onResponseComplete(OnResponseCompleteData data) { CacheData cacheData = data.state.get("cache-data"); if (cacheData != null && cacheData.snapshot != null) StreamUtility.closeQuietly(cacheData.snapshot); CachedSocket cachedSocket = Util.getWrappedSocket(data.socket, CachedSocket.class); if (cachedSocket != null) StreamUtility.closeQuietly((cachedSocket.cacheResponse).getBody()); BodyCacher cacher = data.state.get("body-cacher"); if (cacher != null) { if (data.exception != null) cacher.abort(); else cacher.commit(); } } public void clear() { if (cache != null) { cache.clear(); } } public static class CacheData { FileInputStream[] snapshot; EntryCacheResponse candidate; long contentLength; ResponseHeaders cachedResponseHeaders; } private static class BodyCacher extends FilteredDataEmitter { EntryEditor editor; ByteBufferList cached; @Override protected void report(Exception e) { super.report(e); if (e != null) abort(); } @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { if (cached != null) { super.onDataAvailable(emitter, cached); // couldn't emit it all, so just wait for another day... if (cached.remaining() > 0) return; cached = null; } // write to cache... any data not consumed needs to be retained for the next callback ByteBufferList copy = new ByteBufferList(); try { if (editor != null) { OutputStream outputStream = editor.newOutputStream(ENTRY_BODY); if (outputStream != null) { while (!bb.isEmpty()) { ByteBuffer b = bb.remove(); try { ByteBufferList.writeOutputStream(outputStream, b); } finally { copy.add(b); } } } else { abort(); } } } catch (Exception e) { abort(); } finally { bb.get(copy); copy.get(bb); } super.onDataAvailable(emitter, bb); if (editor != null && bb.remaining() > 0) { cached = new ByteBufferList(); bb.get(cached); } } @Override public void close() { abort(); super.close(); } public void abort() { if (editor != null) { editor.abort(); editor = null; } } public void commit() { if (editor != null) { editor.commit(); editor = null; } } } private static class CachedBodyEmitter extends FilteredDataEmitter { EntryCacheResponse cacheResponse; ByteBufferList pending = new ByteBufferList(); private boolean paused; private Allocator allocator = new Allocator(); boolean allowEnd; public CachedBodyEmitter(EntryCacheResponse cacheResponse, long contentLength) { this.cacheResponse = cacheResponse; allocator.setCurrentAlloc((int)contentLength); } Runnable sendCachedDataRunnable = new Runnable() { @Override public void run() { sendCachedDataOnNetworkThread(); } }; void sendCachedDataOnNetworkThread() { if (pending.remaining() > 0) { super.onDataAvailable(CachedBodyEmitter.this, pending); if (pending.remaining() > 0) return; } // fill pending try { ByteBuffer buffer = allocator.allocate(); assert buffer.position() == 0; FileInputStream din = cacheResponse.getBody(); int read = din.read(buffer.array(), buffer.arrayOffset(), buffer.capacity()); if (read == -1) { ByteBufferList.reclaim(buffer); allowEnd = true; report(null); return; } allocator.track(read); buffer.limit(read); pending.add(buffer); } catch (IOException e) { allowEnd = true; report(e); return; } super.onDataAvailable(this, pending); if (pending.remaining() > 0) return; // this limits max throughput to 256k (aka max alloc) * 100 per second... // roughly 25MB/s getServer().postDelayed(sendCachedDataRunnable, 10); } void sendCachedData() { getServer().post(sendCachedDataRunnable); } @Override public void resume() { paused = false; sendCachedData(); } @Override public boolean isPaused() { return paused; } @Override public void close() { if (getServer().getAffinity() != Thread.currentThread()) { getServer().post(new Runnable() { @Override public void run() { close(); } }); return; } pending.recycle(); StreamUtility.closeQuietly(cacheResponse.getBody()); super.close(); } @Override protected void report(Exception e) { // a 304 response will immediate call report/end since there is no body. // prevent this from happening by waiting for the actual body to be spit out. if (!allowEnd) return; StreamUtility.closeQuietly(cacheResponse.getBody()); super.report(e); } } private static final class Entry { private final String uri; private final RawHeaders varyHeaders; private final String requestMethod; private final RawHeaders responseHeaders; private final String cipherSuite; private final Certificate[] peerCertificates; private final Certificate[] localCertificates; /* * Reads an entry from an input stream. A typical entry looks like this: * http://google.com/foo * GET * 2 * Accept-Language: fr-CA * Accept-Charset: UTF-8 * HTTP/1.1 200 OK * 3 * Content-Type: image/png * Content-Length: 100 * Cache-Control: max-age=600 * * A typical HTTPS file looks like this: * https://google.com/foo * GET * 2 * Accept-Language: fr-CA * Accept-Charset: UTF-8 * HTTP/1.1 200 OK * 3 * Content-Type: image/png * Content-Length: 100 * Cache-Control: max-age=600 * * AES_256_WITH_MD5 * 2 * base64-encoded peerCertificate[0] * base64-encoded peerCertificate[1] * -1 * * The file is newline separated. The first two lines are the URL and * the request method. Next is the number of HTTP Vary request header * lines, followed by those lines. * * Next is the response status line, followed by the number of HTTP * response header lines, followed by those lines. * * HTTPS responses also contain SSL session information. This begins * with a blank line, and then a line containing the cipher suite. Next * is the length of the peer certificate chain. These certificates are * base64-encoded and appear each on their own line. The next line * contains the length of the local certificate chain. These * certificates are also base64-encoded and appear each on their own * line. A length of -1 is used to encode a null array. */ public Entry(InputStream in) throws IOException { StrictLineReader reader = null; try { reader = new StrictLineReader(in, Charsets.US_ASCII); uri = reader.readLine(); requestMethod = reader.readLine(); varyHeaders = new RawHeaders(); int varyRequestHeaderLineCount = reader.readInt(); for (int i = 0; i < varyRequestHeaderLineCount; i++) { varyHeaders.addLine(reader.readLine()); } responseHeaders = new RawHeaders(); responseHeaders.setStatusLine(reader.readLine()); int responseHeaderLineCount = reader.readInt(); for (int i = 0; i < responseHeaderLineCount; i++) { responseHeaders.addLine(reader.readLine()); } // if (isHttps()) { // String blank = reader.readLine(); // if (blank.length() != 0) { // throw new IOException("expected \"\" but was \"" + blank + "\""); // } // cipherSuite = reader.readLine(); // peerCertificates = readCertArray(reader); // localCertificates = readCertArray(reader); // } else { cipherSuite = null; peerCertificates = null; localCertificates = null; // } } finally { StreamUtility.closeQuietly(reader, in); } } public Entry(Uri uri, RawHeaders varyHeaders, AsyncHttpRequest request, RawHeaders responseHeaders) { this.uri = uri.toString(); this.varyHeaders = varyHeaders; this.requestMethod = request.getMethod(); this.responseHeaders = responseHeaders; // if (isHttps()) { // HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection; // cipherSuite = httpsConnection.getCipherSuite(); // Certificate[] peerCertificatesNonFinal = null; // try { // peerCertificatesNonFinal = httpsConnection.getServerCertificates(); // } catch (SSLPeerUnverifiedException ignored) { // } // peerCertificates = peerCertificatesNonFinal; // localCertificates = httpsConnection.getLocalCertificates(); // } else { cipherSuite = null; peerCertificates = null; localCertificates = null; // } } public void writeTo(EntryEditor editor) throws IOException { OutputStream out = editor.newOutputStream(ENTRY_METADATA); Writer writer = new BufferedWriter(new OutputStreamWriter(out, Charsets.UTF_8)); writer.write(uri + '\n'); writer.write(requestMethod + '\n'); writer.write(Integer.toString(varyHeaders.length()) + '\n'); for (int i = 0; i < varyHeaders.length(); i++) { writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\n'); } writer.write(responseHeaders.getStatusLine() + '\n'); writer.write(Integer.toString(responseHeaders.length()) + '\n'); for (int i = 0; i < responseHeaders.length(); i++) { writer.write(responseHeaders.getFieldName(i) + ": " + responseHeaders.getValue(i) + '\n'); } if (isHttps()) { writer.write('\n'); writer.write(cipherSuite + '\n'); writeCertArray(writer, peerCertificates); writeCertArray(writer, localCertificates); } writer.close(); } private boolean isHttps() { return uri.startsWith("https://"); } private Certificate[] readCertArray(StrictLineReader reader) throws IOException { int length = reader.readInt(); if (length == -1) { return null; } try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); Certificate[] result = new Certificate[length]; for (int i = 0; i < result.length; i++) { String line = reader.readLine(); byte[] bytes = Base64.decode(line, Base64.DEFAULT); result[i] = certificateFactory.generateCertificate( new ByteArrayInputStream(bytes)); } return result; } catch (CertificateException e) { throw new IOException(e.getMessage()); } } private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException { if (certificates == null) { writer.write("-1\n"); return; } try { writer.write(Integer.toString(certificates.length) + '\n'); for (Certificate certificate : certificates) { byte[] bytes = certificate.getEncoded(); String line = Base64.encodeToString(bytes, Base64.DEFAULT); writer.write(line + '\n'); } } catch (CertificateEncodingException e) { throw new IOException(e.getMessage()); } } public boolean matches(Uri uri, String requestMethod, Map> requestHeaders) { return this.uri.equals(uri.toString()) && this.requestMethod.equals(requestMethod) && new ResponseHeaders(uri, responseHeaders) .varyMatches(varyHeaders.toMultimap(), requestHeaders); } } static class EntryCacheResponse extends CacheResponse { private final Entry entry; private final FileInputStream snapshot; public EntryCacheResponse(Entry entry, FileInputStream snapshot) { this.entry = entry; this.snapshot = snapshot; } @Override public Map> getHeaders() { return entry.responseHeaders.toMultimap(); } @Override public FileInputStream getBody() { return snapshot; } } private class CachedSSLSocket extends CachedSocket implements AsyncSSLSocket { public CachedSSLSocket(EntryCacheResponse cacheResponse, long contentLength) { super(cacheResponse, contentLength); } @Override public SSLEngine getSSLEngine() { return null; } @Override public X509Certificate[] getPeerCertificates() { return null; } } private class CachedSocket extends CachedBodyEmitter implements AsyncSocket { boolean closed; boolean open; CompletedCallback closedCallback; public CachedSocket(EntryCacheResponse cacheResponse, long contentLength) { super(cacheResponse, contentLength); allowEnd = true; } @Override public void end() { } @Override protected void report(Exception e) { super.report(e); if (closed) return; closed = true; if (closedCallback != null) closedCallback.onCompleted(e); } @Override public void write(ByteBufferList bb) { // it's gonna write headers and stuff... whatever bb.recycle(); } @Override public WritableCallback getWriteableCallback() { return null; } @Override public void setWriteableCallback(WritableCallback handler) { } @Override public boolean isOpen() { return open; } @Override public void close() { open = false; } @Override public CompletedCallback getClosedCallback() { return closedCallback; } @Override public void setClosedCallback(CompletedCallback handler) { closedCallback = handler; } @Override public AsyncServer getServer() { return server; } } class EntryEditor { String key; File[] temps; FileOutputStream[] outs; boolean done; public EntryEditor(String key) { this.key = key; temps = cache.getTempFiles(ENTRY_COUNT); outs = new FileOutputStream[ENTRY_COUNT]; } void commit() { StreamUtility.closeQuietly(outs); if (done) return; cache.commitTempFiles(key, temps); writeSuccessCount++; done = true; } FileOutputStream newOutputStream(int index) throws IOException { if (outs[index] == null) outs[index] = new FileOutputStream(temps[index]); return outs[index]; } void abort() { StreamUtility.closeQuietly(outs); FileCache.removeFiles(temps); if (done) return; writeAbortCount++; done = true; } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/cache/ResponseHeaders.java ================================================ /* * 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 com.jeffmony.async.http.cache; import android.net.Uri; import com.jeffmony.async.http.HttpDate; import java.net.HttpURLConnection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.TimeUnit; /** * Parsed HTTP response headers. */ final class ResponseHeaders { /** HTTP header name for the local time when the request was sent. */ private static final String SENT_MILLIS = "X-Android-Sent-Millis"; /** HTTP header name for the local time when the response was received. */ private static final String RECEIVED_MILLIS = "X-Android-Received-Millis"; private final Uri uri; private final RawHeaders headers; /** The server's time when this response was served, if known. */ private Date servedDate; /** The last modified date of the response, if known. */ private Date lastModified; /** * The expiration date of the response, if known. If both this field and the * max age are set, the max age is preferred. */ private Date expires; /** * Extension header set by HttpURLConnectionImpl specifying the timestamp * when the HTTP request was first initiated. */ private long sentRequestMillis; /** * Extension header set by HttpURLConnectionImpl specifying the timestamp * when the HTTP response was first received. */ private long receivedResponseMillis; /** * In the response, this field's name "no-cache" is misleading. It doesn't * prevent us from caching the response; it only means we have to validate * the response with the origin server before returning it. We can do this * with a conditional get. */ private boolean noCache; /** If true, this response should not be cached. */ private boolean noStore; /** * The duration past the response's served date that it can be served * without validation. */ private int maxAgeSeconds = -1; /** * The "s-maxage" directive is the max age for shared caches. Not to be * confused with "max-age" for non-shared caches, As in Firefox and Chrome, * this directive is not honored by this cache. */ private int sMaxAgeSeconds = -1; /** * This request header field's name "only-if-cached" is misleading. It * actually means "do not use the network". It is set by a client who only * wants to make a request if it can be fully satisfied by the cache. * Cached responses that would require validation (ie. conditional gets) are * not permitted if this header is set. */ private boolean isPublic; private boolean mustRevalidate; private String etag; private int ageSeconds = -1; /** Case-insensitive set of field names. */ private Set varyFields = Collections.emptySet(); private String contentEncoding; private String transferEncoding; private long contentLength = -1; private String connection; private String proxyAuthenticate; private String wwwAuthenticate; public ResponseHeaders(Uri uri, RawHeaders headers) { this.uri = uri; this.headers = headers; HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() { @Override public void handle(String directive, String parameter) { if (directive.equalsIgnoreCase("no-cache")) { noCache = true; } else if (directive.equalsIgnoreCase("no-store")) { noStore = true; } else if (directive.equalsIgnoreCase("max-age")) { maxAgeSeconds = HeaderParser.parseSeconds(parameter); } else if (directive.equalsIgnoreCase("s-maxage")) { sMaxAgeSeconds = HeaderParser.parseSeconds(parameter); } else if (directive.equalsIgnoreCase("public")) { isPublic = true; } else if (directive.equalsIgnoreCase("must-revalidate")) { mustRevalidate = true; } } }; for (int i = 0; i < headers.length(); i++) { String fieldName = headers.getFieldName(i); String value = headers.getValue(i); if ("Cache-Control".equalsIgnoreCase(fieldName)) { HeaderParser.parseCacheControl(value, handler); } else if ("Date".equalsIgnoreCase(fieldName)) { servedDate = HttpDate.parse(value); } else if ("Expires".equalsIgnoreCase(fieldName)) { expires = HttpDate.parse(value); } else if ("Last-Modified".equalsIgnoreCase(fieldName)) { lastModified = HttpDate.parse(value); } else if ("ETag".equalsIgnoreCase(fieldName)) { etag = value; } else if ("Pragma".equalsIgnoreCase(fieldName)) { if (value.equalsIgnoreCase("no-cache")) { noCache = true; } } else if ("Age".equalsIgnoreCase(fieldName)) { ageSeconds = HeaderParser.parseSeconds(value); } else if ("Vary".equalsIgnoreCase(fieldName)) { // Replace the immutable empty set with something we can mutate. if (varyFields.isEmpty()) { varyFields = new TreeSet(String.CASE_INSENSITIVE_ORDER); } for (String varyField : value.split(",")) { varyFields.add(varyField.trim().toLowerCase(Locale.US)); } } else if ("Content-Encoding".equalsIgnoreCase(fieldName)) { contentEncoding = value; } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) { transferEncoding = value; } else if ("Content-Length".equalsIgnoreCase(fieldName)) { try { contentLength = Long.parseLong(value); } catch (NumberFormatException ignored) { } } else if ("Connection".equalsIgnoreCase(fieldName)) { connection = value; } else if ("Proxy-Authenticate".equalsIgnoreCase(fieldName)) { proxyAuthenticate = value; } else if ("WWW-Authenticate".equalsIgnoreCase(fieldName)) { wwwAuthenticate = value; } else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) { sentRequestMillis = Long.parseLong(value); } else if (RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) { receivedResponseMillis = Long.parseLong(value); } } } public boolean isContentEncodingGzip() { return "gzip".equalsIgnoreCase(contentEncoding); } public void stripContentEncoding() { contentEncoding = null; headers.removeAll("Content-Encoding"); } public boolean isChunked() { return "chunked".equalsIgnoreCase(transferEncoding); } public boolean hasConnectionClose() { return "close".equalsIgnoreCase(connection); } public Uri getUri() { return uri; } public RawHeaders getHeaders() { return headers; } public Date getServedDate() { return servedDate; } public Date getLastModified() { return lastModified; } public Date getExpires() { return expires; } public boolean isNoCache() { return noCache; } public boolean isNoStore() { return noStore; } public int getMaxAgeSeconds() { return maxAgeSeconds; } public int getSMaxAgeSeconds() { return sMaxAgeSeconds; } public boolean isPublic() { return isPublic; } public boolean isMustRevalidate() { return mustRevalidate; } public String getEtag() { return etag; } public Set getVaryFields() { return varyFields; } public String getContentEncoding() { return contentEncoding; } public long getContentLength() { return contentLength; } public String getConnection() { return connection; } public String getProxyAuthenticate() { return proxyAuthenticate; } public String getWwwAuthenticate() { return wwwAuthenticate; } public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) { this.sentRequestMillis = sentRequestMillis; headers.add(SENT_MILLIS, Long.toString(sentRequestMillis)); this.receivedResponseMillis = receivedResponseMillis; headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis)); } /** * Returns the current age of the response, in milliseconds. The calculation * is specified by RFC 2616, 13.2.3 Age Calculations. */ private long computeAge(long nowMillis) { long apparentReceivedAge = servedDate != null ? Math.max(0, receivedResponseMillis - servedDate.getTime()) : 0; long receivedAge = ageSeconds != -1 ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds)) : apparentReceivedAge; long responseDuration = receivedResponseMillis - sentRequestMillis; long residentDuration = nowMillis - receivedResponseMillis; return receivedAge + responseDuration + residentDuration; } /** * Returns the number of milliseconds that the response was fresh for, * starting from the served date. */ private long computeFreshnessLifetime() { if (maxAgeSeconds != -1) { return TimeUnit.SECONDS.toMillis(maxAgeSeconds); } else if (expires != null) { long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis; long delta = expires.getTime() - servedMillis; return delta > 0 ? delta : 0; } else if (lastModified != null && uri.getEncodedQuery() == null) { /* * As recommended by the HTTP RFC and implemented in Firefox, the * max age of a document should be defaulted to 10% of the * document's age at the time it was served. Default expiration * dates aren't used for URIs containing a query. */ long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis; long delta = servedMillis - lastModified.getTime(); return delta > 0 ? (delta / 10) : 0; } return 0; } /** * Returns true if computeFreshnessLifetime used a heuristic. If we used a * heuristic to serve a cached response older than 24 hours, we are required * to attach a warning. */ private boolean isFreshnessLifetimeHeuristic() { return maxAgeSeconds == -1 && expires == null; } /** * Returns true if this response can be stored to later serve another * request. */ public boolean isCacheable(RequestHeaders request) { /* * Always go to network for uncacheable response codes (RFC 2616, 13.4), * This implementation doesn't support caching partial content. */ int responseCode = headers.getResponseCode(); if (responseCode != HttpURLConnection.HTTP_OK && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE && responseCode != HttpURLConnection.HTTP_MULT_CHOICE && responseCode != HttpURLConnection.HTTP_MOVED_PERM && responseCode != HttpURLConnection.HTTP_GONE) { return false; } /* * Responses to authorized requests aren't cacheable unless they include * a 'public', 'must-revalidate' or 's-maxage' directive. */ if (request.hasAuthorization() && !isPublic && !mustRevalidate && sMaxAgeSeconds == -1) { return false; } if (noStore) { return false; } return true; } /** * Returns true if a Vary header contains an asterisk. Such responses cannot * be cached. */ public boolean hasVaryAll() { return varyFields.contains("*"); } /** * Returns true if none of the Vary headers on this response have changed * between {@code cachedRequest} and {@code newRequest}. */ public boolean varyMatches(Map> cachedRequest, Map> newRequest) { for (String field : varyFields) { if (!Objects.equal(cachedRequest.get(field), newRequest.get(field))) { return false; } } return true; } /** * Returns the source to satisfy {@code request} given this cached response. */ public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) { /* * If this response shouldn't have been stored, it should never be used * as a response source. This check should be redundant as long as the * persistence store is well-behaved and the rules are constant. */ if (!isCacheable(request)) { return ResponseSource.NETWORK; } if (request.isNoCache() || request.hasConditions()) { return ResponseSource.NETWORK; } long ageMillis = computeAge(nowMillis); long freshMillis = computeFreshnessLifetime(); if (request.getMaxAgeSeconds() != -1) { freshMillis = Math.min(freshMillis, TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds())); } long minFreshMillis = 0; if (request.getMinFreshSeconds() != -1) { minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds()); } long maxStaleMillis = 0; if (!mustRevalidate && request.getMaxStaleSeconds() != -1) { maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds()); } if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { if (ageMillis + minFreshMillis >= freshMillis) { headers.add("Warning", "110 HttpURLConnection \"Response is stale\""); } /* * not available in API 8 if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) { */ if (ageMillis > 24L * 60L * 60L * 1000L && isFreshnessLifetimeHeuristic()) { headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\""); } return ResponseSource.CACHE; } if (etag != null) { request.setIfNoneMatch(etag); } else if (lastModified != null) { request.setIfModifiedSince(lastModified); } else if (servedDate != null) { request.setIfModifiedSince(servedDate); } return request.hasConditions() ? ResponseSource.CONDITIONAL_CACHE : ResponseSource.NETWORK; } /** * Returns true if this cached response should be used; false if the * network response should be used. */ public boolean validate(ResponseHeaders networkResponse) { if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { return true; } /* * The HTTP spec says that if the network's response is older than our * cached response, we may return the cache's response. Like Chrome (but * unlike Firefox), this client prefers to return the newer response. */ if (lastModified != null && networkResponse.lastModified != null && networkResponse.lastModified.getTime() < lastModified.getTime()) { return true; } return false; } /** * Combines this cached header with a network header as defined by RFC 2616, * 13.5.3. */ public ResponseHeaders combine(ResponseHeaders network) { RawHeaders result = new RawHeaders(); for (int i = 0; i < headers.length(); i++) { String fieldName = headers.getFieldName(i); String value = headers.getValue(i); if (fieldName.equals("Warning") && value.startsWith("1")) { continue; // drop 100-level freshness warnings } if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) { result.add(fieldName, value); } } for (int i = 0; i < network.headers.length(); i++) { String fieldName = network.headers.getFieldName(i); if (isEndToEnd(fieldName)) { result.add(fieldName, network.headers.getValue(i)); } } return new ResponseHeaders(uri, result); } /** * Returns true if {@code fieldName} is an end-to-end HTTP header, as * defined by RFC 2616, 13.5.1. */ private static boolean isEndToEnd(String fieldName) { return !fieldName.equalsIgnoreCase("Connection") && !fieldName.equalsIgnoreCase("Keep-Alive") && !fieldName.equalsIgnoreCase("Proxy-Authenticate") && !fieldName.equalsIgnoreCase("Proxy-Authorization") && !fieldName.equalsIgnoreCase("TE") && !fieldName.equalsIgnoreCase("Trailers") && !fieldName.equalsIgnoreCase("Transfer-Encoding") && !fieldName.equalsIgnoreCase("Upgrade"); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/cache/ResponseSource.java ================================================ /* * 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 com.jeffmony.async.http.cache; enum ResponseSource { /** * Return the response from the cache immediately. */ CACHE, /** * Make a conditional request to the host, returning the cache response if * the cache is valid and the network response otherwise. */ CONDITIONAL_CACHE, /** * Return the response from the network. */ NETWORK; public boolean requiresConnection() { return this == CONDITIONAL_CACHE || this == NETWORK; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/cache/StrictLineReader.java ================================================ /* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jeffmony.async.http.cache; import com.jeffmony.async.util.Charsets; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; /** * Buffers input from an {@link InputStream} for reading lines. * * This class is used for buffered reading of lines. For purposes of this class, a line ends with * "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated line at * end of input is invalid and will be ignored, the caller may use {@code hasUnterminatedLine()} * to detect it after catching the {@code EOFException}. * * This class is intended for reading input that strictly consists of lines, such as line-based * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction with * {@link java.io.InputStreamReader} provides similar functionality, this class uses different * end-of-input reporting and a more restrictive definition of a line. * * This class supports only charsets that encode '\r' and '\n' as a single byte with value 13 * and 10, respectively, and the representation of no other character contains these values. * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1. * The default charset is US_ASCII. */ class StrictLineReader implements Closeable { private static final byte CR = (byte)'\r'; private static final byte LF = (byte)'\n'; private final InputStream in; /* * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end * and the data in the range [pos, end) is buffered for reading. At end of input, if there is * an unterminated line, we set end == -1, otherwise end == pos. If the underlying * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1. */ private byte[] buf; private int pos; private int end; /** * Constructs a new {@code StrictLineReader} with the default capacity and charset. * * @param in the {@code InputStream} to read data from. * @throws NullPointerException if {@code in} is null. */ public StrictLineReader(InputStream in) { this(in, 8192); } /** * Constructs a new {@code LineReader} with the specified capacity and the default charset. * * @param in the {@code InputStream} to read data from. * @param capacity the capacity of the buffer. * @throws NullPointerException if {@code in} is null. * @throws IllegalArgumentException for negative or zero {@code capacity}. */ public StrictLineReader(InputStream in, int capacity) { this(in, capacity, Charsets.US_ASCII); } /** * Constructs a new {@code LineReader} with the specified charset and the default capacity. * * @param in the {@code InputStream} to read data from. * @param charset the charset used to decode data. * Only US-ASCII, UTF-8 and ISO-8859-1 is supported. * @throws NullPointerException if {@code in} or {@code charset} is null. * @throws IllegalArgumentException if the specified charset is not supported. */ public StrictLineReader(InputStream in, Charset charset) { this(in, 8192, charset); } /** * Constructs a new {@code LineReader} with the specified capacity and charset. * * @param in the {@code InputStream} to read data from. * @param capacity the capacity of the buffer. * @param charset the charset used to decode data. * Only US-ASCII, UTF-8 and ISO-8859-1 is supported. * @throws NullPointerException if {@code in} or {@code charset} is null. * @throws IllegalArgumentException if {@code capacity} is negative or zero * or the specified charset is not supported. */ public StrictLineReader(InputStream in, int capacity, Charset charset) { if (in == null) { throw new NullPointerException("in == null"); } else if (charset == null) { throw new NullPointerException("charset == null"); } if (capacity < 0) { throw new IllegalArgumentException("capacity <= 0"); } if (!(charset.equals(Charsets.US_ASCII) || charset.equals(Charsets.UTF_8))) { throw new IllegalArgumentException("Unsupported encoding"); } this.in = in; buf = new byte[capacity]; } /** * Closes the reader by closing the underlying {@code InputStream} and * marking this reader as closed. * * @throws IOException for errors when closing the underlying {@code InputStream}. */ @Override public void close() throws IOException { synchronized (in) { if (buf != null) { buf = null; in.close(); } } } /** * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"}, * this end of line marker is not included in the result. * * @return the next line from the input. * @throws IOException for underlying {@code InputStream} errors. * @throws EOFException for the end of source stream. */ public String readLine() throws IOException { synchronized (in) { if (buf == null) { throw new IOException("LineReader is closed"); } // Read more data if we are at the end of the buffered data. // Though it's an error to read after an exception, we will let {@code fillBuf()} // throw again if that happens; thus we need to handle end == -1 as well as end == pos. if (pos >= end) { fillBuf(); } // Try to find LF in the buffered data and return the line if successful. for (int i = pos; i != end; ++i) { if (buf[i] == LF) { int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i; String res = new String(buf, pos, lineEnd - pos); pos = i + 1; return res; } } // Let's anticipate up to 80 characters on top of those already read. ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) { @Override public String toString() { int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count; return new String(buf, 0, length); } }; while (true) { out.write(buf, pos, end - pos); // Mark unterminated line in case fillBuf throws EOFException or IOException. end = -1; fillBuf(); // Try to find LF in the buffered data and return the line if successful. for (int i = pos; i != end; ++i) { if (buf[i] == LF) { if (i != pos) { out.write(buf, pos, i - pos); } pos = i + 1; return out.toString(); } } } } } /** * Read an {@code int} from a line containing its decimal representation. * * @return the value of the {@code int} from the next line. * @throws IOException for underlying {@code InputStream} errors or conversion error. * @throws EOFException for the end of source stream. */ public int readInt() throws IOException { String intString = readLine(); try { return Integer.parseInt(intString); } catch (NumberFormatException e) { throw new IOException("expected an int but was \"" + intString + "\""); } } /** * Check whether there was an unterminated line at end of input after the line reader reported * end-of-input with EOFException. The value is meaningless in any other situation. * * @return true if there was an unterminated line at end of input. */ public boolean hasUnterminatedLine() { return end == -1; } /** * Reads new input data into the buffer. Call only with pos == end or end == -1, * depending on the desired outcome if the function throws. * * @throws IOException for underlying {@code InputStream} errors. * @throws EOFException for the end of source stream. */ private void fillBuf() throws IOException { int result = in.read(buf, 0, buf.length); if (result == -1) { throw new EOFException(); } pos = 0; end = result; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/callback/HttpConnectCallback.java ================================================ package com.jeffmony.async.http.callback; import com.jeffmony.async.http.AsyncHttpResponse; public interface HttpConnectCallback { void onConnectCompleted(Exception ex, AsyncHttpResponse response); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/callback/RequestCallback.java ================================================ package com.jeffmony.async.http.callback; import com.jeffmony.async.callback.ResultCallback; import com.jeffmony.async.http.AsyncHttpResponse; public interface RequestCallback extends ResultCallback { void onConnect(AsyncHttpResponse response); void onProgress(AsyncHttpResponse response, long downloaded, long total); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/filter/ChunkedDataException.java ================================================ package com.jeffmony.async.http.filter; public class ChunkedDataException extends Exception { public ChunkedDataException(String message) { super(message); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/filter/ChunkedInputFilter.java ================================================ package com.jeffmony.async.http.filter; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.FilteredDataEmitter; import com.jeffmony.async.Util; public class ChunkedInputFilter extends FilteredDataEmitter { private int mChunkLength = 0; private int mChunkLengthRemaining = 0; private State mState = State.CHUNK_LEN; private enum State { CHUNK_LEN, CHUNK_LEN_CR, CHUNK_LEN_CRLF, CHUNK, CHUNK_CR, CHUNK_CRLF, COMPLETE, ERROR, } private boolean checkByte(char b, char value) { if (b != value) { mState = State.ERROR; report(new ChunkedDataException(value + " was expected, got " + (char)b)); return false; } return true; } private boolean checkLF(char b) { return checkByte(b, '\n'); } private boolean checkCR(char b) { return checkByte(b, '\r'); } @Override protected void report(Exception e) { if (e == null && mState != State.COMPLETE) e = new ChunkedDataException("chunked input ended before final chunk"); super.report(e); } ByteBufferList pending = new ByteBufferList(); @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { if (mState == State.ERROR) { bb.recycle(); return; } try { while (bb.remaining() > 0) { switch (mState) { case CHUNK_LEN: char c = bb.getByteChar(); if (c == '\r') { mState = State.CHUNK_LEN_CR; } else { mChunkLength *= 16; if (c >= 'a' && c <= 'f') mChunkLength += (c - 'a' + 10); else if (c >= '0' && c <= '9') mChunkLength += c - '0'; else if (c >= 'A' && c <= 'F') mChunkLength += (c - 'A' + 10); else { report(new ChunkedDataException("invalid chunk length: " + c)); return; } } mChunkLengthRemaining = mChunkLength; break; case CHUNK_LEN_CR: if (!checkLF(bb.getByteChar())) return; mState = State.CHUNK; break; case CHUNK: int remaining = bb.remaining(); int reading = Math.min(mChunkLengthRemaining, remaining); mChunkLengthRemaining -= reading; if (mChunkLengthRemaining == 0) { mState = State.CHUNK_CR; } if (reading == 0) break; bb.get(pending, reading); Util.emitAllData(this, pending); break; case CHUNK_CR: if (!checkCR(bb.getByteChar())) return; mState = State.CHUNK_CRLF; break; case CHUNK_CRLF: if (!checkLF(bb.getByteChar())) return; if (mChunkLength > 0) { mState = State.CHUNK_LEN; } else { mState = State.COMPLETE; report(null); } mChunkLength = 0; break; case COMPLETE: assert false; // Exception fail = new Exception("Continued receiving data after chunk complete"); // report(fail); return; } } } catch (Exception ex) { report(ex); } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/filter/ChunkedOutputFilter.java ================================================ package com.jeffmony.async.http.filter; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataSink; import com.jeffmony.async.FilteredDataSink; import java.nio.ByteBuffer; public class ChunkedOutputFilter extends FilteredDataSink { public ChunkedOutputFilter(DataSink sink) { super(sink); } @Override public ByteBufferList filter(ByteBufferList bb) { String chunkLen = Integer.toString(bb.remaining(), 16) + "\r\n"; bb.addFirst(ByteBuffer.wrap(chunkLen.getBytes())); bb.add(ByteBuffer.wrap("\r\n".getBytes())); return bb; } @Override public void end() { setMaxBuffer(Integer.MAX_VALUE); ByteBufferList fin = new ByteBufferList(); write(fin); setMaxBuffer(0); // do NOT call through to super.end, as chunking is a framing protocol. // we don't want to close the underlying transport. } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/filter/ContentLengthFilter.java ================================================ package com.jeffmony.async.http.filter; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.FilteredDataEmitter; public class ContentLengthFilter extends FilteredDataEmitter { public ContentLengthFilter(long contentLength) { this.contentLength = contentLength; } @Override protected void report(Exception e) { if (e == null && totalRead != contentLength) e = new PrematureDataEndException("End of data reached before content length was read: " + totalRead + "/" + contentLength + " Paused: " + isPaused()); super.report(e); } long contentLength; long totalRead; ByteBufferList transformed = new ByteBufferList(); @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { assert totalRead < contentLength; int remaining = bb.remaining(); long toRead = Math.min(contentLength - totalRead, remaining); bb.get(transformed, (int)toRead); int beforeRead = transformed.remaining(); super.onDataAvailable(emitter, transformed); totalRead += (beforeRead - transformed.remaining()); transformed.get(bb); if (totalRead == contentLength) report(null); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/filter/DataRemainingException.java ================================================ package com.jeffmony.async.http.filter; public class DataRemainingException extends Exception { public DataRemainingException(String message, Exception cause) { super(message, cause); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/filter/GZIPInputFilter.java ================================================ package com.jeffmony.async.http.filter; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.PushParser; import com.jeffmony.async.callback.DataCallback; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Locale; import java.util.zip.CRC32; import java.util.zip.GZIPInputStream; import java.util.zip.Inflater; public class GZIPInputFilter extends InflaterInputFilter { static short peekShort(byte[] src, int offset, ByteOrder order) { if (order == ByteOrder.BIG_ENDIAN) { return (short) ((src[offset] << 8) | (src[offset + 1] & 0xff)); } else { return (short) ((src[offset + 1] << 8) | (src[offset] & 0xff)); } } private static final int FCOMMENT = 16; private static final int FEXTRA = 4; private static final int FHCRC = 2; private static final int FNAME = 8; public GZIPInputFilter() { super(new Inflater(true)); } boolean mNeedsHeader = true; protected CRC32 crc = new CRC32(); public static int unsignedToBytes(byte b) { return b & 0xFF; } @Override @SuppressWarnings("unused") public void onDataAvailable(final DataEmitter emitter, ByteBufferList bb) { if (mNeedsHeader) { final PushParser parser = new PushParser(emitter); parser.readByteArray(10, new PushParser.ParseCallback() { int flags; boolean hcrc; public void parsed(byte[] header) { short magic = peekShort(header, 0, ByteOrder.LITTLE_ENDIAN); if (magic != (short) GZIPInputStream.GZIP_MAGIC) { report(new IOException(String.format(Locale.ENGLISH, "unknown format (magic number %x)", magic))); emitter.setDataCallback(new DataCallback.NullDataCallback()); return; } flags = header[3]; hcrc = (flags & FHCRC) != 0; if (hcrc) { crc.update(header, 0, header.length); } if ((flags & FEXTRA) != 0) { parser.readByteArray(2, new PushParser.ParseCallback() { public void parsed(byte[] header) { if (hcrc) { crc.update(header, 0, 2); } int length = peekShort(header, 0, ByteOrder.LITTLE_ENDIAN) & 0xffff; parser.readByteArray(length, new PushParser.ParseCallback() { public void parsed(byte[] buf) { if (hcrc) { crc.update(buf, 0, buf.length); } next(); } }); } }); } else { next(); } } private void next() { PushParser parser = new PushParser(emitter); DataCallback summer = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { if (hcrc) { while (bb.size() > 0) { ByteBuffer b = bb.remove(); crc.update(b.array(), b.arrayOffset() + b.position(), b.remaining()); ByteBufferList.reclaim(b); } } bb.recycle(); done(); } }; if ((flags & FNAME) != 0) { parser.until((byte) 0, summer); return; } if ((flags & FCOMMENT) != 0) { parser.until((byte) 0, summer); return; } done(); } private void done() { if (hcrc) { parser.readByteArray(2, new PushParser.ParseCallback() { public void parsed(byte[] header) { short crc16 = peekShort(header, 0, ByteOrder.LITTLE_ENDIAN); if ((short) crc.getValue() != crc16) { report(new IOException("CRC mismatch")); return; } crc.reset(); mNeedsHeader = false; setDataEmitter(emitter); // emitter.setDataCallback(GZIPInputFilter.this); } }); } else { mNeedsHeader = false; setDataEmitter(emitter); } } }); } else { super.onDataAvailable(emitter, bb); } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/filter/InflaterInputFilter.java ================================================ package com.jeffmony.async.http.filter; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.FilteredDataEmitter; import com.jeffmony.async.Util; import java.nio.ByteBuffer; import java.util.zip.Inflater; public class InflaterInputFilter extends FilteredDataEmitter { private Inflater mInflater; @Override protected void report(Exception e) { mInflater.end(); if (e != null && mInflater.getRemaining() > 0) { e = new DataRemainingException("data still remaining in inflater", e); } super.report(e); } ByteBufferList transformed = new ByteBufferList(); @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { try { ByteBuffer output = ByteBufferList.obtain(bb.remaining() * 2); int totalRead = 0; while (bb.size() > 0) { ByteBuffer b = bb.remove(); if (b.hasRemaining()) { totalRead =+ b.remaining(); mInflater.setInput(b.array(), b.arrayOffset() + b.position(), b.remaining()); do { int inflated = mInflater.inflate(output.array(), output.arrayOffset() + output.position(), output.remaining()); output.position(output.position() + inflated); if (!output.hasRemaining()) { output.flip(); transformed.add(output); assert totalRead != 0; int newSize = output.capacity() * 2; output = ByteBufferList.obtain(newSize); } } while (!mInflater.needsInput() && !mInflater.finished()); } ByteBufferList.reclaim(b); } output.flip(); transformed.add(output); Util.emitAllData(this, transformed); } catch (Exception ex) { report(ex); } } public InflaterInputFilter() { this(new Inflater()); } public InflaterInputFilter(Inflater inflater) { mInflater = inflater; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/filter/PrematureDataEndException.java ================================================ package com.jeffmony.async.http.filter; public class PrematureDataEndException extends Exception { public PrematureDataEndException(String message) { super(message); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/AsyncHttpRequestBodyProvider.java ================================================ package com.jeffmony.async.http.server; import com.jeffmony.async.http.Headers; import com.jeffmony.async.http.body.AsyncHttpRequestBody; public interface AsyncHttpRequestBodyProvider { AsyncHttpRequestBody getBody(Headers headers); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/AsyncHttpServer.java ================================================ package com.jeffmony.async.http.server; import android.annotation.TargetApi; import android.os.Build; import android.util.Log; import com.jeffmony.async.AsyncSSLSocket; import com.jeffmony.async.AsyncSSLSocketWrapper; import com.jeffmony.async.AsyncServer; import com.jeffmony.async.AsyncServerSocket; import com.jeffmony.async.AsyncSocket; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.ListenCallback; import com.jeffmony.async.callback.ValueCallback; import com.jeffmony.async.http.Headers; import com.jeffmony.async.http.HttpUtil; import com.jeffmony.async.http.Multimap; import com.jeffmony.async.http.WebSocket; import com.jeffmony.async.http.body.AsyncHttpRequestBody; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Hashtable; import javax.net.ssl.SSLContext; @TargetApi(Build.VERSION_CODES.ECLAIR) public class AsyncHttpServer extends AsyncHttpServerRouter { ArrayList mListeners = new ArrayList(); public void stop() { if (mListeners != null) { for (AsyncServerSocket listener: mListeners) { listener.stop(); } } } protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { return false; } protected void onResponseCompleted(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { } protected void onRequest(HttpServerRequestCallback callback, AsyncHttpServerRequest request, AsyncHttpServerResponse response) { if (callback != null) { try { callback.onRequest(request, response); } catch (Exception e) { Log.e("AsyncHttpServer", "request callback raised uncaught exception. Catching versus crashing process", e); response.code(500); response.end(); } } } protected boolean isKeepAlive(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { return HttpUtil.isKeepAlive(response.getHttpVersion(), request.getHeaders()); } protected AsyncHttpRequestBody onUnknownBody(Headers headers) { return new UnknownRequestBody(headers.get("Content-Type")); } protected boolean isSwitchingProtocols(AsyncHttpServerResponse res) { return res.code() == 101; } ListenCallback mListenCallback = new ListenCallback() { @Override public void onAccepted(final AsyncSocket socket) { final AsyncHttpServerRequestImpl req = new AsyncHttpServerRequestImpl() { AsyncHttpServerRequestImpl self = this; HttpServerRequestCallback requestCallback; String fullPath; String path; boolean responseComplete; boolean requestComplete; AsyncHttpServerResponseImpl res; boolean hasContinued; boolean handled; final Runnable onFinally = new Runnable() { @Override public void run() { Log.i("HTTP", "Done"); } }; final ValueCallback onException = new ValueCallback() { @Override public void onResult(Exception value) { Log.e("HTTP", "exception", value); } }; void onRequest() { AsyncHttpServer.this.onRequest(requestCallback, this, res); } @Override protected AsyncHttpRequestBody onBody(Headers headers) { String statusLine = getStatusLine(); String[] parts = statusLine.split(" "); fullPath = parts[1]; path = URLDecoder.decode(fullPath.split("\\?")[0]); method = parts[0]; RouteMatch route = route(method, path); if (route == null) return null; matcher = route.matcher; requestCallback = route.callback; if (route.bodyCallback == null) return null; return route.bodyCallback.getBody(headers); } @Override protected AsyncHttpRequestBody onUnknownBody(Headers headers) { return AsyncHttpServer.this.onUnknownBody(headers); } @Override protected void onHeadersReceived() { Headers headers = getHeaders(); // should the negotiation of 100 continue be here, or in the request impl? // probably here, so AsyncResponse can negotiate a 100 continue. if (!hasContinued && "100-continue".equals(headers.get("Expect"))) { pause(); // System.out.println("continuing..."); Util.writeAll(mSocket, "HTTP/1.1 100 Continue\r\n\r\n".getBytes(), new CompletedCallback() { @Override public void onCompleted(Exception ex) { resume(); if (ex != null) { report(ex); return; } hasContinued = true; onHeadersReceived(); } }); return; } // System.out.println(headers.toHeaderString()); res = new AsyncHttpServerResponseImpl(socket, this) { @Override protected void report(Exception e) { super.report(e); if (e != null) { socket.setDataCallback(new NullDataCallback()); socket.setEndCallback(new NullCompletedCallback()); socket.close(); } } @Override protected void onEnd() { responseComplete = true; super.onEnd(); mSocket.setEndCallback(null); onResponseCompleted(getRequest(), res); // reuse the socket for a subsequent request. handleOnCompleted(); } }; handled = AsyncHttpServer.this.onRequest(this, res); if (handled) return; if (requestCallback == null) { res.code(404); res.end(); return; } if (!getBody().readFullyOnRequest() || requestComplete) onRequest(); } @Override public void onCompleted(Exception e) { if (isSwitchingProtocols(res)) return; requestComplete = true; super.onCompleted(e); // no http pipelining, gc trashing if the socket dies // while the request is being sent and is paused or something mSocket.setDataCallback(new NullDataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { super.onDataAvailable(emitter, bb); mSocket.close(); } }); if (e != null) { mSocket.close(); return; } handleOnCompleted(); if (getBody().readFullyOnRequest() && !handled) { onRequest(); } } private void handleOnCompleted() { // response may complete before request. the request may have a body, and // the response may be sent before it is fully sent. // if the protocol was switched off http, abandon the socket, // otherwise attempt to recycle it. if (requestComplete && responseComplete && !isSwitchingProtocols(res)) { if (isKeepAlive(self, res)) { onAccepted(socket); } else { socket.close(); } } } @Override public String getPath() { return path; } @Override public Multimap getQuery() { String[] parts = fullPath.split("\\?", 2); if (parts.length < 2) return new Multimap(); return Multimap.parseQuery(parts[1]); } @Override public String getUrl() { return fullPath; } }; req.setSocket(socket); socket.resume(); } @Override public void onCompleted(Exception error) { report(error); } @Override public void onListening(AsyncServerSocket socket) { mListeners.add(socket); } }; public AsyncServerSocket listen(AsyncServer server, int port) { return server.listen(null, port, mListenCallback); } private void report(Exception ex) { if (mCompletedCallback != null) mCompletedCallback.onCompleted(ex); } public AsyncServerSocket listen(int port) { return listen(AsyncServer.getDefault(), port); } public void listenSecure(final int port, final SSLContext sslContext) { AsyncServer.getDefault().listen(null, port, new ListenCallback() { @Override public void onAccepted(AsyncSocket socket) { AsyncSSLSocketWrapper.handshake(socket, null, port, sslContext.createSSLEngine(), null, null, false, new AsyncSSLSocketWrapper.HandshakeCallback() { @Override public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { if (socket != null) mListenCallback.onAccepted(socket); } }); } @Override public void onListening(AsyncServerSocket socket) { mListenCallback.onListening(socket); } @Override public void onCompleted(Exception ex) { mListenCallback.onCompleted(ex); } }); } public ListenCallback getListenCallback() { return mListenCallback; } CompletedCallback mCompletedCallback; public void setErrorCallback(CompletedCallback callback) { mCompletedCallback = callback; } public CompletedCallback getErrorCallback() { return mCompletedCallback; } private static Hashtable mCodes = new Hashtable(); static { mCodes.put(200, "OK"); mCodes.put(202, "Accepted"); mCodes.put(206, "Partial Content"); mCodes.put(101, "Switching Protocols"); mCodes.put(301, "Moved Permanently"); mCodes.put(302, "Found"); mCodes.put(304, "Not Modified"); mCodes.put(400, "Bad Request"); mCodes.put(404, "Not Found"); mCodes.put(500, "Internal Server Error"); } public static String getResponseCodeDescription(int code) { String d = mCodes.get(code); if (d == null) return "Unknown"; return d; } public static interface WebSocketRequestCallback { void onConnected(WebSocket webSocket, AsyncHttpServerRequest request); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/AsyncHttpServerRequest.java ================================================ package com.jeffmony.async.http.server; import com.jeffmony.async.AsyncSocket; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.http.Headers; import com.jeffmony.async.http.Multimap; import com.jeffmony.async.http.body.AsyncHttpRequestBody; import java.util.Map; import java.util.regex.Matcher; public interface AsyncHttpServerRequest extends DataEmitter { Headers getHeaders(); Matcher getMatcher(); void setMatcher(Matcher matcher); T getBody(); AsyncSocket getSocket(); String getPath(); Multimap getQuery(); String getMethod(); String getUrl(); String get(String name); Map getState(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/AsyncHttpServerRequestImpl.java ================================================ package com.jeffmony.async.http.server; import com.jeffmony.async.AsyncSocket; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.FilteredDataEmitter; import com.jeffmony.async.LineEmitter; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.http.Headers; import com.jeffmony.async.http.HttpUtil; import com.jeffmony.async.http.Multimap; import com.jeffmony.async.http.Protocol; import com.jeffmony.async.http.body.AsyncHttpRequestBody; import java.io.IOException; import java.util.HashMap; public abstract class AsyncHttpServerRequestImpl extends FilteredDataEmitter implements AsyncHttpServerRequest, CompletedCallback { private String statusLine; private Headers mRawHeaders = new Headers(); AsyncSocket mSocket; private HashMap state = new HashMap<>(); @Override public HashMap getState() { return state; } public String getStatusLine() { return statusLine; } private CompletedCallback mReporter = new CompletedCallback() { @Override public void onCompleted(Exception error) { AsyncHttpServerRequestImpl.this.onCompleted(error); } }; @Override public void onCompleted(Exception e) { // if (mBody != null) // mBody.onCompleted(e); report(e); } abstract protected void onHeadersReceived(); protected void onNotHttp() { System.out.println("not http!"); } protected AsyncHttpRequestBody onUnknownBody(Headers headers) { return null; } protected AsyncHttpRequestBody onBody(Headers headers) { return null; } LineEmitter.StringCallback mHeaderCallback = new LineEmitter.StringCallback() { @Override public void onStringAvailable(String s) { if (statusLine == null) { statusLine = s; if (!statusLine.contains("HTTP/")) { onNotHttp(); mSocket.setDataCallback(new NullDataCallback()); report(new IOException("data/header received was not not http")); } return; } if (!"\r".equals(s)){ mRawHeaders.addLine(s); return; } DataEmitter emitter = HttpUtil.getBodyDecoder(mSocket, Protocol.HTTP_1_1, mRawHeaders, true); mBody = onBody(mRawHeaders); if (mBody == null) { mBody = HttpUtil.getBody(emitter, mReporter, mRawHeaders); if (mBody == null) { mBody = onUnknownBody(mRawHeaders); if (mBody == null) mBody = new UnknownRequestBody(mRawHeaders.get("Content-Type")); } } mBody.parse(emitter, mReporter); onHeadersReceived(); } }; String method; @Override public String getMethod() { return method; } void setSocket(AsyncSocket socket) { mSocket = socket; LineEmitter liner = new LineEmitter(); mSocket.setDataCallback(liner); liner.setLineCallback(mHeaderCallback); mSocket.setEndCallback(new NullCompletedCallback()); } @Override public AsyncSocket getSocket() { return mSocket; } @Override public Headers getHeaders() { return mRawHeaders; } @Override public void setDataCallback(DataCallback callback) { mSocket.setDataCallback(callback); } @Override public DataCallback getDataCallback() { return mSocket.getDataCallback(); } @Override public boolean isChunked() { return mSocket.isChunked(); } AsyncHttpRequestBody mBody; @Override public AsyncHttpRequestBody getBody() { return mBody; } @Override public void pause() { mSocket.pause(); } @Override public void resume() { mSocket.resume(); } @Override public boolean isPaused() { return mSocket.isPaused(); } @Override public String toString() { if (mRawHeaders == null) return super.toString(); return mRawHeaders.toPrefixString(statusLine); } @Override public String get(String name) { Multimap query = getQuery(); String ret = query.getString(name); if (ret != null) return ret; AsyncHttpRequestBody body = getBody(); Object bodyObject = body.get(); if (bodyObject instanceof Multimap) { Multimap map = (Multimap)bodyObject; return map.getString(name); } return null; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/AsyncHttpServerResponse.java ================================================ package com.jeffmony.async.http.server; import com.jeffmony.async.AsyncSocket; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataSink; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.http.AsyncHttpResponse; import com.jeffmony.async.http.Headers; import com.jeffmony.async.parser.AsyncParser; import org.json.JSONArray; import org.json.JSONObject; import java.io.File; import java.io.InputStream; import java.nio.ByteBuffer; public interface AsyncHttpServerResponse extends DataSink, CompletedCallback { void end(); void send(String contentType, byte[] bytes); void send(String contentType, ByteBufferList bb); void send(String contentType, ByteBuffer bb); void send(String contentType, String string); void send(String string); void send(JSONObject json); void send(JSONArray jsonArray); void sendFile(File file); void sendStream(InputStream inputStream, long totalLength); void sendBody(AsyncParser body, T value); AsyncHttpServerResponse code(int code); int code(); Headers getHeaders(); void writeHead(); void setContentType(String contentType); void redirect(String location); AsyncHttpServerRequest getRequest(); String getHttpVersion(); void setHttpVersion(String httpVersion); // NOT FINAL void proxy(AsyncHttpResponse response); /** * Alias for end. Used with CompletedEmitters */ void onCompleted(Exception ex); AsyncSocket getSocket(); void setSocket(AsyncSocket socket); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/AsyncHttpServerResponseImpl.java ================================================ package com.jeffmony.async.http.server; import android.text.TextUtils; import com.jeffmony.async.AsyncServer; import com.jeffmony.async.AsyncSocket; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataSink; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.callback.WritableCallback; import com.jeffmony.async.http.AsyncHttpHead; import com.jeffmony.async.http.AsyncHttpResponse; import com.jeffmony.async.http.Headers; import com.jeffmony.async.http.HttpUtil; import com.jeffmony.async.http.Protocol; import com.jeffmony.async.http.filter.ChunkedOutputFilter; import com.jeffmony.async.parser.AsyncParser; import com.jeffmony.async.util.StreamUtility; import org.json.JSONArray; import org.json.JSONObject; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.util.Locale; public class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { private Headers mRawHeaders = new Headers(); private long mContentLength = -1; @Override public Headers getHeaders() { return mRawHeaders; } public AsyncSocket getSocket() { return mSocket; } @Override public void setSocket(AsyncSocket socket) { mSocket = socket; } AsyncSocket mSocket; AsyncHttpServerRequestImpl mRequest; AsyncHttpServerResponseImpl(AsyncSocket socket, AsyncHttpServerRequestImpl req) { mSocket = socket; mRequest = req; if (HttpUtil.isKeepAlive(Protocol.HTTP_1_1, req.getHeaders())) mRawHeaders.set("Connection", "Keep-Alive"); } @Override public AsyncHttpServerRequest getRequest() { return mRequest; } @Override public void write(ByteBufferList bb) { // order is important here... assert !mEnded; // do the header write... this will call onWritable, which may be reentrant if (!headWritten) initFirstWrite(); // now check to see if the list is empty. reentrancy may cause it to empty itself. if (bb.remaining() == 0) return; // null sink means that the header has not finished writing if (mSink == null) return; // can successfully write! mSink.write(bb); } boolean headWritten = false; DataSink mSink; void initFirstWrite() { if (headWritten) return; headWritten = true; final boolean isChunked; String currentEncoding = mRawHeaders.get("Transfer-Encoding"); if ("".equals(currentEncoding)) mRawHeaders.removeAll("Transfer-Encoding"); boolean canUseChunked = ("Chunked".equalsIgnoreCase(currentEncoding) || currentEncoding == null) && !"close".equalsIgnoreCase(mRawHeaders.get("Connection")); if (mContentLength < 0) { String contentLength = mRawHeaders.get("Content-Length"); if (!TextUtils.isEmpty(contentLength)) mContentLength = Long.valueOf(contentLength); } if (mContentLength < 0 && canUseChunked) { mRawHeaders.set("Transfer-Encoding", "Chunked"); isChunked = true; } else { isChunked = false; } String statusLine = String.format(Locale.ENGLISH, "%s %s %s", httpVersion, code, AsyncHttpServer.getResponseCodeDescription(code)); String rh = mRawHeaders.toPrefixString(statusLine); Util.writeAll(mSocket, rh.getBytes(), ex -> { if (ex != null) { report(ex); return; } if (isChunked) { ChunkedOutputFilter chunked = new ChunkedOutputFilter(mSocket); chunked.setMaxBuffer(0); mSink = chunked; } else { mSink = mSocket; } mSink.setClosedCallback(closedCallback); closedCallback = null; mSink.setWriteableCallback(writable); writable = null; if (ended) { // the response ended while headers were written end(); return; } getServer().post(() -> { WritableCallback wb = getWriteableCallback(); if (wb != null) wb.onWriteable(); }); }); } WritableCallback writable; @Override public void setWriteableCallback(WritableCallback handler) { if (mSink != null) mSink.setWriteableCallback(handler); else writable = handler; } @Override public WritableCallback getWriteableCallback() { if (mSink != null) return mSink.getWriteableCallback(); return writable; } boolean ended; @Override public void end() { if (ended) return; ended = true; if (headWritten && mSink == null) { // header is in the process of being written... bail out. // end will be called again after finished. return; } if (!headWritten) { // end was called, and no head or body was yet written, // so strip the transfer encoding as that is superfluous. mRawHeaders.remove("Transfer-Encoding"); } if (mSink instanceof ChunkedOutputFilter) { // this filter won't close the socket underneath. mSink.end(); } else if (!headWritten) { if (!mRequest.getMethod().equalsIgnoreCase(AsyncHttpHead.METHOD)) send("text/html", ""); else { writeHead(); onEnd(); } } else { onEnd(); } } @Override public void writeHead() { initFirstWrite(); } @Override public void setContentType(String contentType) { mRawHeaders.set("Content-Type", contentType); } @Override public void send(final String contentType, final byte[] bytes) { send(contentType, new ByteBufferList(bytes)); } @Override public void sendBody(AsyncParser body, T value) { mRawHeaders.set("Content-Type", body.getMime()); body.write(this, value, ex -> end()); } @Override public void send(String contentType, ByteBuffer bb) { send(contentType, new ByteBufferList(bb)); } @Override public void send(String contentType, ByteBufferList bb) { getServer().post(() -> { mContentLength = bb.remaining(); mRawHeaders.set("Content-Length", Long.toString(mContentLength)); if (contentType != null) mRawHeaders.set("Content-Type", contentType); Util.writeAll(AsyncHttpServerResponseImpl.this, bb, ex -> onEnd()); }); } @Override public void send(String contentType, final String string) { try { send(contentType, string.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { throw new AssertionError(e); } } boolean mEnded; protected void onEnd() { mEnded = true; } protected void report(Exception e) { } @Override public void send(String string) { String contentType = mRawHeaders.get("Content-Type"); if (contentType == null) contentType = "text/html; charset=utf-8"; send(contentType, string); } @Override public void send(JSONObject json) { send("application/json; charset=utf-8", json.toString()); } @Override public void send(JSONArray jsonArray) { send("application/json; charset=utf-8", jsonArray.toString()); } @Override public void sendStream(final InputStream inputStream, long totalLength) { long start = 0; long end = totalLength - 1; String range = mRequest.getHeaders().get("Range"); if (range != null) { String[] parts = range.split("="); if (parts.length != 2 || !"bytes".equals(parts[0])) { // Requested range not satisfiable code(416); end(); return; } parts = parts[1].split("-"); try { if (parts.length > 2) throw new MalformedRangeException(); if (!TextUtils.isEmpty(parts[0])) start = Long.parseLong(parts[0]); if (parts.length == 2 && !TextUtils.isEmpty(parts[1])) end = Long.parseLong(parts[1]); else end = totalLength - 1; code(206); getHeaders().set("Content-Range", String.format(Locale.ENGLISH, "bytes %d-%d/%d", start, end, totalLength)); } catch (Exception e) { code(416); end(); return; } } try { if (start != inputStream.skip(start)) throw new StreamSkipException("skip failed to skip requested amount"); mContentLength = end - start + 1; mRawHeaders.set("Content-Length", String.valueOf(mContentLength)); mRawHeaders.set("Accept-Ranges", "bytes"); if (mRequest.getMethod().equals(AsyncHttpHead.METHOD)) { writeHead(); onEnd(); return; } if (mContentLength == 0) { writeHead(); StreamUtility.closeQuietly(inputStream); onEnd(); return; } getServer().post(() -> Util.pump(inputStream, mContentLength, AsyncHttpServerResponseImpl.this, ex -> { StreamUtility.closeQuietly(inputStream); onEnd(); })); } catch (Exception e) { code(500); end(); } } @Override public void sendFile(File file) { try { if (mRawHeaders.get("Content-Type") == null) mRawHeaders.set("Content-Type", AsyncHttpServer.getContentType(file.getAbsolutePath())); FileInputStream fin = new FileInputStream(file); sendStream(new BufferedInputStream(fin, 64000), file.length()); } catch (FileNotFoundException e) { code(404); end(); } } @Override public void proxy(final AsyncHttpResponse remoteResponse) { code(remoteResponse.code()); remoteResponse.headers().removeAll("Transfer-Encoding"); remoteResponse.headers().removeAll("Content-Encoding"); remoteResponse.headers().removeAll("Connection"); getHeaders().addAll(remoteResponse.headers()); // TODO: remove? remoteResponse.headers().set("Connection", "close"); Util.pump(remoteResponse, this, ex -> { remoteResponse.setEndCallback(new NullCompletedCallback()); remoteResponse.setDataCallback(new DataCallback.NullDataCallback()); end(); }); } int code = 200; @Override public AsyncHttpServerResponse code(int code) { this.code = code; return this; } @Override public int code() { return code; } @Override public void redirect(String location) { code(302); mRawHeaders.set("Location", location); end(); } String httpVersion = "HTTP/1.1"; @Override public String getHttpVersion() { return httpVersion; } @Override public void setHttpVersion(String httpVersion) { this.httpVersion = httpVersion; } @Override public void onCompleted(Exception ex) { end(); } @Override public boolean isOpen() { if (mSink != null) return mSink.isOpen(); return mSocket.isOpen(); } CompletedCallback closedCallback; @Override public void setClosedCallback(CompletedCallback handler) { if (mSink != null) mSink.setClosedCallback(handler); else closedCallback = handler; } @Override public CompletedCallback getClosedCallback() { if (mSink != null) return mSink.getClosedCallback(); return closedCallback; } @Override public AsyncServer getServer() { return mSocket.getServer(); } @Override public String toString() { if (mRawHeaders == null) return super.toString(); String statusLine = String.format(Locale.ENGLISH, "%s %s %s", httpVersion, code, AsyncHttpServer.getResponseCodeDescription(code)); return mRawHeaders.toPrefixString(statusLine); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/AsyncHttpServerRouter.java ================================================ package com.jeffmony.async.http.server; import android.content.Context; import android.content.res.AssetManager; import android.text.TextUtils; import android.util.Log; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.future.Future; import com.jeffmony.async.future.SimpleFuture; import com.jeffmony.async.http.AsyncHttpGet; import com.jeffmony.async.http.AsyncHttpHead; import com.jeffmony.async.http.AsyncHttpPost; import com.jeffmony.async.http.WebSocket; import com.jeffmony.async.http.WebSocketImpl; import com.jeffmony.async.util.StreamUtility; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Hashtable; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public class AsyncHttpServerRouter implements RouteMatcher { private static class RouteInfo { String method; Pattern regex; HttpServerRequestCallback callback; AsyncHttpRequestBodyProvider bodyCallback; } final ArrayList routes = new ArrayList<>(); public void removeAction(String action, String regex) { for (int i = 0; i < routes.size(); i++) { RouteInfo p = routes.get(i); if (TextUtils.equals(p.method, action) && regex.equals(p.regex.toString())) { routes.remove(i); return; } } } public void addAction(String action, String regex, HttpServerRequestCallback callback, AsyncHttpRequestBodyProvider bodyCallback) { RouteInfo p = new RouteInfo(); p.regex = Pattern.compile("^" + regex); p.callback = callback; p.method = action; p.bodyCallback = bodyCallback; synchronized (routes) { routes.add(p); } } public void addAction(String action, String regex, HttpServerRequestCallback callback) { addAction(action, regex, callback, null); } public void websocket(String regex, final AsyncHttpServer.WebSocketRequestCallback callback) { websocket(regex, null, callback); } static public WebSocket checkWebSocketUpgrade(final String protocol, AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { boolean hasUpgrade = false; String connection = request.getHeaders().get("Connection"); if (connection != null) { String[] connections = connection.split(","); for (String c: connections) { if ("Upgrade".equalsIgnoreCase(c.trim())) { hasUpgrade = true; break; } } } if (!"websocket".equalsIgnoreCase(request.getHeaders().get("Upgrade")) || !hasUpgrade) { return null; } String peerProtocol = request.getHeaders().get("Sec-WebSocket-Protocol"); if (!TextUtils.equals(protocol, peerProtocol)) { return null; } return new WebSocketImpl(request, response); } public void websocket(String regex, final String protocol, final AsyncHttpServer.WebSocketRequestCallback callback) { get(regex, (request, response) -> { WebSocket webSocket = checkWebSocketUpgrade(protocol, request, response); if (webSocket == null) { response.code(404); response.end(); return; } callback.onConnected(webSocket, request); }); } public void get(String regex, HttpServerRequestCallback callback) { addAction(AsyncHttpGet.METHOD, regex, callback); } public void post(String regex, HttpServerRequestCallback callback) { addAction(AsyncHttpPost.METHOD, regex, callback); } public static class Asset { public Asset(int available, InputStream inputStream, String path) { this.available = available; this.inputStream = inputStream; this.path = path; } public int available; public InputStream inputStream; public String path; } public static Asset getAssetStream(final Context context, String asset) { return getAssetStream(context.getAssets(), asset); } public static Asset getAssetStream(AssetManager am, String asset) { try { InputStream is = am.open(asset); return new Asset(is.available(), is, asset); } catch (IOException e) { final String[] extensions = new String[] { "/index.htm", "/index.html", "index.htm", "index.html", ".htm", ".html" }; for (String ext: extensions) { try { InputStream is = am.open(asset + ext); return new Asset(is.available(), is, asset + ext); } catch (IOException ex) { } } return null; } } static Hashtable mContentTypes = new Hashtable(); { mContentTypes.put("js", "application/javascript"); mContentTypes.put("json", "application/json"); mContentTypes.put("png", "image/png"); mContentTypes.put("jpg", "image/jpeg"); mContentTypes.put("jpeg", "image/jpeg"); mContentTypes.put("html", "text/html"); mContentTypes.put("css", "text/css"); mContentTypes.put("mp4", "video/mp4"); mContentTypes.put("mov", "video/quicktime"); mContentTypes.put("wmv", "video/x-ms-wmv"); mContentTypes.put("txt", "text/plain"); } public static String getContentType(String path) { return tryGetContentType(path); } public static String tryGetContentType(String path) { int index = path.lastIndexOf("."); if (index != -1) { String e = path.substring(index + 1); String ct = mContentTypes.get(e); if (ct != null) return ct; } return null; } static Hashtable> AppManifests = new Hashtable<>(); static synchronized Manifest ensureManifest(Context context) { Future future = AppManifests.get(context.getPackageName()); if (future != null) return future.tryGet(); ZipFile zip = null; SimpleFuture result = new SimpleFuture<>(); try { zip = new ZipFile(context.getPackageResourcePath()); ZipEntry entry = zip.getEntry("META-INF/MANIFEST.MF"); Manifest manifest = new Manifest(zip.getInputStream(entry)); result.setComplete(manifest); return manifest; } catch (Exception e) { result.setComplete(e); return null; } finally { StreamUtility.closeQuietly(zip); AppManifests.put(context.getPackageName(), result); } } static boolean isClientCached(Context context, AsyncHttpServerRequest request, AsyncHttpServerResponse response, String assetFileName) { Manifest manifest = ensureManifest(context); if (manifest == null) return false; try { String digest = manifest.getEntries().get("assets/" + assetFileName).getValue("SHA-256-Digest"); if (TextUtils.isEmpty(digest)) return false; String etag = String.format("\"%s\"", digest); response.getHeaders().set("ETag", etag); String ifNoneMatch = request.getHeaders().get("If-None-Match"); return TextUtils.equals(ifNoneMatch, etag); } catch (Exception e) { Log.w(AsyncHttpServerRouter.class.getSimpleName(), "Error getting ETag for apk asset", e); return false; } } public void directory(Context context, String regex, final String assetPath) { AssetManager am = context.getAssets(); addAction(AsyncHttpGet.METHOD, regex, (request, response) -> { String path = request.getMatcher().replaceAll(""); Asset pair = getAssetStream(am, assetPath + path); if (pair == null || pair.inputStream == null) { response.code(404); response.end(); return; } if (isClientCached(context, request, response, pair.path)) { StreamUtility.closeQuietly(pair.inputStream); response.code(304); response.end(); return; } response.getHeaders().set("Content-Length", String.valueOf(pair.available)); response.getHeaders().add("Content-Type", getContentType(pair.path)); response.code(200); Util.pump(pair.inputStream, pair.available, response, ex -> { response.end(); StreamUtility.closeQuietly(pair.inputStream); }); }); addAction(AsyncHttpHead.METHOD, regex, (request, response) -> { String path = request.getMatcher().replaceAll(""); Asset pair = getAssetStream(am, assetPath + path); if (pair == null || pair.inputStream == null) { response.code(404); response.end(); return; } StreamUtility.closeQuietly(pair.inputStream); if (isClientCached(context, request, response, pair.path)) { response.code(304); } else { response.getHeaders().set("Content-Length", String.valueOf(pair.available)); response.getHeaders().add("Content-Type", getContentType(pair.path)); response.code(200); } response.end(); }); } public void directory(String regex, final File directory) { directory(regex, directory, false); } public void directory(String regex, final File directory, final boolean list) { assert directory.isDirectory(); addAction(AsyncHttpGet.METHOD, regex, new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { String path = request.getMatcher().replaceAll(""); File file = new File(directory, path); if (file.isDirectory() && list) { ArrayList dirs = new ArrayList(); ArrayList files = new ArrayList(); for (File f: file.listFiles()) { if (f.isDirectory()) dirs.add(f); else files.add(f); } Comparator c = new Comparator() { @Override public int compare(File lhs, File rhs) { return lhs.getName().compareTo(rhs.getName()); } }; Collections.sort(dirs, c); Collections.sort(files, c); files.addAll(0, dirs); StringBuilder builder = new StringBuilder(); for (File f: files) { String p = new File(request.getPath(), f.getName()).getAbsolutePath(); builder.append(String.format("

", p, f.getName())); } response.send(builder.toString()); return; } if (!file.isFile()) { response.code(404); response.end(); return; } try { FileInputStream is = new FileInputStream(file); response.code(200); Util.pump(is, is.available(), response, new CompletedCallback() { @Override public void onCompleted(Exception ex) { response.end(); } }); } catch (IOException ex) { response.code(404); response.end(); } } }); } public static class RouteMatch { public final String method; public final String path; public final Matcher matcher; public final HttpServerRequestCallback callback; public final AsyncHttpRequestBodyProvider bodyCallback; private RouteMatch(String method, String path, Matcher matcher, HttpServerRequestCallback callback, AsyncHttpRequestBodyProvider bodyCallback) { this.method = method; this.path = path; this.matcher = matcher; this.callback = callback; this.bodyCallback = bodyCallback; } } abstract class AsyncHttpServerRequestImpl extends com.jeffmony.async.http.server.AsyncHttpServerRequestImpl { Matcher matcher; @Override public Matcher getMatcher() { return matcher; } @Override public void setMatcher(Matcher matcher) { this.matcher = matcher; } } class Callback implements HttpServerRequestCallback, RouteMatcher { @Override public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { RouteMatch match = route(request.getMethod(), request.getPath()); if (match == null) { response.code(404); response.end(); return; } match.callback.onRequest(request, response); } @Override public RouteMatch route(String method, String path) { return AsyncHttpServerRouter.this.route(method, path); } } private Callback callback = new Callback(); public HttpServerRequestCallback getCallback() { return callback; } @Override public RouteMatch route(String method, String path) { synchronized (routes) { for (RouteInfo p: routes) { // a null method is wildcard. used for nesting routers. if (!TextUtils.equals(method, p.method) && p.method != null) continue; Matcher m = p.regex.matcher(path); if (m.matches()) { if (p.callback instanceof RouteMatcher) { String subPath = m.group(1); return ((RouteMatcher)p.callback).route(method, subPath); } return new RouteMatch(method, path, m, p.callback, p.bodyCallback); } } } return null; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/AsyncProxyServer.java ================================================ package com.jeffmony.async.http.server; import android.net.Uri; import com.jeffmony.async.AsyncServer; import com.jeffmony.async.http.AsyncHttpClient; import com.jeffmony.async.http.AsyncHttpRequest; import com.jeffmony.async.http.AsyncHttpResponse; import com.jeffmony.async.http.callback.HttpConnectCallback; /** * Created by koush on 7/22/14. */ public class AsyncProxyServer extends AsyncHttpServer { AsyncHttpClient proxyClient; public AsyncProxyServer(AsyncServer server) { proxyClient = new AsyncHttpClient(server); } @Override protected void onRequest(HttpServerRequestCallback callback, AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { super.onRequest(callback, request, response); if (callback != null) return; try { Uri uri; try { uri = Uri.parse(request.getPath()); if (uri.getScheme() == null) throw new Exception("no host or full uri provided"); } catch (Exception e) { String host = request.getHeaders().get("Host"); int port = 80; if (host != null) { String[] splits = host.split(":", 2); if (splits.length == 2) { host = splits[0]; port = Integer.parseInt(splits[1]); } } uri = Uri.parse("http://" + host + ":" + port + request.getPath()); } proxyClient.execute(new AsyncHttpRequest(uri, request.getMethod(), request.getHeaders()), new HttpConnectCallback() { @Override public void onConnectCompleted(Exception ex, AsyncHttpResponse remoteResponse) { if (ex != null) { response.code(500); response.send(ex.getMessage()); return; } response.proxy(remoteResponse); } }); } catch (Exception e) { response.code(500); response.send(e.getMessage()); } } @Override protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { return true; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/BoundaryEmitter.java ================================================ package com.jeffmony.async.http.server; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.FilteredDataEmitter; import java.nio.ByteBuffer; public class BoundaryEmitter extends FilteredDataEmitter { private byte[] boundary; public void setBoundary(String boundary) { this.boundary = ("\r\n--" + boundary).getBytes(); } public String getBoundary() { if (boundary == null) return null; return new String(boundary, 4, boundary.length - 4); } public String getBoundaryStart() { assert boundary != null; return new String(boundary, 2, boundary.length - 2); } public String getBoundaryEnd() { assert boundary != null; return getBoundaryStart() + "--\r\n"; } protected void onBoundaryStart() { } protected void onBoundaryEnd() { } // >= 0 matching // -1 matching - (start of boundary end) or \r (boundary start) // -2 matching - (end of boundary end) // -3 matching \r after boundary // -4 matching \n after boundary // the state starts out having already matched \r\n /* Mac:work$ curl -F person=anonymous -F secret=@test.kt http://localhost:5555 POST / HTTP/1.1 Host: localhost:5555 User-Agent: curl/7.54.0 Content-Length: 372 Expect: 100-continue Content-Type: multipart/form-data; boundary=------------------------17903558439eb6ff --------------------------17903558439eb6ff <--- note! two dashes before boundary Content-Disposition: form-data; name="person" anonymous --------------------------17903558439eb6ff <--- note! two dashes before boundary Content-Disposition: form-data; name="secret"; filename="test.kt" Content-Type: application/octet-stream fun main(args: Array) { println("Hello JavaScript!") } --------------------------17903558439eb6ff-- <--- note! two dashes before AND after boundary */ int state = 2; @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { // System.out.println(bb.getString()); // System.out.println("chunk: " + bb.remaining()); // System.out.println("state: " + state); // if we were in the middle of a potential match, let's throw that // at the beginning of the buffer and process it too. if (state > 0) { ByteBuffer b = ByteBufferList.obtain(boundary.length); b.put(boundary, 0, state); b.flip(); bb.addFirst(b); state = 0; } int last = 0; byte[] buf = new byte[bb.remaining()]; bb.get(buf); for (int i = 0; i < buf.length; i++) { if (state >= 0) { if (buf[i] == boundary[state]) { state++; if (state == boundary.length) state = -1; } else if (state > 0) { // let's try matching again one byte after the start // of last match occurrence i -= state; state = 0; } } else if (state == -1) { if (buf[i] == '\r') { state = -4; int len = i - last - boundary.length; if (last != 0 || len != 0) { ByteBuffer b = ByteBufferList.obtain(len).put(buf, last, len); b.flip(); ByteBufferList list = new ByteBufferList(); list.add(b); super.onDataAvailable(this, list); } // System.out.println("bstart"); onBoundaryStart(); } else if (buf[i] == '-') { state = -2; } else { report(new MimeEncodingException("Invalid multipart/form-data. Expected \r or -")); return; } } else if (state == -2) { if (buf[i] == '-') { state = -3; } else { report(new MimeEncodingException("Invalid multipart/form-data. Expected -")); return; } } else if (state == -3) { if (buf[i] == '\r') { state = -4; ByteBuffer b = ByteBufferList.obtain(i - last - boundary.length - 2).put(buf, last, i - last - boundary.length - 2); b.flip(); ByteBufferList list = new ByteBufferList(); list.add(b); super.onDataAvailable(this, list); // System.out.println("bend"); onBoundaryEnd(); } else { report(new MimeEncodingException("Invalid multipart/form-data. Expected \r")); return; } } else if (state == -4) { if (buf[i] == '\n') { last = i + 1; state = 0; } else { report(new MimeEncodingException("Invalid multipart/form-data. Expected \n")); } } else { assert false; report(new MimeEncodingException("Invalid multipart/form-data. Unknown state?")); } } if (last < buf.length) { // System.out.println("amount left at boundary: " + (buf.length - last)); // System.out.println("State: " + state); // System.out.println(state); int keep = Math.max(state, 0); ByteBuffer b = ByteBufferList.obtain(buf.length - last - keep).put(buf, last, buf.length - last - keep); b.flip(); ByteBufferList list = new ByteBufferList(); list.add(b); super.onDataAvailable(this, list); } } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/HttpServerRequestCallback.java ================================================ package com.jeffmony.async.http.server; public interface HttpServerRequestCallback { void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/MalformedRangeException.java ================================================ package com.jeffmony.async.http.server; public class MalformedRangeException extends Exception { } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/MimeEncodingException.java ================================================ package com.jeffmony.async.http.server; public class MimeEncodingException extends Exception { public MimeEncodingException(String message) { super(message); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/RouteMatcher.java ================================================ package com.jeffmony.async.http.server; public interface RouteMatcher { AsyncHttpServerRouter.RouteMatch route(String method, String path); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/StreamSkipException.java ================================================ package com.jeffmony.async.http.server; public class StreamSkipException extends Exception { public StreamSkipException(String message) { super(message); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/http/server/UnknownRequestBody.java ================================================ package com.jeffmony.async.http.server; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.http.AsyncHttpRequest; import com.jeffmony.async.http.body.AsyncHttpRequestBody; public class UnknownRequestBody implements AsyncHttpRequestBody { public UnknownRequestBody(String contentType) { mContentType = contentType; } int length = -1; public UnknownRequestBody(DataEmitter emitter, String contentType, int length) { mContentType = contentType; this.emitter = emitter; this.length = length; } @Override public void write(final AsyncHttpRequest request, DataSink sink, final CompletedCallback completed) { Util.pump(emitter, sink, completed); if (emitter.isPaused()) emitter.resume(); } private String mContentType; @Override public String getContentType() { return mContentType; } @Override public boolean readFullyOnRequest() { return false; } @Override public int length() { return length; } @Override public Void get() { return null; } @Deprecated public void setCallbacks(DataCallback callback, CompletedCallback endCallback) { emitter.setEndCallback(endCallback); emitter.setDataCallback(callback); } public DataEmitter getEmitter() { return emitter; } DataEmitter emitter; @Override public void parse(DataEmitter emitter, CompletedCallback completed) { this.emitter = emitter; emitter.setEndCallback(completed); emitter.setDataCallback(new DataCallback.NullDataCallback()); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/parser/AsyncParser.java ================================================ package com.jeffmony.async.parser; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.future.Future; import java.lang.reflect.Type; /** * Created by koush on 5/27/13. */ public interface AsyncParser { Future parse(DataEmitter emitter); void write(DataSink sink, T value, CompletedCallback completed); Type getType(); String getMime(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/parser/ByteBufferListParser.java ================================================ package com.jeffmony.async.parser; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.DataCallback; import com.jeffmony.async.future.Future; import com.jeffmony.async.future.SimpleFuture; import java.lang.reflect.Type; /** * Created by koush on 5/27/13. */ public class ByteBufferListParser implements AsyncParser { @Override public Future parse(final DataEmitter emitter) { final ByteBufferList bb = new ByteBufferList(); final SimpleFuture ret = new SimpleFuture() { @Override protected void cancelCleanup() { emitter.close(); } }; emitter.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList data) { data.get(bb); } }); emitter.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (ex != null) { ret.setComplete(ex); return; } try { ret.setComplete(bb); } catch (Exception e) { ret.setComplete(e); } } }); return ret; } @Override public void write(DataSink sink, ByteBufferList value, CompletedCallback completed) { Util.writeAll(sink, value, completed); } @Override public Type getType() { return ByteBufferList.class; } @Override public String getMime() { return null; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/parser/DocumentParser.java ================================================ package com.jeffmony.async.parser; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.future.Future; import com.jeffmony.async.http.body.DocumentBody; import com.jeffmony.async.stream.ByteBufferListInputStream; import org.w3c.dom.Document; import java.lang.reflect.Type; import javax.xml.parsers.DocumentBuilderFactory; /** * Created by koush on 8/3/13. */ public class DocumentParser implements AsyncParser { @Override public Future parse(DataEmitter emitter) { return new ByteBufferListParser().parse(emitter) .thenConvert(from -> DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new ByteBufferListInputStream(from))); } @Override public void write(DataSink sink, Document value, CompletedCallback completed) { new DocumentBody(value).write(null, sink, completed); } @Override public Type getType() { return Document.class; } @Override public String getMime() { return "text/xml"; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/parser/JSONArrayParser.java ================================================ package com.jeffmony.async.parser; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.future.Future; import org.json.JSONArray; import java.lang.reflect.Type; /** * Created by koush on 5/27/13. */ public class JSONArrayParser implements AsyncParser { @Override public Future parse(DataEmitter emitter) { return new StringParser().parse(emitter) .thenConvert(JSONArray::new); } @Override public void write(DataSink sink, JSONArray value, CompletedCallback completed) { new StringParser().write(sink, value.toString(), completed); } @Override public Type getType() { return JSONArray.class; } @Override public String getMime() { return "application/json"; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/parser/JSONObjectParser.java ================================================ package com.jeffmony.async.parser; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.future.Future; import org.json.JSONObject; import java.lang.reflect.Type; /** * Created by koush on 5/27/13. */ public class JSONObjectParser implements AsyncParser { @Override public Future parse(DataEmitter emitter) { return new StringParser().parse(emitter).thenConvert(JSONObject::new); } @Override public void write(DataSink sink, JSONObject value, CompletedCallback completed) { new StringParser().write(sink, value.toString(), completed); } @Override public Type getType() { return JSONObject.class; } @Override public String getMime() { return "application/json"; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/parser/StringParser.java ================================================ package com.jeffmony.async.parser; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.DataSink; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.future.Future; import java.lang.reflect.Type; import java.nio.charset.Charset; /** * Created by koush on 5/27/13. */ public class StringParser implements AsyncParser { Charset forcedCharset; public StringParser() { } public StringParser(Charset charset) { this.forcedCharset = charset; } @Override public Future parse(DataEmitter emitter) { final String charset = emitter.charset(); return new ByteBufferListParser().parse(emitter) .thenConvert(from -> { Charset charsetToUse = forcedCharset; if (charsetToUse == null && charset != null) charsetToUse = Charset.forName(charset); return from.readString(charsetToUse); }); } @Override public void write(DataSink sink, String value, CompletedCallback completed) { new ByteBufferListParser().write(sink, new ByteBufferList(value.getBytes()), completed); } @Override public Type getType() { return String.class; } @Override public String getMime() { return null; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/stream/ByteBufferListInputStream.java ================================================ package com.jeffmony.async.stream; import com.jeffmony.async.ByteBufferList; import java.io.IOException; import java.io.InputStream; /** * Created by koush on 6/1/13. */ public class ByteBufferListInputStream extends InputStream { ByteBufferList bb; public ByteBufferListInputStream(ByteBufferList bb) { this.bb = bb; } @Override public int read() throws IOException { if (bb.remaining() <= 0) return -1; return (int)bb.get() & 0x000000ff; } @Override public int read(byte[] buffer) throws IOException { return this.read(buffer, 0, buffer.length); } @Override public int read(byte[] buffer, int offset, int length) throws IOException { if (bb.remaining() <= 0) return -1; int toRead = Math.min(length, bb.remaining()); bb.get(buffer, offset, toRead); return toRead; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/stream/FileDataSink.java ================================================ package com.jeffmony.async.stream; import com.jeffmony.async.AsyncServer; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; /** * Created by koush on 2/2/14. */ public class FileDataSink extends OutputStreamDataSink { File file; public FileDataSink(AsyncServer server, File file) { super(server); this.file = file; } @Override public OutputStream getOutputStream() throws IOException { OutputStream ret = super.getOutputStream(); if (ret == null) { file.getParentFile().mkdirs(); ret = new FileOutputStream(file); setOutputStream(ret); } return ret; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/stream/InputStreamDataEmitter.java ================================================ package com.jeffmony.async.stream; import com.jeffmony.async.AsyncServer; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.Util; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.DataCallback; import java.io.InputStream; import java.nio.ByteBuffer; /** * Created by koush on 5/22/13. */ public class InputStreamDataEmitter implements DataEmitter { AsyncServer server; InputStream inputStream; public InputStreamDataEmitter(AsyncServer server, InputStream inputStream) { this.server = server; this.inputStream = inputStream; doResume(); } DataCallback callback; @Override public void setDataCallback(DataCallback callback) { this.callback = callback; } @Override public DataCallback getDataCallback() { return callback; } @Override public boolean isChunked() { return false; } boolean paused; @Override public void pause() { paused = true; } @Override public void resume() { paused = false; doResume(); } private void report(final Exception e) { getServer().post(new Runnable() { @Override public void run() { Exception ex = e; try { inputStream.close(); } catch (Exception e) { ex = e; } if (endCallback != null) endCallback.onCompleted(ex); } }); } int mToAlloc = 0; ByteBufferList pending = new ByteBufferList(); Runnable pumper = new Runnable() { @Override public void run() { try { if (!pending.isEmpty()) { getServer().run(new Runnable() { @Override public void run() { Util.emitAllData(InputStreamDataEmitter.this, pending); } }); if (!pending.isEmpty()) return; } ByteBuffer b; do { b = ByteBufferList.obtain(Math.min(Math.max(mToAlloc, 2 << 11), 256 * 1024)); int read; if (-1 == (read = inputStream.read(b.array()))) { report(null); return; } mToAlloc = read * 2; b.limit(read); pending.add(b); getServer().run(new Runnable() { @Override public void run() { Util.emitAllData(InputStreamDataEmitter.this, pending); } }); } while (pending.remaining() == 0 && !isPaused()); } catch (Exception e) { report(e); } } }; private void doResume() { new Thread(pumper).start(); } @Override public boolean isPaused() { return paused; } CompletedCallback endCallback; @Override public void setEndCallback(CompletedCallback callback) { endCallback = callback; } @Override public CompletedCallback getEndCallback() { return endCallback; } @Override public AsyncServer getServer() { return server; } @Override public void close() { report(null); try { inputStream.close(); } catch (Exception e) { } } @Override public String charset() { return null; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/stream/OutputStreamDataCallback.java ================================================ package com.jeffmony.async.stream; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataEmitter; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.DataCallback; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; public class OutputStreamDataCallback implements DataCallback, CompletedCallback { private OutputStream mOutput; public OutputStreamDataCallback(OutputStream os) { mOutput = os; } public OutputStream getOutputStream() { return mOutput; } @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { try { while (bb.size() > 0) { ByteBuffer b = bb.remove(); mOutput.write(b.array(), b.arrayOffset() + b.position(), b.remaining()); ByteBufferList.reclaim(b); } } catch (Exception ex) { onCompleted(ex); } finally { bb.recycle(); } } public void close() { try { mOutput.close(); } catch (IOException e) { onCompleted(e); } } @Override public void onCompleted(Exception error) { error.printStackTrace(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/stream/OutputStreamDataSink.java ================================================ package com.jeffmony.async.stream; import com.jeffmony.async.AsyncServer; import com.jeffmony.async.ByteBufferList; import com.jeffmony.async.DataSink; import com.jeffmony.async.callback.CompletedCallback; import com.jeffmony.async.callback.WritableCallback; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; public class OutputStreamDataSink implements DataSink { public OutputStreamDataSink(AsyncServer server) { this(server, null); } @Override public void end() { try { if (mStream != null) mStream.close(); reportClose(null); } catch (IOException e) { reportClose(e); } } AsyncServer server; public OutputStreamDataSink(AsyncServer server, OutputStream stream) { this.server = server; setOutputStream(stream); } OutputStream mStream; public void setOutputStream(OutputStream stream) { mStream = stream; } public OutputStream getOutputStream() throws IOException { return mStream; } @Override public void write(final ByteBufferList bb) { try { while (bb.size() > 0) { ByteBuffer b = bb.remove(); getOutputStream().write(b.array(), b.arrayOffset() + b.position(), b.remaining()); ByteBufferList.reclaim(b); } } catch (IOException e) { reportClose(e); } finally { bb.recycle(); } } WritableCallback mWritable; @Override public void setWriteableCallback(WritableCallback handler) { mWritable = handler; } @Override public WritableCallback getWriteableCallback() { return mWritable; } @Override public boolean isOpen() { return closeReported; } boolean closeReported; Exception closeException; public void reportClose(Exception ex) { if (closeReported) return; closeReported = true; closeException = ex; if (mClosedCallback != null) mClosedCallback.onCompleted(closeException); } CompletedCallback mClosedCallback; @Override public void setClosedCallback(CompletedCallback handler) { mClosedCallback = handler; } @Override public CompletedCallback getClosedCallback() { return mClosedCallback; } @Override public AsyncServer getServer() { return server; } WritableCallback outputStreamCallback; public void setOutputStreamWritableCallback(WritableCallback outputStreamCallback) { this.outputStreamCallback = outputStreamCallback; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/util/Allocator.java ================================================ package com.jeffmony.async.util; import com.jeffmony.async.ByteBufferList; import java.nio.ByteBuffer; /** * Created by koush on 6/28/14. */ public class Allocator { final int maxAlloc; int currentAlloc = 0; int minAlloc = 2 << 11; public Allocator(int maxAlloc) { this.maxAlloc = maxAlloc; } public Allocator() { maxAlloc = ByteBufferList.MAX_ITEM_SIZE; } public ByteBuffer allocate() { return allocate(currentAlloc); } public ByteBuffer allocate(int currentAlloc) { return ByteBufferList.obtain(Math.min(Math.max(currentAlloc, minAlloc), maxAlloc)); } public void track(long read) { currentAlloc = (int)read * 2; } public int getMaxAlloc() { return maxAlloc; } public void setCurrentAlloc(int currentAlloc) { this.currentAlloc = currentAlloc; } public int getMinAlloc() { return minAlloc; } public Allocator setMinAlloc(int minAlloc ) { this.minAlloc = Math.max(0, minAlloc); return this; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/util/ArrayDeque.java ================================================ /* * Written by Josh Bloch of Google Inc. and released to the public domain, * as explained at http://creativecommons.org/publicdomain/zero/1.0/. */ package com.jeffmony.async.util; // BEGIN android-note // removed link to collections framework docs // END android-note import java.util.AbstractCollection; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.NoSuchElementException; /** * Resizable-array implementation of the {@link Deque} interface. Array * deques have no capacity restrictions; they grow as necessary to support * usage. They are not thread-safe; in the absence of external * synchronization, they do not support concurrent access by multiple threads. * Null elements are prohibited. This class is likely to be faster than * {@link java.util.Stack} when used as a stack, and faster than {@link java.util.LinkedList} * when used as a queue. * *

Most ArrayDeque operations run in amortized constant time. * Exceptions include {@link #remove(Object) remove}, {@link * #removeFirstOccurrence removeFirstOccurrence}, {@link #removeLastOccurrence * removeLastOccurrence}, {@link #contains contains}, {@link #iterator * iterator.remove()}, and the bulk operations, all of which run in linear * time. * *

The iterators returned by this class's iterator method are * fail-fast: If the deque is modified at any time after the iterator * is created, in any way except through the iterator's own remove * method, the iterator will generally throw a {@link * ConcurrentModificationException}. Thus, in the face of concurrent * modification, the iterator fails quickly and cleanly, rather than risking * arbitrary, non-deterministic behavior at an undetermined time in the * future. * *

Note that the fail-fast behavior of an iterator cannot be guaranteed * as it is, generally speaking, impossible to make any hard guarantees in the * presence of unsynchronized concurrent modification. Fail-fast iterators * throw ConcurrentModificationException on a best-effort basis. * Therefore, it would be wrong to write a program that depended on this * exception for its correctness: the fail-fast behavior of iterators * should be used only to detect bugs. * *

This class and its iterator implement all of the * optional methods of the {@link Collection} and {@link * Iterator} interfaces. * * @author Josh Bloch and Doug Lea * @since 1.6 * @param the type of elements held in this collection */ public class ArrayDeque extends AbstractCollection implements Deque, Cloneable, java.io.Serializable { /** * The array in which the elements of the deque are stored. * The capacity of the deque is the length of this array, which is * always a power of two. The array is never allowed to become * full, except transiently within an addX method where it is * resized (see doubleCapacity) immediately upon becoming full, * thus avoiding head and tail wrapping around to equal each * other. We also guarantee that all array cells not holding * deque elements are always null. */ private transient Object[] elements; /** * The index of the element at the head of the deque (which is the * element that would be removed by remove() or pop()); or an * arbitrary number equal to tail if the deque is empty. */ private transient int head; /** * The index at which the next element would be added to the tail * of the deque (via addLast(E), add(E), or push(E)). */ private transient int tail; /** * The minimum capacity that we'll use for a newly created deque. * Must be a power of 2. */ private static final int MIN_INITIAL_CAPACITY = 8; // ****** Array allocation and resizing utilities ****** /** * Allocate empty array to hold the given number of elements. * * @param numElements the number of elements to hold */ private void allocateElements(int numElements) { int initialCapacity = MIN_INITIAL_CAPACITY; // Find the best power of two to hold elements. // Tests "<=" because arrays aren't kept full. if (numElements >= initialCapacity) { initialCapacity = numElements; initialCapacity |= (initialCapacity >>> 1); initialCapacity |= (initialCapacity >>> 2); initialCapacity |= (initialCapacity >>> 4); initialCapacity |= (initialCapacity >>> 8); initialCapacity |= (initialCapacity >>> 16); initialCapacity++; if (initialCapacity < 0) // Too many elements, must back off initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements } elements = new Object[initialCapacity]; } /** * Double the capacity of this deque. Call only when full, i.e., * when head and tail have wrapped around to become equal. */ private void doubleCapacity() { assert head == tail; int p = head; int n = elements.length; int r = n - p; // number of elements to the right of p int newCapacity = n << 1; if (newCapacity < 0) throw new IllegalStateException("Sorry, deque too big"); Object[] a = new Object[newCapacity]; System.arraycopy(elements, p, a, 0, r); System.arraycopy(elements, 0, a, r, p); elements = a; head = 0; tail = n; } /** * Copies the elements from our element array into the specified array, * in order (from first to last element in the deque). It is assumed * that the array is large enough to hold all elements in the deque. * * @return its argument */ private T[] copyElements(T[] a) { if (head < tail) { System.arraycopy(elements, head, a, 0, size()); } else if (head > tail) { int headPortionLen = elements.length - head; System.arraycopy(elements, head, a, 0, headPortionLen); System.arraycopy(elements, 0, a, headPortionLen, tail); } return a; } /** * Constructs an empty array deque with an initial capacity * sufficient to hold 16 elements. */ public ArrayDeque() { elements = new Object[16]; } /** * Constructs an empty array deque with an initial capacity * sufficient to hold the specified number of elements. * * @param numElements lower bound on initial capacity of the deque */ public ArrayDeque(int numElements) { allocateElements(numElements); } /** * Constructs a deque containing the elements of the specified * collection, in the order they are returned by the collection's * iterator. (The first element returned by the collection's * iterator becomes the first element, or front of the * deque.) * * @param c the collection whose elements are to be placed into the deque * @throws NullPointerException if the specified collection is null */ public ArrayDeque(Collection c) { allocateElements(c.size()); addAll(c); } // The main insertion and extraction methods are addFirst, // addLast, pollFirst, pollLast. The other methods are defined in // terms of these. /** * Inserts the specified element at the front of this deque. * * @param e the element to add * @throws NullPointerException if the specified element is null */ public void addFirst(E e) { if (e == null) throw new NullPointerException("e == null"); elements[head = (head - 1) & (elements.length - 1)] = e; if (head == tail) doubleCapacity(); } /** * Inserts the specified element at the end of this deque. * *

This method is equivalent to {@link #add}. * * @param e the element to add * @throws NullPointerException if the specified element is null */ public void addLast(E e) { if (e == null) throw new NullPointerException("e == null"); elements[tail] = e; if ( (tail = (tail + 1) & (elements.length - 1)) == head) doubleCapacity(); } /** * Inserts the specified element at the front of this deque. * * @param e the element to add * @return true (as specified by {@link Deque#offerFirst}) * @throws NullPointerException if the specified element is null */ public boolean offerFirst(E e) { addFirst(e); return true; } /** * Inserts the specified element at the end of this deque. * * @param e the element to add * @return true (as specified by {@link Deque#offerLast}) * @throws NullPointerException if the specified element is null */ public boolean offerLast(E e) { addLast(e); return true; } /** * @throws NoSuchElementException {@inheritDoc} */ public E removeFirst() { E x = pollFirst(); if (x == null) throw new NoSuchElementException(); return x; } /** * @throws NoSuchElementException {@inheritDoc} */ public E removeLast() { E x = pollLast(); if (x == null) throw new NoSuchElementException(); return x; } public E pollFirst() { int h = head; @SuppressWarnings("unchecked") E result = (E) elements[h]; // Element is null if deque empty if (result == null) return null; elements[h] = null; // Must null out slot head = (h + 1) & (elements.length - 1); return result; } public E pollLast() { int t = (tail - 1) & (elements.length - 1); @SuppressWarnings("unchecked") E result = (E) elements[t]; if (result == null) return null; elements[t] = null; tail = t; return result; } /** * @throws NoSuchElementException {@inheritDoc} */ public E getFirst() { @SuppressWarnings("unchecked") E result = (E) elements[head]; if (result == null) throw new NoSuchElementException(); return result; } /** * @throws NoSuchElementException {@inheritDoc} */ public E getLast() { @SuppressWarnings("unchecked") E result = (E) elements[(tail - 1) & (elements.length - 1)]; if (result == null) throw new NoSuchElementException(); return result; } public E peekFirst() { @SuppressWarnings("unchecked") E result = (E) elements[head]; // elements[head] is null if deque empty return result; } public E peekLast() { @SuppressWarnings("unchecked") E result = (E) elements[(tail - 1) & (elements.length - 1)]; return result; } /** * Removes the first occurrence of the specified element in this * deque (when traversing the deque from head to tail). * If the deque does not contain the element, it is unchanged. * More formally, removes the first element e such that * o.equals(e) (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * * @param o element to be removed from this deque, if present * @return true if the deque contained the specified element */ public boolean removeFirstOccurrence(Object o) { if (o == null) return false; int mask = elements.length - 1; int i = head; Object x; while ( (x = elements[i]) != null) { if (o.equals(x)) { delete(i); return true; } i = (i + 1) & mask; } return false; } /** * Removes the last occurrence of the specified element in this * deque (when traversing the deque from head to tail). * If the deque does not contain the element, it is unchanged. * More formally, removes the last element e such that * o.equals(e) (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * * @param o element to be removed from this deque, if present * @return true if the deque contained the specified element */ public boolean removeLastOccurrence(Object o) { if (o == null) return false; int mask = elements.length - 1; int i = (tail - 1) & mask; Object x; while ( (x = elements[i]) != null) { if (o.equals(x)) { delete(i); return true; } i = (i - 1) & mask; } return false; } // *** Queue methods *** /** * Inserts the specified element at the end of this deque. * *

This method is equivalent to {@link #addLast}. * * @param e the element to add * @return true (as specified by {@link Collection#add}) * @throws NullPointerException if the specified element is null */ public boolean add(E e) { addLast(e); return true; } /** * Inserts the specified element at the end of this deque. * *

This method is equivalent to {@link #offerLast}. * * @param e the element to add * @return true (as specified by {@link java.util.Queue#offer}) * @throws NullPointerException if the specified element is null */ public boolean offer(E e) { return offerLast(e); } /** * Retrieves and removes the head of the queue represented by this deque. * * This method differs from {@link #poll poll} only in that it throws an * exception if this deque is empty. * *

This method is equivalent to {@link #removeFirst}. * * @return the head of the queue represented by this deque * @throws NoSuchElementException {@inheritDoc} */ public E remove() { return removeFirst(); } /** * Retrieves and removes the head of the queue represented by this deque * (in other words, the first element of this deque), or returns * null if this deque is empty. * *

This method is equivalent to {@link #pollFirst}. * * @return the head of the queue represented by this deque, or * null if this deque is empty */ public E poll() { return pollFirst(); } /** * Retrieves, but does not remove, the head of the queue represented by * this deque. This method differs from {@link #peek peek} only in * that it throws an exception if this deque is empty. * *

This method is equivalent to {@link #getFirst}. * * @return the head of the queue represented by this deque * @throws NoSuchElementException {@inheritDoc} */ public E element() { return getFirst(); } /** * Retrieves, but does not remove, the head of the queue represented by * this deque, or returns null if this deque is empty. * *

This method is equivalent to {@link #peekFirst}. * * @return the head of the queue represented by this deque, or * null if this deque is empty */ public E peek() { return peekFirst(); } // *** Stack methods *** /** * Pushes an element onto the stack represented by this deque. In other * words, inserts the element at the front of this deque. * *

This method is equivalent to {@link #addFirst}. * * @param e the element to push * @throws NullPointerException if the specified element is null */ public void push(E e) { addFirst(e); } /** * Pops an element from the stack represented by this deque. In other * words, removes and returns the first element of this deque. * *

This method is equivalent to {@link #removeFirst()}. * * @return the element at the front of this deque (which is the top * of the stack represented by this deque) * @throws NoSuchElementException {@inheritDoc} */ public E pop() { return removeFirst(); } private void checkInvariants() { assert elements[tail] == null; assert head == tail ? elements[head] == null : (elements[head] != null && elements[(tail - 1) & (elements.length - 1)] != null); assert elements[(head - 1) & (elements.length - 1)] == null; } /** * Removes the element at the specified position in the elements array, * adjusting head and tail as necessary. This can result in motion of * elements backwards or forwards in the array. * *

This method is called delete rather than remove to emphasize * that its semantics differ from those of {@link java.util.List#remove(int)}. * * @return true if elements moved backwards */ private boolean delete(int i) { checkInvariants(); final Object[] elements = this.elements; final int mask = elements.length - 1; final int h = head; final int t = tail; final int front = (i - h) & mask; final int back = (t - i) & mask; // Invariant: head <= i < tail mod circularity if (front >= ((t - h) & mask)) throw new ConcurrentModificationException(); // Optimize for least element motion if (front < back) { if (h <= i) { System.arraycopy(elements, h, elements, h + 1, front); } else { // Wrap around System.arraycopy(elements, 0, elements, 1, i); elements[0] = elements[mask]; System.arraycopy(elements, h, elements, h + 1, mask - h); } elements[h] = null; head = (h + 1) & mask; return false; } else { if (i < t) { // Copy the null tail as well System.arraycopy(elements, i + 1, elements, i, back); tail = t - 1; } else { // Wrap around System.arraycopy(elements, i + 1, elements, i, mask - i); elements[mask] = elements[0]; System.arraycopy(elements, 1, elements, 0, t); tail = (t - 1) & mask; } return true; } } // *** Collection Methods *** /** * Returns the number of elements in this deque. * * @return the number of elements in this deque */ public int size() { return (tail - head) & (elements.length - 1); } /** * Returns true if this deque contains no elements. * * @return true if this deque contains no elements */ public boolean isEmpty() { return head == tail; } /** * Returns an iterator over the elements in this deque. The elements * will be ordered from first (head) to last (tail). This is the same * order that elements would be dequeued (via successive calls to * {@link #remove} or popped (via successive calls to {@link #pop}). * * @return an iterator over the elements in this deque */ public Iterator iterator() { return new DeqIterator(); } public Iterator descendingIterator() { return new DescendingIterator(); } private class DeqIterator implements Iterator { /** * Index of element to be returned by subsequent call to next. */ private int cursor = head; /** * Tail recorded at construction (also in remove), to stop * iterator and also to check for comodification. */ private int fence = tail; /** * Index of element returned by most recent call to next. * Reset to -1 if element is deleted by a call to remove. */ private int lastRet = -1; public boolean hasNext() { return cursor != fence; } public E next() { if (cursor == fence) throw new NoSuchElementException(); @SuppressWarnings("unchecked") E result = (E) elements[cursor]; // This check doesn't catch all possible comodifications, // but does catch the ones that corrupt traversal if (tail != fence || result == null) throw new ConcurrentModificationException(); lastRet = cursor; cursor = (cursor + 1) & (elements.length - 1); return result; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); if (delete(lastRet)) { // if left-shifted, undo increment in next() cursor = (cursor - 1) & (elements.length - 1); fence = tail; } lastRet = -1; } } private class DescendingIterator implements Iterator { /* * This class is nearly a mirror-image of DeqIterator, using * tail instead of head for initial cursor, and head instead of * tail for fence. */ private int cursor = tail; private int fence = head; private int lastRet = -1; public boolean hasNext() { return cursor != fence; } public E next() { if (cursor == fence) throw new NoSuchElementException(); cursor = (cursor - 1) & (elements.length - 1); @SuppressWarnings("unchecked") E result = (E) elements[cursor]; if (head != fence || result == null) throw new ConcurrentModificationException(); lastRet = cursor; return result; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); if (!delete(lastRet)) { cursor = (cursor + 1) & (elements.length - 1); fence = head; } lastRet = -1; } } /** * Returns true if this deque contains the specified element. * More formally, returns true if and only if this deque contains * at least one element e such that o.equals(e). * * @param o object to be checked for containment in this deque * @return true if this deque contains the specified element */ public boolean contains(Object o) { if (o == null) return false; int mask = elements.length - 1; int i = head; Object x; while ( (x = elements[i]) != null) { if (o.equals(x)) return true; i = (i + 1) & mask; } return false; } /** * Removes a single instance of the specified element from this deque. * If the deque does not contain the element, it is unchanged. * More formally, removes the first element e such that * o.equals(e) (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * *

This method is equivalent to {@link #removeFirstOccurrence}. * * @param o element to be removed from this deque, if present * @return true if this deque contained the specified element */ public boolean remove(Object o) { return removeFirstOccurrence(o); } /** * Removes all of the elements from this deque. * The deque will be empty after this call returns. */ public void clear() { int h = head; int t = tail; if (h != t) { // clear all cells head = tail = 0; int i = h; int mask = elements.length - 1; do { elements[i] = null; i = (i + 1) & mask; } while (i != t); } } /** * Returns an array containing all of the elements in this deque * in proper sequence (from first to last element). * *

The returned array will be "safe" in that no references to it are * maintained by this deque. (In other words, this method must allocate * a new array). The caller is thus free to modify the returned array. * *

This method acts as bridge between array-based and collection-based * APIs. * * @return an array containing all of the elements in this deque */ public Object[] toArray() { return copyElements(new Object[size()]); } /** * Returns an array containing all of the elements in this deque in * proper sequence (from first to last element); the runtime type of the * returned array is that of the specified array. If the deque fits in * the specified array, it is returned therein. Otherwise, a new array * is allocated with the runtime type of the specified array and the * size of this deque. * *

If this deque fits in the specified array with room to spare * (i.e., the array has more elements than this deque), the element in * the array immediately following the end of the deque is set to * null. * *

Like the {@link #toArray()} method, this method acts as bridge between * array-based and collection-based APIs. Further, this method allows * precise control over the runtime type of the output array, and may, * under certain circumstances, be used to save allocation costs. * *

Suppose x is a deque known to contain only strings. * The following code can be used to dump the deque into a newly * allocated array of String: * *

 {@code String[] y = x.toArray(new String[0]);}
* * Note that toArray(new Object[0]) is identical in function to * toArray(). * * @param a the array into which the elements of the deque are to * be stored, if it is big enough; otherwise, a new array of the * same runtime type is allocated for this purpose * @return an array containing all of the elements in this deque * @throws ArrayStoreException if the runtime type of the specified array * is not a supertype of the runtime type of every element in * this deque * @throws NullPointerException if the specified array is null */ @SuppressWarnings("unchecked") public T[] toArray(T[] a) { int size = size(); if (a.length < size) a = (T[])java.lang.reflect.Array.newInstance( a.getClass().getComponentType(), size); copyElements(a); if (a.length > size) a[size] = null; return a; } // *** Object methods *** /** * Returns a copy of this deque. * * @return a copy of this deque */ public ArrayDeque clone() { try { @SuppressWarnings("unchecked") ArrayDeque result = (ArrayDeque) super.clone(); System.arraycopy(elements, 0, result.elements, 0, elements.length); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } /** * Appease the serialization gods. */ private static final long serialVersionUID = 2340985798034038923L; /** * Serialize this deque. * * @serialData The current size (int) of the deque, * followed by all of its elements (each an object reference) in * first-to-last order. */ private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject(); // Write out size s.writeInt(size()); // Write out elements in order. int mask = elements.length - 1; for (int i = head; i != tail; i = (i + 1) & mask) s.writeObject(elements[i]); } /** * Deserialize this deque. */ private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); // Read in size and allocate array int size = s.readInt(); allocateElements(size); head = 0; tail = size; // Read in all elements in the proper order. for (int i = 0; i < size; i++) elements[i] = s.readObject(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/util/Charsets.java ================================================ package com.jeffmony.async.util; import java.nio.charset.Charset; /** From java.nio.charset.Charsets */ public class Charsets { public static final Charset US_ASCII = Charset.forName("US-ASCII"); public static final Charset UTF_8 = Charset.forName("UTF-8"); public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/util/Deque.java ================================================ /* * Written by Doug Lea and Josh Bloch with assistance from members of * JCP JSR-166 Expert Group and released to the public domain, as explained * at http://creativecommons.org/publicdomain/zero/1.0/ */ package com.jeffmony.async.util; // BEGIN android-note // removed link to collections framework docs // END android-note import java.util.Iterator; import java.util.Queue; /** * A linear collection that supports element insertion and removal at * both ends. The name deque is short for "double ended queue" * and is usually pronounced "deck". Most Deque * implementations place no fixed limits on the number of elements * they may contain, but this interface supports capacity-restricted * deques as well as those with no fixed size limit. * *

This interface defines methods to access the elements at both * ends of the deque. Methods are provided to insert, remove, and * examine the element. Each of these methods exists in two forms: * one throws an exception if the operation fails, the other returns a * special value (either null or false, depending on * the operation). The latter form of the insert operation is * designed specifically for use with capacity-restricted * Deque implementations; in most implementations, insert * operations cannot fail. * *

The twelve methods described above are summarized in the * following table: * *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
First Element (Head) Last Element (Tail)
Throws exceptionSpecial valueThrows exceptionSpecial value
Insert{@link #addFirst addFirst(e)}{@link #offerFirst offerFirst(e)}{@link #addLast addLast(e)}{@link #offerLast offerLast(e)}
Remove{@link #removeFirst removeFirst()}{@link #pollFirst pollFirst()}{@link #removeLast removeLast()}{@link #pollLast pollLast()}
Examine{@link #getFirst getFirst()}{@link #peekFirst peekFirst()}{@link #getLast getLast()}{@link #peekLast peekLast()}
* *

This interface extends the {@link Queue} interface. When a deque is * used as a queue, FIFO (First-In-First-Out) behavior results. Elements are * added at the end of the deque and removed from the beginning. The methods * inherited from the Queue interface are precisely equivalent to * Deque methods as indicated in the following table: * *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Queue Method Equivalent Deque Method
{@link java.util.Queue#add add(e)}{@link #addLast addLast(e)}
{@link java.util.Queue#offer offer(e)}{@link #offerLast offerLast(e)}
{@link java.util.Queue#remove remove()}{@link #removeFirst removeFirst()}
{@link java.util.Queue#poll poll()}{@link #pollFirst pollFirst()}
{@link java.util.Queue#element element()}{@link #getFirst getFirst()}
{@link java.util.Queue#peek peek()}{@link #peek peekFirst()}
* *

Deques can also be used as LIFO (Last-In-First-Out) stacks. This * interface should be used in preference to the legacy {@link java.util.Stack} class. * When a deque is used as a stack, elements are pushed and popped from the * beginning of the deque. Stack methods are precisely equivalent to * Deque methods as indicated in the table below: * *

* * * * * * * * * * * * * * * * * *
Stack Method Equivalent Deque Method
{@link #push push(e)}{@link #addFirst addFirst(e)}
{@link #pop pop()}{@link #removeFirst removeFirst()}
{@link #peek peek()}{@link #peekFirst peekFirst()}
* *

Note that the {@link #peek peek} method works equally well when * a deque is used as a queue or a stack; in either case, elements are * drawn from the beginning of the deque. * *

This interface provides two methods to remove interior * elements, {@link #removeFirstOccurrence removeFirstOccurrence} and * {@link #removeLastOccurrence removeLastOccurrence}. * *

Unlike the {@link java.util.List} interface, this interface does not * provide support for indexed access to elements. * *

While Deque implementations are not strictly required * to prohibit the insertion of null elements, they are strongly * encouraged to do so. Users of any Deque implementations * that do allow null elements are strongly encouraged not to * take advantage of the ability to insert nulls. This is so because * null is used as a special return value by various methods * to indicated that the deque is empty. * *

Deque implementations generally do not define * element-based versions of the equals and hashCode * methods, but instead inherit the identity-based versions from class * Object. * * @author Doug Lea * @author Josh Bloch * @since 1.6 * @param the type of elements held in this collection */ public interface Deque extends Queue { /** * Inserts the specified element at the front of this deque if it is * possible to do so immediately without violating capacity restrictions. * When using a capacity-restricted deque, it is generally preferable to * use method {@link #offerFirst}. * * @param e the element to add * @throws IllegalStateException if the element cannot be added at this * time due to capacity restrictions * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ void addFirst(E e); /** * Inserts the specified element at the end of this deque if it is * possible to do so immediately without violating capacity restrictions. * When using a capacity-restricted deque, it is generally preferable to * use method {@link #offerLast}. * *

This method is equivalent to {@link #add}. * * @param e the element to add * @throws IllegalStateException if the element cannot be added at this * time due to capacity restrictions * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ void addLast(E e); /** * Inserts the specified element at the front of this deque unless it would * violate capacity restrictions. When using a capacity-restricted deque, * this method is generally preferable to the {@link #addFirst} method, * which can fail to insert an element only by throwing an exception. * * @param e the element to add * @return true if the element was added to this deque, else * false * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ boolean offerFirst(E e); /** * Inserts the specified element at the end of this deque unless it would * violate capacity restrictions. When using a capacity-restricted deque, * this method is generally preferable to the {@link #addLast} method, * which can fail to insert an element only by throwing an exception. * * @param e the element to add * @return true if the element was added to this deque, else * false * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ boolean offerLast(E e); /** * Retrieves and removes the first element of this deque. This method * differs from {@link #pollFirst pollFirst} only in that it throws an * exception if this deque is empty. * * @return the head of this deque * @throws java.util.NoSuchElementException if this deque is empty */ E removeFirst(); /** * Retrieves and removes the last element of this deque. This method * differs from {@link #pollLast pollLast} only in that it throws an * exception if this deque is empty. * * @return the tail of this deque * @throws java.util.NoSuchElementException if this deque is empty */ E removeLast(); /** * Retrieves and removes the first element of this deque, * or returns null if this deque is empty. * * @return the head of this deque, or null if this deque is empty */ E pollFirst(); /** * Retrieves and removes the last element of this deque, * or returns null if this deque is empty. * * @return the tail of this deque, or null if this deque is empty */ E pollLast(); /** * Retrieves, but does not remove, the first element of this deque. * * This method differs from {@link #peekFirst peekFirst} only in that it * throws an exception if this deque is empty. * * @return the head of this deque * @throws java.util.NoSuchElementException if this deque is empty */ E getFirst(); /** * Retrieves, but does not remove, the last element of this deque. * This method differs from {@link #peekLast peekLast} only in that it * throws an exception if this deque is empty. * * @return the tail of this deque * @throws java.util.NoSuchElementException if this deque is empty */ E getLast(); /** * Retrieves, but does not remove, the first element of this deque, * or returns null if this deque is empty. * * @return the head of this deque, or null if this deque is empty */ E peekFirst(); /** * Retrieves, but does not remove, the last element of this deque, * or returns null if this deque is empty. * * @return the tail of this deque, or null if this deque is empty */ E peekLast(); /** * Removes the first occurrence of the specified element from this deque. * If the deque does not contain the element, it is unchanged. * More formally, removes the first element e such that * (o==null ? e==null : o.equals(e)) * (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * * @param o element to be removed from this deque, if present * @return true if an element was removed as a result of this call * @throws ClassCastException if the class of the specified element * is incompatible with this deque (optional) * @throws NullPointerException if the specified element is null and this * deque does not permit null elements (optional) */ boolean removeFirstOccurrence(Object o); /** * Removes the last occurrence of the specified element from this deque. * If the deque does not contain the element, it is unchanged. * More formally, removes the last element e such that * (o==null ? e==null : o.equals(e)) * (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * * @param o element to be removed from this deque, if present * @return true if an element was removed as a result of this call * @throws ClassCastException if the class of the specified element * is incompatible with this deque (optional) * @throws NullPointerException if the specified element is null and this * deque does not permit null elements (optional) */ boolean removeLastOccurrence(Object o); // *** Queue methods *** /** * Inserts the specified element into the queue represented by this deque * (in other words, at the tail of this deque) if it is possible to do so * immediately without violating capacity restrictions, returning * true upon success and throwing an * IllegalStateException if no space is currently available. * When using a capacity-restricted deque, it is generally preferable to * use {@link #offer(Object) offer}. * *

This method is equivalent to {@link #addLast}. * * @param e the element to add * @return true (as specified by {@link java.util.Collection#add}) * @throws IllegalStateException if the element cannot be added at this * time due to capacity restrictions * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ boolean add(E e); /** * Inserts the specified element into the queue represented by this deque * (in other words, at the tail of this deque) if it is possible to do so * immediately without violating capacity restrictions, returning * true upon success and false if no space is currently * available. When using a capacity-restricted deque, this method is * generally preferable to the {@link #add} method, which can fail to * insert an element only by throwing an exception. * *

This method is equivalent to {@link #offerLast}. * * @param e the element to add * @return true if the element was added to this deque, else * false * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ boolean offer(E e); /** * Retrieves and removes the head of the queue represented by this deque * (in other words, the first element of this deque). * This method differs from {@link #poll poll} only in that it throws an * exception if this deque is empty. * *

This method is equivalent to {@link #removeFirst()}. * * @return the head of the queue represented by this deque * @throws java.util.NoSuchElementException if this deque is empty */ E remove(); /** * Retrieves and removes the head of the queue represented by this deque * (in other words, the first element of this deque), or returns * null if this deque is empty. * *

This method is equivalent to {@link #pollFirst()}. * * @return the first element of this deque, or null if * this deque is empty */ E poll(); /** * Retrieves, but does not remove, the head of the queue represented by * this deque (in other words, the first element of this deque). * This method differs from {@link #peek peek} only in that it throws an * exception if this deque is empty. * *

This method is equivalent to {@link #getFirst()}. * * @return the head of the queue represented by this deque * @throws java.util.NoSuchElementException if this deque is empty */ E element(); /** * Retrieves, but does not remove, the head of the queue represented by * this deque (in other words, the first element of this deque), or * returns null if this deque is empty. * *

This method is equivalent to {@link #peekFirst()}. * * @return the head of the queue represented by this deque, or * null if this deque is empty */ E peek(); // *** Stack methods *** /** * Pushes an element onto the stack represented by this deque (in other * words, at the head of this deque) if it is possible to do so * immediately without violating capacity restrictions, returning * true upon success and throwing an * IllegalStateException if no space is currently available. * *

This method is equivalent to {@link #addFirst}. * * @param e the element to push * @throws IllegalStateException if the element cannot be added at this * time due to capacity restrictions * @throws ClassCastException if the class of the specified element * prevents it from being added to this deque * @throws NullPointerException if the specified element is null and this * deque does not permit null elements * @throws IllegalArgumentException if some property of the specified * element prevents it from being added to this deque */ void push(E e); /** * Pops an element from the stack represented by this deque. In other * words, removes and returns the first element of this deque. * *

This method is equivalent to {@link #removeFirst()}. * * @return the element at the front of this deque (which is the top * of the stack represented by this deque) * @throws java.util.NoSuchElementException if this deque is empty */ E pop(); // *** Collection methods *** /** * Removes the first occurrence of the specified element from this deque. * If the deque does not contain the element, it is unchanged. * More formally, removes the first element e such that * (o==null ? e==null : o.equals(e)) * (if such an element exists). * Returns true if this deque contained the specified element * (or equivalently, if this deque changed as a result of the call). * *

This method is equivalent to {@link #removeFirstOccurrence}. * * @param o element to be removed from this deque, if present * @return true if an element was removed as a result of this call * @throws ClassCastException if the class of the specified element * is incompatible with this deque (optional) * @throws NullPointerException if the specified element is null and this * deque does not permit null elements (optional) */ boolean remove(Object o); /** * Returns true if this deque contains the specified element. * More formally, returns true if and only if this deque contains * at least one element e such that * (o==null ? e==null : o.equals(e)). * * @param o element whose presence in this deque is to be tested * @return true if this deque contains the specified element * @throws ClassCastException if the type of the specified element * is incompatible with this deque (optional) * @throws NullPointerException if the specified element is null and this * deque does not permit null elements (optional) */ boolean contains(Object o); /** * Returns the number of elements in this deque. * * @return the number of elements in this deque */ public int size(); /** * Returns an iterator over the elements in this deque in proper sequence. * The elements will be returned in order from first (head) to last (tail). * * @return an iterator over the elements in this deque in proper sequence */ Iterator iterator(); /** * Returns an iterator over the elements in this deque in reverse * sequential order. The elements will be returned in order from * last (tail) to first (head). * * @return an iterator over the elements in this deque in reverse * sequence */ Iterator descendingIterator(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/util/FileCache.java ================================================ package com.jeffmony.async.util; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Provider; import java.security.Security; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Random; import java.util.Set; /** * Created by koush on 4/12/14. */ public class FileCache { class CacheEntry { final long size; public CacheEntry(File file) { size = file.length(); } } public static class Snapshot { FileInputStream[] fins; long[] lens; Snapshot(FileInputStream[] fins, long[] lens) { this.fins = fins; this.lens = lens; } public long getLength(int index) { return lens[index]; } public void close() { StreamUtility.closeQuietly(fins); } } private static String hashAlgorithm = "MD5"; private static MessageDigest findAlternativeMessageDigest() { if ("MD5".equals(hashAlgorithm)) { for (Provider provider : Security.getProviders()) { for (Provider.Service service : provider.getServices()) { hashAlgorithm = service.getAlgorithm(); try { MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm); if (messageDigest != null) return messageDigest; } catch (NoSuchAlgorithmException ignored) { } } } } return null; } static MessageDigest messageDigest; static { try { messageDigest = MessageDigest.getInstance(hashAlgorithm); } catch (NoSuchAlgorithmException e) { messageDigest = findAlternativeMessageDigest(); if (null == messageDigest) throw new RuntimeException(e); } try { messageDigest = (MessageDigest)messageDigest.clone(); } catch (CloneNotSupportedException e) { } } public static synchronized String toKeyString(Object... parts) { messageDigest.reset(); for (Object part : parts) { messageDigest.update(part.toString().getBytes()); } byte[] md5bytes = messageDigest.digest(); return new BigInteger(1, md5bytes).toString(16); } boolean loadAsync; Random random = new Random(); public File getTempFile() { File f; while ((f = new File(directory, new BigInteger(128, random).toString(16))).exists()); return f; } public File[] getTempFiles(int count) { File[] ret = new File[count]; for (int i = 0; i < count; i++) { ret[i] = getTempFile(); } return ret; } public static void removeFiles(File... files) { if (files == null) return; for (File file: files) { file.delete(); } } public void remove(String key) { int i = 0; while (cache.remove(getPartName(key, i)) != null) { i++; } removePartFiles(key); } public boolean exists(String key, int part) { return getPartFile(key, part).exists(); } public boolean exists(String key) { return getPartFile(key, 0).exists(); } public File touch(File file) { cache.get(file.getName()); file.setLastModified(System.currentTimeMillis()); return file; } public FileInputStream get(String key) throws IOException { return new FileInputStream(touch(getPartFile(key, 0))); } public File getFile(String key) { return touch(getPartFile(key, 0)); } public FileInputStream[] get(String key, int count) throws IOException { FileInputStream[] ret = new FileInputStream[count]; try { for (int i = 0; i < count; i++) { ret[i] = new FileInputStream(touch(getPartFile(key, i))); } } catch (IOException e) { // if we can't get all the parts, delete everything for (FileInputStream fin: ret) { StreamUtility.closeQuietly(fin); } remove(key); throw e; } return ret; } String getPartName(String key, int part) { return key + "." + part; } public void commitTempFiles(String key, File... tempFiles) { removePartFiles(key); // try to rename everything for (int i = 0; i < tempFiles.length; i++) { File tmp = tempFiles[i]; File partFile = getPartFile(key, i); if (!tmp.renameTo(partFile)) { // if any rename fails, delete everything removeFiles(tempFiles); remove(key); return; } remove(tmp.getName()); cache.put(getPartName(key, i), new CacheEntry(partFile)); } } void removePartFiles(String key) { int i = 0; File f; while ((f = getPartFile(key, i)).exists()) { f.delete(); i++; } } File getPartFile(String key, int part) { return new File(directory, getPartName(key, part)); } long blockSize = 4096; public void setBlockSize(long blockSize) { this.blockSize = blockSize; } class InternalCache extends LruCache { public InternalCache() { super(size); } @Override protected long sizeOf(String key, CacheEntry value) { return Math.max(blockSize, value.size); } @Override protected void entryRemoved(boolean evicted, String key, CacheEntry oldValue, CacheEntry newValue) { super.entryRemoved(evicted, key, oldValue, newValue); if (newValue != null) return; if (loading) return; new File(directory, key).delete(); } } InternalCache cache; File directory; long size; Comparator dateCompare = new Comparator() { @Override public int compare(File lhs, File rhs) { long l = lhs.lastModified(); long r = rhs.lastModified(); if (l < r) return -1; if (r > l) return 1; return 0; } }; boolean loading; void load() { loading = true; try { File[] files = directory.listFiles(); if (files == null) return; ArrayList list = new ArrayList(); Collections.addAll(list, files); Collections.sort(list, dateCompare); for (File file: list) { String name = file.getName(); CacheEntry entry = new CacheEntry(file); cache.put(name, entry); cache.get(name); } } finally { loading = false; } } private void doLoad() { if (loadAsync) { new Thread() { @Override public void run() { load(); } }.start(); } else { load(); } } public FileCache(File directory, long size, boolean loadAsync) { this.directory = directory; this.size = size; this.loadAsync = loadAsync; cache = new InternalCache(); directory.mkdirs(); doLoad(); } public long size() { return cache.size(); } public void clear() { removeFiles(directory.listFiles()); cache.evictAll(); } public Set keySet() { HashSet ret = new HashSet(); File[] files = directory.listFiles(); if (files == null) return ret; for (File file: files) { String name = file.getName(); int last = name.lastIndexOf('.'); if (last != -1) ret.add(name.substring(0, last)); } return ret; } public void setMaxSize(long maxSize) { cache.setMaxSize(maxSize); doLoad(); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/util/FileUtility.java ================================================ package com.jeffmony.async.util; import java.io.File; /** * Created by koush on 4/7/14. */ public class FileUtility { static public boolean deleteDirectory(File path) { if (path.exists()) { File[] files = path.listFiles(); if (files != null) { for (int i = 0; i < files.length; i++) { if (files[i].isDirectory()) { deleteDirectory(files[i]); } else { files[i].delete(); } } } } return (path.delete()); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/util/HashList.java ================================================ package com.jeffmony.async.util; import java.util.ArrayList; import java.util.Hashtable; import java.util.Set; /** * Created by koush on 5/27/13. */ public class HashList { Hashtable> internal = new Hashtable>(); public HashList() { } public Set keySet() { return internal.keySet(); } public synchronized V tag(String key) { TaggedList list = internal.get(key); if (list == null) return null; return list.tag(); } public synchronized void tag(String key, V tag) { TaggedList list = internal.get(key); if (list == null) { list = new TaggedList(); internal.put(key, list); } list.tag(tag); } public synchronized ArrayList remove(String key) { return internal.remove(key); } public synchronized int size() { return internal.size(); } public synchronized ArrayList get(String key) { return internal.get(key); } synchronized public boolean contains(String key) { ArrayList check = get(key); return check != null && check.size() > 0; } synchronized public void add(String key, T value) { ArrayList ret = get(key); if (ret == null) { TaggedList put = new TaggedList(); ret = put; internal.put(key, put); } ret.add(value); } synchronized public T pop(String key) { TaggedList values = internal.get(key); if (values == null) return null; if (values.size() == 0) return null; return values.remove(values.size() - 1); } synchronized public boolean removeItem(String key, T value) { TaggedList values = internal.get(key); if (values == null) return false; values.remove(value); return values.size() == 0; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/util/IdleTimeout.java ================================================ package com.jeffmony.async.util; import android.os.Handler; import com.jeffmony.async.AsyncServer; public class IdleTimeout extends TimeoutBase { Runnable callback; public IdleTimeout(AsyncServer server, long delay) { super(server, delay); } public IdleTimeout(Handler handler, long delay) { super(handler, delay); } public void setTimeout(Runnable callback) { this.callback = callback; } Object cancellable; public void reset() { handlerish.removeAllCallbacks(cancellable); cancellable = handlerish.postDelayed(callback, delay); } public void cancel() { // must post this, so that when it runs it removes everything in the queue, // preventing any rescheduling. // posting gaurantees there is not a reschedule in progress. handlerish.post(() -> handlerish.removeAllCallbacks(cancellable)); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/util/LruCache.java ================================================ /* * 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 com.jeffmony.async.util; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; /** * Static library version of {@link android.util.LruCache}. Used to write apps * that run on API levels prior to 12. When running on API level 12 or above, * this implementation is still used; it does not try to switch to the * framework's implementation. See the framework SDK documentation for a class * overview. */ public class LruCache { private final LinkedHashMap map; /** Size of this cache in units. Not necessarily the number of elements. */ private long size; private long maxSize; private int putCount; private int createCount; private int evictionCount; private int hitCount; private int missCount; /** * @param maxSize for caches that do not override {@link #sizeOf}, this is * the maximum number of entries in the cache. For all other caches, * this is the maximum sum of the sizes of the entries in this cache. */ public LruCache(long maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; this.map = new LinkedHashMap(0, 0.75f, true); } /** * Returns the value for {@code key} if it exists in the cache or can be * created by {@code #create}. If a value was returned, it is moved to the * head of the queue. This returns null if a value is not cached and cannot * be created. */ public final V get(K key) { if (key == null) { throw new NullPointerException("key == null"); } V mapValue; synchronized (this) { mapValue = map.get(key); if (mapValue != null) { hitCount++; return mapValue; } missCount++; } /* * Attempt to create a value. This may take a long time, and the map * may be different when create() returns. If a conflicting value was * added to the map while create() was working, we leave that value in * the map and release the created value. */ V createdValue = create(key); if (createdValue == null) { return null; } synchronized (this) { createCount++; mapValue = map.put(key, createdValue); if (mapValue != null) { // There was a conflict so undo that last put map.put(key, mapValue); } else { size += safeSizeOf(key, createdValue); } } if (mapValue != null) { entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { trimToSize(maxSize); return createdValue; } } /** * Caches {@code value} for {@code key}. The value is moved to the head of * the queue. * * @return the previous value mapped by {@code key}. */ public final V put(K key, V value) { if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } V previous; synchronized (this) { putCount++; size += safeSizeOf(key, value); previous = map.put(key, value); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, value); } trimToSize(maxSize); return previous; } /** * @param maxSize the maximum size of the cache before returning. May be -1 * to evict even 0-sized elements. */ private void trimToSize(long maxSize) { while (true) { K key; V value; synchronized (this) { if (size < 0 || (map.isEmpty() && size != 0)) { throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!"); } if (size <= maxSize || map.isEmpty()) { break; } Map.Entry toEvict = map.entrySet().iterator().next(); key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null); } } /** * Removes the entry for {@code key} if it exists. * * @return the previous value mapped by {@code key}. */ public final V remove(K key) { if (key == null) { throw new NullPointerException("key == null"); } V previous; synchronized (this) { previous = map.remove(key); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, null); } return previous; } /** * Called for entries that have been evicted or removed. This method is * invoked when a value is evicted to make space, removed by a call to * {@link #remove}, or replaced by a call to {@link #put}. The default * implementation does nothing. * *

The method is called without synchronization: other threads may * access the cache while this method is executing. * * @param evicted true if the entry is being removed to make space, false * if the removal was caused by a {@link #put} or {@link #remove}. * @param newValue the new value for {@code key}, if it exists. If non-null, * this removal was caused by a {@link #put}. Otherwise it was caused by * an eviction or a {@link #remove}. */ protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {} /** * Called after a cache miss to compute a value for the corresponding key. * Returns the computed value or null if no value can be computed. The * default implementation returns null. * *

The method is called without synchronization: other threads may * access the cache while this method is executing. * *

If a value for {@code key} exists in the cache when this method * returns, the created value will be released with {@link #entryRemoved} * and discarded. This can occur when multiple threads request the same key * at the same time (causing multiple values to be created), or when one * thread calls {@link #put} while another is creating a value for the same * key. */ protected V create(K key) { return null; } private long safeSizeOf(K key, V value) { long result = sizeOf(key, value); if (result < 0) { throw new IllegalStateException("Negative size: " + key + "=" + value); } return result; } /** * Returns the size of the entry for {@code key} and {@code value} in * user-defined units. The default implementation returns 1 so that size * is the number of entries and max size is the maximum number of entries. * *

An entry's size must not change while it is in the cache. */ protected long sizeOf(K key, V value) { return 1; } /** * Clear the cache, calling {@link #entryRemoved} on each removed entry. */ public final void evictAll() { trimToSize(-1); // -1 will evict 0-sized elements } /** * For caches that do not override {@link #sizeOf}, this returns the number * of entries in the cache. For all other caches, this returns the sum of * the sizes of the entries in this cache. */ public synchronized final long size() { return size; } public void setMaxSize(long maxSize) { this.maxSize = maxSize; } /** * For caches that do not override {@link #sizeOf}, this returns the maximum * number of entries in the cache. For all other caches, this returns the * maximum sum of the sizes of the entries in this cache. */ public synchronized final long maxSize() { return maxSize; } /** * Returns the number of times {@link #get} returned a value. */ public synchronized final int hitCount() { return hitCount; } /** * Returns the number of times {@link #get} returned null or required a new * value to be created. */ public synchronized final int missCount() { return missCount; } /** * Returns the number of times {@link #create(Object)} returned a value. */ public synchronized final int createCount() { return createCount; } /** * Returns the number of times {@link #put} was called. */ public synchronized final int putCount() { return putCount; } /** * Returns the number of values that have been evicted. */ public synchronized final int evictionCount() { return evictionCount; } /** * Returns a copy of the current contents of the cache, ordered from least * recently accessed to most recently accessed. */ public synchronized final Map snapshot() { return new LinkedHashMap(map); } @Override public synchronized final String toString() { int accesses = hitCount + missCount; int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0; return String.format(Locale.ENGLISH, "LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", maxSize, hitCount, missCount, hitPercent); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/util/StreamUtility.java ================================================ package com.jeffmony.async.util; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; public class StreamUtility { public static void fastChannelCopy(final ReadableByteChannel src, final WritableByteChannel dest) throws IOException { final ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024); while (src.read(buffer) != -1) { // prepare the buffer to be drained buffer.flip(); // write to the channel, may block dest.write(buffer); // If partial transfer, shift remainder down // If buffer is empty, same as doing recycle() buffer.compact(); } // EOF will leave buffer in fill state buffer.flip(); // make sure the buffer is fully drained. while (buffer.hasRemaining()) { dest.write(buffer); } } public static void copyStream(InputStream input, OutputStream output) throws IOException { final ReadableByteChannel inputChannel = Channels.newChannel(input); final WritableByteChannel outputChannel = Channels.newChannel(output); // copy the channels fastChannelCopy(inputChannel, outputChannel); } public static byte[] readToEndAsArray(InputStream input) throws IOException { DataInputStream dis = new DataInputStream(input); byte[] stuff = new byte[1024]; ByteArrayOutputStream buff = new ByteArrayOutputStream(); int read = 0; while ((read = dis.read(stuff)) != -1) { buff.write(stuff, 0, read); } dis.close(); return buff.toByteArray(); } public static String readToEnd(InputStream input) throws IOException { return new String(readToEndAsArray(input)); } static public String readFile(String filename) throws IOException { return readFile(new File(filename)); } static public String readFileSilent(String filename) { try { return readFile(new File(filename)); } catch (IOException e) { return null; } } static public String readFile(File file) throws IOException { byte[] buffer = new byte[(int) file.length()]; DataInputStream input = null; try { input = new DataInputStream(new FileInputStream(file)); input.readFully(buffer); } finally { closeQuietly(input); } return new String(buffer); } public static void writeFile(File file, String string) throws IOException { file.getParentFile().mkdirs(); DataOutputStream dout = new DataOutputStream(new FileOutputStream(file)); dout.write(string.getBytes()); dout.close(); } public static void writeFile(String file, String string) throws IOException { writeFile(new File(file), string); } public static void closeQuietly(Closeable... closeables) { if (closeables == null) return; for (Closeable closeable : closeables) { if (closeable != null) { try { closeable.close(); } catch (Exception e) { // http://stackoverflow.com/a/156525/9636 // also, catch all exceptions because some implementations throw random crap // like ArrayStoreException } } } } public static void eat(InputStream input) throws IOException { byte[] stuff = new byte[1024]; while (input.read(stuff) != -1); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/util/TaggedList.java ================================================ package com.jeffmony.async.util; import java.util.ArrayList; public class TaggedList extends ArrayList { private Object tag; public synchronized V tag() { return (V)tag; } public synchronized void tag(V tag) { this.tag = tag; } public synchronized void tagNull(V tag) { if (this.tag == null) this.tag = tag; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/util/ThrottleTimeout.java ================================================ package com.jeffmony.async.util; import android.os.Handler; import com.jeffmony.async.AsyncServer; import com.jeffmony.async.callback.ValueCallback; import java.util.ArrayList; import java.util.List; /** * Created by koush on 7/19/16. */ public class ThrottleTimeout extends TimeoutBase { ValueCallback> callback; ArrayList values = new ArrayList<>(); ThrottleMode throttleMode = ThrottleMode.Collect; public enum ThrottleMode { /** * The timeout will keep resetting until it expires, at which point all * the collected values will be invoked on the callback. */ Collect, /** * The callback will be invoked immediately with the first, but future values will be * metered until it expires. */ Meter, } public ThrottleTimeout(final AsyncServer server, long delay, ValueCallback> callback) { super(server, delay); this.callback = callback; } public ThrottleTimeout(final Handler handler, long delay, ValueCallback> callback) { super(handler, delay); this.callback = callback; } public void setCallback(ValueCallback> callback) { this.callback = callback; } private void runCallback() { cancellable = null; ArrayList v = new ArrayList<>(values); values.clear(); callback.onResult(v); } Object cancellable; public synchronized void postThrottled(final T value) { handlerish.post(() -> { values.add(value); if (throttleMode == ThrottleMode.Collect) { // cancel the existing, schedule a new one, and wait. handlerish.removeAllCallbacks(cancellable); cancellable = handlerish.postDelayed(this::runCallback, delay); } else { // nothing is pending, so this can be fired off immediately if (cancellable == null) { runCallback(); // meter future invocations cancellable = handlerish.postDelayed(this::runCallback, delay); } } }); } public void setThrottleMode(ThrottleMode throttleMode) { this.throttleMode = throttleMode; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/util/TimeoutBase.java ================================================ package com.jeffmony.async.util; import android.os.Handler; import com.jeffmony.async.AsyncServer; import com.jeffmony.async.future.Cancellable; public class TimeoutBase { protected Handlerish handlerish; protected long delay; interface Handlerish { void post(Runnable r); Object postDelayed(Runnable r, long delay); void removeAllCallbacks(Object cancellable); } protected void onCallback() { } public TimeoutBase(final AsyncServer server, long delay) { this.delay = delay; this.handlerish = new Handlerish() { @Override public void post(Runnable r) { server.post(r); } @Override public Object postDelayed(Runnable r, long delay) { return server.postDelayed(r, delay); } @Override public void removeAllCallbacks(Object cancellable) { if (cancellable == null) return; ((Cancellable)cancellable).cancel(); } }; } public TimeoutBase(final Handler handler, long delay) { this.delay = delay; this.handlerish = new Handlerish() { @Override public void post(Runnable r) { handler.post(r); } @Override public Object postDelayed(Runnable r, long delay) { handler.postDelayed(r, delay); return r; } @Override public void removeAllCallbacks(Object cancellable) { if (cancellable == null) return; handler.removeCallbacks((Runnable)cancellable); } }; } public void setDelay(long delay) { this.delay = delay; } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/util/UntypedHashtable.java ================================================ package com.jeffmony.async.util; import java.util.Hashtable; public class UntypedHashtable { private Hashtable hash = new Hashtable(); public void put(String key, Object value) { hash.put(key, value); } public void remove(String key) { hash.remove(key); } public T get(String key, T defaultValue) { T ret = get(key); if (ret == null) return defaultValue; return ret; } public T get(String key) { return (T)hash.get(key); } } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/wrapper/AsyncSocketWrapper.java ================================================ package com.jeffmony.async.wrapper; import com.jeffmony.async.AsyncSocket; public interface AsyncSocketWrapper extends AsyncSocket, DataEmitterWrapper { AsyncSocket getSocket(); } ================================================ FILE: androidasync/src/main/java/com/jeffmony/async/wrapper/DataEmitterWrapper.java ================================================ package com.jeffmony.async.wrapper; import com.jeffmony.async.DataEmitter; public interface DataEmitterWrapper extends DataEmitter { DataEmitter getDataEmitter(); } ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion 29 buildToolsVersion "29.0.2" defaultConfig { applicationId "com.android.media" minSdkVersion 19 targetSdkVersion 29 versionCode 1 versionName "1.0" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.1.0-alpha03' implementation 'com.github.JeffMony:ORCodeDemo:v1.3.0' implementation 'com.google.zxing:core:3.3.3' implementation project(path: ':playersdk') implementation project(path: ':mediaproxy') implementation project(path: ':base') } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/assets/list.json ================================================ [ { "name": "Test url 1", "uri": "https://tv.youkutv.cc/2019/10/28/6MSVuLec4zbpYFlj/playlist.m3u8" }, { "name": "Test url 2", "uri": "https://kuku.zuida-youku.com/20170616/cBIBaYMJ/index.m3u8" }, { "name": "Test url 3", "uri": "https://tv.youkutv.cc/2020/01/15/SZpLQDUmJZKF9O0D/playlist.m3u8" }, { "name": "Test url 4", "uri": "https://tv.youkutv.cc/2020/01/15/3d97sO5xQUYB5bvY/playlist.m3u8" }, { "name": "Test url 5", "uri": "https://hls.aoxtv.com/v3.szjal.cn/20200122/TIj9Ekt9/index.m3u8" }, { "name": "Test url 6", "uri": "https://hls.aoxtv.com/v3.szjal.cn/20200114/dtOHlPFE/index.m3u8" }, { "name": "Test url 7", "uri": "https://hls.aoxtv.com/v3.szjal.cn/20200115/qNIba0qo/index.m3u8" }, { "name": "Test url 8", "uri": "https://m3u8.soyoung.com/9d0e36edc95edfa34fa049c3bfedf2e1_7d68ac41_new22.m3u8?sign=7f717d032fb7a53b4c628c22d7e9a206&t=5f0000fd" } ] ================================================ FILE: app/src/main/java/com/android/media/DownloadBaseListActivity.java ================================================ package com.android.media; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.Button; import android.widget.ListView; import android.widget.TextView; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.android.baselib.utils.LogUtils; import com.media.cache.model.Video; import com.media.cache.VideoDownloadManager; import com.media.cache.listener.IDownloadListener; import com.media.cache.model.VideoTaskItem; import com.media.cache.model.VideoTaskMode; public class DownloadBaseListActivity extends AppCompatActivity { private ListView mDownloadListView; private TextView mFilePath; private Button mClearBtn; private Button mPauseBtn; private VideoListAdapter mAdapter; private VideoTaskItem[] items = new VideoTaskItem[8]; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_download_list); VideoDownloadManager.getInstance().setGlobalDownloadListener(mListener); initViews(); initDatas(); } private void initViews() { mDownloadListView = (ListView) findViewById(R.id.download_listview); mFilePath = (TextView) findViewById(R.id.file_path); mClearBtn = (Button) findViewById(R.id.clear_cache_btn); mPauseBtn = (Button) findViewById(R.id.pause_task_btn); mFilePath.setText(VideoDownloadManager.getInstance().getCacheFilePath()); } private void initDatas() { VideoTaskItem item1 = new VideoTaskItem("https://tv.youkutv.cc/2019/10/28/6MSVuLec4zbpYFlj/playlist.m3u8", VideoTaskMode.DOWNLOAD_MODE); VideoTaskItem item2 = new VideoTaskItem("https://kuku.zuida-youku.com/20170616/cBIBaYMJ/index.m3u8", VideoTaskMode.DOWNLOAD_MODE); VideoTaskItem item3 = new VideoTaskItem("https://tv.youkutv.cc/2020/01/15/SZpLQDUmJZKF9O0D/playlist.m3u8", VideoTaskMode.DOWNLOAD_MODE); VideoTaskItem item4 = new VideoTaskItem("https://tv.youkutv.cc/2020/01/15/3d97sO5xQUYB5bvY/playlist.m3u8", VideoTaskMode.DOWNLOAD_MODE); VideoTaskItem item5 = new VideoTaskItem("https://hls.aoxtv.com/v3.szjal.cn/20200122/TIj9Ekt9/index.m3u8", VideoTaskMode.DOWNLOAD_MODE); VideoTaskItem item6 = new VideoTaskItem("https://hls.aoxtv.com/v3.szjal.cn/20200114/dtOHlPFE/index.m3u8", VideoTaskMode.DOWNLOAD_MODE); VideoTaskItem item7 = new VideoTaskItem("https://hls.aoxtv.com/v3.szjal.cn/20200115/qNIba0qo/index.m3u8", VideoTaskMode.DOWNLOAD_MODE); VideoTaskItem item8 = new VideoTaskItem("https://hls.aoxtv.com/v3.szjal.cn/20200114/2KwuUDMK/index.m3u8", VideoTaskMode.DOWNLOAD_MODE); items[0] = item1; items[1] = item2; items[2] = item3; items[3] = item4; items[4] = item5; items[5] = item6; items[6] = item7; items[7] = item8; mAdapter = new VideoListAdapter(this, R.layout.download_item, items); mDownloadListView.setAdapter(mAdapter); mDownloadListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { LogUtils.d("jeffmony onItemClick url="+items[position].getUrl()); VideoTaskItem item = items[position]; if (item.isRunningTask()) { LogUtils.d("jeffmony pause downloading."); VideoDownloadManager.getInstance().pauseDownloadTask(item); } else if (item.isSlientTask()) { LogUtils.d("jeffmony start downloading."); VideoDownloadManager.getInstance().startDownload(item); } } }); mDownloadListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { LogUtils.w("jeffmony long click"); return true; } }); mClearBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { VideoDownloadManager.getInstance().deleteVideoTasks(items); } }); mPauseBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { VideoDownloadManager.getInstance().pauseDownloadTasks(items); } }); } private IDownloadListener mListener = new IDownloadListener() { @Override public void onDownloadDefault(VideoTaskItem item) { LogUtils.d("jeffmony onDownloadDefault: " + item.getUrl()); notifyChanged(item); } @Override public void onDownloadPending(VideoTaskItem item) { LogUtils.d("jeffmony onDownloadPending: " + item.getUrl()); notifyChanged(item); } @Override public void onDownloadPrepare(VideoTaskItem item) { LogUtils.d("jeffmony onDownloadPrepare: " + item.getUrl()); notifyChanged(item); } @Override public void onDownloadStart(VideoTaskItem item) { LogUtils.d("onDownloadStart: " + item.getUrl()); notifyChanged(item); } @Override public void onDownloadProxyReady(VideoTaskItem item) { LogUtils.d("jeffmony onDownloadProxyReady: " + item.getProxyUrl()); } @Override public void onDownloadProgress(VideoTaskItem item) { LogUtils.d("jeffmony onDownloadProgress: " + item.getPercentString()); notifyChanged(item); } @Override public void onDownloadSpeed(VideoTaskItem item) { notifyChanged(item); } @Override public void onDownloadPause(VideoTaskItem item) { LogUtils.d("jeffmony onDownloadPause: " + item.getUrl()); notifyChanged(item); } @Override public void onDownloadError(VideoTaskItem item) { LogUtils.d("jeffmony onDownloadError: " + item.getUrl()); notifyChanged(item); } @Override public void onDownloadProxyForbidden(VideoTaskItem item) { LogUtils.d("jeffmony onDownloadForbidden: " + item.getUrl()); notifyChanged(item); } @Override public void onDownloadSuccess(VideoTaskItem item) { LogUtils.d("jeffmony onDownloadSuccess: " + item.getUrl()); notifyChanged(item); } }; private void notifyChanged(VideoTaskItem item) { runOnUiThread(new Runnable() { @Override public void run() { mAdapter.notifyChanged(items, item); } }); } @Override protected void onDestroy() { super.onDestroy(); LogUtils.w("jeffmony onDestroy"); } } ================================================ FILE: app/src/main/java/com/android/media/DownloadFeatureActivity.java ================================================ package com.android.media; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; public class DownloadFeatureActivity extends AppCompatActivity implements View.OnClickListener { private Button mDownloadConfigBtn; private Button mDownloadBaseBtn; private Button mDownloadOrcodeBtn; private Button mCurrentDownloadBtn; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_download_feature); initViews(); } private void initViews() { mDownloadConfigBtn = (Button) findViewById(R.id.download_settings_btn); mDownloadBaseBtn = (Button) findViewById(R.id.download_normal_btn); mDownloadOrcodeBtn = (Button) findViewById(R.id.download_orcode); mCurrentDownloadBtn = (Button) findViewById(R.id.list_download_btn); mDownloadConfigBtn.setOnClickListener(this); mDownloadBaseBtn.setOnClickListener(this); mDownloadOrcodeBtn.setOnClickListener(this); mCurrentDownloadBtn.setOnClickListener(this); } @Override public void onClick(View v) { if (v == mDownloadConfigBtn) { Intent intent = new Intent(DownloadFeatureActivity.this, DownloadSettingsActivity.class); startActivity(intent); } else if (v == mDownloadBaseBtn) { Intent intent = new Intent(DownloadFeatureActivity.this, DownloadBaseListActivity.class); startActivity(intent); } else if (v == mDownloadOrcodeBtn) { Intent intent = new Intent(DownloadFeatureActivity.this, DownloadOrcodeActivity.class); startActivity(intent); } else if (v == mCurrentDownloadBtn) { } } } ================================================ FILE: app/src/main/java/com/android/media/DownloadOrcodeActivity.java ================================================ package com.android.media; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; public class DownloadOrcodeActivity extends AppCompatActivity implements View.OnClickListener { private EditText mDownloadUrlText; private Button mSingleDownloadBtn; private Button mOrcodeScannerBtn; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_orcode); initViews(); } private void initViews() { mDownloadUrlText = (EditText) findViewById(R.id.download_url_text); mSingleDownloadBtn = (Button) findViewById(R.id.download_single_btn); mOrcodeScannerBtn = (Button) findViewById(R.id.orcode_scanner_btn); mSingleDownloadBtn.setOnClickListener(this); mOrcodeScannerBtn.setOnClickListener(this); } @Override public void onClick(View v) { if (v == mSingleDownloadBtn) { } else if (v == mOrcodeScannerBtn) { } } } ================================================ FILE: app/src/main/java/com/android/media/DownloadPlayActivity.java ================================================ package com.android.media; import android.graphics.SurfaceTexture; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.Surface; import android.view.TextureView; import android.view.View; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.android.baselib.utils.LogUtils; import com.android.baselib.utils.ScreenUtils; import com.android.baselib.utils.Utility; import com.android.player.CommonPlayer; import com.android.player.IPlayer; import com.android.player.PlayerAttributes; import com.android.player.PlayerType; import com.media.cache.model.Video; import com.media.cache.model.VideoTaskMode; import java.io.IOException; public class DownloadPlayActivity extends AppCompatActivity implements View.OnClickListener { private String mProxyUrl; private String mOriginUrl; private TextureView mVideoView; private ImageButton mControlBtn; private TextView mTimeView; private SeekBar mProgressView; private CommonPlayer mPlayer; private Surface mSurface; private int mSurfaceWidth; private int mSurfaceHeight; private int mVideoWidth; private int mVideoHeight; private float mPixelRatio; //SAR private long mDuration = 0L; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_download_play); mProxyUrl = getIntent().getStringExtra("proxy_url"); mOriginUrl = getIntent().getStringExtra("origin_url"); mSurfaceWidth = ScreenUtils.getScreenWidth(this); initViews(); } private void initViews() { mVideoView = (TextureView) findViewById(R.id.download_video_view); mProgressView = (SeekBar) findViewById(R.id.download_progress_view); mControlBtn = (ImageButton) findViewById(R.id.download_control_btn); mTimeView = (TextView) findViewById(R.id.download_time_view); mVideoView.setSurfaceTextureListener(mSurfaceTextureListener); mControlBtn.setOnClickListener(this); mProgressView.setOnSeekBarChangeListener(mSeekBarChangeListener); } private TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { mSurface = new Surface(surface); initPlayer(); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return true; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { } }; private void initPlayer() { PlayerAttributes attributes = new PlayerAttributes(""); attributes.setVideoCacheSwitch(true); attributes.setTaskMode(VideoTaskMode.DOWNLOAD_PLAY_MODE); mPlayer = new CommonPlayer(this, PlayerType.EXO_PLAYER, attributes); Uri uri = Uri.parse(mProxyUrl); try { mPlayer.setDataSource(DownloadPlayActivity.this, uri); } catch (IOException e) { e.printStackTrace(); return; } mPlayer.setOriginUrl(mOriginUrl); mPlayer.setSurface(mSurface); mPlayer.setOnPreparedListener(mPreparedListener); mPlayer.setOnErrorListener(mErrorListener); mPlayer.setOnVideoSizeChangedListener(mVideoSizeChangeListener); mPlayer.setOnLocalProxyCacheListener(mProxyCacheListener); mPlayer.prepareAsync(); } private IPlayer.OnPreparedListener mPreparedListener = new IPlayer.OnPreparedListener() { @Override public void onPrepared(IPlayer mp) { doPlayVideo(); } }; private IPlayer.OnErrorListener mErrorListener = new IPlayer.OnErrorListener() { @Override public void onError(IPlayer mp, int what, String msg) { Toast.makeText(DownloadPlayActivity.this, "Play Error", Toast.LENGTH_SHORT).show(); } }; private IPlayer.OnVideoSizeChangedListener mVideoSizeChangeListener = new IPlayer.OnVideoSizeChangedListener() { @Override public void onVideoSizeChanged(IPlayer mp, int width, int height, int rotationDegree, float pixelRatio, float darRatio) { LogUtils.d("PlayerActivity onVideoSizeChanged width="+width+", height="+height + ", pixedlRatio = " + pixelRatio); mVideoWidth = width; mVideoHeight = height; mPixelRatio = pixelRatio; mSurfaceHeight = (int)(mSurfaceWidth * mVideoHeight * 1.0f / mVideoWidth); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mSurfaceWidth, mSurfaceHeight); mVideoView.setLayoutParams(params); } }; @Override public void onClick(View view) { LogUtils.e("click event"); if(view == mControlBtn) { if (!mPlayer.isPlaying()) { mPlayer.start(); mControlBtn.setImageResource(R.mipmap.played_state); } else { mPlayer.pause(); mControlBtn.setImageResource(R.mipmap.paused_state); } } } @Override protected void onPause() { super.onPause(); if (mPlayer != null) { mPlayer.pause(); mControlBtn.setImageResource(R.mipmap.paused_state); } } @Override protected void onDestroy() { super.onDestroy(); doReleasePlayer(); } private void doPlayVideo() { if (mPlayer != null) { mTimeView.setVisibility(View.VISIBLE); mPlayer.start(); mDuration = mPlayer.getDuration(); mControlBtn.setImageResource(R.mipmap.played_state); mHandler.sendEmptyMessage(MSG_UPDATE_PROGRESS); } } private void doReleasePlayer() { if (mPlayer != null) { mPlayer.stop(); mPlayer.release(); mPlayer = null; } } private static final int MSG_UPDATE_PROGRESS = 0x1; private static final int MAX_PROGRESS = 1000; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == MSG_UPDATE_PROGRESS) { updateProgressView(); } } }; private void updateProgressView() { if (mPlayer != null) { long currentPosition = mPlayer.getCurrentPosition(); mTimeView.setText(Utility.getVideoTimeString(currentPosition) + " / " + Utility.getVideoTimeString(mDuration)); // mProgressView.setProgress((int)(1000 * currentPosition * 1.0f / mDuration)); // int cacheProgress = (int)(mPercent * 1.0f / 100 * 1000); // mProgressView.setSecondaryProgress(cacheProgress); } mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, 1000); } private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { if (mPlayer != null) { mHandler.removeMessages(MSG_UPDATE_PROGRESS); } LogUtils.d("onStartTrackingTouch progress="+mProgressView.getProgress()); } @Override public void onStopTrackingTouch(SeekBar seekBar) { LogUtils.d("onStopTrackingTouch progress="+mProgressView.getProgress()); if (mPlayer != null) { int progress = mProgressView.getProgress(); int seekPosition = (int)(progress * 1.0f / MAX_PROGRESS * mDuration); mPlayer.seekTo(seekPosition); mHandler.sendEmptyMessage(MSG_UPDATE_PROGRESS); } } }; private IPlayer.OnLocalProxyCacheListener mProxyCacheListener = new IPlayer.OnLocalProxyCacheListener() { @Override public void onCacheReady(IPlayer mp, String proxyUrl) { } @Override public void onCacheProgressChanged(IPlayer mp, int percent, long cachedSize) { } @Override public void onCacheSpeedChanged(IPlayer mp, float speed) { } @Override public void onCacheForbidden(IPlayer mp, String url) { } @Override public void onCacheFinished(IPlayer mp) { } }; } ================================================ FILE: app/src/main/java/com/android/media/DownloadSettingsActivity.java ================================================ package com.android.media; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.View; import android.widget.RadioButton; import android.widget.TextView; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.android.baselib.utils.Utility; import com.media.cache.VideoDownloadManager; import com.media.cache.utils.StorageUtils; import java.io.File; public class DownloadSettingsActivity extends AppCompatActivity implements View.OnClickListener { private static final int MSG_COUNT_SIZE = 0x1; private TextView mStoreLocText; private TextView mStoreSizeText; private TextView mOpenFileText; private TextView mClearDownloadText; private RadioButton mBtn1; private RadioButton mBtn2; private RadioButton mBtn3; private RadioButton mBtn4; private RadioButton mBtn5; private RadioButton mBtn11; private RadioButton mBtn12; private int mConcurrentNum = 3; private boolean mIgnoreCertErrors = true; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == MSG_COUNT_SIZE) { String filePath = VideoDownloadManager.getInstance().getCacheFilePath(); File file = new File(filePath); if (file.exists()) { long size = StorageUtils.countTotalSize(file); mStoreSizeText.setText(Utility.getSize(size)); } } } }; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_download_settings); initViews(); } private void initViews() { mStoreLocText = (TextView)findViewById(R.id.store_loc_txt); mStoreSizeText = (TextView)findViewById(R.id.store_size); mOpenFileText = (TextView)findViewById(R.id.open_file_txt); mClearDownloadText = (TextView)findViewById(R.id.clear_download_cache); mBtn1 = (RadioButton)findViewById(R.id.btn1); mBtn2 = (RadioButton)findViewById(R.id.btn2); mBtn3 = (RadioButton)findViewById(R.id.btn3); mBtn4 = (RadioButton)findViewById(R.id.btn4); mBtn5 = (RadioButton)findViewById(R.id.btn5); mBtn11 = (RadioButton)findViewById(R.id.btn11); mBtn12 = (RadioButton)findViewById(R.id.btn12); mStoreLocText.setText( VideoDownloadManager.getInstance().getCacheFilePath()); mOpenFileText.setOnClickListener(this); mClearDownloadText.setOnClickListener(this); mBtn1.setOnClickListener(this); mBtn2.setOnClickListener(this); mBtn3.setOnClickListener(this); mBtn4.setOnClickListener(this); mBtn5.setOnClickListener(this); mBtn11.setOnClickListener(this); mBtn12.setOnClickListener(this); } @Override protected void onResume() { super.onResume(); mHandler.sendEmptyMessage(MSG_COUNT_SIZE); checkBtnState(VideoDownloadManager.getInstance() .downloadConfig() .getConcurrentCount()); } @Override public void onClick(View v) { if (v == mClearDownloadText) { VideoDownloadManager.getInstance().deleteAllVideoFiles( DownloadSettingsActivity.this); mHandler.sendEmptyMessage(MSG_COUNT_SIZE); } else if (v == mOpenFileText) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setDataAndType( Uri.parse(VideoDownloadManager.getInstance().getCacheFilePath()), "file/*"); intent.addCategory(Intent.CATEGORY_OPENABLE); startActivity(intent); } else if (v == mBtn1) { checkBtnState(1); } else if (v == mBtn2) { checkBtnState(2); } else if (v == mBtn3) { checkBtnState(3); } else if (v == mBtn4) { checkBtnState(4); } else if (v == mBtn5) { checkBtnState(5); } else if (v == mBtn11) { mBtn11.setChecked(true); mBtn12.setChecked(false); mIgnoreCertErrors = true; } else if (v == mBtn12) { mBtn11.setChecked(false); mBtn12.setChecked(true); mIgnoreCertErrors = false; } VideoDownloadManager.getInstance().setConcurrentCount(mConcurrentNum); VideoDownloadManager.getInstance().setIgnoreAllCertErrors( mIgnoreCertErrors); } private void checkBtnState(int type) { if (type == 1) { mBtn1.setChecked(true); mBtn2.setChecked(false); mBtn3.setChecked(false); mBtn4.setChecked(false); mBtn5.setChecked(false); } else if (type == 2) { mBtn1.setChecked(false); mBtn2.setChecked(true); mBtn3.setChecked(false); mBtn4.setChecked(false); mBtn5.setChecked(false); } else if (type == 3) { mBtn1.setChecked(false); mBtn2.setChecked(false); mBtn3.setChecked(true); mBtn4.setChecked(false); mBtn5.setChecked(false); } else if (type == 4) { mBtn1.setChecked(false); mBtn2.setChecked(false); mBtn3.setChecked(false); mBtn4.setChecked(true); mBtn5.setChecked(false); } else if (type == 5) { mBtn1.setChecked(false); mBtn2.setChecked(false); mBtn3.setChecked(false); mBtn4.setChecked(false); mBtn5.setChecked(true); } mConcurrentNum = type; } @Override protected void onStop() { super.onStop(); } } ================================================ FILE: app/src/main/java/com/android/media/MainActivity.java ================================================ package com.android.media; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity implements View.OnClickListener { private Button mPlayBtn; private Button mDownloadBtn; private Button mScanBtn; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mPlayBtn = (Button) findViewById(R.id.play_btn); mDownloadBtn = (Button) findViewById(R.id.download_btn); mScanBtn = (Button) findViewById(R.id.scan_btn); mPlayBtn.setOnClickListener(this); mDownloadBtn.setOnClickListener(this); mScanBtn.setOnClickListener(this); } @Override public void onClick(View v) { if (v == mPlayBtn) { Intent intent = new Intent(this, PlayFeatureActivity.class); startActivity(intent); } else if (v == mDownloadBtn) { Intent intent = new Intent(this, DownloadFeatureActivity.class); startActivity(intent); } else if (v == mScanBtn) { Intent intent = new Intent(this, MediaScannerActivity.class); startActivity(intent); } } } ================================================ FILE: app/src/main/java/com/android/media/MediaScannerActivity.java ================================================ package com.android.media; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; public class MediaScannerActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_scanner); } } ================================================ FILE: app/src/main/java/com/android/media/MyApplication.java ================================================ package com.android.media; import android.app.Application; import com.media.cache.DownloadConstants; import com.media.cache.LocalProxyConfig; import com.media.cache.VideoDownloadManager; import com.media.cache.utils.StorageUtils; import java.io.File; public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); File file = StorageUtils.getVideoCacheDir(this); if (!file.exists()) { file.mkdir(); } LocalProxyConfig config = new VideoDownloadManager.Build(this) .setCacheRoot(file) .setUrlRedirect(false) .setTimeOut(DownloadConstants.READ_TIMEOUT, DownloadConstants.CONN_TIMEOUT, DownloadConstants.SOCKET_TIMEOUT) .setConcurrentCount(DownloadConstants.CONCURRENT_COUNT) .setIgnoreAllCertErrors(true) .buildConfig(); VideoDownloadManager.getInstance().initConfig(config); } } ================================================ FILE: app/src/main/java/com/android/media/PlayFeatureActivity.java ================================================ package com.android.media; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Environment; import android.text.TextUtils; import android.util.JsonReader; import android.util.JsonToken; import android.view.View; import android.widget.AdapterView; import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.SimpleAdapter; import android.widget.TextView; import android.widget.Toast; import com.android.baselib.utils.LogUtils; import com.media.cache.CacheManager; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; public class PlayFeatureActivity extends AppCompatActivity implements View.OnClickListener, RadioGroup.OnCheckedChangeListener { private static final String WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE"; private static final int REQUEST_PERMISSION_OK = 0x1; private EditText mVideoUrlView; private Button mPlayBtn; private ListView mVideoListView; private RadioGroup mPlayerBtnGroup; private RadioButton mIjkPlayerBtn; private RadioButton mExoPlayerBtn; private CheckBox mVideoCacheBox; private TextView mCachedLocationView; private TextView mCacheSizeView; private TextView mClearCacheView; private List> mVideoList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_play_func); initViews(); initViewListData(); } private void initViews() { mVideoUrlView = (EditText) findViewById(R.id.video_url_view); mPlayBtn = (Button) findViewById(R.id.play_btn); mVideoListView = (ListView) findViewById(R.id.video_list); mPlayerBtnGroup = (RadioGroup) findViewById(R.id.play_btn_group); mIjkPlayerBtn = (RadioButton) findViewById(R.id.ijkplayer_btn); mExoPlayerBtn = (RadioButton) findViewById(R.id.exoplayer_btn); mVideoCacheBox = (CheckBox) findViewById(R.id.local_proxy_box); mCachedLocationView = (TextView) findViewById(R.id.cached_location_view); mCacheSizeView = (TextView) findViewById(R.id.cache_size_view); mClearCacheView = (TextView) findViewById(R.id.clear_cache_view); mExoPlayerBtn.setChecked(true); mPlayBtn.setOnClickListener(this); mClearCacheView.setOnClickListener(this); mPlayerBtnGroup.setOnCheckedChangeListener(this); mCachedLocationView.setText(CacheManager.getCachePath()); } private void initViewListData() { mVideoList = new ArrayList<>(); try { InputStream is = null; try { String PATH = Environment.getExternalStorageDirectory() + "/list.json"; File file = new File(PATH); if (file.exists()) { is = new FileInputStream(PATH); } else { is = getAssets().open("list.json"); } JsonReader reader = new JsonReader(new InputStreamReader(is)); reader.beginArray(); while (reader.hasNext()) { reader.beginObject(); HashMap item = new HashMap<>(); while (reader.hasNext()) { String name = reader.nextName(); if (name.equals("name")) { String videoName = reader.nextString(); item.put("name", videoName); } else if (name.equals("age") || reader.peek() != JsonToken.NULL) { // 当前获取的字段是否为:null String videoUrl = reader.nextString(); item.put("url", videoUrl); } } reader.endObject(); mVideoList.add(item); } reader.endArray(); } finally { if (null != is) { is.close(); } } } catch (IOException e) { throw new RuntimeException(e); } SimpleAdapter adapter = new SimpleAdapter(this, mVideoList, R.layout.video_item, new String[]{"name"}, new int[]{R.id.video_name}); mVideoListView.setAdapter(adapter); mVideoListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { String url = mVideoList.get(position).get("url"); mVideoUrlView.setText(url); } }); } @Override protected void onResume() { super.onResume(); checkPermission(); mCacheSizeView.setText(CacheManager.getCachedSize()); } private void checkPermission() { if (ContextCompat.checkSelfPermission(this, WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[] { WRITE_EXTERNAL_STORAGE }, REQUEST_PERMISSION_OK); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == REQUEST_PERMISSION_OK) { if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Toast.makeText(this, "存储权限已开通", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "存储权限被拒绝", Toast.LENGTH_SHORT).show(); } } } @Override public void onClick(View v) { if (v == mPlayBtn) { doPlayVideo(); } else if (v == mClearCacheView) { clearVideoCache(); } } @Override public void onCheckedChanged(RadioGroup group, int checkedId) { LogUtils.d("onCheckedChanged checkedId = " + checkedId); } private void doPlayVideo() { String url = mVideoUrlView.getText().toString(); if (TextUtils.isEmpty(url)) { Toast.makeText(this, "输入的url为空", Toast.LENGTH_SHORT).show(); } else { Intent intent = new Intent(this, PlayerActivity.class); intent.putExtra("url", url); int playerType = -1; if (mIjkPlayerBtn.isChecked()) { playerType = 1; } else if (mExoPlayerBtn.isChecked()) { playerType = 2; } intent.putExtra("playerType", playerType); boolean videoCached = mVideoCacheBox.isChecked(); intent.putExtra("videoCached", videoCached); startActivity(intent); } } private void clearVideoCache() { CacheManager.deleteCacheFile(); mCacheSizeView.setText("0 MB"); } } ================================================ FILE: app/src/main/java/com/android/media/PlayerActivity.java ================================================ package com.android.media; import android.graphics.SurfaceTexture; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.Gravity; import android.view.Surface; import android.view.TextureView; import android.view.View; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import com.android.baselib.utils.Utility; import com.android.baselib.utils.ScreenUtils; import com.android.baselib.utils.LogUtils; import com.android.player.CommonPlayer; import com.android.player.IPlayer; import com.android.player.PlayerAttributes; import com.android.player.PlayerType; import java.io.IOException; public class PlayerActivity extends AppCompatActivity implements View.OnClickListener { private TextureView mVideoView; private ImageButton mControlBtn; private TextView mTimeView; private SeekBar mProgressView; private TextView mPlayTipView; private int mSurfaceWidth; private int mSurfaceHeight; private int mVideoWidth; private int mVideoHeight; private float mPixelRatio; //SAR private float mDarRatio; private long mDuration = 0L; private CommonPlayer mPlayer; private Surface mSurface; private String mUrl = "https://tv.youkutv.cc/2020/01/15/SZpLQDUmJZKF9O0D/playlist.m3u8"; private int mPlayerType = -1; private boolean mVideoCached = false; private int mPercent = 0; private long mCacheSize = 0L; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_player); getWindow().setFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); mUrl = getIntent().getStringExtra("url"); mPlayerType = getIntent().getIntExtra("playerType", -1); if (mPlayerType == -1) { mPlayerType = 1; } mVideoCached = getIntent().getBooleanExtra("videoCached", false); mSurfaceWidth = ScreenUtils.getScreenWidth(this); initViews(); } private void initViews() { mVideoView = (TextureView) findViewById(R.id.video_view); mTimeView = (TextView) findViewById(R.id.video_time_view); mProgressView = (SeekBar) findViewById(R.id.video_progress_view); mControlBtn = (ImageButton) findViewById(R.id.video_control_btn); mPlayTipView = (TextView) findViewById(R.id.play_tip_view); mControlBtn.setOnClickListener(this); mVideoView.setSurfaceTextureListener(mSurfaceTextureListener); mProgressView.setOnSeekBarChangeListener(mSeekBarChangeListener); } private void initPlayer() { PlayerAttributes attributes = new PlayerAttributes(""); attributes.setVideoCacheSwitch(mVideoCached); if (mPlayerType == 1) { mPlayer = new CommonPlayer(this, PlayerType.IJK_PLAYER, attributes); } else if (mPlayerType == 2) { mPlayer = new CommonPlayer(this, PlayerType.EXO_PLAYER, attributes); } else if (mPlayerType == 3) { mPlayer = new CommonPlayer(this, PlayerType.MEDIA_PLAYER, attributes); } if (mVideoCached) { mPlayer.setOnLocalProxyCacheListener(mOnLocalProxyCacheListener); mPlayer.startLocalProxy(mUrl); } else { Uri uri = Uri.parse(mUrl); try { mPlayer.setDataSource(PlayerActivity.this, uri); } catch (IOException e) { e.printStackTrace(); return; } mPlayer.setSurface(mSurface); mPlayer.setOnPreparedListener(mPreparedListener); mPlayer.setOnErrorListener(mErrorListener); mPlayer.setOnVideoSizeChangedListener(mVideoSizeChangeListener); mPlayer.prepareAsync(); } } private TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { mSurface = new Surface(surface); initPlayer(); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return true; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { } }; private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { if (mPlayer != null) { mHandler.removeMessages(MSG_UPDATE_PROGRESS); } LogUtils.d("onStartTrackingTouch progress="+mProgressView.getProgress()); } @Override public void onStopTrackingTouch(SeekBar seekBar) { LogUtils.d("onStopTrackingTouch progress="+mProgressView.getProgress()); if (mPlayer != null) { int progress = mProgressView.getProgress(); int seekPosition = (int)(progress * 1.0f / MAX_PROGRESS * mDuration); mPlayer.seekTo(seekPosition); mHandler.sendEmptyMessage(MSG_UPDATE_PROGRESS); } } }; @Override protected void onPause() { super.onPause(); if (mPlayer != null) { mPlayer.pause(); mControlBtn.setBackgroundResource(R.mipmap.paused_state); } } @Override protected void onStop() { super.onStop(); } @Override protected void onDestroy() { super.onDestroy(); mHandler.removeMessages(MSG_UPDATE_PROGRESS); doReleasePlayer(); } private IPlayer.OnPreparedListener mPreparedListener = new IPlayer.OnPreparedListener() { @Override public void onPrepared(IPlayer mp) { doPlayVideo(); } }; private IPlayer.OnErrorListener mErrorListener = new IPlayer.OnErrorListener() { @Override public void onError(IPlayer mp, int what, String msg) { Toast.makeText(PlayerActivity.this, "Play Error", Toast.LENGTH_SHORT).show(); } }; private IPlayer.OnVideoSizeChangedListener mVideoSizeChangeListener = new IPlayer.OnVideoSizeChangedListener() { @Override public void onVideoSizeChanged(IPlayer mp, int width, int height, int rotationDegree, float pixelRatio, float darRatio) { LogUtils.d("PlayerActivity onVideoSizeChanged width="+width+", height="+height + ", mDarRatio = " + darRatio); mVideoWidth = width; mVideoHeight = height; mPixelRatio = pixelRatio; mDarRatio = darRatio; if (mPlayerType != 1 || Math.abs(mDarRatio) < 0.001f) { mSurfaceHeight = (int) (mSurfaceWidth * mVideoHeight * 1.0f / mVideoWidth); } else { mSurfaceHeight = (int) (mSurfaceWidth * 1.0f / mDarRatio); } LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mSurfaceWidth, mSurfaceHeight); params.gravity = Gravity.CENTER; mVideoView.setLayoutParams(params); } }; private IPlayer.OnLocalProxyCacheListener mOnLocalProxyCacheListener = new IPlayer.OnLocalProxyCacheListener() { @Override public void onCacheReady(IPlayer mp, String proxyUrl) { Uri uri = Uri.parse(proxyUrl); try { mPlayer.setDataSource(PlayerActivity.this, uri); } catch (IOException e) { e.printStackTrace(); return; } mPlayer.setSurface(mSurface); mPlayer.setOnPreparedListener(mPreparedListener); mPlayer.setOnVideoSizeChangedListener(mVideoSizeChangeListener); mPlayer.setOnErrorListener(mErrorListener); mPlayer.prepareAsync(); mPlayTipView.setVisibility(View.VISIBLE); } @Override public void onCacheProgressChanged(IPlayer mp, int percent, long cachedSize) { mPercent = percent; mCacheSize = cachedSize; mPlayTipView.setText("边下边播: " + Utility.getSize(cachedSize)); } @Override public void onCacheSpeedChanged(IPlayer mp, float speed) { } @Override public void onCacheForbidden(IPlayer mp, String url) { LogUtils.w("onCacheForbidden url = " + url); } @Override public void onCacheFinished(IPlayer mp) { mPercent = 100; } }; private static final int MSG_UPDATE_PROGRESS = 1; private static final int MAX_PROGRESS = 1000; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == MSG_UPDATE_PROGRESS) { updateProgressView(); } } }; @Override public void onClick(View view) { LogUtils.e("click event"); if(view == mControlBtn) { if (!mPlayer.isPlaying()) { mPlayer.start(); mControlBtn.setBackgroundResource(R.mipmap.played_state); } else { mPlayer.pause(); mControlBtn.setBackgroundResource(R.mipmap.paused_state); } } } private void doPlayVideo() { if (mPlayer != null) { mTimeView.setVisibility(View.VISIBLE); mPlayer.start(); mControlBtn.setBackgroundResource(R.mipmap.played_state); mDuration = mPlayer.getDuration(); LogUtils.d("total duration ="+mDuration +", timeString="+ Utility.getVideoTimeString(mDuration)); mHandler.sendEmptyMessage(MSG_UPDATE_PROGRESS); } } private void updateProgressView() { if (mPlayer != null) { long currentPosition = mPlayer.getCurrentPosition(); mTimeView.setText(Utility.getVideoTimeString(currentPosition) + " / " + Utility.getVideoTimeString(mDuration)); mProgressView.setProgress((int)(1000 * currentPosition * 1.0f / mDuration)); int cacheProgress = (int)(mPercent * 1.0f / 100 * 1000); mProgressView.setSecondaryProgress(cacheProgress); } mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, 1000); } private void doReleasePlayer() { if (mPlayer != null) { mPlayer.stop(); mPlayer.release(); mPlayer = null; } } } ================================================ FILE: app/src/main/java/com/android/media/VideoListAdapter.java ================================================ package com.android.media; import android.content.Context; import android.content.Intent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.baselib.utils.LogUtils; import com.media.cache.model.VideoTaskItem; import com.media.cache.model.VideoTaskState; public class VideoListAdapter extends ArrayAdapter { private Context mContext; public VideoListAdapter(Context context, int resource, VideoTaskItem[] items) { super(context, resource, items); mContext = context; } @NonNull @Override public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { View view = LayoutInflater.from(getContext()).inflate(R.layout.download_item, null); VideoTaskItem item = getItem(position); TextView urlTextView = (TextView) view.findViewById(R.id.url_text); urlTextView.setText(item.getUrl()); TextView stateTextView = (TextView) view.findViewById(R.id.status_txt); TextView playBtn = (TextView) view.findViewById(R.id.download_play_btn); playBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(mContext, DownloadPlayActivity.class); intent.putExtra("proxy_url", item.getProxyUrl()); intent.putExtra("origin_url", item.getUrl()); mContext.startActivity(intent); } }); setStateText(stateTextView, playBtn, item); TextView infoTextView = (TextView) view.findViewById(R.id.download_txt); setDownloadInfoText(infoTextView, item); return view; } private void setStateText(TextView stateView, TextView playBtn, VideoTaskItem item) { switch (item.getTaskState()) { case VideoTaskState.PENDING: playBtn.setVisibility(View.INVISIBLE); stateView.setText("等待中"); break; case VideoTaskState.PREPARE: playBtn.setVisibility(View.INVISIBLE); stateView.setText("准备好"); break; case VideoTaskState.START: playBtn.setVisibility(View.INVISIBLE); stateView.setText("开始下载"); break; case VideoTaskState.DOWNLOADING: case VideoTaskState.PROXYREADY: if (item.getProxyReady()) { stateView.setText("下载中...(可播放)"); playBtn.setVisibility(View.VISIBLE); } else { stateView.setText("下载中..."); } break; case VideoTaskState.PAUSE: playBtn.setVisibility(View.INVISIBLE); stateView.setText("下载暂停"); break; case VideoTaskState.SUCCESS: playBtn.setVisibility(View.VISIBLE); stateView.setText("下载完成, 总大小=" + item.getDownloadSizeString()); break; case VideoTaskState.ERROR: playBtn.setVisibility(View.INVISIBLE); stateView.setText("下载错误"); break; default: playBtn.setVisibility(View.INVISIBLE); stateView.setText("未下载"); break; } } private void setDownloadInfoText(TextView infoView, VideoTaskItem item) { switch (item.getTaskState()) { case VideoTaskState.DOWNLOADING: infoView.setText("进度:" + item.getPercentString() + ", 速度:" + item.getSpeedString() +", 已下载:" + item.getDownloadSizeString()); break; case VideoTaskState.SUCCESS: infoView.setText("进度:" + item.getPercentString()); break; case VideoTaskState.PAUSE: infoView.setText("进度:" + item.getPercentString()); break; default: break; } } public void notifyChanged(VideoTaskItem[] items, VideoTaskItem item) { for (int index = 0; index < getCount(); index++) { if (getItem(index).equals(item)) { items[index] = item; notifyDataSetChanged(); } } } } ================================================ FILE: app/src/main/res/drawable/border.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_download_feature.xml ================================================