├── app
├── .gitignore
├── libs
│ └── nanohttpd-2.3.1.jar
├── src
│ ├── main
│ │ ├── ic_launcher-playstore.png
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── absinthe
│ │ │ │ └── kage
│ │ │ │ ├── connect
│ │ │ │ ├── tcp
│ │ │ │ │ ├── Response.kt
│ │ │ │ │ ├── Packet.kt
│ │ │ │ │ ├── IPacketReader.kt
│ │ │ │ │ ├── IPacketWriter.kt
│ │ │ │ │ ├── PacketWriter.kt
│ │ │ │ │ └── Request.kt
│ │ │ │ ├── IProxy.kt
│ │ │ │ ├── protocol
│ │ │ │ │ ├── Config.kt
│ │ │ │ │ ├── IProtocolHandler.kt
│ │ │ │ │ ├── IpMessageProtocol.kt
│ │ │ │ │ └── IpMessageConst.kt
│ │ │ │ ├── Const.kt
│ │ │ │ └── proxy
│ │ │ │ │ ├── BaseProxy.kt
│ │ │ │ │ └── RemoteControlProxy.kt
│ │ │ │ ├── viewholder
│ │ │ │ ├── model
│ │ │ │ │ ├── CastItem.kt
│ │ │ │ │ ├── ConnectItem.kt
│ │ │ │ │ ├── ServiceRunningItem.kt
│ │ │ │ │ └── DeviceItem.kt
│ │ │ │ ├── SpacesItemDecoration.kt
│ │ │ │ ├── WrappedLinearLayoutManager.kt
│ │ │ │ ├── delegate
│ │ │ │ │ ├── DeviceItemViewBinder.kt
│ │ │ │ │ ├── CastItemViewBinder.kt
│ │ │ │ │ ├── ConnectItemViewBinder.kt
│ │ │ │ │ └── DeviceInfoItemViewBinder.kt
│ │ │ │ └── HolderConstant.kt
│ │ │ │ ├── device
│ │ │ │ ├── heartbeat
│ │ │ │ │ ├── ErrorResponse.kt
│ │ │ │ │ ├── CancelBeatResponse.kt
│ │ │ │ │ └── HeartbeatRequest.kt
│ │ │ │ ├── model
│ │ │ │ │ ├── VideoInfo.kt
│ │ │ │ │ ├── AudioInfo.kt
│ │ │ │ │ ├── DeviceConfig.kt
│ │ │ │ │ └── DeviceInfo.kt
│ │ │ │ ├── Command.kt
│ │ │ │ ├── CommandBuilder.kt
│ │ │ │ ├── KageObservable.kt
│ │ │ │ ├── IDeviceObserver.kt
│ │ │ │ ├── DeviceObserverImpl.kt
│ │ │ │ └── cmd
│ │ │ │ │ ├── SetPlayIndexCommand.kt
│ │ │ │ │ ├── SetPlayStateCommand.kt
│ │ │ │ │ ├── SetPlayStatusCommand.kt
│ │ │ │ │ ├── PlayNextCommand.kt
│ │ │ │ │ ├── PlayPreviousCommand.kt
│ │ │ │ │ ├── InquiryAudioModeCommand.kt
│ │ │ │ │ ├── SetAudioModeCommand.kt
│ │ │ │ │ ├── SetPlayingPositionCommand.kt
│ │ │ │ │ ├── InquiryDeviceInfoCommand.kt
│ │ │ │ │ ├── StopCommand.kt
│ │ │ │ │ ├── ResumePlayCommand.kt
│ │ │ │ │ ├── InquiryDurationCommand.kt
│ │ │ │ │ ├── InquiryPlayingPositionCommand.kt
│ │ │ │ │ ├── SetDurationCommand.kt
│ │ │ │ │ ├── InquiryPlayStateCommand.kt
│ │ │ │ │ ├── InquiryPlayerStatusCommand.kt
│ │ │ │ │ ├── PromptPhoneConnectedCommand.kt
│ │ │ │ │ ├── HeartbeatCommand.kt
│ │ │ │ │ ├── MediaPausePlayingCommand.kt
│ │ │ │ │ ├── SeekToCommand.kt
│ │ │ │ │ ├── ImageInfoCommand.kt
│ │ │ │ │ ├── RemoteControlKeyCommand.kt
│ │ │ │ │ └── DeviceRotationCommand.kt
│ │ │ │ ├── manager
│ │ │ │ └── GlobalManager.kt
│ │ │ │ ├── utils
│ │ │ │ ├── timber
│ │ │ │ │ ├── ThreadAwareDebugTree.kt
│ │ │ │ │ └── ReleaseTree.kt
│ │ │ │ ├── StorageUtils.kt
│ │ │ │ ├── ToastUtil.kt
│ │ │ │ ├── NotificationUtils.kt
│ │ │ │ └── AnimationUtil.kt
│ │ │ │ ├── view
│ │ │ │ ├── AlwaysMarqueeTextView.kt
│ │ │ │ └── CategoryCardView.kt
│ │ │ │ ├── media
│ │ │ │ ├── Playback.kt
│ │ │ │ └── audio
│ │ │ │ │ ├── LocalMusic.kt
│ │ │ │ │ └── MusicHelper.kt
│ │ │ │ ├── viewmodel
│ │ │ │ └── MusicViewModel.kt
│ │ │ │ ├── KageApplication.kt
│ │ │ │ └── BaseActivity.kt
│ │ └── res
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── pic_image.webp
│ │ │ ├── pic_music.webp
│ │ │ ├── pic_video.webp
│ │ │ ├── ic_launcher.png
│ │ │ ├── pic_rabbit.webp
│ │ │ ├── ic_launcher_round.png
│ │ │ └── pic_album_placeholder.webp
│ │ │ ├── drawable-hdpi
│ │ │ └── ic_stat_logo.png
│ │ │ ├── drawable-mdpi
│ │ │ └── ic_stat_logo.png
│ │ │ ├── drawable-xhdpi
│ │ │ └── ic_stat_logo.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ ├── pic_splash.png
│ │ │ └── ic_stat_logo.png
│ │ │ ├── drawable-xxhdpi
│ │ │ └── ic_stat_logo.png
│ │ │ ├── values
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── attrs.xml
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── styles.xml
│ │ │ └── strings.xml
│ │ │ ├── anim
│ │ │ ├── anim_fade_in.xml
│ │ │ └── anim_fade_out.xml
│ │ │ ├── drawable
│ │ │ ├── bg_circle_button.xml
│ │ │ ├── selector_connect.xml
│ │ │ ├── bg_video_bottom_bar_mask.xml
│ │ │ ├── bg_card_mask.xml
│ │ │ ├── ic_pause.xml
│ │ │ ├── ic_skip_next.xml
│ │ │ ├── ic_skip_previous.xml
│ │ │ ├── ic_play_arrow.xml
│ │ │ ├── ic_arrow_back.xml
│ │ │ ├── ic_ring.xml
│ │ │ ├── ic_info.xml
│ │ │ ├── ic_done.xml
│ │ │ ├── ic_no.xml
│ │ │ ├── splash_drawable.xml
│ │ │ ├── ic_album.xml
│ │ │ ├── ic_delivery.xml
│ │ │ ├── ic_main_connect.xml
│ │ │ ├── ic_logo.xml
│ │ │ ├── ic_launcher_foreground.xml
│ │ │ ├── ic_connected.xml
│ │ │ └── ic_android_outline.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ │ ├── menu
│ │ │ └── main_menu.xml
│ │ │ ├── values-night
│ │ │ ├── colors.xml
│ │ │ └── styles.xml
│ │ │ ├── values-night-v26
│ │ │ └── styles.xml
│ │ │ ├── values-v26
│ │ │ └── styles.xml
│ │ │ ├── xml
│ │ │ └── backup_rules.xml
│ │ │ ├── layout
│ │ │ ├── activity_receiver.xml
│ │ │ ├── activity_video.xml
│ │ │ ├── layout_category_card_view.xml
│ │ │ ├── layout_receiver_loading.xml
│ │ │ ├── view_video_player.xml
│ │ │ ├── layout_music_seek_bar.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── layout_empty_list_tip.xml
│ │ │ ├── item_device.xml
│ │ │ ├── item_cast_card.xml
│ │ │ ├── item_connect_card.xml
│ │ │ ├── activity_connect.xml
│ │ │ ├── activity_music_list.xml
│ │ │ ├── layout_video_seek_bar.xml
│ │ │ ├── item_music.xml
│ │ │ └── item_service_running_card.xml
│ │ │ ├── layout-land
│ │ │ └── layout_empty_list_tip.xml
│ │ │ └── values-zh
│ │ │ └── strings.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── absinthe
│ │ │ └── kage
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── absinthe
│ │ └── kage
│ │ └── ExampleInstrumentedTest.java
└── release
│ ├── output.json
│ └── output-metadata.json
├── matisse
├── gradle.properties
├── src
│ └── main
│ │ ├── res
│ │ ├── drawable-hdpi
│ │ │ ├── ic_gif.png
│ │ │ ├── ic_empty_zhihu.png
│ │ │ ├── ic_empty_dracula.png
│ │ │ ├── ic_check_white_18dp.png
│ │ │ ├── ic_preview_radio_off.webp
│ │ │ ├── ic_preview_radio_on.webp
│ │ │ ├── ic_arrow_drop_down_white_24dp.png
│ │ │ └── ic_play_circle_outline_white_48dp.png
│ │ ├── drawable-mdpi
│ │ │ ├── ic_gif.png
│ │ │ ├── ic_empty_zhihu.png
│ │ │ ├── ic_empty_dracula.png
│ │ │ ├── ic_check_white_18dp.png
│ │ │ ├── ic_preview_radio_off.webp
│ │ │ ├── ic_preview_radio_on.webp
│ │ │ ├── ic_arrow_drop_down_white_24dp.png
│ │ │ └── ic_play_circle_outline_white_48dp.png
│ │ ├── drawable-xhdpi
│ │ │ ├── ic_gif.png
│ │ │ ├── ic_empty_dracula.png
│ │ │ ├── ic_empty_zhihu.png
│ │ │ ├── ic_check_white_18dp.png
│ │ │ ├── ic_preview_radio_off.webp
│ │ │ ├── ic_preview_radio_on.webp
│ │ │ ├── ic_arrow_drop_down_white_24dp.png
│ │ │ └── ic_play_circle_outline_white_48dp.png
│ │ ├── drawable-xxhdpi
│ │ │ ├── ic_gif.png
│ │ │ ├── ic_empty_zhihu.png
│ │ │ ├── ic_empty_dracula.png
│ │ │ ├── ic_check_white_18dp.png
│ │ │ ├── ic_arrow_drop_down_white_24dp.png
│ │ │ └── ic_play_circle_outline_white_48dp.png
│ │ ├── drawable-xxxhdpi
│ │ │ ├── ic_gif.png
│ │ │ ├── ic_empty_zhihu.png
│ │ │ ├── ic_empty_dracula.png
│ │ │ ├── ic_check_white_18dp.png
│ │ │ ├── ic_photo_camera_white_24dp.png
│ │ │ ├── ic_arrow_drop_down_white_24dp.png
│ │ │ └── ic_play_circle_outline_white_48dp.png
│ │ ├── values
│ │ │ ├── dimens.xml
│ │ │ ├── colors.xml
│ │ │ ├── attrs.xml
│ │ │ ├── colors_dracula.xml
│ │ │ ├── colors_zhihu.xml
│ │ │ └── strings.xml
│ │ ├── layout
│ │ │ ├── media_grid_item.xml
│ │ │ ├── fragment_media_selection.xml
│ │ │ ├── fragment_preview_item.xml
│ │ │ ├── photo_capture_item.xml
│ │ │ └── media_grid_content.xml
│ │ ├── color
│ │ │ ├── zhihu_bottom_toolbar_apply.xml
│ │ │ ├── dracula_bottom_toolbar_apply.xml
│ │ │ ├── zhihu_bottom_toolbar_preview.xml
│ │ │ ├── dracula_bottom_toolbar_preview.xml
│ │ │ ├── dracula_preview_bottom_toolbar_apply.xml
│ │ │ └── zhihu_preview_bottom_toolbar_apply.xml
│ │ └── values-zh
│ │ │ └── strings.xml
│ │ ├── java
│ │ └── com
│ │ │ └── zhihu
│ │ │ └── matisse
│ │ │ ├── listener
│ │ │ ├── OnChooseItemListener.java
│ │ │ ├── OnCheckedListener.java
│ │ │ ├── OnFragmentInteractionListener.java
│ │ │ └── OnSelectedListener.java
│ │ │ ├── internal
│ │ │ ├── utils
│ │ │ │ ├── Platform.java
│ │ │ │ └── SingleMediaScanner.java
│ │ │ ├── entity
│ │ │ │ └── CaptureStrategy.java
│ │ │ └── ui
│ │ │ │ ├── widget
│ │ │ │ ├── SquareFrameLayout.java
│ │ │ │ ├── PreviewViewPager.java
│ │ │ │ └── CheckRadioView.java
│ │ │ │ └── SelectedPreviewActivity.java
│ │ │ ├── engine
│ │ │ └── impl
│ │ │ │ └── PicassoEngine.java
│ │ │ └── filter
│ │ │ └── Filter.java
│ │ └── AndroidManifest.xml
└── proguard-rules.pro
├── settings.gradle
├── icons
├── playstore
│ └── icon.png
├── play_store_banner.png
├── mipmap-hdpi
│ └── ic_launcher.png
├── mipmap-mdpi
│ └── ic_launcher.png
├── mipmap-xhdpi
│ └── ic_launcher.png
├── mipmap-xxhdpi
│ └── ic_launcher.png
├── mipmap-xxxhdpi
│ └── ic_launcher.png
└── LICENSE
│ └── LICENSE.txt
├── source
└── coolapk-badge.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── vcs.xml
├── smartfox_info.xml
├── encodings.xml
├── modules.xml
├── runConfigurations.xml
├── jarRepositories.xml
└── navEditor.xml
├── config.gradle
├── README.md
├── gradle.properties
└── .gitignore
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/matisse/gradle.properties:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':matisse'
2 | rootProject.name='Kage'
--------------------------------------------------------------------------------
/icons/playstore/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/icons/playstore/icon.png
--------------------------------------------------------------------------------
/source/coolapk-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/source/coolapk-badge.png
--------------------------------------------------------------------------------
/app/libs/nanohttpd-2.3.1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/libs/nanohttpd-2.3.1.jar
--------------------------------------------------------------------------------
/icons/play_store_banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/icons/play_store_banner.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/icons/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/icons/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/icons/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/icons/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/icons/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/icons/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/icons/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/icons/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/icons/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/icons/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/connect/tcp/Response.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.connect.tcp
2 |
3 | open class Response
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/viewholder/model/CastItem.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.viewholder.model
2 |
3 | class CastItem
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/viewholder/model/ConnectItem.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.viewholder.model
2 |
3 | class ConnectItem
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/pic_image.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-xxxhdpi/pic_image.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/pic_music.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-xxxhdpi/pic_music.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/pic_video.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-xxxhdpi/pic_video.webp
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-hdpi/ic_gif.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-hdpi/ic_gif.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-mdpi/ic_gif.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-mdpi/ic_gif.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xhdpi/ic_gif.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xhdpi/ic_gif.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_stat_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/drawable-hdpi/ic_stat_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_stat_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/drawable-mdpi/ic_stat_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_stat_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/drawable-xhdpi/ic_stat_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/pic_splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/drawable-xxxhdpi/pic_splash.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/pic_rabbit.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-xxxhdpi/pic_rabbit.webp
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xxhdpi/ic_gif.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xxhdpi/ic_gif.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xxxhdpi/ic_gif.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xxxhdpi/ic_gif.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_stat_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/drawable-xxhdpi/ic_stat_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_stat_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/drawable-xxxhdpi/ic_stat_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-hdpi/ic_empty_zhihu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-hdpi/ic_empty_zhihu.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-mdpi/ic_empty_zhihu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-mdpi/ic_empty_zhihu.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-hdpi/ic_empty_dracula.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-hdpi/ic_empty_dracula.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-mdpi/ic_empty_dracula.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-mdpi/ic_empty_dracula.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xhdpi/ic_empty_dracula.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xhdpi/ic_empty_dracula.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xhdpi/ic_empty_zhihu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xhdpi/ic_empty_zhihu.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xxhdpi/ic_empty_zhihu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xxhdpi/ic_empty_zhihu.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xxxhdpi/ic_empty_zhihu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xxxhdpi/ic_empty_zhihu.png
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/connect/tcp/Packet.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.connect.tcp
2 |
3 | open class Packet {
4 | var data: String? = null
5 | }
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/pic_album_placeholder.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/app/src/main/res/mipmap-xxxhdpi/pic_album_placeholder.webp
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-hdpi/ic_check_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-hdpi/ic_check_white_18dp.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-mdpi/ic_check_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-mdpi/ic_check_white_18dp.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xxhdpi/ic_empty_dracula.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xxhdpi/ic_empty_dracula.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xxxhdpi/ic_empty_dracula.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xxxhdpi/ic_empty_dracula.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-hdpi/ic_preview_radio_off.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-hdpi/ic_preview_radio_off.webp
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-hdpi/ic_preview_radio_on.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-hdpi/ic_preview_radio_on.webp
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-mdpi/ic_preview_radio_off.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-mdpi/ic_preview_radio_off.webp
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-mdpi/ic_preview_radio_on.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-mdpi/ic_preview_radio_on.webp
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xhdpi/ic_check_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xhdpi/ic_check_white_18dp.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xhdpi/ic_preview_radio_off.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xhdpi/ic_preview_radio_off.webp
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xhdpi/ic_preview_radio_on.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xhdpi/ic_preview_radio_on.webp
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xxhdpi/ic_check_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xxhdpi/ic_check_white_18dp.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xxxhdpi/ic_check_white_18dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xxxhdpi/ic_check_white_18dp.png
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFC107
4 |
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-hdpi/ic_arrow_drop_down_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-hdpi/ic_arrow_drop_down_white_24dp.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-mdpi/ic_arrow_drop_down_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-mdpi/ic_arrow_drop_down_white_24dp.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xxxhdpi/ic_photo_camera_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xxxhdpi/ic_photo_camera_white_24dp.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xhdpi/ic_arrow_drop_down_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xhdpi/ic_arrow_drop_down_white_24dp.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xxhdpi/ic_arrow_drop_down_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xxhdpi/ic_arrow_drop_down_white_24dp.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xxxhdpi/ic_arrow_drop_down_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xxxhdpi/ic_arrow_drop_down_white_24dp.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-hdpi/ic_play_circle_outline_white_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-hdpi/ic_play_circle_outline_white_48dp.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-mdpi/ic_play_circle_outline_white_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-mdpi/ic_play_circle_outline_white_48dp.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xhdpi/ic_play_circle_outline_white_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xhdpi/ic_play_circle_outline_white_48dp.png
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xxhdpi/ic_play_circle_outline_white_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xxhdpi/ic_play_circle_outline_white_48dp.png
--------------------------------------------------------------------------------
/matisse/src/main/res/drawable-xxxhdpi/ic_play_circle_outline_white_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhaobozhen/Kage/HEAD/matisse/src/main/res/drawable-xxxhdpi/ic_play_circle_outline_white_48dp.png
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/connect/tcp/IPacketReader.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.connect.tcp
2 |
3 | interface IPacketReader {
4 | fun addRequest(request: Request)
5 | fun shutdown()
6 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/connect/tcp/IPacketWriter.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.connect.tcp
2 |
3 | interface IPacketWriter {
4 | fun writePacket(packet: Packet)
5 | fun shutdown()
6 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/heartbeat/ErrorResponse.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.heartbeat
2 |
3 | import com.absinthe.kage.connect.tcp.Response
4 |
5 | class ErrorResponse : Response()
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/heartbeat/CancelBeatResponse.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.heartbeat
2 |
3 | import com.absinthe.kage.connect.tcp.Response
4 |
5 | class CancelBeatResponse : Response()
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/viewholder/model/ServiceRunningItem.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.viewholder.model
2 |
3 | class ServiceRunningItem(running: Boolean) {
4 | var isServiceRunning: Boolean = running
5 | }
--------------------------------------------------------------------------------
/.idea/smartfox_info.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/viewholder/model/DeviceItem.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.viewholder.model
2 |
3 | class DeviceItem(name: String = "", ip: String = "") {
4 | val deviceName = name
5 | val deviceIp = ip
6 | }
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/anim_fade_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/anim_fade_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_circle_button.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/connect/IProxy.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.connect
2 |
3 | import com.absinthe.kage.device.Device
4 |
5 | interface IProxy {
6 | fun onDeviceConnected(device: Device)
7 | fun onDeviceDisconnected(device: Device)
8 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Jun 07 13:52:11 CST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-all.zip
7 |
--------------------------------------------------------------------------------
/matisse/src/main/java/com/zhihu/matisse/listener/OnChooseItemListener.java:
--------------------------------------------------------------------------------
1 | package com.zhihu.matisse.listener;
2 |
3 | public interface OnChooseItemListener {
4 | void onChoose(String itemUri);
5 | void onStop();
6 | void onPreview();
7 | void onNext();
8 | void onNotConnect();
9 | }
10 |
--------------------------------------------------------------------------------
/matisse/src/main/java/com/zhihu/matisse/listener/OnCheckedListener.java:
--------------------------------------------------------------------------------
1 | package com.zhihu.matisse.listener;
2 |
3 |
4 | /**
5 | * when original is enabled , callback immediately when user check or uncheck original.
6 | */
7 | public interface OnCheckedListener {
8 | void onCheck(boolean isChecked);
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/connect/protocol/Config.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.connect.protocol
2 |
3 | object Config {
4 | const val VERSION = 1 //Protocol version
5 | const val ADDRESS = "127.0.0.1" //Server address
6 | const val PORT = 2025 //Server port
7 | const val HTTP_SERVER_PORT = 2026 //HTTP Server port
8 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/selector_connect.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/connect/tcp/PacketWriter.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.connect.tcp
2 |
3 | import com.absinthe.kage.connect.tcp.KageSocket.ISocketCallback
4 | import java.io.DataOutputStream
5 |
6 | class PacketWriter(out: DataOutputStream, socketCallback: ISocketCallback?)
7 | : AbstractPacketWriter(out, socketCallback)
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_video_bottom_bar_mask.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/heartbeat/HeartbeatRequest.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.heartbeat
2 |
3 | import com.absinthe.kage.connect.tcp.Request
4 | import com.absinthe.kage.device.cmd.HeartbeatCommand
5 |
6 | class HeartbeatRequest : Request() {
7 |
8 | init {
9 | data = HeartbeatCommand().pack()
10 | }
11 |
12 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_card_mask.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pause.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_skip_next.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_skip_previous.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/model/VideoInfo.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.model
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageProtocol
4 |
5 | class VideoInfo {
6 |
7 | var url: String? = null
8 | var title: String? = null
9 |
10 | val info: String
11 | get() = url + IpMessageProtocol.DELIMITER + title
12 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_play_arrow.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/config.gradle:
--------------------------------------------------------------------------------
1 | ext {
2 | android = [
3 | compileSdkVersion : 29,
4 | buildToolsVersion : '29.0.3',
5 | minSdkVersion : 21,
6 | targetSdkVersion : 29
7 | ]
8 |
9 | app_compat_version = '1.2.0'
10 | recyclerview_version = '1.1.0'
11 | glide_version = '4.11.0'
12 | lifecycle_version = '2.2.0'
13 | }
--------------------------------------------------------------------------------
/matisse/src/main/java/com/zhihu/matisse/listener/OnFragmentInteractionListener.java:
--------------------------------------------------------------------------------
1 | package com.zhihu.matisse.listener;
2 |
3 | /**
4 | * PreViewItemFragment 和 BasePreViewActivity 通信的接口 ,为了方便拿到 ImageViewTouch 的点击事件
5 | */
6 | public interface OnFragmentInteractionListener {
7 | /**
8 | * ImageViewTouch 被点击了
9 | */
10 | void onClick();
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/main_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/manager/GlobalManager.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.manager
2 |
3 | import androidx.lifecycle.MutableLiveData
4 | import com.absinthe.kage.media.audio.LocalMusic
5 | import java.util.ArrayList
6 |
7 | object GlobalManager {
8 |
9 | var isServiceRunning: MutableLiveData = MutableLiveData()
10 | var musicList: MutableList = ArrayList()
11 |
12 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arrow_back.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/utils/timber/ThreadAwareDebugTree.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.utils.timber
2 |
3 | import timber.log.Timber.DebugTree
4 |
5 | open class ThreadAwareDebugTree : DebugTree() {
6 |
7 | override fun createStackElementTag(element: StackTraceElement): String? {
8 | //日志显示行号
9 | return super.createStackElementTag(element) + " (Line ${element.lineNumber})"
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFFFF
4 | #FF121212
5 | #FF121212
6 | #FF121212
7 |
8 | #FF888888
9 | #FFEEEEEE
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_ring.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/release/output.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "artifactType": {
4 | "type": "APK",
5 | "kind": "Directory"
6 | },
7 | "applicationId": "com.absinthe.kage",
8 | "variantName": "release",
9 | "elements": [
10 | {
11 | "type": "SINGLE",
12 | "filters": [],
13 | "properties": [],
14 | "versionCode": 501,
15 | "versionName": "501",
16 | "enabled": true,
17 | "outputFile": "app-release.apk"
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/app/release/output-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "artifactType": {
4 | "type": "APK",
5 | "kind": "Directory"
6 | },
7 | "applicationId": "com.absinthe.kage",
8 | "variantName": "release",
9 | "elements": [
10 | {
11 | "type": "SINGLE",
12 | "filters": [],
13 | "properties": [],
14 | "versionCode": 501,
15 | "versionName": "0.5.0",
16 | "enabled": true,
17 | "outputFile": "app-release.apk"
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/connect/Const.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.connect
2 |
3 | import com.absinthe.kage.connect.protocol.Config
4 |
5 | object Const {
6 | const val LOCAL_IP_IN_AP = "192.168.43.1"
7 | const val BROADCAST_IP_IN_AP = "192.168.43.255"
8 | const val BROADCAST_IP_IN_WIFI = "255.255.255.255"
9 | const val HTTP_SERVER_FORMAT = "http://%s:${Config.HTTP_SERVER_PORT}"
10 |
11 | const val APP_CENTER_SECRET = "4b4faea6-9eed-4c30-a734-3fb9330da2cc"
12 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/absinthe/kage/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/matisse/src/main/java/com/zhihu/matisse/internal/utils/Platform.java:
--------------------------------------------------------------------------------
1 | package com.zhihu.matisse.internal.utils;
2 |
3 | import android.os.Build;
4 |
5 | /**
6 | * @author JoongWon Baik
7 | */
8 | public class Platform {
9 | public static boolean hasICS() {
10 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
11 | }
12 |
13 | public static boolean hasKitKat() {
14 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_info.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_done.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_no.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/splash_drawable.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
8 |
9 |
10 |
11 | -
12 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/Command.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageProtocol
4 | import com.absinthe.kage.device.client.Client
5 |
6 | abstract class Command protected constructor() {
7 |
8 | var cmd = 0
9 |
10 | abstract fun pack(): String
11 | abstract fun doWork(client: Client, received: String)
12 | abstract fun parseReceived(received: String): Boolean
13 |
14 | companion object {
15 | const val DELIMITER = IpMessageProtocol.DELIMITER
16 | }
17 | }
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/model/AudioInfo.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | class AudioInfo {
6 |
7 | @SerializedName("url")
8 | var url: String? = null //播放地址
9 |
10 | @SerializedName("name")
11 | var name: String? = null //歌曲名
12 |
13 | @SerializedName("artist")
14 | var artist: String? = null //演唱者
15 |
16 | @SerializedName("album")
17 | var album: String? = null //专辑
18 |
19 | @SerializedName("coverPath")
20 | var coverPath: String? = null //封面地址
21 |
22 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/CommandBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device
2 |
3 | class CommandBuilder {
4 |
5 | private val mStringBuilder: StringBuilder = StringBuilder()
6 |
7 | fun with(command: Command): CommandBuilder {
8 | mStringBuilder.append(command.cmd)
9 | return this
10 | }
11 |
12 | fun append(param: String?): CommandBuilder {
13 | mStringBuilder.append(Command.DELIMITER).append(param)
14 | return this
15 | }
16 |
17 | fun build(): String {
18 | return mStringBuilder.toString()
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/utils/timber/ReleaseTree.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.utils.timber
2 |
3 | import android.util.Log
4 | import timber.log.Timber
5 |
6 | class ReleaseTree : Timber.Tree() {
7 |
8 | override fun isLoggable(tag: String?, priority: Int): Boolean {
9 | return !(priority == Log.VERBOSE || priority == Log.DEBUG)
10 | }
11 |
12 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
13 | if (!isLoggable(tag, priority)) {
14 | return
15 | }
16 | super.log(priority, tag, message, t)
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/utils/StorageUtils.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.utils
2 |
3 | import android.graphics.Bitmap
4 | import java.io.File
5 | import java.io.FileOutputStream
6 | import java.io.IOException
7 |
8 | object StorageUtils {
9 |
10 | @JvmStatic
11 | fun saveBitmap(bmp: Bitmap, file: File) {
12 | try {
13 | val out = FileOutputStream(file)
14 | bmp.compress(Bitmap.CompressFormat.PNG, 100, out)
15 | out.flush()
16 | out.close()
17 | } catch (e: IOException) {
18 | e.printStackTrace()
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/view/AlwaysMarqueeTextView.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.view
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import androidx.appcompat.widget.AppCompatTextView
6 |
7 | class AlwaysMarqueeTextView : AppCompatTextView {
8 |
9 | constructor(context: Context) : super(context)
10 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
11 | constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle)
12 |
13 | override fun isFocused(): Boolean {
14 | return true
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/KageObservable.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device
2 |
3 | abstract class KageObservable {
4 | abstract fun register(observer: IDeviceObserver?)
5 | abstract fun unregister(observer: IDeviceObserver?)
6 | protected abstract fun notifyFindDevice(device: Device)
7 | protected abstract fun notifyLostDevice(device: Device)
8 | protected abstract fun notifyDeviceConnected(device: Device)
9 | protected abstract fun notifyDeviceDisconnect(device: Device)
10 | protected abstract fun notifyDeviceConnectFailed(device: Device, errorCode: Int, errorMessage: String?)
11 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/connect/protocol/IProtocolHandler.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.connect.protocol
2 |
3 | interface IProtocolHandler {
4 |
5 | fun handleSocketConnectedEvent()
6 | fun handleSocketMassage(msg: String)
7 | fun handleSocketDisConnectEvent()
8 | fun handleSocketConnectFail(errorCode: Int, e: Exception)
9 | fun handleSocketSendOrReceiveError()
10 |
11 | interface IProtocolHandleCallback {
12 | fun onProtocolConnected()
13 | fun onProtocolDisConnect()
14 | fun onProtocolConnectedFailed(errorCode: Int, e: Exception?)
15 | fun onProtocolSendOrReceiveError()
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/IDeviceObserver.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device
2 |
3 | import com.absinthe.kage.device.model.DeviceInfo
4 |
5 | interface IDeviceObserver {
6 | fun onFindDevice(deviceInfo: DeviceInfo)
7 | fun onLostDevice(deviceInfo: DeviceInfo)
8 | fun onDeviceConnected(deviceInfo: DeviceInfo)
9 | fun onDeviceDisConnect(deviceInfo: DeviceInfo)
10 | fun onDeviceConnectFailed(deviceInfo: DeviceInfo, errorCode: Int, errorMessage: String?)
11 | fun onDeviceInfoChanged(deviceInfo: DeviceInfo)
12 | fun onDeviceNotice(deviceInfo: DeviceInfo)
13 | fun onDeviceConnecting(deviceInfo: DeviceInfo)
14 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_album.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/connect/proxy/BaseProxy.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.connect.proxy
2 |
3 | import com.absinthe.kage.connect.IProxy
4 | import com.absinthe.kage.device.Device
5 |
6 | const val MODE_IMAGE = 0
7 | const val MODE_AUDIO = 1
8 | const val MODE_VIDEO = 2
9 |
10 | open class BaseProxy : IProxy {
11 | var mDevice: Device? = null
12 |
13 | override fun onDeviceConnected(device: Device) {
14 | mDevice = device
15 | }
16 |
17 | override fun onDeviceDisconnected(device: Device) {
18 | mDevice = null
19 | }
20 |
21 | companion object {
22 | var CURRENT_MODE = MODE_IMAGE
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/model/DeviceConfig.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.model
2 |
3 | import com.absinthe.kage.connect.Const
4 | import com.absinthe.kage.connect.protocol.Config
5 | import com.absinthe.kage.utils.NetUtils
6 | import java.util.*
7 |
8 | class DeviceConfig {
9 | var name: String = "Unknown"
10 | var uuid: String = UUID.randomUUID().toString()
11 | var localHost: String = NetUtils.localAddress
12 | var broadcastHostInWifi: String = Const.BROADCAST_IP_IN_WIFI
13 | var broadcastHostInAp: String = Const.BROADCAST_IP_IN_AP
14 | var broadcastMonitorPort = Config.PORT
15 | var broadcastPort = Config.PORT
16 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-night-v26/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/connect/proxy/RemoteControlProxy.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.connect.proxy
2 |
3 | import android.view.KeyEvent
4 | import com.absinthe.kage.device.cmd.RemoteControlKeyCommand
5 |
6 | object RemoteControlProxy : BaseProxy() {
7 |
8 | fun sendVolumeUpKeyAction() {
9 | sendKeyAction(KeyEvent.KEYCODE_VOLUME_UP)
10 | }
11 |
12 | fun sendVolumeDownKeyAction() {
13 | sendKeyAction(KeyEvent.KEYCODE_VOLUME_DOWN)
14 | }
15 |
16 | private fun sendKeyAction(key: Int) {
17 | mDevice?.let {
18 | it.sendCommand(RemoteControlKeyCommand().apply {
19 | keyCode = key
20 | })
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/media/Playback.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.media
2 |
3 | const val TYPE_LOCAL = 1
4 | const val TYPE_REMOTE = 2
5 |
6 | interface Playback {
7 | interface Callback {
8 | fun onCompletion()
9 | fun onError(error: String)
10 | fun onMediaMetadataChanged(localMedia: LocalMedia)
11 | fun onPlaybackStateChanged(state: Int)
12 | }
13 |
14 | val bufferPosition: Int
15 | val currentPosition: Int
16 | val duration: Int
17 | val state: Int
18 |
19 | fun pause()
20 | fun play()
21 | fun playMedia(localMedia: LocalMedia)
22 | fun seekTo(position: Int)
23 | fun setCallback(callback: Callback)
24 | fun stop(isStop: Boolean)
25 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-v26/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_delivery.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/DeviceObserverImpl.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device
2 |
3 | import com.absinthe.kage.device.model.DeviceInfo
4 |
5 | open class DeviceObserverImpl : IDeviceObserver {
6 | override fun onFindDevice(deviceInfo: DeviceInfo) {}
7 | override fun onLostDevice(deviceInfo: DeviceInfo) {}
8 | override fun onDeviceConnected(deviceInfo: DeviceInfo) {}
9 | override fun onDeviceDisConnect(deviceInfo: DeviceInfo) {}
10 | override fun onDeviceConnectFailed(deviceInfo: DeviceInfo, errorCode: Int, errorMessage: String?) {}
11 | override fun onDeviceInfoChanged(deviceInfo: DeviceInfo) {}
12 | override fun onDeviceNotice(deviceInfo: DeviceInfo) {}
13 | override fun onDeviceConnecting(deviceInfo: DeviceInfo) {}
14 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_receiver.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/matisse/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Library/android-sdk-macosx/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the ProGuard
5 | # include property in project.properties.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | -dontwarn com.squareup.okhttp.**
20 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/utils/ToastUtil.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.utils
2 |
3 | import android.widget.Toast
4 | import androidx.annotation.StringRes
5 | import com.blankj.utilcode.util.Utils
6 |
7 | object ToastUtil {
8 |
9 | /**
10 | * make a toast via a string
11 | *
12 | * @param text a string text
13 | */
14 | fun makeText(text: String) {
15 | Toast.makeText(Utils.getApp().applicationContext, text, Toast.LENGTH_SHORT).show()
16 | }
17 |
18 | /**
19 | * make a toast via a resource id
20 | *
21 | * @param resId a string resource id
22 | */
23 | @JvmStatic
24 | fun makeText(@StringRes resId: Int) {
25 | Toast.makeText(Utils.getApp().applicationContext, Utils.getApp().applicationContext.getText(resId), Toast.LENGTH_SHORT).show()
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/SetPlayIndexCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.device.Command
5 | import com.absinthe.kage.device.CommandBuilder
6 | import com.absinthe.kage.device.client.Client
7 |
8 | class SetPlayIndexCommand : Command() {
9 | var index = 0
10 |
11 | override fun pack(): String {
12 | return CommandBuilder()
13 | .with(this)
14 | .append(index.toString())
15 | .build()
16 | }
17 |
18 | override fun doWork(client: Client, received: String) {}
19 |
20 | override fun parseReceived(received: String): Boolean {
21 | return false
22 | }
23 |
24 | init {
25 | cmd = IpMessageConst.MEDIA_SET_PLAY_INDEX
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/SetPlayStateCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.device.Command
5 | import com.absinthe.kage.device.CommandBuilder
6 | import com.absinthe.kage.device.client.Client
7 |
8 | class SetPlayStateCommand : Command() {
9 | var stateCode = 0
10 |
11 | override fun pack(): String {
12 | return CommandBuilder()
13 | .with(this)
14 | .append(stateCode.toString())
15 | .build()
16 | }
17 |
18 | override fun doWork(client: Client, received: String) {}
19 |
20 | override fun parseReceived(received: String): Boolean {
21 | return false
22 | }
23 |
24 | init {
25 | cmd = IpMessageConst.MEDIA_SET_PLAYING_STATE
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/SetPlayStatusCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.device.Command
5 | import com.absinthe.kage.device.CommandBuilder
6 | import com.absinthe.kage.device.client.Client
7 |
8 | class SetPlayStatusCommand : Command() {
9 | var statusCode = 0
10 |
11 | override fun pack(): String {
12 | return CommandBuilder()
13 | .with(this)
14 | .append(statusCode.toString())
15 | .build()
16 | }
17 |
18 | override fun doWork(client: Client, received: String) {}
19 |
20 | override fun parseReceived(received: String): Boolean {
21 | return false
22 | }
23 |
24 | init {
25 | cmd = IpMessageConst.MEDIA_SET_PLAYER_STATUS
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_main_connect.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/viewholder/SpacesItemDecoration.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.viewholder
2 |
3 | import android.graphics.Rect
4 | import android.view.View
5 | import androidx.recyclerview.widget.RecyclerView
6 | import androidx.recyclerview.widget.RecyclerView.ItemDecoration
7 |
8 | class SpacesItemDecoration(private val space: Int) : ItemDecoration() {
9 |
10 | override fun getItemOffsets(outRect: Rect, view: View,
11 | parent: RecyclerView, state: RecyclerView.State) {
12 | outRect.apply {
13 | left = space
14 | right = space
15 | bottom = space
16 | }
17 |
18 | // Add top margin only for the first item to avoid double space between items
19 | if (parent.getChildLayoutPosition(view) == 0) {
20 | outRect.top = space
21 | }
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/PlayNextCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.device.Command
5 | import com.absinthe.kage.device.CommandBuilder
6 | import com.absinthe.kage.device.client.Client
7 |
8 | class PlayNextCommand : Command() {
9 |
10 | override fun pack(): String {
11 | return CommandBuilder()
12 | .with(this)
13 | .append(MESSAGE)
14 | .build()
15 | }
16 |
17 | override fun doWork(client: Client, received: String) {}
18 |
19 | override fun parseReceived(received: String): Boolean {
20 | return false
21 | }
22 |
23 | companion object {
24 | const val MESSAGE = "PLAY_NEXT"
25 | }
26 |
27 | init {
28 | cmd = IpMessageConst.MEDIA_PLAY_NEXT
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/model/DeviceInfo.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.model
2 |
3 | import com.absinthe.kage.connect.protocol.Config
4 | import com.absinthe.kage.connect.protocol.IpMessageConst
5 |
6 | class DeviceInfo {
7 |
8 | var name: String = "Unknown"
9 | var ip: String = Config.ADDRESS
10 | var protocolVersion: String = IpMessageConst.VERSION.toString()
11 | var functionCode: String = ""
12 | var isConnected = false
13 | var state = STATE_IDLE
14 |
15 | fun setStateConnecting() {
16 | state = STATE_CONNECTING
17 | }
18 |
19 | override fun toString(): String {
20 | return "DeviceInfo: Name = $name, IP = $ip"
21 | }
22 |
23 | companion object {
24 | const val STATE_IDLE = 0 //未连接
25 | const val STATE_CONNECTING = 1 //连接中
26 | const val STATE_CONNECTED = 2 //已连接
27 | }
28 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Kage
2 | A real-time displayed multimedia projection system based on Android.
3 |
4 | ## What's this?
5 | This application is my graduation project. First and foremost, I would like to express my sincere gratitude to my supervisor, Professor Xu, for his intellectual guidance, invaluable instructions and comments on my thesis. It is with his valuable assistance that I have finally accomplished this thesis.
6 |
7 | Kage(かげ), means "shadow", which means that the sending end and the receiving end are consistent like an object and its shadow. The innovations of this application are as follows:
8 |
9 | - A real-time displayed multimedia projection system
10 | - Adapt to different sizes and different models of devices
11 | - Integrate the sending end and receiving end in the same application
12 |
13 | ## Download
14 | [](https://www.coolapk.com/apk/com.absinthe.kage)
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/PlayPreviousCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.device.Command
5 | import com.absinthe.kage.device.CommandBuilder
6 | import com.absinthe.kage.device.client.Client
7 |
8 | class PlayPreviousCommand : Command() {
9 |
10 | override fun pack(): String {
11 | return CommandBuilder()
12 | .with(this)
13 | .append(MESSAGE)
14 | .build()
15 | }
16 |
17 | override fun doWork(client: Client, received: String) {}
18 |
19 | override fun parseReceived(received: String): Boolean {
20 | return false
21 | }
22 |
23 | companion object {
24 | const val MESSAGE = "PLAY_PRE"
25 | }
26 |
27 | init {
28 | cmd = IpMessageConst.MEDIA_PLAY_PREVIOUS
29 | }
30 | }
--------------------------------------------------------------------------------
/matisse/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 | 48dp
19 | 4dp
20 |
21 | 72dp
22 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/viewholder/WrappedLinearLayoutManager.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.viewholder
2 |
3 | import android.content.Context
4 | import androidx.recyclerview.widget.LinearLayoutManager
5 | import androidx.recyclerview.widget.RecyclerView
6 | import androidx.recyclerview.widget.RecyclerView.Recycler
7 | import timber.log.Timber
8 |
9 | class WrappedLinearLayoutManager : LinearLayoutManager {
10 |
11 | constructor(context: Context?) : super(context)
12 |
13 | constructor(context: Context?, @RecyclerView.Orientation orientation: Int) : super(context, orientation, false)
14 |
15 | override fun onLayoutChildren(recycler: Recycler, state: RecyclerView.State) {
16 | try {
17 | super.onLayoutChildren(recycler, state)
18 | } catch (e: IndexOutOfBoundsException) {
19 | Timber.e("encounter an IndexOutOfBoundsException")
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/absinthe/kage/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.test.platform.app.InstrumentationRegistry;
6 | import androidx.test.ext.junit.runners.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Instrumented test, which will execute on an Android device.
15 | *
16 | * @see Testing documentation
17 | */
18 | @RunWith(AndroidJUnit4.class)
19 | public class ExampleInstrumentedTest {
20 | @Test
21 | public void useAppContext() {
22 | // Context of the app under test.
23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
24 |
25 | assertEquals("com.absinthe.kage", appContext.getPackageName());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/InquiryAudioModeCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.device.Command
5 | import com.absinthe.kage.device.CommandBuilder
6 | import com.absinthe.kage.device.client.Client
7 |
8 | class InquiryAudioModeCommand : Command() {
9 |
10 | override fun pack(): String {
11 | return CommandBuilder()
12 | .with(this)
13 | .append(MESSAGE)
14 | .build()
15 | }
16 |
17 | override fun doWork(client: Client, received: String) {}
18 |
19 | override fun parseReceived(received: String): Boolean {
20 | return false
21 | }
22 |
23 | companion object {
24 | const val MESSAGE = "INQUIRY_AUDIO_MODE"
25 | }
26 |
27 | init {
28 | cmd = IpMessageConst.RESPONSE_SET_AUDIO_MODE
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/SetAudioModeCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.device.Command
5 | import com.absinthe.kage.device.CommandBuilder
6 | import com.absinthe.kage.device.client.Client
7 |
8 | class SetAudioModeCommand : Command() {
9 | var mode = MODE_
10 |
11 | override fun pack(): String {
12 | return CommandBuilder()
13 | .with(this)
14 | .append(mode.toString())
15 | .build()
16 | }
17 |
18 | override fun doWork(client: Client, received: String) {}
19 |
20 | override fun parseReceived(received: String): Boolean {
21 | return false
22 | }
23 |
24 | companion object {
25 | const val MODE_ = 0
26 | }
27 |
28 | init {
29 | cmd = IpMessageConst.MEDIA_SET_AUDIO_MODE
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/SetPlayingPositionCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.device.Command
5 | import com.absinthe.kage.device.CommandBuilder
6 | import com.absinthe.kage.device.client.Client
7 |
8 | class SetPlayingPositionCommand : Command() {
9 | var position = 0
10 |
11 | override fun pack(): String {
12 | return CommandBuilder()
13 | .with(this)
14 | .append(position.toString())
15 | .build()
16 | }
17 |
18 | override fun doWork(client: Client, received: String) {}
19 |
20 | override fun parseReceived(received: String): Boolean {
21 | return false
22 | }
23 |
24 | companion object {
25 | const val LENGTH = 2
26 | }
27 |
28 | init {
29 | cmd = IpMessageConst.RESPONSE_SET_PLAYBACK_PROGRESS
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/connect/tcp/Request.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.connect.tcp
2 |
3 | import com.absinthe.kage.device.heartbeat.ErrorResponse
4 | import timber.log.Timber
5 | import java.util.concurrent.ArrayBlockingQueue
6 | import java.util.concurrent.TimeUnit
7 |
8 | open class Request : Packet() {
9 |
10 | var id: String = System.currentTimeMillis().toString()
11 | private val responses = ArrayBlockingQueue(1)
12 |
13 | fun setResponse(response: Response) {
14 | try {
15 | responses.put(response)
16 | } catch (e: InterruptedException) {
17 | Timber.e(e)
18 | }
19 | }
20 |
21 | fun waitResponse(timeout: Int): Response {
22 | return try {
23 | responses.poll(timeout.toLong(), TimeUnit.MILLISECONDS)
24 | } catch (e: InterruptedException) {
25 | e.printStackTrace()
26 | ErrorResponse()
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/matisse/src/main/res/layout/media_grid_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
--------------------------------------------------------------------------------
/matisse/src/main/res/color/zhihu_bottom_toolbar_apply.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
20 |
21 |
--------------------------------------------------------------------------------
/matisse/src/main/res/color/dracula_bottom_toolbar_apply.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
20 |
21 |
--------------------------------------------------------------------------------
/matisse/src/main/res/color/zhihu_bottom_toolbar_preview.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/viewmodel/MusicViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.viewmodel
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import androidx.lifecycle.AndroidViewModel
6 | import androidx.lifecycle.MutableLiveData
7 | import com.absinthe.kage.media.audio.LocalMusic
8 | import com.absinthe.kage.media.audio.MusicHelper
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.GlobalScope
11 | import kotlinx.coroutines.launch
12 | import kotlinx.coroutines.withContext
13 |
14 | class MusicViewModel(application: Application) : AndroidViewModel(application) {
15 |
16 | val musicList = MutableLiveData>()
17 |
18 | fun loadMusic(context: Context) {
19 | GlobalScope.launch(Dispatchers.IO) {
20 | val list = MusicHelper.getAllLocalMusic(context)
21 | withContext(Dispatchers.Main) {
22 | musicList.setValue(list as MutableList)
23 | }
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/matisse/src/main/res/color/dracula_bottom_toolbar_preview.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
20 |
21 |
--------------------------------------------------------------------------------
/matisse/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 | #CC000000
20 | #61FFFFFF
21 | #00000000
22 |
23 | #FF0077D9
24 | #FFE3170D
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/utils/NotificationUtils.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.utils
2 |
3 | import android.app.NotificationChannel
4 | import android.app.NotificationManager
5 | import android.content.Context
6 | import android.os.Build
7 | import com.absinthe.kage.R
8 |
9 | object NotificationUtils {
10 |
11 | const val TCP_CHANNEL_ID = "tcp_channel"
12 |
13 | @JvmStatic
14 | fun createTCPChannel(context: Context) {
15 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
16 | val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
17 | val name = context.getText(R.string.notification_channel_tcp)
18 | val importance = NotificationManager.IMPORTANCE_LOW
19 | val mChannel = NotificationChannel(TCP_CHANNEL_ID, name, importance)
20 |
21 | mChannel.setShowBadge(false)
22 | mChannel.setSound(null, null)
23 | manager.createNotificationChannel(mChannel)
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/matisse/src/main/res/color/dracula_preview_bottom_toolbar_apply.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
20 |
21 |
--------------------------------------------------------------------------------
/matisse/src/main/res/color/zhihu_preview_bottom_toolbar_apply.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_logo.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_video.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
17 |
18 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/view/CategoryCardView.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.view
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.LayoutInflater
6 | import android.widget.ImageView
7 | import android.widget.TextView
8 | import com.absinthe.kage.R
9 | import com.google.android.material.card.MaterialCardView
10 |
11 | class CategoryCardView : MaterialCardView {
12 |
13 | constructor(context: Context?) : super(context)
14 |
15 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
16 | LayoutInflater.from(context).inflate(R.layout.layout_category_card_view, this)
17 | context.obtainStyledAttributes(attrs, R.styleable.CategoryCardView).apply {
18 | findViewById(R.id.tv_title)?.text = getString(R.styleable.CategoryCardView_categoryTitle)
19 | findViewById(R.id.image)?.setImageResource(getResourceId(R.styleable.CategoryCardView_categoryImage, 0))
20 | recycle()
21 | }
22 |
23 | isClickable = true
24 | isFocusable = true
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/KageApplication.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage
2 |
3 | import android.app.Application
4 | import com.absinthe.kage.connect.Const
5 | import com.absinthe.kage.service.TCPService
6 | import com.absinthe.kage.utils.timber.ReleaseTree
7 | import com.absinthe.kage.utils.timber.ThreadAwareDebugTree
8 | import com.blankj.utilcode.util.ServiceUtils
9 | import com.microsoft.appcenter.AppCenter
10 | import com.microsoft.appcenter.analytics.Analytics
11 | import com.microsoft.appcenter.crashes.Crashes
12 | import timber.log.Timber
13 |
14 | class KageApplication : Application() {
15 |
16 | override fun onCreate() {
17 | super.onCreate()
18 |
19 | if (BuildConfig.DEBUG) {
20 | Timber.plant(ThreadAwareDebugTree())
21 | } else {
22 | Timber.plant(ReleaseTree())
23 | AppCenter.start(this, Const.APP_CENTER_SECRET,
24 | Analytics::class.java, Crashes::class.java)
25 | }
26 |
27 | if (!ServiceUtils.isServiceRunning(TCPService::class.java)) {
28 | TCPService.start(this)
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFC107
4 | #FFFFA000
5 | #FFFFD740
6 | #FF202020
7 | #FFFFFFFF
8 |
9 | #FFFFFFFF
10 | #FF000000
11 | #FF888888
12 | #FF666666
13 | #FFBBBBBB
14 | #FF4CB1FA
15 | #FF99D48D
16 | #FFFAC14C
17 | #FFE57373
18 | #FFBBBBBB
19 |
20 | #FFDDDDDD
21 | #FF737373
22 |
23 | #FFFFC107
24 | #FF232323
25 | #4C000000
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_category_card_view.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
15 |
16 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_receiver_loading.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 |
21 |
--------------------------------------------------------------------------------
/matisse/src/main/java/com/zhihu/matisse/listener/OnSelectedListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Zhihu Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.zhihu.matisse.listener;
18 |
19 | import android.net.Uri;
20 | import androidx.annotation.NonNull;
21 |
22 | import java.util.List;
23 |
24 | public interface OnSelectedListener {
25 | /**
26 | * @param uriList the selected item {@link Uri} list.
27 | * @param pathList the selected item file path list.
28 | */
29 | void onSelected(@NonNull List uriList, @NonNull List pathList);
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_video_player.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
16 |
17 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/InquiryDeviceInfoCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.device.Command
5 | import com.absinthe.kage.device.CommandBuilder
6 | import com.absinthe.kage.device.DeviceManager.config
7 | import com.absinthe.kage.device.client.Client
8 | import java.io.IOException
9 |
10 | class InquiryDeviceInfoCommand : Command() {
11 |
12 | var phoneName: String? = null
13 |
14 | override fun pack(): String {
15 | return CommandBuilder()
16 | .with(this)
17 | .append(phoneName)
18 | .build()
19 | }
20 |
21 | override fun doWork(client: Client, received: String) {
22 | try {
23 | phoneName = config.name
24 | client.writeToStream(pack())
25 | } catch (e: IOException) {
26 | e.printStackTrace()
27 | client.offline()
28 | }
29 | }
30 |
31 | override fun parseReceived(received: String): Boolean {
32 | return false
33 | }
34 |
35 | init {
36 | cmd = IpMessageConst.GET_DEVICE_INFO
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/viewholder/delegate/DeviceItemViewBinder.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.viewholder.delegate
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.TextView
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.absinthe.kage.R
9 | import com.absinthe.kage.viewholder.model.DeviceItem
10 | import com.drakeet.multitype.ItemViewBinder
11 |
12 | class DeviceItemViewBinder : ItemViewBinder() {
13 |
14 | override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
15 | val root = inflater.inflate(R.layout.item_device_card, parent, false)
16 | return ViewHolder(root)
17 | }
18 |
19 | override fun onBindViewHolder(holder: ViewHolder, item: DeviceItem) {
20 | holder.deviceName.text = item.deviceName
21 | holder.deviceIp.text = item.deviceIp
22 | }
23 |
24 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
25 | val deviceName: TextView = itemView.findViewById(R.id.tv_device_name)
26 | val deviceIp: TextView = itemView.findViewById(R.id.tv_device_ip)
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 | 60dp
7 | 70dp
8 | 32dp
9 |
10 | 300dp
11 | 60dp
12 | 5dp
13 | 30dp
14 | 30dp
15 |
16 | 200dp
17 | 3dp
18 | 5dp
19 | 20dp
20 |
21 | 45dp
22 | 10dp
23 | 5dp
24 | 20dp
25 | 30dp
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
21 |
22 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/StopCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import android.content.Intent
4 | import com.absinthe.kage.connect.protocol.IpMessageConst
5 | import com.absinthe.kage.device.Command
6 | import com.absinthe.kage.device.CommandBuilder
7 | import com.absinthe.kage.device.client.Client
8 | import com.absinthe.kage.ui.receiver.ReceiverActivity
9 |
10 | class StopCommand : Command() {
11 |
12 | override fun pack(): String {
13 | return CommandBuilder()
14 | .with(this)
15 | .append(STOP)
16 | .build()
17 | }
18 |
19 | override fun doWork(client: Client, received: String) {
20 | val stopIntent = Intent(client.context, ReceiverActivity::class.java)
21 | stopIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
22 | stopIntent.putExtra(ReceiverActivity.EXTRA_IMAGE_URI, ReceiverActivity.EXTRA_FINISH)
23 | client.context.startActivity(stopIntent)
24 | }
25 |
26 | override fun parseReceived(received: String): Boolean {
27 | return false
28 | }
29 |
30 | companion object {
31 | private const val STOP = "STOP"
32 | }
33 |
34 | init {
35 | cmd = IpMessageConst.MEDIA_STOP
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/icons/LICENSE/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Android Material Icon Generator License
2 | ================================
3 |
4 | Icons generated with the Android Material Icon Generator come with the Creative Common
5 | Attribution 4.0 International License (CC-BY 4.0). You are free to change,
6 | combine and sell any of the icons as you please. Attribution would be great,
7 | but is not strictly required.
8 |
9 | This text only applies to the icons (.zip file) you download from the icon
10 | generator. The software behind the generator has its own license
11 | (https://www.apache.org/licenses/LICENSE-2.0). See the GitHub repository for
12 | details (https://github.com/Maddoc42/Android-Material-Icon-Generator).
13 |
14 |
15 |
16 | Google Material Icons License
17 | =============================
18 |
19 | (Copied from https://github.com/google/material-design-icons)
20 | We have made these icons available for you to incorporate them into your
21 | products under the Creative Common Attribution 4.0 International License (CC-BY
22 | 4.0, https://creativecommons.org/licenses/by/4.0/). Feel free to remix and
23 | re-share these icons and documentation in your products. We'd love attribution
24 | in your app's *about* screen, but it's not required. The only thing we ask is
25 | that you not re-sell the icons themselves.
--------------------------------------------------------------------------------
/matisse/src/main/java/com/zhihu/matisse/internal/entity/CaptureStrategy.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Zhihu Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.zhihu.matisse.internal.entity;
17 |
18 | public class CaptureStrategy {
19 |
20 | public final boolean isPublic;
21 | public final String authority;
22 | public final String directory;
23 |
24 | public CaptureStrategy(boolean isPublic, String authority) {
25 | this(isPublic, authority, null);
26 | }
27 |
28 | public CaptureStrategy(boolean isPublic, String authority, String directory) {
29 | this.isPublic = isPublic;
30 | this.authority = authority;
31 | this.directory = directory;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Proguard folder generated by Eclipse
24 | proguard/
25 |
26 | # Log Files
27 | *.log
28 |
29 | # Android Studio Navigation editor temp files
30 | .navigation/
31 |
32 | # Android Studio captures folder
33 | captures/
34 |
35 | # IntelliJ
36 | *.iml
37 | .idea/workspace.xml
38 | .idea/tasks.xml
39 | .idea/gradle.xml
40 | .idea/assetWizardSettings.xml
41 | .idea/dictionaries
42 | .idea/libraries
43 | .idea/caches
44 |
45 | # Keystore files
46 | # Uncomment the following line if you do not want to check your keystore files in.
47 | #*.jks
48 |
49 | # External native build folder generated in Android Studio 2.2 and later
50 | .externalNativeBuild
51 |
52 | # Google Services (e.g. APIs or Firebase)
53 | google-services.json
54 |
55 | # Freeline
56 | freeline.py
57 | freeline/
58 | freeline_project_description.json
59 |
60 | # fastlane
61 | fastlane/report.xml
62 | fastlane/Preview.html
63 | fastlane/screenshots
64 | fastlane/test_output
65 | fastlane/readme.md
66 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/ResumePlayCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.connect.proxy.BaseProxy
5 | import com.absinthe.kage.connect.proxy.MODE_AUDIO
6 | import com.absinthe.kage.connect.proxy.MODE_VIDEO
7 | import com.absinthe.kage.device.Command
8 | import com.absinthe.kage.device.CommandBuilder
9 | import com.absinthe.kage.device.client.Client
10 | import com.absinthe.kage.media.audio.AudioPlayer
11 | import com.absinthe.kage.media.video.LocalVideoPlayback
12 | import timber.log.Timber
13 |
14 | class ResumePlayCommand : Command() {
15 |
16 | override fun pack(): String {
17 | return CommandBuilder()
18 | .with(this)
19 | .build()
20 | }
21 |
22 | override fun doWork(client: Client, received: String) {
23 | if (BaseProxy.CURRENT_MODE == MODE_AUDIO) {
24 | AudioPlayer.play()
25 | } else if (BaseProxy.CURRENT_MODE == MODE_VIDEO) {
26 | LocalVideoPlayback.INSTANCE?.play()
27 | }
28 | }
29 |
30 | override fun parseReceived(received: String): Boolean {
31 | return false
32 | }
33 |
34 | init {
35 | cmd = IpMessageConst.MEDIA_RESUME_PLAY
36 | }
37 | }
--------------------------------------------------------------------------------
/matisse/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/InquiryDurationCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.device.Command
5 | import com.absinthe.kage.device.CommandBuilder
6 | import com.absinthe.kage.device.client.Client
7 | import com.absinthe.kage.media.audio.AudioPlayer
8 | import java.io.IOException
9 |
10 | class InquiryDurationCommand : Command() {
11 |
12 | override fun pack(): String {
13 | return CommandBuilder()
14 | .with(this)
15 | .append(INQUIRY_MESSAGE)
16 | .build()
17 | }
18 |
19 | override fun doWork(client: Client, received: String) {
20 | val duration = AudioPlayer.duration
21 | val command = SetDurationCommand()
22 | command.duration = duration
23 |
24 | try {
25 | client.writeToStream(command.pack())
26 | } catch (e: IOException) {
27 | e.printStackTrace()
28 | }
29 | }
30 |
31 | override fun parseReceived(received: String): Boolean {
32 | return false
33 | }
34 |
35 | companion object {
36 | const val INQUIRY_MESSAGE = "INQUIRY_DURATION"
37 | }
38 |
39 | init {
40 | cmd = IpMessageConst.MEDIA_GET_DURATION
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/InquiryPlayingPositionCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.device.Command
5 | import com.absinthe.kage.device.CommandBuilder
6 | import com.absinthe.kage.device.client.Client
7 | import com.absinthe.kage.media.audio.AudioPlayer.currentPosition
8 | import java.io.IOException
9 |
10 | class InquiryPlayingPositionCommand : Command() {
11 |
12 | override fun pack(): String {
13 | return CommandBuilder()
14 | .with(this)
15 | .append(MESSAGE)
16 | .build()
17 | }
18 |
19 | override fun doWork(client: Client, received: String) {
20 | val position = currentPosition
21 | val command = SetPlayingPositionCommand()
22 | command.position = position
23 |
24 | try {
25 | client.writeToStream(command.pack())
26 | } catch (e: IOException) {
27 | e.printStackTrace()
28 | }
29 | }
30 |
31 | override fun parseReceived(received: String): Boolean {
32 | return false
33 | }
34 |
35 | companion object {
36 | const val MESSAGE = "INQUIRY_PLAYING_POSITION"
37 | }
38 |
39 | init {
40 | cmd = IpMessageConst.MEDIA_GET_PLAYING_POSITION
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/viewholder/delegate/CastItemViewBinder.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.viewholder.delegate
2 |
3 | import android.content.Intent
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.absinthe.kage.R
9 | import com.absinthe.kage.ui.sender.SenderActivity
10 | import com.absinthe.kage.viewholder.HolderConstant
11 | import com.absinthe.kage.viewholder.model.CastItem
12 | import com.drakeet.multitype.ItemViewBinder
13 |
14 | class CastItemViewBinder : ItemViewBinder() {
15 |
16 | override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
17 | val root = inflater.inflate(R.layout.item_cast_card, parent, false)
18 | return ViewHolder(root)
19 | }
20 |
21 | override fun onBindViewHolder(holder: ViewHolder, item: CastItem) {
22 | holder.itemView.setOnClickListener {
23 | val intent = Intent(holder.itemView.context, SenderActivity::class.java)
24 | holder.itemView.context.startActivity(intent)
25 | }
26 |
27 | holder.itemView.setOnTouchListener(HolderConstant.onTouchListener)
28 | }
29 |
30 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
31 | }
--------------------------------------------------------------------------------
/matisse/src/main/java/com/zhihu/matisse/internal/utils/SingleMediaScanner.java:
--------------------------------------------------------------------------------
1 | package com.zhihu.matisse.internal.utils;
2 |
3 | import android.content.Context;
4 | import android.media.MediaScannerConnection;
5 | import android.net.Uri;
6 |
7 | /**
8 | * @author 工藤
9 | * @email gougou@16fan.com
10 | * create at 2018年10月23日12:17:59
11 | * description:媒体扫描
12 | */
13 | public class SingleMediaScanner implements MediaScannerConnection.MediaScannerConnectionClient {
14 |
15 | private MediaScannerConnection mMsc;
16 | private String mPath;
17 | private ScanListener mListener;
18 |
19 | public interface ScanListener {
20 |
21 | /**
22 | * scan finish
23 | */
24 | void onScanFinish();
25 | }
26 |
27 | public SingleMediaScanner(Context context, String mPath, ScanListener mListener) {
28 | this.mPath = mPath;
29 | this.mListener = mListener;
30 | this.mMsc = new MediaScannerConnection(context, this);
31 | this.mMsc.connect();
32 | }
33 |
34 | @Override public void onMediaScannerConnected() {
35 | mMsc.scanFile(mPath, null);
36 | }
37 |
38 | @Override public void onScanCompleted(String mPath, Uri mUri) {
39 | mMsc.disconnect();
40 | if (mListener != null) {
41 | mListener.onScanFinish();
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/SquareFrameLayout.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Zhihu Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.zhihu.matisse.internal.ui.widget;
17 |
18 | import android.content.Context;
19 | import android.util.AttributeSet;
20 | import android.widget.FrameLayout;
21 |
22 | public class SquareFrameLayout extends FrameLayout {
23 |
24 | public SquareFrameLayout(Context context) {
25 | super(context);
26 | }
27 |
28 | public SquareFrameLayout(Context context, AttributeSet attrs) {
29 | super(context, attrs);
30 | }
31 |
32 | @Override
33 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
34 | super.onMeasure(widthMeasureSpec, widthMeasureSpec);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/viewholder/delegate/ConnectItemViewBinder.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.viewholder.delegate
2 |
3 | import android.content.Intent
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.absinthe.kage.R
9 | import com.absinthe.kage.ui.connect.ConnectActivity
10 | import com.absinthe.kage.viewholder.HolderConstant
11 | import com.absinthe.kage.viewholder.model.ConnectItem
12 | import com.drakeet.multitype.ItemViewBinder
13 |
14 | class ConnectItemViewBinder : ItemViewBinder() {
15 |
16 | override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
17 | val root = inflater.inflate(R.layout.item_connect_card, parent, false)
18 | return ViewHolder(root)
19 | }
20 |
21 | override fun onBindViewHolder(holder: ViewHolder, item: ConnectItem) {
22 | holder.itemView.setOnClickListener {
23 | val intent = Intent(holder.itemView.context, ConnectActivity::class.java)
24 | holder.itemView.context.startActivity(intent)
25 | }
26 |
27 | holder.itemView.setOnTouchListener(HolderConstant.onTouchListener)
28 | }
29 |
30 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
31 | }
--------------------------------------------------------------------------------
/matisse/src/main/res/layout/fragment_media_selection.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
21 |
22 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/SetDurationCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.connect.protocol.IpMessageProtocol
5 | import com.absinthe.kage.device.Command
6 | import com.absinthe.kage.device.CommandBuilder
7 | import com.absinthe.kage.device.client.Client
8 |
9 | class SetDurationCommand : Command() {
10 | var duration = 0
11 |
12 | override fun pack(): String {
13 | return CommandBuilder()
14 | .with(this)
15 | .append(duration.toString())
16 | .build()
17 | }
18 |
19 | override fun doWork(client: Client, received: String) {}
20 |
21 | override fun parseReceived(received: String): Boolean {
22 | val splits = received.split(IpMessageProtocol.DELIMITER).toTypedArray()
23 |
24 | return if (splits.size == LENGTH) {
25 | try {
26 | duration = splits[1].toInt()
27 | true
28 | } catch (e: NumberFormatException) {
29 | e.printStackTrace()
30 | false
31 | }
32 | } else {
33 | false
34 | }
35 | }
36 |
37 | companion object {
38 | const val LENGTH = 2
39 | }
40 |
41 | init {
42 | cmd = IpMessageConst.RESPONSE_SET_MEDIA_DURATION
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_music_seek_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
22 |
23 |
31 |
32 |
--------------------------------------------------------------------------------
/matisse/src/main/res/layout/fragment_preview_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
20 |
21 |
25 |
26 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
21 |
22 |
23 |
24 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/InquiryPlayStateCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.connect.proxy.BaseProxy
5 | import com.absinthe.kage.connect.proxy.MODE_AUDIO
6 | import com.absinthe.kage.device.Command
7 | import com.absinthe.kage.device.CommandBuilder
8 | import com.absinthe.kage.device.client.Client
9 | import com.absinthe.kage.media.audio.AudioPlayer
10 | import com.absinthe.kage.media.video.LocalVideoPlayback
11 | import java.io.IOException
12 |
13 | class InquiryPlayStateCommand : Command() {
14 |
15 | override fun pack(): String {
16 | return CommandBuilder()
17 | .with(this)
18 | .build()
19 | }
20 |
21 | override fun doWork(client: Client, received: String) {
22 | val command = SetPlayStateCommand().apply {
23 | stateCode = if (BaseProxy.CURRENT_MODE == MODE_AUDIO)
24 | AudioPlayer.playState
25 | else
26 | LocalVideoPlayback.INSTANCE?.state ?: 0
27 | }
28 |
29 | try {
30 | client.writeToStream(command.pack())
31 | } catch (e: IOException) {
32 | e.printStackTrace()
33 | client.offline()
34 | }
35 | }
36 |
37 | override fun parseReceived(received: String): Boolean {
38 | return true
39 | }
40 |
41 | init {
42 | cmd = IpMessageConst.MEDIA_GET_PLAYING_STATE
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/InquiryPlayerStatusCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.connect.proxy.BaseProxy
5 | import com.absinthe.kage.connect.proxy.MODE_AUDIO
6 | import com.absinthe.kage.device.Command
7 | import com.absinthe.kage.device.CommandBuilder
8 | import com.absinthe.kage.device.client.Client
9 | import com.absinthe.kage.media.audio.AudioPlayer
10 | import com.absinthe.kage.media.video.LocalVideoPlayback
11 | import java.io.IOException
12 |
13 | class InquiryPlayerStatusCommand : Command() {
14 |
15 | override fun pack(): String {
16 | return CommandBuilder()
17 | .with(this)
18 | .build()
19 | }
20 |
21 | override fun doWork(client: Client, received: String) {
22 | val command = SetPlayStatusCommand().apply {
23 | statusCode = if (BaseProxy.CURRENT_MODE == MODE_AUDIO)
24 | AudioPlayer.playState
25 | else
26 | LocalVideoPlayback.INSTANCE?.state ?: 0
27 | }
28 |
29 | try {
30 | client.writeToStream(command.pack())
31 | } catch (e: IOException) {
32 | e.printStackTrace()
33 | client.offline()
34 | }
35 | }
36 |
37 | override fun parseReceived(received: String): Boolean {
38 | return true
39 | }
40 |
41 | init {
42 | cmd = IpMessageConst.MEDIA_GET_PLAYER_STATUS
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/PromptPhoneConnectedCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.connect.protocol.IpMessageProtocol
5 | import com.absinthe.kage.device.Command
6 | import com.absinthe.kage.device.CommandBuilder
7 | import com.absinthe.kage.device.client.Client
8 |
9 | class PromptPhoneConnectedCommand : Command() {
10 |
11 | var phoneName: String? = null
12 | var localIp: String? = null
13 |
14 | override fun pack(): String {
15 | return CommandBuilder()
16 | .with(this)
17 | .append(phoneName)
18 | .append(localIp)
19 | .build()
20 | }
21 |
22 | override fun doWork(client: Client, received: String) {
23 | if (parseReceived(received)) {
24 | client.deviceInfo.name = phoneName!!
25 | client.deviceInfo.ip = localIp!!
26 | }
27 | }
28 |
29 | override fun parseReceived(received: String): Boolean {
30 | val splits = received.split(IpMessageProtocol.DELIMITER).toTypedArray()
31 |
32 | return if (splits.size == LENGTH) {
33 | phoneName = splits[1]
34 | localIp = splits[2]
35 | true
36 | } else {
37 | false
38 | }
39 | }
40 |
41 | companion object {
42 | const val LENGTH = 3
43 | }
44 |
45 | init {
46 | cmd = IpMessageConst.PROMPT_PHONE_CONNECT
47 | }
48 | }
--------------------------------------------------------------------------------
/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/PreviewViewPager.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Zhihu Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.zhihu.matisse.internal.ui.widget;
17 |
18 | import android.content.Context;
19 | import androidx.viewpager.widget.ViewPager;
20 | import android.util.AttributeSet;
21 | import android.view.View;
22 |
23 | import it.sephiroth.android.library.imagezoom.ImageViewTouch;
24 |
25 | public class PreviewViewPager extends ViewPager {
26 |
27 | public PreviewViewPager(Context context, AttributeSet attrs) {
28 | super(context, attrs);
29 | }
30 |
31 | @Override
32 | protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
33 | if (v instanceof ImageViewTouch) {
34 | return ((ImageViewTouch) v).canScroll(dx) || super.canScroll(v, checkV, dx, x, y);
35 | }
36 | return super.canScroll(v, checkV, dx, x, y);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_empty_list_tip.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
20 |
21 |
29 |
30 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_connected.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/connect/protocol/IpMessageProtocol.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.connect.protocol
2 |
3 | import java.util.*
4 |
5 | class IpMessageProtocol {
6 |
7 | var version: String = "1" //Version number
8 | var packetNum: String //Packet number
9 | var senderName: String = "Unknown" //Sender name
10 | var cmd: Int = 0 //Command
11 | var additionalSection: String = ""
12 |
13 | constructor() {
14 | packetNum = seconds
15 | }
16 |
17 | // 根据协议字符串初始化
18 | constructor(protocolString: String) {
19 | val args = protocolString.split(DELIMITER).toTypedArray()
20 | version = args[0]
21 | packetNum = args[1]
22 | senderName = args[2]
23 | cmd = args[3].toInt()
24 | additionalSection = if (args.size >= 5) args[4] else ""
25 | }
26 |
27 | constructor(senderName: String, cmd: Int, additionalSection: String = "") {
28 | packetNum = seconds
29 | this.senderName = senderName
30 | this.cmd = cmd
31 | this.additionalSection = additionalSection
32 | }
33 |
34 | //得到协议串
35 | val protocolString: String
36 | get() = version + DELIMITER +
37 | packetNum + DELIMITER +
38 | senderName + DELIMITER +
39 | cmd + DELIMITER +
40 | additionalSection
41 |
42 | //得到数据包编号,毫秒数
43 | private val seconds: String
44 | get() {
45 | return Date().time.toString()
46 | }
47 |
48 | companion object {
49 | const val DELIMITER = "-->"
50 | }
51 | }
--------------------------------------------------------------------------------
/matisse/src/main/res/layout/photo_capture_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
23 |
24 |
35 |
36 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/HeartbeatCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.connect.protocol.IpMessageProtocol
5 | import com.absinthe.kage.device.Command
6 | import com.absinthe.kage.device.CommandBuilder
7 | import com.absinthe.kage.device.client.Client
8 | import java.io.IOException
9 |
10 | class HeartbeatCommand : Command() {
11 |
12 | override fun pack(): String {
13 | return CommandBuilder()
14 | .with(this)
15 | .append(HEARTBEAT_MESSAGE)
16 | .build()
17 | }
18 |
19 | override fun doWork(client: Client, received: String) {
20 | try {
21 | client.writeToStream(pack())
22 | } catch (e: IOException) {
23 | e.printStackTrace()
24 | client.offline()
25 | }
26 | }
27 |
28 | override fun parseReceived(received: String): Boolean {
29 | val splits = received.split(IpMessageProtocol.DELIMITER).toTypedArray()
30 |
31 | return if (splits.size == LENGTH) {
32 | try {
33 | splits[0].toInt() == cmd
34 | } catch (e: NumberFormatException) {
35 | e.printStackTrace()
36 | false
37 | }
38 | } else {
39 | false
40 | }
41 | }
42 |
43 | companion object {
44 | const val HEARTBEAT_MESSAGE = "HEARTBEAT"
45 | const val LENGTH = 2
46 | }
47 |
48 | init {
49 | cmd = IpMessageConst.HEARTBEAT
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/MediaPausePlayingCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import android.media.session.PlaybackState
4 | import com.absinthe.kage.connect.protocol.IpMessageConst
5 | import com.absinthe.kage.connect.proxy.*
6 | import com.absinthe.kage.device.Command
7 | import com.absinthe.kage.device.CommandBuilder
8 | import com.absinthe.kage.device.client.Client
9 | import com.absinthe.kage.media.audio.AudioPlayer
10 | import com.absinthe.kage.media.video.LocalVideoPlayback
11 | import timber.log.Timber
12 |
13 | class MediaPausePlayingCommand : Command() {
14 |
15 | override fun pack(): String {
16 | return CommandBuilder()
17 | .with(this)
18 | .append(PAUSE_MESSAGE)
19 | .build()
20 | }
21 |
22 | override fun doWork(client: Client, received: String) {
23 | if (BaseProxy.CURRENT_MODE == MODE_AUDIO) {
24 | Timber.d("PlaybackState.STATE_PLAYING")
25 | if (AudioPlayer.playState == PlaybackState.STATE_PLAYING) {
26 | AudioPlayer.pause()
27 | }
28 | } else if (BaseProxy.CURRENT_MODE == MODE_VIDEO) {
29 | LocalVideoPlayback.INSTANCE?.let {
30 | if (it.state == PlaybackState.STATE_PAUSED)
31 | it.play()
32 | }
33 | }
34 | }
35 |
36 | override fun parseReceived(received: String): Boolean {
37 | return false
38 | }
39 |
40 | companion object {
41 | const val PAUSE_MESSAGE = "PAUSE"
42 | }
43 |
44 | init {
45 | cmd = IpMessageConst.MEDIA_PAUSE
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_device.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
28 |
29 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_cast_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
19 |
20 |
27 |
28 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_connect_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
19 |
20 |
27 |
28 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_connect.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
21 |
22 |
23 |
24 |
30 |
31 |
32 |
33 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/utils/AnimationUtil.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.utils
2 |
3 | import android.view.View
4 | import android.view.animation.AlphaAnimation
5 | import android.view.animation.Animation
6 |
7 | const val LONG = 1000
8 | const val SHORT = 500
9 |
10 | object AnimationUtil {
11 |
12 | /**
13 | * Fade in/out animation
14 | *
15 | * @param view triggered view
16 | * @param state fade state
17 | * @param duration fade duration (ms)
18 | */
19 | @JvmStatic
20 | fun showAndHiddenAnimation(view: View, state: AnimationState, duration: Long) {
21 | var start = 0f
22 | var end = 0f
23 |
24 | when (state) {
25 | AnimationState.STATE_SHOW -> {
26 | end = 1f
27 | view.visibility = View.VISIBLE
28 | }
29 | AnimationState.STATE_HIDDEN -> {
30 | start = 1f
31 | view.visibility = View.INVISIBLE
32 | }
33 | AnimationState.STATE_GONE -> {
34 | start = 1f
35 | view.visibility = View.GONE
36 | }
37 | }
38 |
39 | val animation = AlphaAnimation(start, end)
40 | animation.duration = duration
41 | animation.fillAfter = true
42 | animation.setAnimationListener(object : Animation.AnimationListener {
43 | override fun onAnimationStart(animation: Animation) {}
44 | override fun onAnimationRepeat(animation: Animation) {}
45 | override fun onAnimationEnd(animation: Animation) {
46 | view.clearAnimation()
47 | }
48 | })
49 | view.animation = animation
50 | animation.start()
51 | }
52 |
53 | enum class AnimationState {
54 | STATE_SHOW, STATE_HIDDEN, STATE_GONE
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout-land/layout_empty_list_tip.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
20 |
21 |
27 |
28 |
36 |
37 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.res.Configuration
5 | import android.os.Bundle
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.appcompat.widget.Toolbar
8 | import com.absinthe.kage.device.DeviceManager
9 | import com.absinthe.kage.device.cmd.DeviceRotationCommand
10 | import com.absinthe.kage.manager.ActivityStackManager
11 | import com.absinthe.kage.utils.UiUtils
12 | import com.absinthe.kage.utils.UiUtils.setDarkMode
13 | import com.blankj.utilcode.util.BarUtils
14 | import java.lang.ref.WeakReference
15 |
16 | @SuppressLint("Registered")
17 | abstract class BaseActivity : AppCompatActivity() {
18 |
19 | private lateinit var reference: WeakReference
20 | protected var mToolbar: Toolbar? = null
21 |
22 | protected abstract fun setViewBinding()
23 | protected abstract fun setToolbar()
24 |
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | super.onCreate(savedInstanceState)
27 | reference = WeakReference(this)
28 | ActivityStackManager.addActivity(reference)
29 | setViewBinding()
30 |
31 | setDarkMode(this)
32 | UiUtils.setSystemBarTransparent(this)
33 | setToolbar()
34 | mToolbar?.setPadding(0, BarUtils.getStatusBarHeight(), 0, 0)
35 | }
36 |
37 | override fun onDestroy() {
38 | ActivityStackManager.removeActivity(reference)
39 | super.onDestroy()
40 | }
41 |
42 | override fun onConfigurationChanged(newConfig: Configuration) {
43 | super.onConfigurationChanged(newConfig)
44 |
45 | if (DeviceManager.isConnected) {
46 | val command = DeviceRotationCommand().apply {
47 | flag = newConfig.orientation
48 | }
49 | DeviceManager.sendCommandToCurrentDevice(command)
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/media/audio/LocalMusic.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.media.audio
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.absinthe.kage.media.LocalMedia
6 |
7 | class LocalMusic : LocalMedia, Parcelable {
8 | var album: String? = null
9 | var albumId = 0
10 | var artist: String? = null
11 | var artistId = 0
12 | var coverPath: String? = null
13 |
14 | constructor()
15 |
16 | constructor(parcel: Parcel) {
17 | title = parcel.readString()
18 | filePath = parcel.readString()
19 | date = parcel.readString()
20 | sortLetters = parcel.readString()
21 | type = parcel.readInt()
22 | duration = parcel.readLong()
23 | size = parcel.readFloat()
24 | album = parcel.readString()
25 | albumId = parcel.readInt()
26 | artist = parcel.readString()
27 | artistId = parcel.readInt()
28 | coverPath = parcel.readString()
29 | }
30 |
31 | override fun describeContents(): Int {
32 | return 0
33 | }
34 |
35 | override fun writeToParcel(dest: Parcel, flags: Int) {
36 | dest.writeString(title)
37 | dest.writeString(filePath)
38 | dest.writeString(date)
39 | dest.writeString(sortLetters)
40 | dest.writeInt(type)
41 | dest.writeLong(duration)
42 | dest.writeFloat(size)
43 | dest.writeString(album)
44 | dest.writeInt(albumId)
45 | dest.writeString(artist)
46 | dest.writeInt(artistId)
47 | dest.writeString(coverPath)
48 | }
49 |
50 | companion object CREATOR : Parcelable.Creator {
51 | override fun createFromParcel(parcel: Parcel): LocalMusic {
52 | return LocalMusic(parcel)
53 | }
54 |
55 | override fun newArray(size: Int): Array {
56 | return arrayOfNulls(size)
57 | }
58 | }
59 |
60 | }
--------------------------------------------------------------------------------
/matisse/src/main/java/com/zhihu/matisse/internal/ui/SelectedPreviewActivity.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Zhihu Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.zhihu.matisse.internal.ui;
17 |
18 | import android.os.Bundle;
19 | import androidx.annotation.Nullable;
20 |
21 | import com.zhihu.matisse.internal.entity.Item;
22 | import com.zhihu.matisse.internal.entity.SelectionSpec;
23 | import com.zhihu.matisse.internal.model.SelectedItemCollection;
24 |
25 | import java.util.List;
26 |
27 | public class SelectedPreviewActivity extends BasePreviewActivity {
28 |
29 | @Override
30 | protected void onCreate(@Nullable Bundle savedInstanceState) {
31 | super.onCreate(savedInstanceState);
32 | if (!SelectionSpec.getInstance().hasInited) {
33 | setResult(RESULT_CANCELED);
34 | finish();
35 | return;
36 | }
37 |
38 | Bundle bundle = getIntent().getBundleExtra(EXTRA_DEFAULT_BUNDLE);
39 | List- selected = bundle.getParcelableArrayList(SelectedItemCollection.STATE_SELECTION);
40 | mAdapter.addAll(selected);
41 | mAdapter.notifyDataSetChanged();
42 | if (mSpec.countable) {
43 | mCheckView.setCheckedNum(1);
44 | } else {
45 | mCheckView.setChecked(true);
46 | }
47 | mPreviousPos = 0;
48 | updateSize(selected.get(0));
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/matisse/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Kage
4 | 连接
5 | 投射
6 | 接收器
7 |
8 |
9 | 空列表
10 | 请确保您的设备在同一局域网下并且已开启 Kage 服务
11 | 连接中
12 | 已连接
13 | 连接
14 |
15 |
16 | 投射图片
17 | 投射视频
18 | 投射音乐
19 |
20 |
21 | 加载中
22 | 保存
23 | 是否保存此媒体到设备?
24 | 保存中…
25 | 保存成功
26 |
27 |
28 | Kage - 一款可以向其它 Android 设备投射多媒体文件的 App
29 |
30 |
31 | Kage 服务正在运行
32 | 点按以回到主页
33 |
34 |
35 | 已连接
36 | 请授权存储权限
37 | 音乐列表
38 | 关于
39 | 投射到设备
40 | 投射
41 | Kage 服务正在运行
42 | Kage 服务尚未运行
43 | 点按以启动服务
44 | 点按以停止服务
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_music_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
21 |
22 |
23 |
24 |
30 |
31 |
34 |
35 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/SeekToCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import com.absinthe.kage.connect.protocol.IpMessageConst
4 | import com.absinthe.kage.connect.protocol.IpMessageProtocol
5 | import com.absinthe.kage.connect.proxy.AudioProxy
6 | import com.absinthe.kage.connect.proxy.BaseProxy
7 | import com.absinthe.kage.connect.proxy.MODE_AUDIO
8 | import com.absinthe.kage.connect.proxy.MODE_VIDEO
9 | import com.absinthe.kage.device.Command
10 | import com.absinthe.kage.device.CommandBuilder
11 | import com.absinthe.kage.device.client.Client
12 | import com.absinthe.kage.media.audio.AudioPlayer
13 | import com.absinthe.kage.media.video.LocalVideoPlayback
14 |
15 | class SeekToCommand : Command() {
16 | var position = 0
17 |
18 | override fun pack(): String {
19 | return CommandBuilder()
20 | .with(this)
21 | .append(position.toString())
22 | .build()
23 | }
24 |
25 | override fun doWork(client: Client, received: String) {
26 | if (parseReceived(received)) {
27 | if (BaseProxy.CURRENT_MODE == MODE_AUDIO) {
28 | AudioPlayer.seekTo(position)
29 | } else if (BaseProxy.CURRENT_MODE == MODE_VIDEO) {
30 | LocalVideoPlayback.INSTANCE?.seekTo(position)
31 | }
32 | }
33 | }
34 |
35 | override fun parseReceived(received: String): Boolean {
36 | val splits = received.split(IpMessageProtocol.DELIMITER).toTypedArray()
37 |
38 | return if (splits.size == LENGTH) {
39 | position = try {
40 | splits[1].toInt()
41 | } catch (e: NumberFormatException) {
42 | e.printStackTrace()
43 | return false
44 | }
45 | true
46 | } else {
47 | false
48 | }
49 | }
50 |
51 | companion object {
52 | const val LENGTH = 2
53 | }
54 |
55 | init {
56 | cmd = IpMessageConst.MEDIA_SEEK_TO
57 | }
58 | }
--------------------------------------------------------------------------------
/matisse/src/main/res/values-zh/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 | 全部
19 |
20 | 预览
21 | 使用
22 | 使用(%d)
23 | 投射
24 | 停止投射
25 | 返回
26 | 拍一张
27 | 还没有图片或视频
28 | 我知道了
29 |
30 | 您已经达到最大选择数量
31 | 最多只能选择 %1$d 个文件
32 | 图片质量太低
33 | 图片质量太高
34 | 不支持的文件类型
35 | 不能同时选择图片和视频
36 | 没有支持视频预览的应用
37 | "该照片大于 %1$d M,无法上传将取消勾选原图"
38 | "有 %1$d 张照片大于 %2$d M\n无法上传,将取消勾选原图"
39 | 原图
40 | 确定
41 | 确定(%1$d)
42 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_video_seek_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
20 |
21 |
29 |
30 |
41 |
42 |
--------------------------------------------------------------------------------
/matisse/src/main/res/layout/media_grid_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
23 |
24 |
30 |
31 |
40 |
41 |
51 |
--------------------------------------------------------------------------------
/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckRadioView.java:
--------------------------------------------------------------------------------
1 | package com.zhihu.matisse.internal.ui.widget;
2 |
3 | import android.content.Context;
4 | import android.graphics.PorterDuff;
5 | import android.graphics.drawable.Drawable;
6 | import androidx.core.content.res.ResourcesCompat;
7 | import androidx.appcompat.widget.AppCompatImageView;
8 | import android.util.AttributeSet;
9 |
10 | import com.zhihu.matisse.R;
11 |
12 | public class CheckRadioView extends AppCompatImageView {
13 |
14 | private Drawable mDrawable;
15 |
16 | private int mSelectedColor;
17 | private int mUnSelectUdColor;
18 |
19 | public CheckRadioView(Context context) {
20 | super(context);
21 | init();
22 | }
23 |
24 |
25 |
26 | public CheckRadioView(Context context, AttributeSet attrs) {
27 | super(context, attrs);
28 | init();
29 | }
30 |
31 | private void init() {
32 | mSelectedColor = ResourcesCompat.getColor(
33 | getResources(), R.color.zhihu_item_checkCircle_backgroundColor,
34 | getContext().getTheme());
35 | mUnSelectUdColor = ResourcesCompat.getColor(
36 | getResources(), R.color.zhihu_check_original_radio_disable,
37 | getContext().getTheme());
38 | setChecked(false);
39 | }
40 |
41 | public void setChecked(boolean enable) {
42 | if (enable) {
43 | setImageResource(R.drawable.ic_preview_radio_on);
44 | mDrawable = getDrawable();
45 | mDrawable.setColorFilter(mSelectedColor, PorterDuff.Mode.SRC_IN);
46 | } else {
47 | setImageResource(R.drawable.ic_preview_radio_off);
48 | mDrawable = getDrawable();
49 | mDrawable.setColorFilter(mUnSelectUdColor, PorterDuff.Mode.SRC_IN);
50 | }
51 | }
52 |
53 |
54 | public void setColor(int color) {
55 | if (mDrawable == null) {
56 | mDrawable = getDrawable();
57 | }
58 | mDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/ImageInfoCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import android.content.Intent
4 | import android.text.TextUtils
5 | import com.absinthe.kage.connect.Const
6 | import com.absinthe.kage.connect.protocol.IpMessageConst
7 | import com.absinthe.kage.connect.protocol.IpMessageProtocol
8 | import com.absinthe.kage.device.Command
9 | import com.absinthe.kage.device.CommandBuilder
10 | import com.absinthe.kage.device.client.Client
11 | import com.absinthe.kage.ui.receiver.ReceiverActivity
12 |
13 | class ImageInfoCommand : Command() {
14 | var info: String
15 |
16 | override fun pack(): String {
17 | return CommandBuilder()
18 | .with(this)
19 | .append(info)
20 | .build()
21 | }
22 |
23 | override fun doWork(client: Client, received: String) {
24 | if (parseReceived(received)) {
25 | if (!TextUtils.isEmpty(info)) {
26 | var imageUri = info
27 | val ip = client.deviceInfo.ip
28 |
29 | if (!TextUtils.isEmpty(ip)) {
30 | imageUri = String.format(Const.HTTP_SERVER_FORMAT, ip) + imageUri
31 |
32 | val intent = Intent(client.context, ReceiverActivity::class.java)
33 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
34 | intent.putExtra(ReceiverActivity.EXTRA_IMAGE_URI, imageUri)
35 | client.context.startActivity(intent)
36 | }
37 | }
38 | }
39 | }
40 |
41 | override fun parseReceived(received: String): Boolean {
42 | val splits = received.split(IpMessageProtocol.DELIMITER).toTypedArray()
43 |
44 | return if (splits.size == LENGTH) {
45 | info = splits[1]
46 | true
47 | } else {
48 | false
49 | }
50 | }
51 |
52 | companion object {
53 | const val LENGTH = 2
54 | }
55 |
56 | init {
57 | cmd = IpMessageConst.MEDIA_IMAGE_INFO
58 | info = ""
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/RemoteControlKeyCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import android.content.Context
4 | import android.media.AudioManager
5 | import android.view.KeyEvent
6 | import com.absinthe.kage.connect.protocol.IpMessageConst
7 | import com.absinthe.kage.connect.protocol.IpMessageProtocol
8 | import com.absinthe.kage.device.Command
9 | import com.absinthe.kage.device.CommandBuilder
10 | import com.absinthe.kage.device.client.Client
11 |
12 | class RemoteControlKeyCommand : Command() {
13 |
14 | var keyCode = 0
15 |
16 | override fun pack(): String {
17 | return CommandBuilder()
18 | .with(this)
19 | .append(keyCode.toString())
20 | .build()
21 | }
22 |
23 | override fun doWork(client: Client, received: String) {
24 | if (parseReceived(received)) {
25 | when (keyCode) {
26 | KeyEvent.KEYCODE_VOLUME_UP -> {
27 | val audioManager = client.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
28 | audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI)
29 | }
30 | KeyEvent.KEYCODE_VOLUME_DOWN -> {
31 | val audioManager = client.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
32 | audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI)
33 | }
34 | }
35 | }
36 | }
37 |
38 | override fun parseReceived(received: String): Boolean {
39 | val splits = received.split(IpMessageProtocol.DELIMITER).toTypedArray()
40 |
41 | return if (splits.size == LENGTH) {
42 | keyCode = splits[1].toInt()
43 | true
44 | } else {
45 | false
46 | }
47 | }
48 |
49 | init {
50 | cmd = IpMessageConst.KEY_EVENT
51 | }
52 |
53 | companion object {
54 | const val LENGTH = 2
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/device/cmd/DeviceRotationCommand.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.device.cmd
2 |
3 | import android.content.pm.ActivityInfo
4 | import android.content.res.Configuration
5 | import com.absinthe.kage.connect.protocol.IpMessageConst
6 | import com.absinthe.kage.connect.protocol.IpMessageProtocol
7 | import com.absinthe.kage.device.Command
8 | import com.absinthe.kage.device.CommandBuilder
9 | import com.absinthe.kage.device.client.Client
10 | import com.absinthe.kage.manager.ActivityStackManager.topActivity
11 |
12 | class DeviceRotationCommand : Command() {
13 |
14 | var flag = TYPE_LAND
15 |
16 | override fun pack(): String {
17 | return CommandBuilder()
18 | .with(this)
19 | .append(flag.toString())
20 | .build()
21 | }
22 |
23 | override fun doWork(client: Client, received: String) {
24 | if (parseReceived(received)) {
25 | val topActivity = topActivity
26 |
27 | if (topActivity != null) {
28 | topActivity.requestedOrientation = flag
29 | }
30 | }
31 | }
32 |
33 | override fun parseReceived(received: String): Boolean {
34 | val splits = received.split(IpMessageProtocol.DELIMITER).toTypedArray()
35 |
36 | return if (splits.size == LENGTH) {
37 | try {
38 | flag = splits[1].toInt()
39 | flag = if (flag == Configuration.ORIENTATION_LANDSCAPE) {
40 | TYPE_LAND
41 | } else {
42 | TYPE_PORT
43 | }
44 | } catch (e: NumberFormatException) {
45 | e.printStackTrace()
46 | return false
47 | }
48 | true
49 | } else {
50 | false
51 | }
52 | }
53 |
54 | companion object {
55 | const val TYPE_LAND = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
56 | const val TYPE_PORT = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
57 | const val LENGTH = 2
58 | }
59 |
60 | init {
61 | cmd = IpMessageConst.DEVICE_ROTATION
62 | }
63 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/viewholder/delegate/DeviceInfoItemViewBinder.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.viewholder.delegate
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.Button
7 | import android.widget.TextView
8 | import androidx.recyclerview.widget.RecyclerView
9 | import com.absinthe.kage.R
10 | import com.absinthe.kage.device.DeviceManager
11 | import com.absinthe.kage.device.model.DeviceInfo
12 | import com.drakeet.multitype.ItemViewBinder
13 |
14 | class DeviceInfoItemViewBinder : ItemViewBinder() {
15 |
16 | override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
17 | val root = inflater.inflate(R.layout.item_device, parent, false)
18 | return ViewHolder(root)
19 | }
20 |
21 | override fun onBindViewHolder(holder: ViewHolder, item: DeviceInfo) {
22 | holder.deviceName.text = item.name
23 | holder.deviceIp.text = item.ip
24 |
25 | when (item.state) {
26 | DeviceInfo.STATE_IDLE -> holder.btnConnect.text = holder.itemView.context.getString(R.string.connect_state_connect)
27 | DeviceInfo.STATE_CONNECTING -> holder.btnConnect.text = holder.itemView.context.getString(R.string.connect_state_connecting)
28 | DeviceInfo.STATE_CONNECTED -> holder.btnConnect.text = holder.itemView.context.getString(R.string.connect_state_connected)
29 | }
30 |
31 | holder.btnConnect.setOnClickListener {
32 | if (!item.isConnected) {
33 | DeviceManager.onlineDevice(item)
34 | DeviceManager.connectDevice(item)
35 | } else {
36 | DeviceManager.disConnectDevice()
37 | }
38 | }
39 | }
40 |
41 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
42 | val deviceName: TextView = itemView.findViewById(R.id.tv_device_name)
43 | val deviceIp: TextView = itemView.findViewById(R.id.tv_device_ip)
44 | val btnConnect: Button = itemView.findViewById(R.id.btn_connect)
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_music.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
18 |
19 |
37 |
38 |
47 |
48 |
--------------------------------------------------------------------------------
/.idea/navEditor.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/matisse/src/main/res/values/colors_dracula.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 | #263237
19 | #1D282C
20 |
21 | #34474E
22 | #DEFFFFFF
23 | #89FFFFFF
24 | #455A64
25 | #4DFFFFFF
26 |
27 | #37474F
28 | #263237
29 | #FFFFFF
30 | #FFFFFF
31 |
32 | #232E32
33 | #34474E
34 |
35 | #DEFFFFFF
36 | #4DFFFFFF
37 | #03A9F4
38 | #4D03A9F4
39 |
40 | #FFFFFF
41 | #03A9F4
42 | #4D03A9F4
43 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/viewholder/HolderConstant.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.viewholder
2 |
3 | import android.animation.ObjectAnimator
4 | import android.annotation.SuppressLint
5 | import android.view.MotionEvent
6 | import android.view.View
7 | import android.view.ViewConfiguration
8 | import com.blankj.utilcode.util.Utils
9 |
10 | class HolderConstant {
11 | companion object {
12 | private const val ANIMATION_DURATION: Long = 300
13 | private const val TRANSLATION_Z: Float = 10f
14 |
15 | @SuppressLint("ClickableViewAccessibility")
16 | val onTouchListener: View.OnTouchListener = View.OnTouchListener { v, event ->
17 | var touchFlag = false
18 | var lastX = 0
19 | var lastY = 0
20 | val touchSlop = ViewConfiguration.get(Utils.getApp()).scaledTouchSlop
21 |
22 | when(event?.actionMasked) {
23 | MotionEvent.ACTION_DOWN -> {
24 | touchFlag = false
25 | lastX = event.rawX.toInt()
26 | lastY = event.rawY.toInt()
27 |
28 | val animator = ObjectAnimator.ofFloat(v, "translationZ", 0f, TRANSLATION_Z)
29 | animator.duration = ANIMATION_DURATION
30 | animator.start()
31 | }
32 | MotionEvent.ACTION_UP -> {
33 | val animator = ObjectAnimator.ofFloat(v, "translationZ", TRANSLATION_Z, 0f)
34 | animator.duration = ANIMATION_DURATION
35 | animator.start()
36 | }
37 | MotionEvent.ACTION_MOVE -> {
38 | touchFlag = true
39 |
40 | val x = event.rawX.toInt() - lastX
41 | val y = event.rawY.toInt() - lastY
42 |
43 | if (x * x + y * y > touchSlop * touchSlop) {
44 | val animator = ObjectAnimator.ofFloat(v, "translationZ", TRANSLATION_Z, 0f)
45 | animator.duration = ANIMATION_DURATION
46 | animator.start()
47 | }
48 | }
49 | }
50 | touchFlag
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/matisse/src/main/res/values/colors_zhihu.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 | #1E8AE8
19 | #176EB9
20 |
21 | #FFFFFF
22 | #DE000000
23 | #999999
24 | #EAEEF4
25 | #4D000000
26 |
27 | #EAEEF4
28 | #1E8AE8
29 | #FFFFFF
30 | #424242
31 |
32 | #FFFFFF
33 | #FFFFFF
34 |
35 | #DE000000
36 | #4D000000
37 | #0077D9
38 | #4D0077D9
39 |
40 | #FFFFFF
41 | #0077D9
42 | #4D0077D9
43 |
44 | #808080
45 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/media/audio/MusicHelper.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.media.audio
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import android.provider.MediaStore
6 | import com.absinthe.kage.media.TYPE_AUDIO
7 | import java.util.*
8 |
9 | object MusicHelper {
10 |
11 | fun getAllLocalMusic(context: Context): List {
12 | val result: MutableList = ArrayList()
13 | val cursor = context.contentResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
14 | null, null, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER)
15 |
16 | if (cursor != null && cursor.moveToFirst()) {
17 | do {
18 | val title = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE))
19 | val album = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM))
20 | val artist = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST))
21 | val path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA))
22 | val albumId = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID))
23 | val artistId = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID))
24 |
25 | if (title.isNotEmpty()) {
26 | val music = LocalMusic().apply {
27 | this.title = title
28 | this.album = album
29 | this.albumId = albumId
30 | this.artist = artist
31 | this.artistId = artistId
32 | this.filePath = path
33 | this.type = TYPE_AUDIO
34 | }
35 | result.add(music)
36 | }
37 | } while (cursor.moveToNext())
38 | cursor.close()
39 | }
40 | return result
41 | }
42 |
43 | fun getAlbumArt(albumId: Long): Uri {
44 | val uriAlbums = "content://media/external/audio/albumart"
45 | return Uri.withAppendedPath(Uri.parse(uriAlbums), albumId.toString())
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/absinthe/kage/connect/protocol/IpMessageConst.kt:
--------------------------------------------------------------------------------
1 | package com.absinthe.kage.connect.protocol
2 |
3 | object IpMessageConst {
4 |
5 | const val VERSION = 0x001 // Version
6 | const val IP_MSG_BR_ENTRY = 0x00000001 //Online
7 | const val IP_MSG_BR_EXIT = 0x00000002 //Offline
8 | const val IP_MSG_ANS_ENTRY = 0x00000003 //Online response
9 | const val IP_MSG_BR_ABSENCE = 0x00000004
10 |
11 | const val HEARTBEAT = 0x00000001 //Heartbeat
12 | const val GET_DEVICE_INFO = 0x00000002 //Get device info
13 | const val PROMPT_PHONE_CONNECT = 0x00000003 //Device connected response
14 | const val MEDIA_SET_PLAYER_STATUS = 0x00000004 //Set player status
15 | const val MEDIA_SET_PLAYING_STATE = 0x00000005 //Set playing state
16 | const val MEDIA_PLAY_PREVIOUS = 0x00000006 //Play previous media
17 | const val MEDIA_PLAY_NEXT = 0x00000007 //Play next media
18 | const val MEDIA_STOP = 0x00000008 //Stop media
19 | const val MEDIA_PREPARE_PLAY = 0x00000009 //Prepare play media
20 | const val MEDIA_IMAGE_INFO = 0x0000000A //Get image info like URI etc.
21 | const val MEDIA_GET_PLAYING_STATE = 0x0000000B //Get player status
22 | const val MEDIA_GET_PLAYER_STATUS = 0x0000000C //Get playing state
23 | const val DEVICE_ROTATION = 0x0000000D //Device rotate
24 | const val MEDIA_AUDIO_INFO = 0x0000000E //Get audio info like URI etc.
25 | const val MEDIA_PAUSE = 0x0000000F //Pause playing media
26 | const val MEDIA_SEEK_TO = 0x00000010 //Media seek to position
27 | const val MEDIA_GET_DURATION = 0x00000011 //Get media duration
28 | const val RESPONSE_SET_PLAYBACK_PROGRESS = 0x00000012 //Set media playback progress
29 | const val RESPONSE_SET_MEDIA_DURATION = 0x00000013 //Set media duration
30 | const val RESPONSE_PLAYING_INDEX = 0x00000014
31 | const val RESPONSE_SET_AUDIO_MODE = 0x00000015
32 | const val MEDIA_GET_PLAYING_POSITION = 0x00000016
33 | const val MEDIA_PLAY_AUDIO_LIST = 0x00000017
34 | const val MEDIA_SET_AUDIO_MODE = 0x00000018
35 | const val MEDIA_SET_PLAY_INDEX = 0x00000019
36 | const val MEDIA_RESUME_PLAY = 0x0000001A
37 | const val MEDIA_VIDEO_INFO = 0x0000001B //Get video info like URI etc.
38 | const val KEY_EVENT = 0x0000001C //Send key event
39 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_service_running_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
19 |
20 |
25 |
26 |
32 |
33 |
39 |
40 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_android_outline.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/matisse/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
20 | All Media
21 |
22 | Preview
23 | Apply
24 | Apply(%d)
25 | Cast
26 | Stop Cast
27 | Back
28 | Camera
29 | No media yet
30 | OK
31 |
32 | You have reached max selectable
33 | You can only select up to %1$d media files
34 | Under quality
35 | Over quality
36 | Unsupported file type
37 | Can\'t select images and videos at the same time
38 | No App found supporting video preview
39 | Can\'t select the images larger than %1$d MB
40 | %1$d images over %2$d MB. Original will be unchecked
41 | Original
42 | Sure
43 | Sure(%1$d)
44 |
45 |
--------------------------------------------------------------------------------
/matisse/src/main/java/com/zhihu/matisse/engine/impl/PicassoEngine.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Zhihu Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.zhihu.matisse.engine.impl;
17 |
18 | import android.content.Context;
19 | import android.graphics.drawable.Drawable;
20 | import android.net.Uri;
21 | import android.widget.ImageView;
22 |
23 | import com.squareup.picasso.Picasso;
24 | import com.zhihu.matisse.engine.ImageEngine;
25 |
26 | /**
27 | * {@link ImageEngine} implementation using Picasso.
28 | */
29 |
30 | public class PicassoEngine implements ImageEngine {
31 |
32 | @Override
33 | public void loadThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView, Uri uri) {
34 | Picasso.with(context).load(uri).placeholder(placeholder)
35 | .resize(resize, resize)
36 | .centerCrop()
37 | .into(imageView);
38 | }
39 |
40 | @Override
41 | public void loadGifThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView,
42 | Uri uri) {
43 | loadThumbnail(context, resize, placeholder, imageView, uri);
44 | }
45 |
46 | @Override
47 | public void loadImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) {
48 | Picasso.with(context).load(uri).resize(resizeX, resizeY).priority(Picasso.Priority.HIGH)
49 | .centerInside().into(imageView);
50 | }
51 |
52 | @Override
53 | public void loadGifImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) {
54 | loadImage(context, resizeX, resizeY, imageView, uri);
55 | }
56 |
57 | @Override
58 | public boolean supportAnimatedGif() {
59 | return false;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/matisse/src/main/java/com/zhihu/matisse/filter/Filter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Zhihu Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.zhihu.matisse.filter;
17 |
18 | import android.content.Context;
19 |
20 | import com.zhihu.matisse.MimeType;
21 | import com.zhihu.matisse.SelectionCreator;
22 | import com.zhihu.matisse.internal.entity.Item;
23 | import com.zhihu.matisse.internal.entity.IncapableCause;
24 |
25 | import java.util.Set;
26 |
27 | /**
28 | * Filter for choosing a {@link Item}. You can add multiple Filters through
29 | * {@link SelectionCreator#addFilter(Filter)}.
30 | */
31 | @SuppressWarnings("unused")
32 | public abstract class Filter {
33 | /**
34 | * Convenient constant for a minimum value.
35 | */
36 | public static final int MIN = 0;
37 | /**
38 | * Convenient constant for a maximum value.
39 | */
40 | public static final int MAX = Integer.MAX_VALUE;
41 | /**
42 | * Convenient constant for 1024.
43 | */
44 | public static final int K = 1024;
45 |
46 | /**
47 | * Against what mime types this filter applies.
48 | */
49 | protected abstract Set constraintTypes();
50 |
51 | /**
52 | * Invoked for filtering each item.
53 | *
54 | * @return null if selectable, {@link IncapableCause} if not selectable.
55 | */
56 | public abstract IncapableCause filter(Context context, Item item);
57 |
58 | /**
59 | * Whether an {@link Item} need filtering.
60 | */
61 | protected boolean needFiltering(Context context, Item item) {
62 | for (MimeType type : constraintTypes()) {
63 | if (type.checkType(context.getContentResolver(), item.getContentUri())) {
64 | return true;
65 | }
66 | }
67 | return false;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Kage
4 | Connect
5 | Cast
6 | Receiver
7 | Music List
8 | About
9 |
10 |
11 | Kage service is running
12 | Kage service is not running
13 | Tap to start service
14 | Tap to stop service
15 |
16 |
17 | Empty List
18 | Please make sure your device is on the same LAN and Kage service is turning on
19 | Connecting
20 | Connected
21 | Connect
22 |
23 |
24 | Cast Image
25 | Cast Video
26 | Cast Music
27 |
28 |
29 | Loading
30 | Save
31 | Do you want to save this media to device?
32 | Saving…
33 | Saving Success
34 |
35 |
36 | Kage - An app which can cast multi-media files to other Android devices.
37 |
38 |
39 | Kage Service is running
40 | Tap to back home page
41 |
42 |
43 | Connected
44 | Please grant Storage permissions
45 |
46 |
47 | Cast to Device
48 | Cast
49 |
50 |
51 |
--------------------------------------------------------------------------------