Compare commits

..

181 Commits

Author SHA1 Message Date
B. Petersen 46463f13c6 Bump version. 2017-04-22 22:12:44 +02:00
B. Petersen 35c8fb2399 Update changelog. 2017-04-22 22:09:48 +02:00
B. Petersen 8cda23ccaf Update messenger-backend submodule. 2017-04-22 21:56:54 +02:00
B. Petersen f7831cc6a0 Update messenger-backend submodule. 2017-04-22 21:50:58 +02:00
B. Petersen 2be0a78f6f Merge branch 'master' of https://github.com/r10s/deltachat-android 2017-04-22 21:39:54 +02:00
B. Petersen b505d9b020 Find out LTR/RTL in a compatible way; however, RTL would require some polishing. 2017-04-22 21:39:40 +02:00
B. Petersen 9320d992b5 Create time formatter using always the current locale - or why was Locale.US used for everything but Arabic and Korean? 2017-04-22 21:38:12 +02:00
Dr. Tobias Quathamer c9f81e447a The minimum SDK version is already 14, so the last 'else' is never reached 2017-04-22 21:29:47 +02:00
B. Petersen 9d5edba4aa Remove unused name-order code. 2017-04-22 21:18:22 +02:00
B. Petersen f689b2fb94 Rely on defaults for the vertical scrollbar position. 2017-04-22 20:47:55 +02:00
Dr. Tobias Quathamer 0c22f40bf0 Fix typos 2017-04-22 18:31:14 +02:00
Dr. Tobias Quathamer 8957823dae Use Gravity.START and .END rather than .LEFT and .RIGHT 2017-04-22 18:28:10 +02:00
B. Petersen 243ec39eb8 Stop GIFs by a long click, (re-)start by a normal click. 2017-04-22 18:01:59 +02:00
B. Petersen 529435316a Add option to disable 'GIF autoplay'. 2017-04-22 16:52:53 +02:00
B. Petersen f541fd668b Faster searching and selection (we avoid reloading data from disk whereever possible). 2017-04-22 15:53:53 +02:00
B. Petersen ec29371d3f Simplify 'Reset all notifications' button. 2017-04-22 14:36:15 +02:00
B. Petersen 1add172770 Play GIFs. 2017-04-22 14:09:40 +02:00
B. Petersen 37f401f2cd Remove dead code. 2017-04-22 11:14:37 +02:00
B. Petersen f88c8231eb Check for 24-hour format when creating the formatters. 2017-04-22 10:36:04 +02:00
B. Petersen 04b318f753 Comment some locale functions (well, the rest of it ;-) 2017-04-22 10:29:19 +02:00
B. Petersen 2af0e1ffb6 Revert "Remove unnecessary LocaleController calls"
This reverts commit 6d26bbd3fb.
2017-04-22 10:16:35 +02:00
B. Petersen 806ed69432 Revert "Remove unneeded methods"
This reverts commit 58de8d2830.
2017-04-22 10:16:16 +02:00
B. Petersen 491e5e0190 Merge branch 'master' of https://github.com/r10s/deltachat-android 2017-04-22 09:57:34 +02:00
B. Petersen 93d9d053ee Remove only partly translated permantent notification strings (they came from renaming a similar string). 2017-04-22 09:56:43 +02:00
Dr. Tobias Quathamer 58de8d2830 Remove unneeded methods 2017-04-22 07:15:35 +02:00
Dr. Tobias Quathamer 6d26bbd3fb Remove unnecessary LocaleController calls 2017-04-22 07:08:45 +02:00
B. Petersen e7c432e0de Get rid of the local locale dictionary. 2017-04-22 01:15:59 +02:00
B. Petersen 8c00d6e09e Do not load the locale from the config file. 2017-04-22 00:46:10 +02:00
B. Petersen 310d8f79bc Merge branch 'master' of https://github.com/r10s/deltachat-android 2017-04-21 23:59:37 +02:00
B. Petersen 8127e3427e Update Portuguese. 2017-04-21 23:56:33 +02:00
Dr. Tobias Quathamer 4a12b055e7 Refactor: Remove LocaleController.formatStringSimple() method 2017-04-21 21:21:36 +02:00
Dr. Tobias Quathamer de7c71931c Refactor: Remove LocaleController.formatString() method 2017-04-21 21:17:58 +02:00
Dr. Tobias Quathamer 5a6fc7761c Refactor: Finally remove LocaleController.getString calls completely 2017-04-21 20:48:07 +02:00
Dr. Tobias Quathamer 7410cb21a0 Refactor: Remove LocaleController.getString calls 2017-04-21 20:45:16 +02:00
Dr. Tobias Quathamer 99d4142c13 Refactor: Remove LocaleController.getString calls 2017-04-21 20:38:40 +02:00
Dr. Tobias Quathamer 27de528e57 Refactor: Remove LocaleController.getString calls 2017-04-21 19:46:31 +02:00
Dr. Tobias Quathamer 521360f667 Refactor: Remove LocaleController.getString calls 2017-04-21 19:40:14 +02:00
Dr. Tobias Quathamer 4e003ec6f5 Refactor: Remove LocaleController.getString calls 2017-04-21 19:35:43 +02:00
Dr. Tobias Quathamer 98c8a0c550 Refactor: Remove LocaleController.getString calls 2017-04-21 18:43:42 +02:00
Dr. Tobias Quathamer 039eb0d924 Refactor: Remove LocaleController.getString calls 2017-04-21 18:40:12 +02:00
Dr. Tobias Quathamer e7636cee4e Refactor: Remove LocaleController.getString calls 2017-04-21 18:36:48 +02:00
Dr. Tobias Quathamer 4b844ebb71 Refactor: Remove LocaleController.getString calls 2017-04-21 18:34:05 +02:00
Dr. Tobias Quathamer c704ed4898 Refactor: Remove LocaleController.getString calls 2017-04-21 18:19:54 +02:00
Dr. Tobias Quathamer ada4ef461e Refactor: Remove LocaleController.getString calls 2017-04-21 18:17:31 +02:00
Dr. Tobias Quathamer 8dd4193f4e Refactor: Remove LocaleController.getString calls 2017-04-21 18:12:42 +02:00
Dr. Tobias Quathamer 704d6f0516 Refactor: Remove LocaleController.getString calls 2017-04-21 18:01:29 +02:00
Dr. Tobias Quathamer 097f3e8149 Refactor: Remove LocaleController.getString calls 2017-04-21 17:59:42 +02:00
Dr. Tobias Quathamer 9d8988b08b Refactor: Remove LocaleController.getString calls 2017-04-21 17:57:12 +02:00
Dr. Tobias Quathamer 0dcdc2d0bf Refactor: Remove LocaleController.getString calls 2017-04-21 17:50:57 +02:00
Dr. Tobias Quathamer 83ed86575e Refactor: Remove LocaleController.getString calls 2017-04-21 17:48:22 +02:00
Dr. Tobias Quathamer 40e7b08238 Refactor: Remove LocaleController.getString calls 2017-04-21 17:37:49 +02:00
Dr. Tobias Quathamer 0ac7648bd6 Refactor: Remove LocaleController.getString calls 2017-04-21 17:28:46 +02:00
Dr. Tobias Quathamer 41f1233a27 Update changelog 2017-04-21 17:18:05 +02:00
Dr. Tobias Quathamer 0bba3d94c7 Rename id which has been assigned twice (lines 42 and 93) 2017-04-21 17:17:46 +02:00
Dr. Tobias Quathamer 67c5b1a281 Remove unneeded LanguageSelectActivity class 2017-04-21 17:08:10 +02:00
Dr. Tobias Quathamer dbf6c98ddb Remove now unused code 2017-04-21 17:00:52 +02:00
Dr. Tobias Quathamer af666374ea Remove languageRow from settings 2017-04-21 16:58:02 +02:00
Dr. Tobias Quathamer 45d10ab435 Refactor: Remove LocaleController.getString calls 2017-04-21 16:56:14 +02:00
Dr. Tobias Quathamer 602ad36e6f Refactor: Remove method getStringInternal() 2017-04-21 16:48:28 +02:00
B. Petersen 6fc3c38b28 Remove unused resources. 2017-04-21 15:59:54 +02:00
B. Petersen a0e2aed195 Show long-press-hint for some selected icons in the action bar. 2017-04-21 15:59:32 +02:00
B. Petersen 9aff89ea9a Language. 2017-04-21 13:44:27 +02:00
B. Petersen 60a8ab9bcf Remove empty strings to force english defaults. 2017-04-21 10:12:31 +02:00
B. Petersen 0d21883546 Add 'Details' button to about screen. 2017-04-21 10:10:31 +02:00
B. Petersen 9fde45d272 Use system-default hardware-acceleration (enabled by default since target API 14, T'gram disabled it for API 14 and 15, for whatever reason). 2017-04-20 18:03:34 +02:00
B. Petersen 61c15ea56d Bump version. 2017-04-20 17:32:15 +02:00
B. Petersen d821e43fe9 Remove tablet detection. The isTablet() code is potentially buggy as not really tested; we should not use it therefore. _If_ we want to handle tablets specially, we can do this by just checking the screen size in the concrete situation. We should do this as less as possible. 2017-04-20 17:18:31 +02:00
B. Petersen 03e5ee1098 Make intro screen accessible from settings page, simplify intro screen layout. 2017-04-20 17:07:56 +02:00
Dr. Tobias Quathamer 646b2f028c Revert 38d3c6c6 for now 2017-04-20 14:37:07 +02:00
Dr. Tobias Quathamer 794bdeafce Update sqlite to version 3.18.0, released on 2017-03-28 2017-04-20 14:26:16 +02:00
Dr. Tobias Quathamer bd4d3b3e11 Remove a few LocaleController calls 2017-04-20 14:18:34 +02:00
B. Petersen 58c8cecee8 Merge branch 'master' of https://github.com/r10s/deltachat-android 2017-04-20 14:05:32 +02:00
B. Petersen 801406ce58 Use only authorized names when attaching contacts to a chat. 2017-04-20 14:05:14 +02:00
Dr. Tobias Quathamer 20a93a30b3 Remove a few LocaleController calls 2017-04-20 13:53:43 +02:00
Dr. Tobias Quathamer 38d3c6c634 Use AppCompat* classes 2017-04-20 12:47:23 +02:00
Dr. Tobias Quathamer 43bf4a4616 Remove outdated translations 2017-04-20 12:37:01 +02:00
Dr. Tobias Quathamer 059edeaf18 Korean has only one plural form, so remove unneeded items 2017-04-20 12:33:02 +02:00
Dr. Tobias Quathamer e1e9462fb1 Remove wrong translation 2017-04-20 12:31:01 +02:00
Dr. Tobias Quathamer 125e70c04b Update appcompat v7 library to current stable release 2017-04-20 12:27:48 +02:00
Dr. Tobias Quathamer e4fc9502fe Update gradle 2017-04-20 12:26:37 +02:00
B. Petersen 72a6627059 Update permanent forground notification on language change. 2017-04-19 22:18:01 +02:00
B. Petersen 73cf5105dd Update Spanish translation. 2017-04-19 22:09:08 +02:00
Björn Petersen c69e128e83 Update README.md 2017-04-19 18:06:06 +02:00
B. Petersen fcb037122d Update messenger-backend submodule. 2017-04-19 14:33:14 +02:00
B. Petersen 7eab23aa19 Update changelog. 2017-04-19 14:32:46 +02:00
B. Petersen 4d839e40ca Fix Russian. 2017-04-19 11:31:47 +02:00
Björn Petersen 7a06e3ac0c Merge pull request #74 from guland2000/patch-1
Russian language strings.xml
2017-04-19 11:04:32 +02:00
guland2000 8392c34f65 Update strings_russian.xml 2017-04-16 21:59:28 +02:00
guland2000 67b8486b99 Russian language string.xml 2017-04-16 20:10:01 +02:00
B. Petersen 308018e6e6 Bump version. 2017-04-14 16:49:46 +02:00
Björn Petersen 6c625d078f Update CHANGELOG.md 2017-04-14 14:29:47 +02:00
B. Petersen a69afe11f4 Pimp notifications. 2017-04-13 23:55:51 +02:00
B. Petersen 4ff15e433b Use 'white' as default LED-color, show text 'Default' if the default LED-color is selected (instead of a white circle on white background). 2017-04-13 23:03:02 +02:00
B. Petersen 6c81d03c50 Use delta-chat default sound if choosing 'default' in the chat-sound-settings. 2017-04-13 22:37:15 +02:00
B. Petersen 9f0b322249 Remove unsued strings that have no default. 2017-04-13 16:26:41 +02:00
B. Petersen e8831b0dfc Remove bad libEtPan version declaration. 2017-04-13 13:58:53 +02:00
B. Petersen d6e9ba2ea9 Rewording the 'Mute for' options - thanks for the hint, Tobi. 2017-04-13 12:33:41 +02:00
B. Petersen 2c8c5a7558 Adapt copyright note. 2017-04-13 11:05:30 +02:00
B. Petersen d2e76a5e61 Pimp 'Notifications and sounds' dialog. 2017-04-12 23:02:54 +02:00
B. Petersen 2263c24019 Typo 2017-04-12 14:13:35 +02:00
B. Petersen 19e414e65c Update messenger-backend submodule. 2017-04-12 14:11:22 +02:00
B. Petersen f5a52c1aa2 Changelog. 2017-04-12 14:10:09 +02:00
B. Petersen 1cf5039c81 Additional heartbeat call. 2017-04-12 11:36:07 +02:00
B. Petersen 90a5e39872 Personalize needed foreground notification. 2017-04-11 14:44:24 +02:00
B. Petersen 30868b107e Remove dead code. 2017-04-11 12:37:16 +02:00
B. Petersen 546d4a3bda Remove dead code. 2017-04-11 12:23:11 +02:00
B. Petersen 33eaf71e5b Remove currently unused AOSP Volley. 2017-04-11 12:08:15 +02:00
B. Petersen a61e73b91b Improve color selector, selection is still a little bit weird, however, much better. 2017-04-10 22:51:10 +02:00
B. Petersen f03a3a9b50 New background-active-icon. 2017-04-10 21:09:56 +02:00
B. Petersen e2225be331 Installing an permanent foreground service to satisfy Googles battery optimization stuff, hopefully, *REQUEST_IGNORE_BATTERY_OPTIMIZATIONS (which is only possible with the mercy of Google) is no longer needed. 2017-04-10 18:11:52 +02:00
B. Petersen f1f23a5099 Additionally log to a file to fix temporary notification problems. 2017-04-10 13:30:13 +02:00
B. Petersen db3bcb017c Remove void FileLog commands. 2017-04-10 12:39:50 +02:00
B. Petersen ab0ce1393e Remove void FileLog commands. 2017-04-10 12:01:56 +02:00
B. Petersen b941b9e21a Request the user to ignore the battery optimizations which avoid running Delta Chat properly. 2017-04-07 19:36:05 +02:00
B. Petersen b838e0d69e If possible, use Java's Log-class instead of JNI's __android_log_print() which is not available on some devices (eg. LG X Cam). 2017-04-05 16:11:56 +02:00
B. Petersen 8459f5552d Avoid global JNI-namespace pollution. 2017-04-05 14:35:28 +02:00
B. Petersen 293d249024 Changelog. 2017-04-04 22:48:15 +02:00
B. Petersen 38a39aec17 Slight logo update. 2017-04-04 22:17:28 +02:00
B. Petersen f4fb855cb6 Add vertical scrollbar, where appropriate, and use system 'glow' color. 2017-04-04 18:49:24 +02:00
B. Petersen 8fa34e8659 Do not connect if we're not configured (otherwise a 'not connected' error pops up during the first app run). 2017-04-04 17:59:32 +02:00
B. Petersen eb2f2de29c Remove Exoplayer, we prefer the native video player. 2017-03-31 21:23:16 +02:00
B. Petersen 1b2c5a028a Remove dead http-code, still continued. 2017-03-31 20:09:24 +02:00
B. Petersen 4057c08df9 Remove dead http-code, still continued. 2017-03-31 20:05:32 +02:00
B. Petersen 074d2054ec Remove dead http-code, continued. 2017-03-31 19:50:53 +02:00
B. Petersen cf2ea4cc56 Remove dead http-code. 2017-03-31 19:34:49 +02:00
B. Petersen cff6b6bba7 Update Korean. 2017-03-31 12:56:00 +02:00
B. Petersen 595111fb56 Update messenger-backend submodule. 2017-03-31 11:41:53 +02:00
B. Petersen cfc0485579 Changelog. 2017-03-31 11:40:20 +02:00
B. Petersen 7e7d4176f2 Adapt layout. 2017-03-31 11:36:54 +02:00
B. Petersen 8abaa7bc02 Dynamically adapt video bitrate for longer videos to an attachment-size of max. 25 MB. 2017-03-31 01:40:38 +02:00
B. Petersen 714e079b17 Split video-result-calculation from original-value-loading. 2017-03-31 00:38:10 +02:00
B. Petersen 6983491586 Check, if we can really compress videos. 2017-03-31 00:02:30 +02:00
B. Petersen 2cea64ccff Remove unneeded file loader calls. 2017-03-30 23:35:25 +02:00
B. Petersen 0ef81716c5 Use system 'CheckBox' instead of user-defined 'Switch'; the latter is not a perfect choice in a list of settings, see Android GUI guidelines, and did never worked well, eg. moving the switch did never worked as expected. 2017-03-30 21:14:19 +02:00
B. Petersen c780ddf081 Support intents with mailto:-links, show preview before sending a message. 2017-03-30 17:55:43 +02:00
B. Petersen 30b778c4f6 Allow sharing images or documents from other apps to Delta Chat. 2017-03-29 21:50:57 +02:00
B. Petersen 5e0c7553e2 Show error toasts only if the app is in foreground. 2017-03-29 21:19:44 +02:00
B. Petersen b5fe0acd89 Cleanup. 2017-03-29 21:13:15 +02:00
B. Petersen 01551fe71a Cleanup. 2017-03-29 20:35:08 +02:00
B. Petersen edd4176890 Update messenger-backend submodule. 2017-03-28 16:00:23 +02:00
B. Petersen f4f27ac0ae Changelog. 2017-03-28 16:00:04 +02:00
B. Petersen aeb297548c Remove unused variable. 2017-03-28 11:04:52 +02:00
B. Petersen eb3aa46ff7 After the app is paused, show messages the next 10 minutes immediately. Affter that, we sleep most time (11/12th=55 seconds) of a minute. 2017-03-28 10:59:50 +02:00
B. Petersen 05fbf5bdce Add missing file. 2017-03-27 22:17:03 +02:00
B. Petersen 8a5f05fe2e Create an alert that wakes up the app from time to time; if there's stuff to do, the backend acquires a wake loke then. Additionally, if the app is just dismissed, we keep another wakelock for some minutes to catch messages the user just expects in that moment. 2017-03-27 22:16:26 +02:00
B. Petersen 590841d139 Clearer bootup-code naming. 2017-03-24 19:36:16 +01:00
B. Petersen 44caf01cae Cleanup code to load the native library. 2017-03-24 17:11:31 +01:00
B. Petersen db36341251 Cleanup. 2017-03-24 15:43:28 +01:00
B. Petersen 0a63816ece Acquire wakeLock to receive notifications if 'keep-alive' is on. 2017-03-23 22:18:17 +01:00
B. Petersen ceb0155f5f Cleanup keep-alive code. 2017-03-23 21:01:36 +01:00
B. Petersen 723e677b2b Use more general text 'Audio' instead of 'Music' for audio-files with unknown content. 2017-03-23 16:00:11 +01:00
B. Petersen fab24ee956 Update messenger-backend submodule. 2017-03-22 23:26:07 +01:00
B. Petersen 94f4c8b302 Update changelog. 2017-03-22 23:25:50 +01:00
B. Petersen 008a2410a7 Update changelog. 2017-03-22 15:55:46 +01:00
B. Petersen b43f337af9 Show language name in english in language select activity. 2017-03-22 15:49:56 +01:00
Björn Petersen 21895cf00f Merge pull request #59 from aligitor/master
Add Hungarian translation.
2017-03-22 15:31:53 +01:00
aligitor 721d016d86 Create strings.xml
Hungarian translation file.
2017-03-22 12:42:38 +01:00
B. Petersen 89aaa9c072 Show percentage in the configuration progress dialog. 2017-03-22 00:04:27 +01:00
B. Petersen bcb00a9d23 Add IMAP/SMTP security options. 2017-03-20 21:11:59 +01:00
B. Petersen 34cd40fdd9 Support HTML-only messages. 2017-03-18 11:18:35 +01:00
B. Petersen c8001cedce Provide an http/https-get function for the backend. 2017-03-15 22:23:28 +01:00
B. Petersen 880353888b Add input types for the account configuration dialog. 2017-03-15 21:04:10 +01:00
B. Petersen 2c67d20233 Add options for SSL/TLS and STARTTLS. 2017-03-15 16:32:15 +01:00
B. Petersen 42c08a4d5b Test IMAP and SMTP server when calling the configure-function. 2017-03-14 18:25:20 +01:00
B. Petersen 01bf0f3af5 Remove unused notifications. 2017-03-14 11:41:29 +01:00
B. Petersen e765968c2c Language (do not show 'configuring account' message as the may be interpreted as we do something with the server settings). 2017-03-14 11:09:49 +01:00
B. Petersen 3837463e41 Use larger labels for the basic account settings. 2017-03-13 23:37:13 +01:00
B. Petersen 545a2ddc24 Hide advanced account settings by default (in general, we'll try to find them out automatically). 2017-03-13 23:12:56 +01:00
B. Petersen 4f6f0b4685 Cleanup cache settings code. 2017-03-13 19:36:41 +01:00
B. Petersen 1d1bad8b3b Fine-tuning error handling. 2017-03-13 14:00:30 +01:00
B. Petersen af5769f231 Improve error handling. 2017-03-12 21:03:28 +01:00
B. Petersen 42c6337c4d Show popup errors. 2017-03-12 00:33:08 +01:00
B. Petersen 37a49c9d39 Add Korean. 2017-03-11 22:41:45 +01:00
B. Petersen 3b2f778114 Repair 'Default language' option and cleanup some language stuff. 2017-03-11 21:10:31 +01:00
B. Petersen b55cc03f6d Correct string. 2017-03-11 18:31:02 +01:00
B. Petersen 2e6eead045 Change the order of the account input fields. 2017-03-11 18:17:44 +01:00
B. Petersen e77c3303a1 Show versionCode in about dialog. 2017-03-11 11:55:32 +01:00
B. Petersen e83c4f315c Force unique video names in the blob directory. 2017-03-11 11:35:34 +01:00
B. Petersen 833c75d874 Change order of 'new group icons'. 2017-03-11 10:45:47 +01:00
B. Petersen 9688003648 Force video-recoding for videos which size is okay but the bitrate is too high. Correct maximum bitrate handing (was checked too son before and scaled down afterwards, on typical devices, this resulted in a bitrate of about 500 kbit which we use as a maximum now; everyone is welcome to improve all this stuff). 2017-03-10 19:15:54 +01:00
B. Petersen a2609d5bbe Catch video thumbnail creation errors. 2017-03-10 19:11:49 +01:00
519 changed files with 18183 additions and 72730 deletions
+66
View File
@@ -1,5 +1,71 @@
# Delta Chat Changelog
## v0.1.32
2017-04-22
* Update Spanish and Portuguese translations
* Update internal sqlite library to version 3.18.0, released on 2017-03-28
* Remove more of the custom language handling, use Android's routines instead
* General code cleanup
* Play GIF files
* Option to disable autoplaying GIF files
* When sending contacts, only use the names the receivers have set themselves
* Show some hints when long-pressing icons in the action bar
## v0.1.29
2017-04-19
* Add Russian translation
* For outgoing (group-)messages, only use the names the receivers have set themselves
## v0.1.28
2017-04-14
* Pimp notifications
* Bug fixes
## v0.1.27
2017-04-12
* Use a permanent foreground service for reliable notifications
* Monitor the IMAP-IDLE thread and reconnect if IMAP-IDLE seems to hang
* Various battery and background optimizations
## v0.1.25
2017-04-04
* Use system or user selected video player.
* Do not connect if not configured (avoids a warning on the first time startup)
* Add vertical scrollbar, eg. to settings activities.
* Pimp GUI and logo.
* Update Korean.
## v0.1.24
2017-03-31
* Share images and documents from other apps to Delta Chat
* Offer to mailto:-link-support to other apps
* Ignore implausible sending time of incoming messages; use the receive time in these rare cases
* Show errors only when Delta Chat is in foreground
* Dynamically adapt video bitrate for longer videos to an attachment-size of max. 25 MB
## v0.1.23
2017-03-28
* Retry connecting to IMAP if there is not network available on the first try
* Notify about new messages if the app is not active for hours, optimize battery consumption
## v0.1.22
2017-03-22
* Show HTML-only messages
* Show connection errors
* Add options for SSL/TLS and STARTTLS
* Automatic account configuration, if possible
* Recode large videos
* Add Hungarian translation
* Add Korean translation
## v0.1.21
2017-03-10
+5 -5
View File
@@ -24,7 +24,7 @@ tasks.withType(JavaCompile) {
}
dependencies {
compile 'com.android.support:appcompat-v7:25.1.1'
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.googlecode.mp4parser:isoparser:1.0.6'
compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
compile fileTree(dir: "$buildDir/native-libs", include: 'native-libs.jar')
@@ -76,7 +76,7 @@ android {
}
}
defaultConfig.versionCode = 20
defaultConfig.versionCode = 32
sourceSets.main {
jniLibs.srcDir 'libs'
@@ -115,8 +115,8 @@ android {
}
defaultConfig {
minSdkVersion 14 // 14: Android 4.0 Ice Cream Sandwich 2011 (Telegram default), 21: Android 5.0 Lollipop 2014 (recommended for InstantRun)
targetSdkVersion 25
versionName "0.1.21" // do NOT forget to increase defaultConfig.versionCode!
minSdkVersion 14 // 14: Android 4.0 Ice Cream Sandwich 2011 (Telegram default), 21: Android 5.0 Lollipop 2014 (recommended for InstantRun)
targetSdkVersion 25 // 25: Nougat. CAVE: Do NOT target "Andoid O" without checking the background tasks carefully, see https://developer.android.com/preview/behavior-changes.html#back-all . As long as we target "Nougat", everything works as expected even for "Andoid O" or later
versionName "0.1.32" // do NOT forget to increase defaultConfig.versionCode!
}
}
+4 -3
View File
@@ -1598,9 +1598,8 @@ ifeq ($(TARGET_ARCH_ABI),armeabi-v7a)
endif
LOCAL_SRC_FILES += \
./jni.c \
./mrjnimain.c \
./audio.c \
./utils.c \
./image.c \
./video.c \
./gifvideo.cpp \
@@ -1611,14 +1610,16 @@ LOCAL_SRC_FILES += \
./messenger-backend/src/mre2ee.c \
./messenger-backend/src/mrimap.c \
./messenger-backend/src/mrjob.c \
./messenger-backend/src/mrlog.c \
./messenger-backend/src/mrloginparam.c \
./messenger-backend/src/mrmailbox.c \
./messenger-backend/src/mrmailbox_configure.c \
./messenger-backend/src/mrmailbox_log.c \
./messenger-backend/src/mrmimeparser.c \
./messenger-backend/src/mrmsg.c \
./messenger-backend/src/mrosnative.c \
./messenger-backend/src/mrparam.c \
./messenger-backend/src/mrpoortext.c \
./messenger-backend/src/mrsaxparser.c \
./messenger-backend/src/mrsimplify.c \
./messenger-backend/src/mrsmtp.c \
./messenger-backend/src/mrsqlite3.c \
+64 -40
View File
@@ -1,3 +1,26 @@
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2013-2016 Nikolai Kudashov
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see http://www.gnu.org/licenses/ .
*
******************************************************************************/
#include <jni.h>
#include <ogg/ogg.h>
#include <stdio.h>
@@ -6,7 +29,7 @@
#include <time.h>
#include <opusfile.h>
#include <math.h>
#include "utils.h"
#include "mrjnimain.h"
typedef struct {
int version;
@@ -115,7 +138,7 @@ static int read_chars(ROPacket *p, unsigned char *str, int nb_chars)
return 1;
}
int opus_header_to_packet(const OpusHeader *h, unsigned char *packet, int len) {
static int opus_header_to_packet(const OpusHeader *h, unsigned char *packet, int len) {
int i;
Packet p;
unsigned char ch;
@@ -219,34 +242,34 @@ static int writeOggPage(ogg_page *page, FILE *os) {
return written;
}
const opus_int32 bitrate = 16000;
const opus_int32 rate = 16000;
const opus_int32 frame_size = 960;
const int with_cvbr = 1;
const int max_ogg_delay = 0;
const int comment_padding = 512;
static const opus_int32 bitrate = 16000;
static const opus_int32 rate = 16000;
static const opus_int32 frame_size = 960;
static const int with_cvbr = 1;
static const int max_ogg_delay = 0;
static const int comment_padding = 512;
opus_int32 coding_rate = 16000;
ogg_int32_t _packetId;
OpusEncoder *_encoder = 0;
uint8_t *_packet = 0;
ogg_stream_state os;
FILE *_fileOs = 0;
oe_enc_opt inopt;
OpusHeader header;
opus_int32 min_bytes;
int max_frame_bytes;
ogg_packet op;
ogg_page og;
opus_int64 bytes_written;
opus_int64 pages_out;
opus_int64 total_samples;
ogg_int64_t enc_granulepos;
ogg_int64_t last_granulepos;
int size_segments;
int last_segments;
static opus_int32 coding_rate = 16000;
static ogg_int32_t _packetId;
static OpusEncoder *_encoder = 0;
static uint8_t *_packet = 0;
static ogg_stream_state os;
static FILE *_fileOs = 0;
static oe_enc_opt inopt;
static OpusHeader header;
static opus_int32 min_bytes;
static int max_frame_bytes;
static ogg_packet op;
static ogg_page og;
static opus_int64 bytes_written;
static opus_int64 pages_out;
static opus_int64 total_samples;
static ogg_int64_t enc_granulepos;
static ogg_int64_t last_granulepos;
static int size_segments;
static int last_segments;
void cleanupRecorder() {
static void cleanupRecorder() {
ogg_stream_flush(&os, &og);
@@ -282,7 +305,7 @@ void cleanupRecorder() {
memset(&og, 0, sizeof(ogg_page));
}
int initRecorder(const char *path) {
static int initRecorder(const char *path) {
cleanupRecorder();
if (!path) {
@@ -421,7 +444,7 @@ int initRecorder(const char *path) {
return 1;
}
int writeFrame(uint8_t *framePcmBytes, unsigned int frameByteCount) {
static int writeFrame(uint8_t *framePcmBytes, unsigned int frameByteCount) {
int cur_frame_size = frame_size;
_packetId++;
@@ -527,15 +550,15 @@ JNIEXPORT void Java_com_b44t_messenger_MediaController_stopRecord(JNIEnv *env, j
}
//player
OggOpusFile *_opusFile;
int _isSeekable = 0;
int64_t _totalPcmDuration = 0;
int64_t _currentPcmOffset = 0;
int _finished = 0;
static OggOpusFile *_opusFile;
static int _isSeekable = 0;
static int64_t _totalPcmDuration = 0;
static int64_t _currentPcmOffset = 0;
static int _finished = 0;
static const int playerBuffersCount = 3;
static const int playerSampleRate = 48000;
void cleanupPlayer() {
static void cleanupPlayer() {
if (_opusFile) {
op_free(_opusFile);
_opusFile = 0;
@@ -546,7 +569,7 @@ void cleanupPlayer() {
_finished = 0;
}
int seekPlayer(float position) {
static int seekPlayer(float position) {
if (!_opusFile || !_isSeekable || position < 0) {
return 0;
}
@@ -559,7 +582,7 @@ int seekPlayer(float position) {
return result == OPUS_OK;
}
int initPlayer(const char *path) {
static int initPlayer(const char *path) {
cleanupPlayer();
int openError = OPUS_OK;
@@ -576,7 +599,7 @@ int initPlayer(const char *path) {
return 1;
}
void fillBuffer(uint8_t *buffer, int capacity, int *args) {
static void fillBuffer(uint8_t *buffer, int capacity, int *args) {
if (_opusFile) {
args[1] = max(0, op_pcm_tell(_opusFile));
@@ -735,7 +758,7 @@ JNIEXPORT jbyteArray Java_com_b44t_messenger_MediaController_getWaveform2(JNIEnv
return result;
}
int16_t *sampleBuffer = NULL;
static int16_t *sampleBuffer = NULL;
JNIEXPORT jbyteArray Java_com_b44t_messenger_MediaController_getWaveform(JNIEnv *env, jclass class, jstring path) {
@@ -819,3 +842,4 @@ JNIEXPORT jbyteArray Java_com_b44t_messenger_MediaController_getWaveform(JNIEnv
return result;
}
+30 -11
View File
@@ -1,5 +1,28 @@
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2013-2016 Nikolai Kudashov
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see http://www.gnu.org/licenses/ .
*
******************************************************************************/
#include <jni.h>
#include <utils.h>
#include "mrjnimain.h"
#include <libyuv.h>
#include <android/bitmap.h>
#include <cstdint>
@@ -53,19 +76,14 @@ typedef struct VideoInfo {
AVPacket orig_pkt;
};
jobject makeGlobarRef(JNIEnv *env, jobject object) {
if (object) {
return env->NewGlobalRef(object);
}
return 0;
}
int gifvideoOnJNILoad(JavaVM *vm, JNIEnv *env) {
int gifvideoOnJNILoad(JavaVM *vm, JNIEnv *env) { /* called from JNI_OnLoad() */
av_register_all();
return 0;
}
int open_codec_context(int *stream_idx, AVFormatContext *fmt_ctx, enum AVMediaType type) {
static int open_codec_context(int *stream_idx, AVFormatContext *fmt_ctx, enum AVMediaType type) {
int ret;
AVStream *st;
AVCodecContext *dec_ctx = NULL;
@@ -97,7 +115,7 @@ int open_codec_context(int *stream_idx, AVFormatContext *fmt_ctx, enum AVMediaTy
return 0;
}
int decode_packet(VideoInfo *info, int *got_frame) {
static int decode_packet(VideoInfo *info, int *got_frame) {
int ret = 0;
int decoded = info->pkt.size;
@@ -263,4 +281,5 @@ jint Java_com_b44t_ui_Components_AnimatedFileDrawable_getVideoFrame(JNIEnv *env,
}
return 0;
}
}
} /* extern "C" */
@@ -21,11 +21,13 @@
******************************************************************************/
package com.b44t.messenger;
#ifndef __GIFVIDEO_H__
#define __GIFVIDEO_H__
public class UserObject {
public static String getUserName(TLRPC.User user) {
return "ErrName"; // use MrContact.getName() instead
}
}
#include <jni.h>
int gifvideoOnJNILoad(JavaVM *vm, JNIEnv *env);
#endif /* __GIFVIDEO_H__ */
+52 -15
View File
@@ -1,3 +1,26 @@
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2013-2016 Nikolai Kudashov
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see http://www.gnu.org/licenses/ .
*
******************************************************************************/
#include <jni.h>
#include <stdio.h>
#include <setjmp.h>
@@ -5,21 +28,35 @@
#include <android/bitmap.h>
#include <libwebp/webp/decode.h>
#include <libwebp/webp/encode.h>
#include "utils.h"
#include "mrjnimain.h"
#include "image.h"
jclass jclass_NullPointerException;
jclass jclass_RuntimeException;
jclass jclass_Options;
jfieldID jclass_Options_inJustDecodeBounds;
jfieldID jclass_Options_outHeight;
jfieldID jclass_Options_outWidth;
static jclass jclass_NullPointerException;
static jclass jclass_RuntimeException;
const uint32_t PGPhotoEnhanceHistogramBins = 256;
const uint32_t PGPhotoEnhanceSegments = 4;
static jclass jclass_Options;
static jfieldID jclass_Options_inJustDecodeBounds;
static jfieldID jclass_Options_outHeight;
static jfieldID jclass_Options_outWidth;
jclass createGlobarRef(JNIEnv *env, jclass class) {
static const uint32_t PGPhotoEnhanceHistogramBins = 256;
static const uint32_t PGPhotoEnhanceSegments = 4;
static void throwException(JNIEnv *env, char *format, ...) {
jclass exClass = (*env)->FindClass(env, "java/lang/UnsupportedOperationException");
if (!exClass) {
return;
}
char dest[256];
va_list argptr;
va_start(argptr, format);
vsprintf(dest, format, argptr);
va_end(argptr);
(*env)->ThrowNew(env, exClass, dest);
}
static jclass createGlobalRef(JNIEnv *env, jclass class) {
if (class) {
return (*env)->NewGlobalRef(env, class);
}
@@ -27,16 +64,16 @@ jclass createGlobarRef(JNIEnv *env, jclass class) {
}
jint imageOnJNILoad(JavaVM *vm, void *reserved, JNIEnv *env) {
jclass_NullPointerException = createGlobarRef(env, (*env)->FindClass(env, "java/lang/NullPointerException"));
jclass_NullPointerException = createGlobalRef(env, (*env)->FindClass(env, "java/lang/NullPointerException"));
if (jclass_NullPointerException == 0) {
return -1;
}
jclass_RuntimeException = createGlobarRef(env, (*env)->FindClass(env, "java/lang/RuntimeException"));
jclass_RuntimeException = createGlobalRef(env, (*env)->FindClass(env, "java/lang/RuntimeException"));
if (jclass_RuntimeException == 0) {
return -1;
}
jclass_Options = createGlobarRef(env, (*env)->FindClass(env, "android/graphics/BitmapFactory$Options"));
jclass_Options = createGlobalRef(env, (*env)->FindClass(env, "android/graphics/BitmapFactory$Options"));
if (jclass_Options == 0) {
return -1;
}
@@ -262,7 +299,7 @@ typedef struct my_error_mgr {
} *my_error_ptr;
METHODDEF(void) my_error_exit(j_common_ptr cinfo) {
METHODDEF(void) my_jpeglib_error_exit(j_common_ptr cinfo) {
my_error_ptr myerr = (my_error_ptr) cinfo->err;
(*cinfo->err->output_message) (cinfo);
longjmp(myerr->setjmp_buffer, 1);
@@ -424,7 +461,7 @@ JNIEXPORT void Java_com_b44t_messenger_Utilities_loadBitmap(JNIEnv *env, jclass
struct jpeg_decompress_struct cinfo;
cinfo.err = jpeg_std_error(&jerr.pub);
jerr.pub.error_exit = my_error_exit;
jerr.pub.error_exit = my_jpeglib_error_exit;
if (!setjmp(jerr.setjmp_buffer)) {
jpeg_create_decompress(&cinfo);
+29 -3
View File
@@ -1,8 +1,34 @@
#ifndef image_h
#define image_h
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2013-2016 Nikolai Kudashov
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see http://www.gnu.org/licenses/ .
*
******************************************************************************/
#ifndef __IMAGE_H__
#define __IMAGE_H__
#include <jni.h>
jint imageOnJNILoad(JavaVM *vm, void *reserved, JNIEnv *env);
#endif
#endif /* __IMAGE_H__ */
-67
View File
@@ -1,67 +0,0 @@
#include <stdio.h>
#include <string.h>
#include <jni.h>
#include <sys/types.h>
#include <inttypes.h>
#include <stdlib.h>
#include <openssl/aes.h>
#include <unistd.h>
#include "utils.h"
#include "image.h"
int gifvideoOnJNILoad(JavaVM *vm, JNIEnv *env);
jint JNI_OnLoad(JavaVM *vm, void *reserved) { /* this function is called automatically by the JNI when the library gets loaded */
JNIEnv *env = 0;
srand(time(NULL));
if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
if (imageOnJNILoad(vm, reserved, env) == -1) {
return -1;
}
if (gifvideoOnJNILoad(vm, env) == -1) {
return -1;
}
return JNI_VERSION_1_6;
}
void JNI_OnUnload(JavaVM *vm, void *reserved) {
}
/*
JNIEXPORT void Java_com_b44t_messenger_Utilities_aesIgeEncryption(JNIEnv *env, jclass class, jobject buffer, jbyteArray key, jbyteArray iv, jboolean encrypt, int offset, int length) {
jbyte *what = (*env)->GetDirectBufferAddress(env, buffer) + offset;
unsigned char *keyBuff = (unsigned char *)(*env)->GetByteArrayElements(env, key, NULL);
unsigned char *ivBuff = (unsigned char *)(*env)->GetByteArrayElements(env, iv, NULL);
AES_KEY akey;
if (!encrypt) {
AES_set_decrypt_key(keyBuff, 32 * 8, &akey);
AES_ige_encrypt(what, what, length, &akey, ivBuff, AES_DECRYPT);
} else {
AES_set_encrypt_key(keyBuff, 32 * 8, &akey);
AES_ige_encrypt(what, what, length, &akey, ivBuff, AES_ENCRYPT);
}
(*env)->ReleaseByteArrayElements(env, key, keyBuff, JNI_ABORT);
(*env)->ReleaseByteArrayElements(env, iv, ivBuff, 0);
}
*/
JNIEXPORT jstring Java_com_b44t_messenger_Utilities_readlink(JNIEnv *env, jclass class, jstring path) {
static char buf[1000];
char *fileName = (*env)->GetStringUTFChars(env, path, NULL);
int result = readlink(fileName, buf, 999);
jstring value = 0;
if (result != -1) {
buf[result] = '\0';
value = (*env)->NewStringUTF(env, buf);
}
(*env)->ReleaseStringUTFChars(env, path, fileName);
return value;
}
@@ -122,13 +122,13 @@
#define LIBETPAN_REENTRANT 1
/* Define this to the version of libEtPan */
#define LIBETPAN_VERSION "1.2-dev-20141203"
//#define LIBETPAN_VERSION "1.2-dev-20141203"
/* Define this to the major version of libEtPan */
#define LIBETPAN_VERSION_MAJOR 1
//#define LIBETPAN_VERSION_MAJOR 1
/* Define this to the minor version of libEtPan */
#define LIBETPAN_VERSION_MINOR 2
//#define LIBETPAN_VERSION_MINOR 2
/* Define to the sub-directory in which libtool stores uninstalled libraries.
*/
@@ -144,7 +144,7 @@
#define PACKAGE_NAME "libetpan"
/* Define to the full name and version of this package. */
#define PACKAGE_STRING "libetpan 1.2"
//#define PACKAGE_STRING "libetpan 1.2"
/* Define to the one symbol short name of this package. */
#define PACKAGE_TARNAME "libetpan"
@@ -153,7 +153,7 @@
#define PACKAGE_URL ""
/* Define to the version of this package. */
#define PACKAGE_VERSION "1.2"
//#define PACKAGE_VERSION "1.2"
/* Define to 1 if you have the ANSI C header files. */
#define STDC_HEADERS 1
@@ -171,7 +171,7 @@
#define USE_SSL 1
/* Version number of package */
#define VERSION "1.2"
//#define VERSION "1.2"
/* Define to `__inline__' or `__inline' if that's what the C compiler
calls it, or to nothing if 'inline' is not supported under any name. */
+77
View File
@@ -0,0 +1,77 @@
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2013-2016 Nikolai Kudashov
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see http://www.gnu.org/licenses/ .
*
******************************************************************************/
#include <stdio.h>
#include <string.h>
#include <jni.h>
#include <sys/types.h>
#include <inttypes.h>
#include <stdlib.h>
#include <openssl/aes.h>
#include <unistd.h>
#include "mrjnimain.h"
#include "image.h"
#include "gifvideo.h"
jint JNI_OnLoad(JavaVM *vm, void *reserved) { /* this function is called automatically by the JNI when the library gets loaded */
JNIEnv *env = 0;
srand(time(NULL));
__android_log_print(ANDROID_LOG_INFO, "DeltaChat", "JNI_OnLoad() ..."); /* please note, that __android_log_print() may not work (eg. on LG X Cam), however, we don't have an option here. */
if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
if (imageOnJNILoad(vm, reserved, env) == -1) {
return -1;
}
if (gifvideoOnJNILoad(vm, env) == -1) {
return -1;
}
__android_log_print(ANDROID_LOG_INFO, "DeltaChat", "JNI_OnLoad() succeeded.");
return JNI_VERSION_1_6;
}
void JNI_OnUnload(JavaVM *vm, void *reserved) {
}
JNIEXPORT jstring Java_com_b44t_messenger_Utilities_readlink(JNIEnv *env, jclass class, jstring path) {
static char buf[1000];
char *fileName = (*env)->GetStringUTFChars(env, path, NULL);
int result = readlink(fileName, buf, 999);
jstring value = 0;
if (result != -1) {
buf[result] = '\0';
value = (*env)->NewStringUTF(env, buf);
}
(*env)->ReleaseStringUTFChars(env, path, fileName);
return value;
}
@@ -1,6 +1,7 @@
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2013-2016 Nikolai Kudashov
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
@@ -20,8 +21,34 @@
******************************************************************************/
package com.b44t.messenger;
#ifndef __MRJNIMAIN_H__
#define __MRJNIMAIN_H__
#include <android/log.h>
#include <jni.h>
#define LOG_TAG "DeltaChat"
#ifndef LOG_DISABLED
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#else
#define LOGI(...)
#define LOGD(...)
#define LOGE(...)
#define LOGV(...)
#endif
#ifndef max
#define max(x, y) ((x) > (y)) ? (x) : (y)
#endif
#ifndef min
#define min(x, y) ((x) < (y)) ? (x) : (y)
#endif
#endif /* __MRJNIMAIN_H__*/
public class BuildVars {
public static boolean DEBUG_VERSION = false;
}
+99 -59
View File
@@ -23,8 +23,8 @@
* Purpose: The C part of the Java<->C Wrapper, see also MrMailbox.java
*
******************************************************************************/
#include <jni.h>
#include <android/log.h>
#include "messenger-backend/src/mrmailbox.h"
@@ -66,45 +66,51 @@ static jstring jstring_new__(JNIEnv* env, const char* a)
}
/* our log handler */
static void s_log_callback_(int type, const char* msg)
{
int prio;
switch( type ) {
case 'd': prio = ANDROID_LOG_DEBUG; break;
case 'i': prio = ANDROID_LOG_INFO; break;
case 'w': prio = ANDROID_LOG_WARN; break;
default: prio = ANDROID_LOG_ERROR; break;
}
__android_log_print(prio, "DeltaChat", "%s\n", msg); /* on problems, add `-llog` to `Android.mk` */
}
/* global stuff */
static JavaVM* s_jvm = NULL;
static jclass s_MrMailbox_class = NULL;
static jmethodID s_MrCallback_methodID = NULL;
static int s_global_init_done = 0;
static void s_init_globals(JNIEnv *env, jclass MrMailbox_class)
{
/* make sure, the intialisation is done only once */
static bool s_global_init_done = 0;
if( s_global_init_done ) { return; }
s_global_init_done = 1;
/* init global callback */
mrlog_set_handler(s_log_callback_);
/* prepare calling back a Java function */
__android_log_print(ANDROID_LOG_INFO, "DeltaChat", "JNI: s_init_globals()..."); /* low-level logging, mrmailbox_log_*() may not be yet available. However, please note that __android_log_print() may not work (eg. on LG X Cam) */
(*env)->GetJavaVM(env, &s_jvm); /* JNIEnv cannot be shared between threads, so we share the JavaVM object */
s_MrMailbox_class = (*env)->NewGlobalRef(env, MrMailbox_class);
s_MrCallback_methodID = (*env)->GetStaticMethodID(env, MrMailbox_class, "MrCallback","(IJJ)J" /*signature as "(param)ret" with I=int, J=long*/ );
}
/* system-specific backend initialisations */
mrosnative_init_android(env); /*this should be called before any other "important" routine is called*/
/* setup threads, only called directly by the backed */
int mrosnative_setup_thread(mrmailbox_t* mailbox)
{
if( s_jvm == NULL ) {
mrmailbox_log_error(mailbox, 0, "Not ready, cannot setup thread.");
return 0;
}
mrmailbox_log_info(mailbox, 0, "Attaching C-thread to Java VM...");
JNIEnv* env = NULL;
(*s_jvm)->AttachCurrentThread(s_jvm, &env, NULL);
mrmailbox_log_info(mailbox, 0, "Attaching ok.");
return 1;
}
void mrosnative_unsetup_thread(mrmailbox_t* mailbox)
{
mrmailbox_log_info(mailbox, 0, "Detaching C-thread from Java VM...");
(*s_jvm)->DetachCurrentThread(s_jvm);
mrmailbox_log_info(mailbox, 0, "DeltaChat", "Detaching done.");
}
@@ -189,15 +195,24 @@ static uintptr_t s_mailbox_callback_(mrmailbox_t* mailbox, int event, uintptr_t
jlong l;
JNIEnv* env;
#if 0 /* -- __android_log_print() does not log eg. on LG X Cam - but Javas Log.i() etc. do. So, we do not optimize these calls and just use the Java logging. */
if( event==MR_EVENT_INFO || event==MR_EVENT_WARNING ) {
__android_log_print(event==MR_EVENT_INFO? ANDROID_LOG_INFO : ANDROID_LOG_WARN, "DeltaChat", "%s", (char*)data2); /* on problems, add `-llog` to `Android.mk` */
return 0; /* speed up things for info/warning */
}
else if( event == MR_EVENT_ERROR ) {
__android_log_print(ANDROID_LOG_ERROR, "DeltaChat", "%s", (char*)data2);
/* errors are also forwarded to Java to show them in a bubble or so */
}
#endif
if( s_jvm==NULL || s_MrMailbox_class==NULL || s_MrCallback_methodID==NULL ) {
s_log_callback_('e', "Callback called but JavaVM not ready.");
return 0;
return 0; /* may happen on startup */
}
(*s_jvm)->GetEnv(s_jvm, &env, JNI_VERSION_1_6); /* as this function may be called from _any_ thread, we cannot use a static pointer to JNIEnv */
if( env==NULL ) {
s_log_callback_('e', "Callback called but cannot get JNIEnv.");
return 0;
return 0; /* may happen on startup */
}
l = (*env)->CallStaticLongMethod(env, s_MrMailbox_class, s_MrCallback_methodID, (jint)event, (jlong)data1, (jlong)data2);
@@ -236,42 +251,33 @@ JNIEXPORT jstring Java_com_b44t_messenger_MrMailbox_getBlobdir(JNIEnv *env, jcla
}
JNIEXPORT jint Java_com_b44t_messenger_MrMailbox_MrMailboxConfigure(JNIEnv *env, jclass c, jlong hMailbox)
JNIEXPORT void Java_com_b44t_messenger_MrMailbox_configureAndConnect(JNIEnv *env, jclass cls)
{
return mrmailbox_configure((mrmailbox_t*)hMailbox);
mrmailbox_configure_and_connect(get_mrmailbox_t(env, cls));
}
JNIEXPORT jint Java_com_b44t_messenger_MrMailbox_MrMailboxIsConfigured(JNIEnv *env, jclass c, jlong hMailbox)
JNIEXPORT void Java_com_b44t_messenger_MrMailbox_configureCancel(JNIEnv *env, jclass cls)
{
return (jint)mrmailbox_is_configured((mrmailbox_t*)hMailbox);
mrmailbox_configure_cancel(get_mrmailbox_t(env, cls));
}
JNIEXPORT jint Java_com_b44t_messenger_MrMailbox_MrMailboxConnect(JNIEnv *env, jclass c, jlong hMailbox)
JNIEXPORT jint Java_com_b44t_messenger_MrMailbox_isConfigured(JNIEnv *env, jclass cls)
{
return mrmailbox_connect((mrmailbox_t*)hMailbox);
return (jint)mrmailbox_is_configured(get_mrmailbox_t(env, cls));
}
JNIEXPORT void Java_com_b44t_messenger_MrMailbox_MrMailboxDisconnect(JNIEnv *env, jclass c, jlong hMailbox)
JNIEXPORT void Java_com_b44t_messenger_MrMailbox_connect(JNIEnv *env, jclass cls)
{
mrmailbox_disconnect((mrmailbox_t*)hMailbox);
mrmailbox_connect(get_mrmailbox_t(env, cls));
}
JNIEXPORT jint Java_com_b44t_messenger_MrMailbox_MrMailboxFetch(JNIEnv *env, jclass c, jlong hMailbox)
JNIEXPORT void Java_com_b44t_messenger_MrMailbox_disconnect(JNIEnv *env, jclass cls)
{
return mrmailbox_fetch((mrmailbox_t*)hMailbox);
}
JNIEXPORT jstring Java_com_b44t_messenger_MrMailbox_getErrorDescr(JNIEnv *env, jclass cls)
{
char* c = mrmailbox_get_error_descr(get_mrmailbox_t(env, cls));
jstring ret = JSTRING_NEW(c);
free(c);
return ret;
mrmailbox_disconnect(get_mrmailbox_t(env, cls));
}
@@ -351,9 +357,9 @@ JNIEXPORT jlong Java_com_b44t_messenger_MrMailbox_MrMailboxGetChat(JNIEnv *env,
}
JNIEXPORT jint Java_com_b44t_messenger_MrMailbox_MrMailboxGetChatIdByContactId(JNIEnv *env, jclass c, jlong hMailbox, jint contact_id)
JNIEXPORT jint Java_com_b44t_messenger_MrMailbox_getChatIdByContactId(JNIEnv *env, jclass cls, jint contact_id)
{
return (jint)mrmailbox_get_chat_id_by_contact_id((mrmailbox_t*)hMailbox, contact_id);
return (jint)mrmailbox_get_chat_id_by_contact_id(get_mrmailbox_t(env, cls), contact_id);
}
@@ -369,9 +375,9 @@ JNIEXPORT jint Java_com_b44t_messenger_MrMailbox_markseenChat(JNIEnv *env, jclas
}
JNIEXPORT jint Java_com_b44t_messenger_MrMailbox_MrMailboxCreateChatByContactId(JNIEnv *env, jclass c, jlong hMailbox, jint contact_id)
JNIEXPORT jint Java_com_b44t_messenger_MrMailbox_createChatByContactId(JNIEnv *env, jclass cls, jint contact_id)
{
return (jint)mrmailbox_create_chat_by_contact_id((mrmailbox_t*)hMailbox, contact_id);
return (jint)mrmailbox_create_chat_by_contact_id(get_mrmailbox_t(env, cls), contact_id);
}
@@ -454,22 +460,29 @@ JNIEXPORT void Java_com_b44t_messenger_MrMailbox_forwardMsgs(JNIEnv *env, jclass
/* MrMailbox - handle config */
JNIEXPORT jint Java_com_b44t_messenger_MrMailbox_MrMailboxSetConfig(JNIEnv *env, jclass c, jlong hMailbox, jstring key, jstring value)
JNIEXPORT void Java_com_b44t_messenger_MrMailbox_setConfig(JNIEnv *env, jclass cls, jstring key, jstring value)
{
CHAR_REF(key);
CHAR_REF(value);
jint ret = (jint)mrmailbox_set_config((mrmailbox_t*)hMailbox, keyPtr, valuePtr);
mrmailbox_set_config(get_mrmailbox_t(env, cls), keyPtr, valuePtr);
CHAR_UNREF(key);
CHAR_UNREF(value);
return ret;
}
JNIEXPORT jstring Java_com_b44t_messenger_MrMailbox_MrMailboxGetConfig(JNIEnv *env, jclass c, jlong hMailbox, jstring key, jstring def)
JNIEXPORT void Java_com_b44t_messenger_MrMailbox_setConfigInt(JNIEnv *env, jclass cls, jstring key, jint value)
{
CHAR_REF(key);
mrmailbox_set_config_int(get_mrmailbox_t(env, cls), keyPtr, value);
CHAR_UNREF(key);
}
JNIEXPORT jstring Java_com_b44t_messenger_MrMailbox_getConfig(JNIEnv *env, jclass cls, jstring key, jstring def)
{
CHAR_REF(key);
CHAR_REF(def);
char* temp = mrmailbox_get_config((mrmailbox_t*)hMailbox, keyPtr, defPtr);
char* temp = mrmailbox_get_config(get_mrmailbox_t(env, cls), keyPtr, defPtr);
jstring ret = JSTRING_NEW(temp);
free(temp);
CHAR_UNREF(key);
@@ -478,10 +491,10 @@ JNIEXPORT jstring Java_com_b44t_messenger_MrMailbox_MrMailboxGetConfig(JNIEnv *e
}
JNIEXPORT jint Java_com_b44t_messenger_MrMailbox_MrMailboxGetConfigInt(JNIEnv *env, jclass c, jlong hMailbox, jstring key, jint def)
JNIEXPORT jint Java_com_b44t_messenger_MrMailbox_getConfigInt(JNIEnv *env, jclass cls, jstring key, jint def)
{
CHAR_REF(key);
jint ret = mrmailbox_get_config_int((mrmailbox_t*)hMailbox, keyPtr, def);
jint ret = mrmailbox_get_config_int(get_mrmailbox_t(env, cls), keyPtr, def);
CHAR_UNREF(key);
return ret;
}
@@ -509,6 +522,12 @@ JNIEXPORT jstring Java_com_b44t_messenger_MrMailbox_cmdline(JNIEnv *env, jclass
}
JNIEXPORT void Java_com_b44t_messenger_MrMailbox_heartbeat(JNIEnv *env, jclass cls)
{
mrmailbox_heartbeat(get_mrmailbox_t(env, cls));
}
JNIEXPORT jint Java_com_b44t_messenger_MrMailbox_MrMailboxAddAddressBook(JNIEnv *env, jclass c, jlong hMailbox, jstring adrbook)
{
CHAR_REF(adrbook);
@@ -678,7 +697,7 @@ JNIEXPORT jint Java_com_b44t_messenger_MrChat_sendMedia(JNIEnv *env, jclass cls,
CHAR_REF(file);
mrparam_set(msg->m_param, 'f', filePtr);
CHAR_UNREF(file);
if( type == MR_MSG_IMAGE || type == MR_MSG_VIDEO ) {
if( type == MR_MSG_IMAGE || type == MR_MSG_GIF || type == MR_MSG_VIDEO ) {
mrparam_set_int(msg->m_param, 'w', w);
mrparam_set_int(msg->m_param, 'h', h);
}
@@ -905,6 +924,20 @@ JNIEXPORT jint Java_com_b44t_messenger_MrMsg_isIncreation(JNIEnv *env, jobject o
******************************************************************************/
static mrcontact_t* get_mrcontact_t(JNIEnv *env, jobject obj)
{
static jfieldID fid = 0;
if( fid == 0 ) {
jclass cls = (*env)->GetObjectClass(env, obj);
fid = (*env)->GetFieldID(env, cls, "m_hContact", "J" /*Signature, J=long*/);
}
if( fid ) {
return (mrcontact_t*)(*env)->GetLongField(env, obj, fid);
}
return NULL;
}
JNIEXPORT void Java_com_b44t_messenger_MrContact_MrContactUnref(JNIEnv *env, jclass c, jlong hContact)
{
mrcontact_unref((mrcontact_t*)hContact);
@@ -918,6 +951,13 @@ JNIEXPORT jstring Java_com_b44t_messenger_MrContact_MrContactGetName(JNIEnv *env
}
JNIEXPORT jstring Java_com_b44t_messenger_MrContact_getAuthName(JNIEnv *env, jclass cls)
{
mrcontact_t* ths = get_mrcontact_t(env, cls); if( ths == NULL ) { return JSTRING_NEW(NULL); }
return JSTRING_NEW(ths->m_authname);
}
JNIEXPORT jstring Java_com_b44t_messenger_MrContact_MrContactGetAddr(JNIEnv *env, jclass c, jlong hContact)
{
mrcontact_t* ths = (mrcontact_t*)hContact; if( ths == NULL ) { return JSTRING_NEW(NULL); }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-14
View File
@@ -1,14 +0,0 @@
#include "utils.h"
void throwException(JNIEnv *env, char *format, ...) {
jclass exClass = (*env)->FindClass(env, "java/lang/UnsupportedOperationException");
if (!exClass) {
return;
}
char dest[256];
va_list argptr;
va_start(argptr, format);
vsprintf(dest, format, argptr);
va_end(argptr);
(*env)->ThrowNew(env, exClass, dest);
}
-29
View File
@@ -1,29 +0,0 @@
#ifndef log_h
#define log_h
#include <android/log.h>
#include <jni.h>
#define LOG_TAG "DeltaChat"
#ifndef LOG_DISABLED
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#else
#define LOGI(...)
#define LOGD(...)
#define LOGE(...)
#define LOGV(...)
#endif
#ifndef max
#define max(x, y) ((x) > (y)) ? (x) : (y)
#endif
#ifndef min
#define min(x, y) ((x) < (y)) ? (x) : (y)
#endif
void throwException(JNIEnv *env, char *format, ...);
#endif
+26 -2
View File
@@ -1,6 +1,30 @@
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2013-2016 Nikolai Kudashov
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see http://www.gnu.org/licenses/ .
*
******************************************************************************/
#include <jni.h>
#include <libyuv.h>
#include <utils.h>
#include "mrjnimain.h"
enum COLOR_FORMATTYPE {
COLOR_FormatMonochrome = 1,
@@ -52,7 +76,7 @@ enum COLOR_FORMATTYPE {
COLOR_QCOM_FormatYUV420SemiPlanar = 0x7fa30c00
};
int isSemiPlanarYUV(int colorFormat) {
static int isSemiPlanarYUV(int colorFormat) {
switch (colorFormat) {
case COLOR_FormatYUV420Planar:
case COLOR_FormatYUV420PackedPlanar:
+11 -23
View File
@@ -46,7 +46,6 @@
<application
android:name=".ApplicationLoader"
android:allowBackup="false"
android:hardwareAccelerated="@bool/useHardwareAcceleration"
android:icon="@drawable/ic_launcher"
android:label="@string/AppName"
android:largeHeap="true"
@@ -56,7 +55,6 @@
<activity
android:name="com.b44t.ui.LaunchActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:hardwareAccelerated="@bool/useHardwareAcceleration"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustPan">
<intent-filter>
@@ -99,27 +97,17 @@
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="vnd.android.cursor.item/vnd.com.b44t.messenger.android.profile"/>
</intent-filter>
<!-- <intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="t'gram.me" android:scheme="http" />
<data android:host="t'gram.me" android:scheme="https" />
<data android:host="t'gram.dog" android:scheme="http" />
<data android:host="t'gram.dog" android:scheme="https" />
</intent-filter> -->
<!-- <intent-filter android:icon="@drawable/ic_launcher" android:priority="1">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="tg" />
</intent-filter> -->
<!-- see note below, <meta-data android:name="android.service.chooser.chooser_target_service" android:value=".TgChooserTargetService" /> -->
<action android:name="android.intent.action.SENDTO"/> <!-- VIEW seems to be standard, however, SENDTO is also mentioned in some examples -->
<data android:scheme="mailto"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
</intent-filter>
</activity>
<activity
android:name="com.b44t.ui.ManageSpaceActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:hardwareAccelerated="@bool/useHardwareAcceleration"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustPan">
</activity>
@@ -181,7 +169,7 @@
<!-- this would implement the "direct share" functionality (the system shows some direct links eg.
to the most recent chats when calling "share" from another app, see https://developer.android.com/reference/android/service/chooser/ChooserTargetService.html )
however, if we want this feature, we have to reimplement TgChooserTargetService or sth. like that.
however, if we want this feature, we have to implement a ChooserTargetService and add <meta-data android:name="android.service.chooser.chooser_target_service" android:value=".OurChooserTargetService" />
<service
android:name="com.b44t.messenger.TgChooserTargetService"
android:label="@string/AppName"
@@ -191,9 +179,8 @@
</intent-filter>
</service> -->
<service android:name=".NotificationsService" android:enabled="true"/>
<service android:name=".KeepAliveService" android:enabled="true"/>
<service android:name=".NotificationRepeat" android:exported="false"/>
<service android:name=".ClearCacheService" android:exported="false"/>
<service android:name=".MusicPlayerService" android:exported="true" android:enabled="true"/>
<!-- Do not expose data through MediaBrowserService, this seems unexpected for an messenger.
Instead, the user can use 'save to music' or 'share' explicitly for items not privacy related.
@@ -215,9 +202,8 @@
</intent-filter>
</receiver>
<receiver android:name=".AppStartReceiver" android:enabled="true">
<receiver android:name=".BootCompletedReceiver" android:enabled="true">
<intent-filter>
<action android:name="com.b44t.start" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
@@ -229,6 +215,8 @@
<receiver android:name=".ShareBroadcastReceiver" android:enabled="true"/>
-->
<receiver android:name=".TimerReceiver" android:enabled="true"/>
<receiver android:name=".NotificationDismissReceiver" android:exported="false"/>
<provider android:name=".AttachmentsContentProvider" android:authorities="${applicationId}.attachments" android:exported="true" />
@@ -51,6 +51,7 @@ import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.StateSet;
import android.view.Display;
import android.view.Surface;
@@ -113,7 +114,7 @@ public class AndroidUtilities {
final String HOST_NAME = "(" + IRI + "\\.)+" + GTLD;
final Pattern DOMAIN_NAME = Pattern.compile("(" + HOST_NAME + "|" + IP_ADDRESS + ")");
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -193,7 +194,7 @@ public class AndroidUtilities {
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -207,7 +208,7 @@ public class AndroidUtilities {
prevOrientation = -10;
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -219,7 +220,7 @@ public class AndroidUtilities {
InputMethodManager inputManager = (InputMethodManager)view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -234,7 +235,7 @@ public class AndroidUtilities {
}
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -244,7 +245,7 @@ public class AndroidUtilities {
try {
state = Environment.getExternalStorageState();
} catch (Exception e) {
FileLog.e("messenger", e);
}
if (state == null || state.startsWith(Environment.MEDIA_MOUNTED)) {
try {
@@ -253,7 +254,7 @@ public class AndroidUtilities {
return file;
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
try {
@@ -262,7 +263,7 @@ public class AndroidUtilities {
return file;
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
return new File("");
}
@@ -301,11 +302,10 @@ public class AndroidUtilities {
if (display != null) {
display.getMetrics(displayMetrics);
display.getSize(displaySize);
FileLog.e("messenger", "display size = " + displaySize.x + " " + displaySize.y + " " + displayMetrics.xdpi + "x" + displayMetrics.ydpi);
Log.i("DeltaChat", "Display size: " + displaySize.x + " x " + displaySize.y + ", DPI: " + displayMetrics.xdpi + " x " + displayMetrics.ydpi);
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -330,15 +330,21 @@ public class AndroidUtilities {
}
public static boolean isTablet() {
/* -- we do not make any special for tablet or not. _If_ sth. like this is desired, check for an appropriate screen size.
if (isTablet == null) {
isTablet = ApplicationLoader.applicationContext.getResources().getBoolean(R.bool.isTablet);
}
return isTablet;
*/
return false;
}
public static boolean isSmallTablet() {
/*
float minSide = Math.min(displaySize.x, displaySize.y) / density;
return minSide <= 700;
*/
return false;
}
public static int getMinTabletSide() {
@@ -380,7 +386,7 @@ public class AndroidUtilities {
mCursorDrawableRes.setAccessible(true);
mCursorDrawableRes.setInt(editText, 0);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -410,7 +416,7 @@ public class AndroidUtilities {
addIntent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
ApplicationLoader.applicationContext.sendBroadcast(addIntent);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -423,7 +429,7 @@ public class AndroidUtilities {
addIntent.setAction("com.android.launcher.action.UNINSTALL_SHORTCUT");
ApplicationLoader.applicationContext.sendBroadcast(addIntent);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}*/
@@ -442,7 +448,7 @@ public class AndroidUtilities {
return insets.bottom;
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
return 0;
}
@@ -460,11 +466,11 @@ public class AndroidUtilities {
size.set((Integer) mGetRawW.invoke(windowManager.getDefaultDisplay()), (Integer) mGetRawH.invoke(windowManager.getDefaultDisplay()));
} catch (Exception e) {
size.set(windowManager.getDefaultDisplay().getWidth(), windowManager.getDefaultDisplay().getHeight());
FileLog.e("messenger", e);
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
return size;
}
@@ -482,28 +488,6 @@ public class AndroidUtilities {
return src;
}
public static void setListViewEdgeEffectColor(AbsListView listView, int color) {
if (Build.VERSION.SDK_INT >= 21) {
try {
Field field = AbsListView.class.getDeclaredField("mEdgeGlowTop");
field.setAccessible(true);
EdgeEffect mEdgeGlowTop = (EdgeEffect) field.get(listView);
if (mEdgeGlowTop != null) {
mEdgeGlowTop.setColor(color);
}
field = AbsListView.class.getDeclaredField("mEdgeGlowBottom");
field.setAccessible(true);
EdgeEffect mEdgeGlowBottom = (EdgeEffect) field.get(listView);
if (mEdgeGlowBottom != null) {
mEdgeGlowBottom.setColor(color);
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
@SuppressLint("NewApi")
public static void clearDrawableAnimation(View view) {
if (Build.VERSION.SDK_INT < 21 || view == null) {
@@ -560,7 +544,7 @@ public class AndroidUtilities {
}
}
ArrayList<Integer> colors = new ArrayList<>();
if ((flag & FLAG_TAG_COLOR) != 0) {
if ((flag & FLAG_TAG_COLOR) != 0) { // used eg. by the message preview when following a mailto:-link
while ((start = stringBuilder.indexOf("<c#")) != -1) {
stringBuilder.replace(start, start + 2, "");
end = stringBuilder.indexOf(">", start);
@@ -582,7 +566,7 @@ public class AndroidUtilities {
}
return spannableStringBuilder;
} catch (Exception e) {
FileLog.e("messenger", e);
}
return new SpannableStringBuilder(str);
}
@@ -593,7 +577,7 @@ public class AndroidUtilities {
ForegroundDetector.getInstance().resetBackgroundVar();
}
return UserConfig.passcodeHash.length() > 0 && wasInBackground &&
(UserConfig.appLocked || UserConfig.autoLockIn != 0 && UserConfig.lastPauseTime != 0 && !UserConfig.appLocked && (UserConfig.lastPauseTime + UserConfig.autoLockIn) <= ConnectionsManager.getInstance().getCurrentTime());
(UserConfig.appLocked || UserConfig.autoLockIn != 0 && UserConfig.lastPauseTime != 0 && !UserConfig.appLocked && (UserConfig.lastPauseTime + UserConfig.autoLockIn) <= MrMailbox.getCurrentTime());
}
public static void shakeView(final View view, final float x, final int num) {
@@ -639,7 +623,7 @@ public class AndroidUtilities {
android.content.ClipData clip = android.content.ClipData.newPlainText("label", str);
clipboard.setPrimaryClip(clip);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -661,7 +645,7 @@ public class AndroidUtilities {
mediaScanIntent.setData(uri);
ApplicationLoader.applicationContext.sendBroadcast(mediaScanIntent);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -674,12 +658,12 @@ public class AndroidUtilities {
storageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Delta Chat");
if (!storageDir.mkdirs()) {
if (!storageDir.exists()){
FileLog.d("messenger", "failed to create directory");
Log.w("DeltaChat", "failed to create directory");
return null;
}
}
} else {
FileLog.d("messenger", "External storage is not mounted READ/WRITE.");
Log.w("DeltaChat", "External storage is not mounted READ/WRITE.");
}
return storageDir;
@@ -732,7 +716,7 @@ public class AndroidUtilities {
return uri.getPath();
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
return null;
}
@@ -756,7 +740,7 @@ public class AndroidUtilities {
return value;
}
} catch (Exception e) {
FileLog.e("messenger", e);
} finally {
if (cursor != null) {
cursor.close();
@@ -783,7 +767,7 @@ public class AndroidUtilities {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
return new File(storageDir, "IMG_" + timeStamp + ".jpg");
} catch (Exception e) {
FileLog.e("messenger", e);
}
return null;
}
@@ -794,7 +778,7 @@ public class AndroidUtilities {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
return new File(storageDir, "VID_" + timeStamp + ".mp4");
} catch (Exception e) {
FileLog.e("messenger", e);
}
return null;
}
@@ -824,7 +808,7 @@ public class AndroidUtilities {
final int l = Character.digit((char) bytes[++i], 16);
buffer.write((char) ((u << 4) + l));
} catch (Exception e) {
FileLog.e("messenger", e);
return null;
}
} else {
@@ -835,7 +819,7 @@ public class AndroidUtilities {
try {
buffer.close();
} catch (Exception e) {
FileLog.e("messenger", e);
}
return array;
}
@@ -863,7 +847,7 @@ public class AndroidUtilities {
destination = new FileOutputStream(destFile);
destination.getChannel().transferFrom(source.getChannel(), 0, source.getChannel().size());
} catch (Exception e) {
FileLog.e("messenger", e);
return false;
} finally {
if (source != null) {
@@ -906,7 +890,7 @@ public class AndroidUtilities {
try {
File file = new File(path);
if( !file.exists() ) {
showHint(activity, LocaleController.formatString("FileNotFound", R.string.FileNotFound, path));
showHint(activity, String.format(ApplicationLoader.applicationContext.getString(R.string.FileNotFound), path));
return;
}
@@ -933,11 +917,11 @@ public class AndroidUtilities {
}
catch (Exception e) {
showHint(activity, LocaleController.formatString("NoHandleAppInstalled", R.string.NoHandleAppInstalled, mimeType));
showHint(activity, String.format(ApplicationLoader.applicationContext.getString(R.string.NoHandleAppInstalled), mimeType));
}
}
private static File getFineFilename(File path, String desiredName)
public static File getFineFilename(File path, String desiredName)
{
// get a fine file name by adding a number to the basename and avoid overwrites
for( int i = 0; i < 1000; i++ ) {
@@ -973,11 +957,11 @@ public class AndroidUtilities {
final String msg_file_name = msg.getFilename();
final File sourceFile = new File(msg_file_path);
if( !sourceFile.exists() ) {
showHint(context, LocaleController.formatString("FileNotFound", R.string.FileNotFound, msg_file_path));
showHint(context, String.format(context.getString(R.string.FileNotFound), msg_file_path));
return;
}
final Toast waitingHint = showHint(context, ApplicationLoader.applicationContext.getString(R.string.OneMoment));
final Toast waitingHint = showHint(context, context.getString(R.string.OneMoment));
new Thread(new Runnable() {
@Override
@@ -988,7 +972,7 @@ public class AndroidUtilities {
// get destination path
File destPath = null;
boolean add_to_download_manager = false;
if (msg_type == MrMsg.MR_MSG_IMAGE || msg_type == MrMsg.MR_MSG_VIDEO) {
if (msg_type == MrMsg.MR_MSG_IMAGE || msg_type == MrMsg.MR_MSG_GIF || msg_type == MrMsg.MR_MSG_VIDEO) {
destPath = AndroidUtilities.getAlbumDir();
} else if (msg_type == MrMsg.MR_MSG_FILE) {
destPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
@@ -33,40 +33,38 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.res.Configuration;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Handler;
import android.os.PowerManager;
import android.os.SystemClock;
import android.util.Log;
import com.b44t.ui.Components.ForegroundDetector;
import com.b44t.ui.SettingsAdvActivity;
import java.io.File;
public class ApplicationLoader extends Application {
private static PendingIntent pendingIntent;
private static Drawable cachedWallpaper;
private static int selectedColor;
private static boolean isCustomTheme;
private static final Object sync = new Object();
public static volatile Context applicationContext;
public static volatile Handler applicationHandler;
private static volatile boolean applicationInited = false;
public static volatile boolean isScreenOn = false;
public static volatile boolean mainInterfacePaused = true;
public static boolean isCustomTheme() {
return isCustomTheme;
}
public static PowerManager.WakeLock backendWakeLock = null;
public static PowerManager.WakeLock wakeupWakeLock = null;
private static PowerManager.WakeLock stayAwakeWakeLock = null;
public static int getSelectedColor() {
return selectedColor;
}
public static int fontSize;
public static void reloadWallpaper() {
cachedWallpaper = null;
@@ -93,15 +91,12 @@ public class ApplicationLoader extends Application {
if (selectedColor == 0) {
if (selectedBackground == 1000001) {
cachedWallpaper = applicationContext.getResources().getDrawable(R.drawable.background_hd);
isCustomTheme = false;
} else {
File toFile = new File(getFilesDirFixed(), "wallpaper.jpg");
if (toFile.exists()) {
cachedWallpaper = Drawable.createFromPath(toFile.getAbsolutePath());
isCustomTheme = true;
} else {
cachedWallpaper = applicationContext.getResources().getDrawable(R.drawable.background_hd);
isCustomTheme = false;
}
}
}
@@ -138,25 +133,56 @@ public class ApplicationLoader extends Application {
path.mkdirs();
return path;
} catch (Exception e) {
FileLog.e("messenger", e);
}
return new File("/data/data/com.b44t.messenger/files"); // EDIT BY MR
return new File("/data/data/com.b44t.messenger/files");
}
public static void postInitApplication() {
if (applicationInited) {
return;
@Override
public void onCreate() {
super.onCreate();
applicationContext = getApplicationContext(); // should be set very first as it may be needed eg. for logging
MrMailbox.log_i("DeltaChat", "*************** ApplicationLoader.onCreate() ***************");
System.loadLibrary("messenger.1");
new ForegroundDetector(this);
applicationHandler = new Handler(applicationContext.getMainLooper());
// create wake locks
try {
PowerManager pm = (PowerManager) ApplicationLoader.applicationContext.getSystemService(Context.POWER_SERVICE);
backendWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "backendWakeLock" /*any name*/);
// bakendWakeLock _is_ reference counted by the backend (every acquire() has a release())
wakeupWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "wakeupWakeLock" /*any name*/);
wakeupWakeLock.setReferenceCounted(false);
stayAwakeWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "stayAwakeWakeLock" /*any name*/);
stayAwakeWakeLock.setReferenceCounted(false);
} catch (Exception e) {
Log.e("DeltaChat", "Cannot acquire wakeLock");
}
applicationInited = true;
//convertConfig();
// create a MrMailbox object; as android stops the App by just killing it, we do never call MrMailboxUnref()
// however, we may want to to have a look at onPause() eg. of activities (eg. for flushing data, if needed)
MrMailbox.MrCallback(0, 0, 0); // do not remove this call; this makes sure, the function is not removed from build or warnings are printed!
MrMailbox.init();
// start keep-alive service that restarts the app as soon it is terminated
// (this is done by just marking the service as START_STICKY which recreates the service as
// it goes away which also inititialized the app indirectly by calling this function)
applicationContext.startService(new Intent(applicationContext, KeepAliveService.class));
// init locale
try {
LocaleController.getInstance();
LocaleController.getInstance(); // this call does _not_ do nothing; in fact, it creates eg. the formatters while creating the static object.
} catch (Exception e) {
e.printStackTrace();
}
// track screen on/ff
try {
final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
@@ -169,49 +195,20 @@ public class ApplicationLoader extends Application {
try {
PowerManager pm = (PowerManager)ApplicationLoader.applicationContext.getSystemService(Context.POWER_SERVICE);
isScreenOn = pm.isScreenOn();
FileLog.e("messenger", "screen state = " + isScreenOn);
//Log.i("DeltaChat", "screen state = " + isScreenOn);
} catch (Exception e) {
FileLog.e("messenger", e);
}
UserConfig.loadConfig();
String deviceModel;
String langCode;
String appVersion;
String systemVersion;
String configPath = getFilesDirFixed().toString();
try {
langCode = LocaleController.getLocaleStringIso639();
deviceModel = Build.MANUFACTURER + Build.MODEL;
PackageInfo pInfo = ApplicationLoader.applicationContext.getPackageManager().getPackageInfo(ApplicationLoader.applicationContext.getPackageName(), 0);
appVersion = pInfo.versionName + " (" + pInfo.versionCode + ")";
systemVersion = "SDK " + Build.VERSION.SDK_INT;
} catch (Exception e) {
langCode = "en";
deviceModel = "Android unknown";
appVersion = "App version unknown";
systemVersion = "SDK " + Build.VERSION.SDK_INT;
}
if (langCode.trim().length() == 0) {
langCode = "en";
}
if (deviceModel.trim().length() == 0) {
deviceModel = "Android unknown";
}
if (appVersion.trim().length() == 0) {
appVersion = "App version unknown";
}
if (systemVersion.trim().length() == 0) {
systemVersion = "SDK Unknown";
}
// create a timer that wakes up the CPU from time to time
TimerReceiver.scheduleNextAlarm();
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE);
boolean enablePushConnection = preferences.getBoolean("pushConnection", true);
if( preferences.getInt("notify2_"+MrChat.MR_CHAT_ID_DEADDROP, 666)==666 ) {
// make sure, the notifications for the "deaddrop" dialog are muted by default
SharedPreferences.Editor editor = preferences.edit();
// make sure, the notifications for the "deaddrop" dialog are muted by default
SharedPreferences notificationPreferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE);
if( notificationPreferences.getInt("notify2_"+MrChat.MR_CHAT_ID_DEADDROP, 666)==666 ) {
SharedPreferences.Editor editor = notificationPreferences.edit();
editor.putInt("notify2_"+MrChat.MR_CHAT_ID_DEADDROP, 2);
editor.apply();
}
@@ -220,70 +217,53 @@ public class ApplicationLoader extends Application {
// open() should be called before MessagesController.getInstance() as this also initilizes directories based upon getBlobdir().
File dbfile = new File(getFilesDirFixed(), "messenger.db");
MrMailbox.open(dbfile.getAbsolutePath());
MrMailbox.connect();
if( MrMailbox.isConfigured()!=0 ) {
MrMailbox.connect();
}
// create other default objects
MessagesController.getInstance();
ConnectionsManager.getInstance().init(deviceModel, systemVersion, appVersion, langCode, configPath, FileLog.getNetworkLogPath(), UserConfig.getClientUserId(), enablePushConnection);
if (UserConfig.getCurrentUser() != null) {
SendMessagesHelper.getInstance().checkUnsentMessages();
}
SharedPreferences mainPreferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE);
fontSize = mainPreferences.getInt("msg_font_size", SettingsAdvActivity.defMsgFontSize());
ImageLoader.getInstance();
MediaController.getInstance();
}
@Override
public void onCreate() {
super.onCreate();
applicationContext = getApplicationContext();
NativeLoader.initNativeLibs(ApplicationLoader.applicationContext);
//ConnectionsManager.native_setJava(Build.VERSION.SDK_INT == 14 || Build.VERSION.SDK_INT == 15);
new ForegroundDetector(this);
// EDIT BY MR - create a MrMailbox object; as android stops the App by just killing it, we do never call MrMailboxUnref()
// however, we may want to to have a look at onPause() eg. of activities (eg. for flushing data, if needed)
MrMailbox.MrCallback(0, 0, 0); // do not remove this call; this makes sure, the function is not removed from build or warnings are printed!
MrMailbox.init();
applicationHandler = new Handler(applicationContext.getMainLooper());
startPushService();
}
public static void startPushService() {
SharedPreferences preferences = applicationContext.getSharedPreferences("Notifications", MODE_PRIVATE);
if (preferences.getBoolean("pushService", true)) {
AlarmManager am = (AlarmManager) applicationContext.getSystemService(Context.ALARM_SERVICE);
Intent i = new Intent(applicationContext, ApplicationLoader.class);
pendingIntent = PendingIntent.getBroadcast(applicationContext, 0, i, 0);
am.cancel(pendingIntent);
am.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), 60000, pendingIntent);
applicationContext.startService(new Intent(applicationContext, NotificationsService.class));
} else {
stopPushService();
}
}
public static void stopPushService() {
applicationContext.stopService(new Intent(applicationContext, NotificationsService.class));
PendingIntent pintent = PendingIntent.getService(applicationContext, 0, new Intent(applicationContext, NotificationsService.class), 0);
AlarmManager alarm = (AlarmManager)applicationContext.getSystemService(Context.ALARM_SERVICE);
alarm.cancel(pintent);
alarm.cancel(pendingIntent);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
// this function is needed to react to changed system configuration, eg. locale or display size
super.onConfigurationChanged(newConfig);
try {
// change some locale stuff that is not updated automatically, see comments in onDeviceConfigurationChange() and rebuildUiParts()
LocaleController.getInstance().onDeviceConfigurationChange(newConfig);
// re-calculate some things regarding the display (the size may have changed)
AndroidUtilities.checkDisplaySize();
} catch (Exception e) {
e.printStackTrace();
}
}
private static int lastClassGuid = 1;
public static int generateClassGuid() {
return lastClassGuid++;
}
public static boolean isNetworkOnline() {
try {
ConnectivityManager cm = (ConnectivityManager) ApplicationLoader.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo netInfo = cm.getActiveNetworkInfo();
if (netInfo != null && netInfo.isConnected()) {
return true;
}
} catch (Exception e) {
}
return false;
}
public static void stayAwakeForAMoment()
{
stayAwakeWakeLock.acquire(1*60*1000); // 1 Minute to wait for "after chat" messages, after that, we sleep most time, see wakeupWakeLock
}
}
@@ -26,12 +26,13 @@ package com.b44t.messenger;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class AutoMessageHeardReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
ApplicationLoader.postInitApplication();
long dialog_id = intent.getLongExtra("dialog_id", 0);
int max_id = intent.getIntExtra("max_id", 0);
if (dialog_id == 0 || max_id == 0) {
@@ -28,12 +28,13 @@ import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.RemoteInput;
import android.util.Log;
public class AutoMessageReplyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
ApplicationLoader.postInitApplication();
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput == null) {
return;
@@ -1,7 +1,6 @@
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2013-2016 Nikolai Kudashov
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
@@ -26,14 +25,13 @@ package com.b44t.messenger;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class AppStartReceiver extends BroadcastReceiver {
public class BootCompletedReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
ApplicationLoader.startPushService();
}
});
MrMailbox.log_i("DeltaChat", "*** BootCompletedReceiver.onReceive()");
// there's nothing more to do here as all initialisation stuff is already done in
// ApplicationLoader.onCreate() which is called before this broadcast is sended.
}
}
@@ -1,99 +0,0 @@
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2013-2016 Nikolai Kudashov
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see http://www.gnu.org/licenses/ .
*
******************************************************************************/
package com.b44t.messenger;
import android.app.Activity;
import android.app.IntentService;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.system.Os;
import android.system.StructStat;
import java.io.File;
import java.util.HashMap;
public class ClearCacheService extends IntentService {
public ClearCacheService() {
super("ClearCacheService");
}
@Override
protected void onHandleIntent(Intent intent) {
ApplicationLoader.postInitApplication();
/*
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE);
final int keepMedia = preferences.getInt("keep_media", 2);
if (keepMedia == 2) {
return;
}
Utilities.globalQueue.postRunnable(new Runnable() {
@Override
public void run() {
long currentTime = System.currentTimeMillis();
long diff = 60 * 60 * 1000 * 24 * (keepMedia == 0 ? 7 : 30);
final HashMap<Integer, File> paths = ImageLoader.getInstance().createMediaPaths();
for (HashMap.Entry<Integer, File> entry : paths.entrySet()) {
if (entry.getKey() == FileLoader.MEDIA_DIR_CACHE) {
continue;
}
try {
File[] array = entry.getValue().listFiles();
if (array != null) {
for (int b = 0; b < array.length; b++) {
File f = array[b];
if (f.isFile()) {
if (f.getName().equals(".nomedia")) {
continue;
}
if (Build.VERSION.SDK_INT >= 21) {
try {
StructStat stat = Os.stat(f.getPath());
if (stat.st_atime != 0) {
if (stat.st_atime + diff < currentTime) {
f.delete();
}
} else if (stat.st_mtime + diff < currentTime) {
f.delete();
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
} else if (f.lastModified() + diff < currentTime) {
f.delete();
}
}
}
}
} catch (Throwable e) {
FileLog.e("messenger", e);
}
}
}
});
*/
}
}
@@ -1,172 +0,0 @@
package com.b44t.messenger;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.PowerManager;
public class ConnectionsManager {
public final static int RequestFlagFailOnServerErrors = 2;
public final static int ConnectionStateConnecting = 1;
public final static int ConnectionStateWaitingForNetwork = 2;
public final static int ConnectionStateConnected = 3;
public final static int ConnectionStateUpdating = 4;
private long lastPauseTime = System.currentTimeMillis();
private boolean appPaused = true;
private int lastClassGuid = 1;
private PowerManager.WakeLock wakeLock = null;
private static volatile ConnectionsManager Instance = null;
public static ConnectionsManager getInstance() {
ConnectionsManager localInstance = Instance;
if (localInstance == null) {
synchronized (ConnectionsManager.class) {
localInstance = Instance;
if (localInstance == null) {
Instance = localInstance = new ConnectionsManager();
}
}
}
return localInstance;
}
public ConnectionsManager() {
try {
PowerManager pm = (PowerManager) ApplicationLoader.applicationContext.getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "lock");
wakeLock.setReferenceCounted(false);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
public int getCurrentTime() {
return MrMailbox.getCurrentTime();
}
public void cancelRequest(int token, boolean notifyServer) {
//native_cancelRequest(token, notifyServer);
}
public void cleanup() {
//native_cleanUp();
}
public int getConnectionState() {
return ConnectionStateWaitingForNetwork; // EDIT BY MR - we're always disconnected from the view of the caller
/* EDIT BY MR
if (connectionState == ConnectionStateConnected && isUpdating) {
return ConnectionStateUpdating;
}
return connectionState;
*/
}
public void setPushConnectionEnabled(boolean value) {
//native_setPushConnectionEnabled(value);
}
public void init(String deviceModel, String systemVersion, String appVersion, String langCode, String configPath, String logPath, int userId, boolean enablePushConnection) {
//native_init(version, layer, apiId, deviceModel, systemVersion, appVersion, langCode, configPath, logPath, userId, enablePushConnection);
//checkConnection();
BroadcastReceiver networkStateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//checkConnection();
}
};
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
ApplicationLoader.applicationContext.registerReceiver(networkStateReceiver, filter);
}
public void resumeNetworkMaybe() {
//native_resumeNetwork(true);
}
public void setAppPaused(final boolean value, final boolean byScreenState) {
if (!byScreenState) {
appPaused = value;
FileLog.d("messenger", "app paused = " + value);
}
if (value) {
if (lastPauseTime == 0) {
lastPauseTime = System.currentTimeMillis();
}
//native_pauseNetwork();
} else {
if (appPaused) {
return;
}
FileLog.e("messenger", "reset app pause time");
/*if (lastPauseTime != 0 && System.currentTimeMillis() - lastPauseTime > 5000) {
ContactsController.getInstance().checkContacts();
}*/
lastPauseTime = 0;
//native_resumeNetwork(false);
}
}
public int generateClassGuid() {
return lastClassGuid++;
}
/* -- leave this for future use
public static boolean isRoaming() {
try {
ConnectivityManager cm = (ConnectivityManager) ApplicationLoader.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo netInfo = cm.getActiveNetworkInfo();
if (netInfo != null) {
return netInfo.isRoaming();
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
return false;
} */
/* -- leave this for future use
public static boolean isConnectedToWiFi() {
try {
ConnectivityManager cm = (ConnectivityManager) ApplicationLoader.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo netInfo = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
if (netInfo != null && netInfo.getState() == NetworkInfo.State.CONNECTED) {
return true;
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
return false;
} */
public static boolean isNetworkOnline() {
try {
ConnectivityManager cm = (ConnectivityManager) ApplicationLoader.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo netInfo = cm.getActiveNetworkInfo();
if (netInfo != null && (netInfo.isConnectedOrConnecting() || netInfo.isAvailable())) {
return true;
}
netInfo = cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
if (netInfo != null && netInfo.isConnectedOrConnecting()) {
return true;
} else {
netInfo = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
if (netInfo != null && netInfo.isConnectedOrConnecting()) {
return true;
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
return true;
}
return false;
}
}
@@ -92,7 +92,7 @@ public class ContactsController {
pCur.close();
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
return allContacts;
}
@@ -122,40 +122,6 @@ public class ContactsController {
return true;
}
public static String formatName(String firstName, String lastName) {
if (firstName != null) {
firstName = firstName.trim();
}
if (lastName != null) {
lastName = lastName.trim();
}
StringBuilder result = new StringBuilder((firstName != null ? firstName.length() : 0) + (lastName != null ? lastName.length() : 0) + 1);
if (LocaleController.nameDisplayOrder == 1) {
if (firstName != null && firstName.length() > 0) {
result.append(firstName);
if (lastName != null && lastName.length() > 0) {
result.append(" ");
result.append(lastName);
}
} else if (lastName != null && lastName.length() > 0) {
result.append(lastName);
}
} else {
if (lastName != null && lastName.length() > 0) {
result.append(lastName);
if (firstName != null && firstName.length() > 0) {
result.append(" ");
result.append(firstName);
}
} else if (firstName != null && firstName.length() > 0) {
result.append(firstName);
}
}
return result.toString();
}
/* Handle contact images
**********************************************************************************************/
@@ -48,7 +48,7 @@ public class DispatchQueue extends Thread {
handler.sendMessageDelayed(msg, delay);
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
*/
@@ -58,7 +58,7 @@ public class DispatchQueue extends Thread {
syncLatch.await();
handler.removeCallbacks(runnable);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -75,7 +75,7 @@ public class DispatchQueue extends Thread {
handler.postDelayed(runnable, delay);
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -85,7 +85,7 @@ public class DispatchQueue extends Thread {
syncLatch.await();
handler.removeCallbacksAndMessages(null);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
*/
@@ -117,7 +117,7 @@ public class Emoji {
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
@@ -158,7 +158,7 @@ public class Emoji {
}
});
} catch (Throwable x) {
FileLog.e("messenger", "Error loading emoji", x);
//Log.i("DeltaChat", "Error loading emoji", x);
}
}
@@ -209,7 +209,7 @@ public class Emoji {
public static EmojiDrawable getEmojiDrawable(CharSequence code) {
DrawableInfo info = rects.get(code);
if (info == null) {
FileLog.e("messenger", "No drawable for emoji " + code);
//Log.i("DeltaChat", "No drawable for emoji " + code);
return null;
}
EmojiDrawable ed = new EmojiDrawable(info);
@@ -434,7 +434,7 @@ public class Emoji {
// }
// }
// } catch (Exception e) {
// FileLog.e("messenger", e);
//
// return cs;
// }
return s;
@@ -1,443 +0,0 @@
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2013-2016 Nikolai Kudashov
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see http://www.gnu.org/licenses/ .
*
******************************************************************************/
package com.b44t.messenger;
import java.io.RandomAccessFile;
import java.io.File;
import java.util.ArrayList;
public class FileLoadOperation {
private static class RequestInfo {
private int requestToken;
private int offset;
}
private final static int stateIdle = 0;
private final static int stateDownloading = 1;
private final static int stateFailed = 2;
private final static int stateFinished = 3;
private final static int downloadChunkSize = 1024 * 32;
private final static int downloadChunkSizeBig = 1024 * 128;
private final static int maxDownloadRequests = 4;
private final static int maxDownloadRequestsBig = 2;
private final static int bigFileSizeFrom = 1024 * 1024;
private int datacenter_id;
private TLRPC.InputFileLocation location;
private volatile int state = stateIdle;
private int downloadedBytes;
private int totalBytesCount;
private int bytesCountPadding;
private FileLoadOperationDelegate delegate;
private byte[] key;
private byte[] iv;
private int currentDownloadChunkSize;
private int currentMaxDownloadRequests;
private int requestsCount;
private int renameRetryCount;
private int nextDownloadOffset;
private ArrayList<RequestInfo> requestInfos;
private ArrayList<RequestInfo> delayedRequestInfos;
private File cacheFileTemp;
private File cacheFileFinal;
private File cacheIvTemp;
private String ext;
private RandomAccessFile fileOutputStream;
private RandomAccessFile fiv;
private File storePath;
private File tempPath;
private boolean isForceRequest;
public interface FileLoadOperationDelegate {
void didFinishLoadingFile(FileLoadOperation operation, File finalFile);
void didFailedLoadingFile(FileLoadOperation operation, int state);
void didChangedLoadProgress(FileLoadOperation operation, float progress);
}
public FileLoadOperation(TLRPC.FileLocation photoLocation, String extension, int size) {
/*if (photoLocation instanceof TLRPC.TL_fileEncryptedLocation) {
location = new TLRPC.TL_inputEncryptedFileLocation();
location.id = photoLocation.volume_id;
location.volume_id = photoLocation.volume_id;
location.access_hash = photoLocation.secret;
location.local_id = photoLocation.local_id;
iv = new byte[32];
System.arraycopy(photoLocation.iv, 0, iv, 0, iv.length);
key = photoLocation.key;
datacenter_id = photoLocation.dc_id;
} else*/ if (photoLocation instanceof TLRPC.TL_fileLocation) {
location = new TLRPC.TL_inputFileLocation();
location.volume_id = photoLocation.volume_id;
location.local_id = photoLocation.local_id;
datacenter_id = photoLocation.dc_id;
}
totalBytesCount = size;
ext = extension != null ? extension : "jpg";
}
public FileLoadOperation(TLRPC.Document documentLocation) {
try {
/*if (documentLocation instanceof TLRPC.TL_documentEncrypted) {
location = new TLRPC.TL_inputEncryptedFileLocation();
location.id = documentLocation.id;
location.access_hash = documentLocation.access_hash;
datacenter_id = documentLocation.dc_id;
iv = new byte[32];
System.arraycopy(documentLocation.iv, 0, iv, 0, iv.length);
key = documentLocation.key;
} else*/ if (documentLocation instanceof TLRPC.TL_document) {
location = new TLRPC.TL_inputDocumentFileLocation();
location.id = documentLocation.id;
location.access_hash = documentLocation.access_hash;
datacenter_id = documentLocation.dc_id;
}
totalBytesCount = documentLocation.size;
if (key != null) {
int toAdd = 0;
if (totalBytesCount % 16 != 0) {
bytesCountPadding = 16 - totalBytesCount % 16;
totalBytesCount += bytesCountPadding;
}
}
ext = FileLoader.getDocumentFileName(documentLocation);
int idx;
if (ext == null || (idx = ext.lastIndexOf('.')) == -1) {
ext = "";
} else {
ext = ext.substring(idx);
}
if (ext.length() <= 1) {
if (documentLocation.mime_type != null) {
switch (documentLocation.mime_type) {
case "video/mp4":
ext = ".mp4";
break;
case "audio/ogg":
ext = ".ogg";
break;
default:
ext = "";
break;
}
} else {
ext = "";
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
state = stateFailed;
cleanup();
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
delegate.didFailedLoadingFile(FileLoadOperation.this, 0);
}
});
}
}
public void setForceRequest(boolean forceRequest) {
isForceRequest = forceRequest;
}
public boolean isForceRequest() {
return isForceRequest;
}
public void setPaths(File store, File temp) {
storePath = store;
tempPath = temp;
}
public void start() {
if (state != stateIdle) {
return;
}
currentDownloadChunkSize = totalBytesCount >= bigFileSizeFrom ? downloadChunkSizeBig : downloadChunkSize;
currentMaxDownloadRequests = totalBytesCount >= bigFileSizeFrom ? maxDownloadRequestsBig : maxDownloadRequests;
requestInfos = new ArrayList<>(currentMaxDownloadRequests);
delayedRequestInfos = new ArrayList<>(currentMaxDownloadRequests - 1);
state = stateDownloading;
if (location == null) {
cleanup();
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
delegate.didFailedLoadingFile(FileLoadOperation.this, 0);
}
});
return;
}
String fileNameFinal;
String fileNameTemp;
String fileNameIv = null;
if (location.volume_id != 0 && location.local_id != 0) {
fileNameTemp = location.volume_id + "_" + location.local_id + ".temp";
fileNameFinal = location.volume_id + "_" + location.local_id + "." + ext;
if (key != null) {
fileNameIv = location.volume_id + "_" + location.local_id + ".iv";
}
if (datacenter_id == Integer.MIN_VALUE || location.volume_id == Integer.MIN_VALUE || datacenter_id == 0) {
cleanup();
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
delegate.didFailedLoadingFile(FileLoadOperation.this, 0);
}
});
return;
}
} else {
fileNameTemp = datacenter_id + "_" + location.id + ".temp";
fileNameFinal = datacenter_id + "_" + location.id + ext;
if (key != null) {
fileNameIv = datacenter_id + "_" + location.id + ".iv";
}
if (datacenter_id == 0 || location.id == 0) {
cleanup();
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
delegate.didFailedLoadingFile(FileLoadOperation.this, 0);
}
});
return;
}
}
cacheFileFinal = new File(storePath, fileNameFinal);
boolean exist = cacheFileFinal.exists();
if (exist && totalBytesCount != 0 && totalBytesCount != cacheFileFinal.length()) {
cacheFileFinal.delete();
}
if (!cacheFileFinal.exists()) {
cacheFileTemp = new File(tempPath, fileNameTemp);
if (cacheFileTemp.exists()) {
downloadedBytes = (int) cacheFileTemp.length();
nextDownloadOffset = downloadedBytes = downloadedBytes / currentDownloadChunkSize * currentDownloadChunkSize;
}
if (BuildVars.DEBUG_VERSION) {
FileLog.d("messenger", "start loading file to temp = " + cacheFileTemp + " final = " + cacheFileFinal);
}
if (fileNameIv != null) {
cacheIvTemp = new File(tempPath, fileNameIv);
try {
fiv = new RandomAccessFile(cacheIvTemp, "rws");
long len = cacheIvTemp.length();
if (len > 0 && len % 32 == 0) {
fiv.read(iv, 0, 32);
} else {
downloadedBytes = 0;
}
} catch (Exception e) {
FileLog.e("messenger", e);
downloadedBytes = 0;
}
}
try {
fileOutputStream = new RandomAccessFile(cacheFileTemp, "rws");
if (downloadedBytes != 0) {
fileOutputStream.seek(downloadedBytes);
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
if (fileOutputStream == null) {
cleanup();
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
delegate.didFailedLoadingFile(FileLoadOperation.this, 0);
}
});
return;
}
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
if (totalBytesCount != 0 && downloadedBytes == totalBytesCount) {
try {
onFinishLoadingFile();
} catch (Exception e) {
delegate.didFailedLoadingFile(FileLoadOperation.this, 0);
}
} else {
startDownloadRequest();
}
}
});
} else {
try {
onFinishLoadingFile();
} catch (Exception e) {
delegate.didFailedLoadingFile(FileLoadOperation.this, 0);
}
}
}
public void cancel() {
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
if (state == stateFinished || state == stateFailed) {
return;
}
state = stateFailed;
cleanup();
if (requestInfos != null) {
for (int a = 0; a < requestInfos.size(); a++) {
RequestInfo requestInfo = requestInfos.get(a);
if (requestInfo.requestToken != 0) {
ConnectionsManager.getInstance().cancelRequest(requestInfo.requestToken, true);
}
}
}
delegate.didFailedLoadingFile(FileLoadOperation.this, 1);
}
});
}
private void cleanup() {
try {
if (fileOutputStream != null) {
try {
fileOutputStream.getChannel().close();
} catch (Exception e) {
FileLog.e("messenger", e);
}
fileOutputStream.close();
fileOutputStream = null;
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
try {
if (fiv != null) {
fiv.close();
fiv = null;
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
if (delayedRequestInfos != null) {
for (int a = 0; a < delayedRequestInfos.size(); a++) {
RequestInfo requestInfo = delayedRequestInfos.get(a);
/*if (requestInfo.response != null) {
requestInfo.response.disableFree = false;
requestInfo.response.freeResources();
}*/
}
delayedRequestInfos.clear();
}
}
private void onFinishLoadingFile() throws Exception {
if (state != stateDownloading) {
return;
}
state = stateFinished;
cleanup();
if (cacheIvTemp != null) {
cacheIvTemp.delete();
cacheIvTemp = null;
}
if (cacheFileTemp != null) {
boolean renameResult = cacheFileTemp.renameTo(cacheFileFinal);
if (!renameResult) {
if (BuildVars.DEBUG_VERSION) {
FileLog.e("messenger", "unable to rename temp = " + cacheFileTemp + " to final = " + cacheFileFinal + " retry = " + renameRetryCount);
}
renameRetryCount++;
if (renameRetryCount < 3) {
state = stateDownloading;
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
try {
onFinishLoadingFile();
} catch (Exception e) {
delegate.didFailedLoadingFile(FileLoadOperation.this, 0);
}
}
}, 200);
return;
}
cacheFileFinal = cacheFileTemp;
}
}
if (BuildVars.DEBUG_VERSION) {
FileLog.e("messenger", "finished downloading file to " + cacheFileFinal);
}
delegate.didFinishLoadingFile(FileLoadOperation.this, cacheFileFinal);
}
private void startDownloadRequest() {
if (state != stateDownloading || totalBytesCount > 0 && nextDownloadOffset >= totalBytesCount || requestInfos.size() + delayedRequestInfos.size() >= currentMaxDownloadRequests) {
return;
}
int count = 1;
if (totalBytesCount > 0) {
count = Math.max(0, currentMaxDownloadRequests - requestInfos.size() - delayedRequestInfos.size());
}
for (int a = 0; a < count; a++) {
if (totalBytesCount > 0 && nextDownloadOffset >= totalBytesCount) {
break;
}
boolean isLast = totalBytesCount <= 0 || a == count - 1 || totalBytesCount > 0 && nextDownloadOffset + currentDownloadChunkSize >= totalBytesCount;
//TLRPC.TL_upload_getFile req = new TLRPC.TL_upload_getFile();
//req.location = location;
//req.offset = nextDownloadOffset;
//req.limit = currentDownloadChunkSize;
nextDownloadOffset += currentDownloadChunkSize;
final RequestInfo requestInfo = new RequestInfo();
requestInfos.add(requestInfo);
requestInfo.offset = nextDownloadOffset;
requestInfo.requestToken = 0;/*ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() {
@Override
public void run(TLObject response, TLRPC.TL_error error) {
requestInfo.response = (TLRPC.TL_upload_file) response;
processRequestResult(requestInfo, error);
}
}, null, (isForceRequest ? ConnectionsManager.RequestFlagForceDownload : 0) | ConnectionsManager.RequestFlagFailOnServerErrors, datacenter_id, requestsCount % 2 == 0 ? ConnectionsManager.ConnectionTypeDownload : ConnectionsManager.ConnectionTypeDownload2, isLast);*/
requestsCount++;
}
}
public void setDelegate(FileLoadOperationDelegate delegate) {
this.delegate = delegate;
}
}
@@ -32,15 +32,6 @@ import java.util.concurrent.Semaphore;
public class FileLoader {
public interface FileLoaderDelegate {
void fileDidLoaded(String location, File finalFile, int type);
void fileDidFailedLoad(String location, int state);
void fileLoadProgressChanged(String location, float progress);
}
public static final int MEDIA_DIR_IMAGE = 0;
public static final int MEDIA_DIR_AUDIO = 1;
public static final int MEDIA_DIR_VIDEO = 2;
@@ -48,18 +39,6 @@ public class FileLoader {
public static final int MEDIA_DIR_CACHE = 4;
private HashMap<Integer, File> mediaDirs = null;
private volatile DispatchQueue fileLoaderQueue = new DispatchQueue("fileUploadQueue");
private LinkedList<FileLoadOperation> loadOperationQueue = new LinkedList<>();
private LinkedList<FileLoadOperation> audioLoadOperationQueue = new LinkedList<>();
private LinkedList<FileLoadOperation> photoLoadOperationQueue = new LinkedList<>();
private ConcurrentHashMap<String, FileLoadOperation> loadOperationPaths = new ConcurrentHashMap<>();
private FileLoaderDelegate delegate = null;
private int currentLoadOperationsCount = 0;
private int currentAudioLoadOperationsCount = 0;
private int currentPhotoLoadOperationsCount = 0;
private static volatile FileLoader Instance = null;
@@ -95,257 +74,8 @@ public class FileLoader {
return dir;
}
public void cancelLoadFile(TLRPC.Document document) {
cancelLoadFile(document, null, null);
}
public void cancelLoadFile(TLRPC.PhotoSize photo) {
cancelLoadFile(null, photo.location, null);
}
public void cancelLoadFile(TLRPC.FileLocation location, String ext) {
cancelLoadFile(null, location, ext);
}
private void cancelLoadFile(final TLRPC.Document document, final TLRPC.FileLocation location, final String locationExt) {
if (location == null && document == null) {
return;
}
fileLoaderQueue.postRunnable(new Runnable() {
@Override
public void run() {
String fileName = null;
if (location != null) {
fileName = getAttachFileName(location, locationExt);
} else if (document != null) {
fileName = getAttachFileName(document);
}
if (fileName == null) {
return;
}
FileLoadOperation operation = loadOperationPaths.remove(fileName);
if (operation != null) {
if (MessageObject.isVoiceDocument(document)) {
audioLoadOperationQueue.remove(operation);
} else if (location != null) {
photoLoadOperationQueue.remove(operation);
} else {
loadOperationQueue.remove(operation);
}
operation.cancel();
}
}
});
}
public boolean isLoadingFile(final String fileName) {
final Semaphore semaphore = new Semaphore(0);
final Boolean[] result = new Boolean[1];
fileLoaderQueue.postRunnable(new Runnable() {
@Override
public void run() {
result[0] = loadOperationPaths.containsKey(fileName);
semaphore.release();
}
});
try {
semaphore.acquire();
} catch (Exception e) {
FileLog.e("messenger", e);
}
return result[0];
}
public void loadFile(TLRPC.PhotoSize photo, String ext, boolean cacheOnly) {
loadFile(null, photo.location, ext, photo.size, false, cacheOnly || (photo != null && photo.size == 0 || photo.location.key != null));
}
public void loadFile(TLRPC.Document document, boolean force, boolean cacheOnly) {
loadFile(document, null, null, 0, force, cacheOnly || document != null && document.key != null);
}
public void loadFile(TLRPC.FileLocation location, String ext, int size, boolean cacheOnly) {
loadFile(null, location, ext, size, true, cacheOnly || size == 0 || (location != null && location.key != null));
}
private void loadFile(final TLRPC.Document document, final TLRPC.FileLocation location, final String locationExt, final int locationSize, final boolean force, final boolean cacheOnly) {
fileLoaderQueue.postRunnable(new Runnable() {
@Override
public void run() {
String fileName = null;
if (location != null) {
fileName = getAttachFileName(location, locationExt);
} else if (document != null) {
fileName = getAttachFileName(document);
}
if (fileName == null || fileName.contains("" + Integer.MIN_VALUE)) {
return;
}
FileLoadOperation operation;
operation = loadOperationPaths.get(fileName);
if (operation != null) {
if (force) {
LinkedList<FileLoadOperation> downloadQueue;
if (MessageObject.isVoiceDocument(document)) {
downloadQueue = audioLoadOperationQueue;
} else if (location != null) {
downloadQueue = photoLoadOperationQueue;
} else {
downloadQueue = loadOperationQueue;
}
if (downloadQueue != null) {
int index = downloadQueue.indexOf(operation);
if (index != -1) {
downloadQueue.remove(index);
downloadQueue.add(0, operation);
operation.setForceRequest(true);
}
}
}
return;
}
File tempDir = getDirectory(MEDIA_DIR_CACHE);
File storeDir = tempDir;
int type = MEDIA_DIR_CACHE;
if (location != null) {
operation = new FileLoadOperation(location, locationExt, locationSize);
type = MEDIA_DIR_IMAGE;
} else if (document != null) {
operation = new FileLoadOperation(document);
if (MessageObject.isVoiceDocument(document)) {
type = MEDIA_DIR_AUDIO;
} else if (MessageObject.isVideoDocument(document)) {
type = MEDIA_DIR_VIDEO;
} else {
type = MEDIA_DIR_DOCUMENT;
}
}
if (!cacheOnly) {
storeDir = getDirectory(type);
}
operation.setPaths(storeDir, tempDir);
final String finalFileName = fileName;
final int finalType = type;
loadOperationPaths.put(fileName, operation);
operation.setDelegate(new FileLoadOperation.FileLoadOperationDelegate() {
@Override
public void didFinishLoadingFile(FileLoadOperation operation, File finalFile) {
if (delegate != null) {
delegate.fileDidLoaded(finalFileName, finalFile, finalType);
}
checkDownloadQueue(document, location, finalFileName);
}
@Override
public void didFailedLoadingFile(FileLoadOperation operation, int canceled) {
checkDownloadQueue(document, location, finalFileName);
if (delegate != null) {
delegate.fileDidFailedLoad(finalFileName, canceled);
}
}
@Override
public void didChangedLoadProgress(FileLoadOperation operation, float progress) {
if (delegate != null) {
delegate.fileLoadProgressChanged(finalFileName, progress);
}
}
});
int maxCount = force ? 3 : 1;
if (type == MEDIA_DIR_AUDIO) {
if (currentAudioLoadOperationsCount < maxCount) {
currentAudioLoadOperationsCount++;
operation.start();
} else {
if (force) {
audioLoadOperationQueue.add(0, operation);
} else {
audioLoadOperationQueue.add(operation);
}
}
} else if (location != null) {
if (currentPhotoLoadOperationsCount < maxCount) {
currentPhotoLoadOperationsCount++;
operation.start();
} else {
if (force) {
photoLoadOperationQueue.add(0, operation);
} else {
photoLoadOperationQueue.add(operation);
}
}
} else {
if (currentLoadOperationsCount < maxCount) {
currentLoadOperationsCount++;
operation.start();
} else {
if (force) {
loadOperationQueue.add(0, operation);
} else {
loadOperationQueue.add(operation);
}
}
}
}
});
}
private void checkDownloadQueue(final TLRPC.Document document, final TLRPC.FileLocation location, final String arg1) {
fileLoaderQueue.postRunnable(new Runnable() {
@Override
public void run() {
loadOperationPaths.remove(arg1);
FileLoadOperation operation;
if (MessageObject.isVoiceDocument(document)) {
currentAudioLoadOperationsCount--;
if (!audioLoadOperationQueue.isEmpty()) {
operation = audioLoadOperationQueue.get(0);
int maxCount = operation.isForceRequest() ? 3 : 1;
if (currentAudioLoadOperationsCount < maxCount) {
operation = audioLoadOperationQueue.poll();
if (operation != null) {
currentAudioLoadOperationsCount++;
operation.start();
}
}
}
} else if (location != null) {
currentPhotoLoadOperationsCount--;
if (!photoLoadOperationQueue.isEmpty()) {
operation = photoLoadOperationQueue.get(0);
int maxCount = operation.isForceRequest() ? 3 : 1;
if (currentPhotoLoadOperationsCount < maxCount) {
operation = photoLoadOperationQueue.poll();
if (operation != null) {
currentPhotoLoadOperationsCount++;
operation.start();
}
}
}
} else {
currentLoadOperationsCount--;
if (!loadOperationQueue.isEmpty()) {
operation = loadOperationQueue.get(0);
int maxCount = operation.isForceRequest() ? 3 : 1;
if (currentLoadOperationsCount < maxCount) {
operation = loadOperationQueue.poll();
if (operation != null) {
currentLoadOperationsCount++;
operation.start();
}
}
}
}
}
});
}
public void setDelegate(FileLoaderDelegate delegate) {
this.delegate = delegate;
return false;
}
public static String getMessageFileName(TLRPC.Message message) {
@@ -23,8 +23,6 @@
package com.b44t.messenger;
import android.util.Log;
import com.b44t.messenger.time.FastDateFormat;
import java.io.File;
@@ -37,7 +35,6 @@ public class FileLog {
private FastDateFormat dateFormat = null;
private DispatchQueue logQueue = null;
private File currentFile = null;
private File networkFile = null;
private static volatile FileLog Instance = null;
public static FileLog getInstance() {
@@ -54,10 +51,7 @@ public class FileLog {
}
public FileLog() {
if (!BuildVars.DEBUG_VERSION) {
return;
}
dateFormat = FastDateFormat.getInstance("dd_MM_yyyy_HH_mm_ss", Locale.US);
dateFormat = FastDateFormat.getInstance("yyyyMMdd_HHmmss", Locale.US);
try {
File sdCard = ApplicationLoader.applicationContext.getExternalFilesDir(null);
if (sdCard == null) {
@@ -81,37 +75,14 @@ public class FileLog {
}
}
public static String getNetworkLogPath() {
if (!BuildVars.DEBUG_VERSION) {
return "";
}
try {
File sdCard = ApplicationLoader.applicationContext.getExternalFilesDir(null);
if (sdCard == null) {
return "";
}
File dir = new File(sdCard.getAbsolutePath() + "/logs");
dir.mkdirs();
getInstance().networkFile = new File(dir, getInstance().dateFormat.format(System.currentTimeMillis()) + "_net.txt");
return getInstance().networkFile.getAbsolutePath();
} catch (Throwable e) {
e.printStackTrace();
}
return "";
}
public static void e(final String tag, final String message, final Throwable exception) {
if (!BuildVars.DEBUG_VERSION) {
return;
}
Log.e(tag, message, exception);
private static void log(final String what, final String tag, final String message)
{
if (getInstance().streamWriter != null) {
getInstance().logQueue.postRunnable(new Runnable() {
@Override
public void run() {
try {
getInstance().streamWriter.write(getInstance().dateFormat.format(System.currentTimeMillis()) + " E/" + tag + " " + message + "\n");
getInstance().streamWriter.write(exception.toString());
getInstance().streamWriter.write(getInstance().dateFormat.format(System.currentTimeMillis()) + " " + what + "/" + tag + ": " + message + "\n");
getInstance().streamWriter.flush();
} catch (Exception e) {
e.printStackTrace();
@@ -122,109 +93,14 @@ public class FileLog {
}
public static void e(final String tag, final String message) {
if (!BuildVars.DEBUG_VERSION) {
return;
}
Log.e(tag, message);
if (getInstance().streamWriter != null) {
getInstance().logQueue.postRunnable(new Runnable() {
@Override
public void run() {
try {
getInstance().streamWriter.write(getInstance().dateFormat.format(System.currentTimeMillis()) + " E/" + tag + "" + message + "\n");
getInstance().streamWriter.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
public static void e(final String tag, final Throwable e) {
if (!BuildVars.DEBUG_VERSION) {
return;
}
e.printStackTrace();
if (getInstance().streamWriter != null) {
getInstance().logQueue.postRunnable(new Runnable() {
@Override
public void run() {
try {
getInstance().streamWriter.write(getInstance().dateFormat.format(System.currentTimeMillis()) + " E/" + tag + "" + e + "\n");
StackTraceElement[] stack = e.getStackTrace();
for (int a = 0; a < stack.length; a++) {
getInstance().streamWriter.write(getInstance().dateFormat.format(System.currentTimeMillis()) + " E/" + tag + "" + stack[a] + "\n");
}
getInstance().streamWriter.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
});
} else {
e.printStackTrace();
}
}
public static void d(final String tag, final String message) {
if (!BuildVars.DEBUG_VERSION) {
return;
}
Log.d(tag, message);
if (getInstance().streamWriter != null) {
getInstance().logQueue.postRunnable(new Runnable() {
@Override
public void run() {
try {
getInstance().streamWriter.write(getInstance().dateFormat.format(System.currentTimeMillis()) + " D/" + tag + "" + message + "\n");
getInstance().streamWriter.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
log("E", tag, message);
}
public static void w(final String tag, final String message) {
if (!BuildVars.DEBUG_VERSION) {
return;
}
Log.w(tag, message);
if (getInstance().streamWriter != null) {
getInstance().logQueue.postRunnable(new Runnable() {
@Override
public void run() {
try {
getInstance().streamWriter.write(getInstance().dateFormat.format(System.currentTimeMillis()) + " W/" + tag + ": " + message + "\n");
getInstance().streamWriter.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
log("W", tag, message);
}
public static void cleanupLogs() {
File sdCard = ApplicationLoader.applicationContext.getExternalFilesDir(null);
if (sdCard == null) {
return;
}
File dir = new File (sdCard.getAbsolutePath() + "/logs");
File[] files = dir.listFiles();
if (files != null) {
for (int a = 0; a < files.length; a++) {
File file = files[a];
if (getInstance().currentFile != null && file.getAbsolutePath().equals(getInstance().currentFile.getAbsolutePath())) {
continue;
}
if (getInstance().networkFile != null && file.getAbsolutePath().equals(getInstance().networkFile.getAbsolutePath())) {
continue;
}
file.delete();
}
}
public static void i(final String tag, final String message) {
log("I", tag, message);
}
}
@@ -35,7 +35,6 @@ import android.graphics.drawable.BitmapDrawable;
import android.media.ExifInterface;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.provider.MediaStore;
@@ -47,20 +46,10 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ImageLoader {
@@ -71,29 +60,20 @@ public class ImageLoader {
private HashMap<Integer, CacheImage> imageLoadingByTag = new HashMap<>();
private HashMap<String, ThumbGenerateInfo> waitingForQualityThumb = new HashMap<>();
private HashMap<Integer, String> waitingForQualityThumbByTag = new HashMap<>();
private LinkedList<HttpImageTask> httpTasks = new LinkedList<>();
private DispatchQueue cacheOutQueue = new DispatchQueue("cacheOutQueue");
private DispatchQueue cacheThumbOutQueue = new DispatchQueue("cacheThumbOutQueue");
private DispatchQueue thumbGeneratingQueue = new DispatchQueue("thumbGeneratingQueue");
private DispatchQueue imageLoadQueue = new DispatchQueue("imageLoadQueue");
private ConcurrentHashMap<String, Float> fileProgresses = new ConcurrentHashMap<>();
private HashMap<String, ThumbGenerateTask> thumbGenerateTasks = new HashMap<>();
private static byte[] bytes;
private static byte[] bytesThumb;
private static byte[] header = new byte[12];
private static byte[] headerThumb = new byte[12];
private int currentHttpTasksCount = 0;
private LinkedList<HttpFileTask> httpFileLoadTasks = new LinkedList<>();
private HashMap<String, HttpFileTask> httpFileLoadTasksByKeys = new HashMap<>();
private HashMap<String, Runnable> retryHttpsTasks = new HashMap<>();
private int currentHttpFileLoadTasksCount = 0;
private String ignoreRemoval = null;
private volatile long lastCacheOutTime = 0;
private int lastImageNum = 0;
private long lastProgressUpdateTime = 0;
private class ThumbGenerateInfo {
private int count;
@@ -101,343 +81,6 @@ public class ImageLoader {
private String filter;
}
private class HttpFileTask extends AsyncTask<Void, Void, Boolean> {
private String url;
private File tempFile;
private String ext;
private RandomAccessFile fileOutputStream = null;
private boolean canRetry = true;
public HttpFileTask(String url, File tempFile, String ext) {
this.url = url;
this.tempFile = tempFile;
this.ext = ext;
}
protected Boolean doInBackground(Void... voids) {
InputStream httpConnectionStream = null;
boolean done = false;
URLConnection httpConnection = null;
try {
URL downloadUrl = new URL(url);
httpConnection = downloadUrl.openConnection();
httpConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/_BuildID_) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36");
httpConnection.addRequestProperty("Referer", "google.com");
httpConnection.setConnectTimeout(5000);
httpConnection.setReadTimeout(5000);
if (httpConnection instanceof HttpURLConnection) {
HttpURLConnection httpURLConnection = (HttpURLConnection) httpConnection;
httpURLConnection.setInstanceFollowRedirects(true);
int status = httpURLConnection.getResponseCode();
if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM || status == HttpURLConnection.HTTP_SEE_OTHER) {
String newUrl = httpURLConnection.getHeaderField("Location");
String cookies = httpURLConnection.getHeaderField("Set-Cookie");
downloadUrl = new URL(newUrl);
httpConnection = downloadUrl.openConnection();
httpConnection.setRequestProperty("Cookie", cookies);
httpConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/_BuildID_) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36");
httpConnection.addRequestProperty("Referer", "google.com");
}
}
httpConnection.connect();
httpConnectionStream = httpConnection.getInputStream();
fileOutputStream = new RandomAccessFile(tempFile, "rws");
} catch (Throwable e) {
if (e instanceof UnknownHostException) {
canRetry = false;
}
FileLog.e("messenger", e);
}
if (canRetry) {
try {
if (httpConnection != null && httpConnection instanceof HttpURLConnection) {
int code = ((HttpURLConnection) httpConnection).getResponseCode();
if (code != HttpURLConnection.HTTP_OK && code != HttpURLConnection.HTTP_ACCEPTED && code != HttpURLConnection.HTTP_NOT_MODIFIED) {
canRetry = false;
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
if (httpConnectionStream != null) {
try {
byte[] data = new byte[1024 * 4];
while (true) {
if (isCancelled()) {
break;
}
try {
int read = httpConnectionStream.read(data);
if (read > 0) {
fileOutputStream.write(data, 0, read);
} else if (read == -1) {
done = true;
break;
} else {
break;
}
} catch (Exception e) {
FileLog.e("messenger", e);
break;
}
}
} catch (Throwable e) {
FileLog.e("messenger", e);
}
}
try {
if (fileOutputStream != null) {
fileOutputStream.close();
fileOutputStream = null;
}
} catch (Throwable e) {
FileLog.e("messenger", e);
}
try {
if (httpConnectionStream != null) {
httpConnectionStream.close();
}
} catch (Throwable e) {
FileLog.e("messenger", e);
}
}
return done;
}
@Override
protected void onPostExecute(Boolean result) {
runHttpFileLoadTasks(this, result ? 2 : 1);
}
@Override
protected void onCancelled() {
runHttpFileLoadTasks(this, 2);
}
}
private class HttpImageTask extends AsyncTask<Void, Void, Boolean> {
private CacheImage cacheImage = null;
private RandomAccessFile fileOutputStream = null;
private int imageSize;
private long lastProgressTime;
private boolean canRetry = true;
private URLConnection httpConnection = null;
public HttpImageTask(CacheImage cacheImage, int size) {
this.cacheImage = cacheImage;
imageSize = size;
}
private void reportProgress(final float progress) {
long currentTime = System.currentTimeMillis();
if (progress == 1 || lastProgressTime == 0 || lastProgressTime < currentTime - 500) {
lastProgressTime = currentTime;
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
fileProgresses.put(cacheImage.url, progress);
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.FileLoadProgressChanged, cacheImage.url, progress);
}
});
}
});
}
}
protected Boolean doInBackground(Void... voids) {
InputStream httpConnectionStream = null;
boolean done = false;
if (!isCancelled()) {
try {
URL downloadUrl = new URL(cacheImage.httpUrl);
httpConnection = downloadUrl.openConnection();
httpConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/_BuildID_) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36");
httpConnection.addRequestProperty("Referer", "google.com");
httpConnection.setConnectTimeout(5000);
httpConnection.setReadTimeout(5000);
if (httpConnection instanceof HttpURLConnection) {
((HttpURLConnection) httpConnection).setInstanceFollowRedirects(true);
}
if (!isCancelled()) {
httpConnection.connect();
httpConnectionStream = httpConnection.getInputStream();
fileOutputStream = new RandomAccessFile(cacheImage.tempFilePath, "rws");
}
} catch (Throwable e) {
if (e instanceof SocketTimeoutException) {
if (ConnectionsManager.isNetworkOnline()) {
canRetry = false;
}
} else if (e instanceof UnknownHostException) {
canRetry = false;
} else if (e instanceof SocketException) {
if (e.getMessage() != null && e.getMessage().contains("ECONNRESET")) {
canRetry = false;
}
}
FileLog.e("messenger", e);
}
}
if (!isCancelled()) {
try {
if (httpConnection != null && httpConnection instanceof HttpURLConnection) {
int code = ((HttpURLConnection) httpConnection).getResponseCode();
if (code != HttpURLConnection.HTTP_OK && code != HttpURLConnection.HTTP_ACCEPTED && code != HttpURLConnection.HTTP_NOT_MODIFIED) {
canRetry = false;
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
if (imageSize == 0 && httpConnection != null) {
try {
Map<String, List<String>> headerFields = httpConnection.getHeaderFields();
if (headerFields != null) {
List values = headerFields.get("content-Length");
if (values != null && !values.isEmpty()) {
String length = (String) values.get(0);
if (length != null) {
imageSize = Utilities.parseInt(length);
}
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
if (httpConnectionStream != null) {
try {
byte[] data = new byte[1024 * 8];
int totalLoaded = 0;
while (true) {
if (isCancelled()) {
break;
}
try {
int read = httpConnectionStream.read(data);
if (read > 0) {
totalLoaded += read;
fileOutputStream.write(data, 0, read);
if (imageSize != 0) {
reportProgress(totalLoaded / (float) imageSize);
}
} else if (read == -1) {
done = true;
if (imageSize != 0) {
reportProgress(1.0f);
}
break;
} else {
break;
}
} catch (Exception e) {
FileLog.e("messenger", e);
break;
}
}
} catch (Throwable e) {
FileLog.e("messenger", e);
}
}
}
try {
if (fileOutputStream != null) {
fileOutputStream.close();
fileOutputStream = null;
}
} catch (Throwable e) {
FileLog.e("messenger", e);
}
try {
if (httpConnectionStream != null) {
httpConnectionStream.close();
}
} catch (Throwable e) {
FileLog.e("messenger", e);
}
if (done) {
if (cacheImage.tempFilePath != null) {
if (!cacheImage.tempFilePath.renameTo(cacheImage.finalFilePath)) {
cacheImage.finalFilePath = cacheImage.tempFilePath;
}
}
}
return done;
}
@Override
protected void onPostExecute(final Boolean result) {
if (result || !canRetry) {
fileDidLoaded(cacheImage.url, cacheImage.finalFilePath, FileLoader.MEDIA_DIR_IMAGE);
} else {
httpFileLoadError(cacheImage.url);
}
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
fileProgresses.remove(cacheImage.url);
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
if (result) {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.FileDidLoaded, cacheImage.url);
} else {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.FileDidFailedLoad, cacheImage.url, 2);
}
}
});
}
});
imageLoadQueue.postRunnable(new Runnable() {
@Override
public void run() {
runHttpTasks(true);
}
});
}
@Override
protected void onCancelled() {
imageLoadQueue.postRunnable(new Runnable() {
@Override
public void run() {
runHttpTasks(true);
}
});
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
fileProgresses.remove(cacheImage.url);
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.FileDidFailedLoad, cacheImage.url, 1);
}
});
}
});
}
}
private class ThumbGenerateTask implements Runnable {
private File originalPath;
@@ -514,7 +157,7 @@ public class ImageLoader {
try {
stream.close();
} catch (Exception e) {
FileLog.e("messenger", e);
}
final BitmapDrawable bitmapDrawable = new BitmapDrawable(originalBitmap);
AndroidUtilities.runOnUIThread(new Runnable() {
@@ -531,7 +174,7 @@ public class ImageLoader {
}
});
} catch (Throwable e) {
FileLog.e("messenger", e);
removeTask();
}
}
@@ -593,13 +236,13 @@ public class ImageLoader {
}
randomAccessFile.close();
} catch (Exception e) {
FileLog.e("messenger", e);
} finally {
if (randomAccessFile != null) {
try {
randomAccessFile.close();
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
@@ -684,7 +327,7 @@ public class ImageLoader {
}
}
} catch (Throwable e) {
FileLog.e("messenger", e);
}
} else {
try {
@@ -933,7 +576,7 @@ public class ImageLoader {
trackAllocation = cl.getMethod("trackExternalAllocation", new Class[]{long.class});
trackFree = cl.getMethod("trackExternalFree", new Class[]{long.class});
} catch (Exception e) {
FileLog.e("messenger", e);
runtime = null;
trackAllocation = null;
trackFree = null;
@@ -955,7 +598,6 @@ public class ImageLoader {
protected boolean thumb;
protected String httpUrl;
protected HttpImageTask httpTask;
protected CacheOutTask cacheTask;
protected ArrayList<ImageReceiver> imageReceiverArray = new ArrayList<>();
@@ -992,9 +634,9 @@ public class ImageLoader {
imageReceiverArray.clear();
if (location != null) {
if (location instanceof TLRPC.FileLocation) {
FileLoader.getInstance().cancelLoadFile((TLRPC.FileLocation) location, ext);
//FileLoader.getInstance().cancelLoadFile((TLRPC.FileLocation) location, ext);
} else if (location instanceof TLRPC.Document) {
FileLoader.getInstance().cancelLoadFile((TLRPC.Document) location);
//FileLoader.getInstance().cancelLoadFile((TLRPC.Document) location);
}
}
if (cacheTask != null) {
@@ -1006,11 +648,11 @@ public class ImageLoader {
cacheTask.cancel();
cacheTask = null;
}
if (httpTask != null) {
httpTasks.remove(httpTask);
httpTask.cancel(true);
httpTask = null;
}
//if (httpTask != null) {
// httpTasks.remove(httpTask);
// httpTask.cancel(true);
// httpTask = null;
//}
if (url != null) {
imageLoadingByUrl.remove(url);
}
@@ -1106,106 +748,9 @@ public class ImageLoader {
}
};
FileLoader.getInstance().setDelegate(new FileLoader.FileLoaderDelegate() {
/*
@Override
public void fileUploadProgressChanged(final String location, final float progress, final boolean isEncrypted) {
fileProgresses.put(location, progress);
long currentTime = System.currentTimeMillis();
if (lastProgressUpdateTime == 0 || lastProgressUpdateTime < currentTime - 500) {
lastProgressUpdateTime = currentTime;
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.FileUploadProgressChanged, location, progress, isEncrypted);
}
});
}
}
@Override
public void fileDidUploaded(final String location, final TLRPC.InputFile inputFile, final TLRPC.InputEncryptedFile inputEncryptedFile, final byte[] key, final byte[] iv, final long totalFileSize) {
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.FileDidUpload, location, inputFile, inputEncryptedFile, key, iv, totalFileSize);
}
});
fileProgresses.remove(location);
}
});
}
@Override
public void fileDidFailedUpload(final String location, final boolean isEncrypted) {
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.FileDidFailUpload, location, isEncrypted);
}
});
fileProgresses.remove(location);
}
});
}
*/
@Override
public void fileDidLoaded(final String location, final File finalFile, final int type) {
fileProgresses.remove(location);
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
/*if (MediaController.getInstance().canSaveToGallery() && messengerPath != null && finalFile != null && (location.endsWith(".mp4") || location.endsWith(".jpg"))) {
if (finalFile.toString().startsWith(messengerPath.toString())) {
AndroidUtilities.addMediaToGallery(finalFile.toString());
}
}*/
NotificationCenter.getInstance().postNotificationName(NotificationCenter.FileDidLoaded, location);
ImageLoader.this.fileDidLoaded(location, finalFile, type);
}
});
}
@Override
public void fileDidFailedLoad(final String location, final int canceled) {
fileProgresses.remove(location);
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
ImageLoader.this.fileDidFailedLoad(location, canceled);
NotificationCenter.getInstance().postNotificationName(NotificationCenter.FileDidFailedLoad, location, canceled);
}
});
}
@Override
public void fileLoadProgressChanged(final String location, final float progress) {
fileProgresses.put(location, progress);
long currentTime = System.currentTimeMillis();
if (lastProgressUpdateTime == 0 || lastProgressUpdateTime < currentTime - 500) {
lastProgressUpdateTime = currentTime;
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.FileLoadProgressChanged, location, progress);
}
});
}
}
});
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context arg0, Intent intent) {
FileLog.e("messenger", "file system changed");
Runnable r = new Runnable() {
public void run() {
checkMediaPaths();
@@ -1247,13 +792,6 @@ public class ImageLoader {
FileLoader.getInstance().setMediaDirs(mediaDirs);
}
public Float getFileProgress(String location) {
if (location == null) {
return null;
}
return fileProgresses.get(location);
}
public void incrementUseCount(String key) {
Integer count = bitmapUseCounts.get(key);
if (count == null) {
@@ -1490,18 +1028,18 @@ public class ImageLoader {
if (httpLocation == null) {
if (imageLocation instanceof TLRPC.FileLocation) {
TLRPC.FileLocation location = (TLRPC.FileLocation) imageLocation;
FileLoader.getInstance().loadFile(location, ext, size, size == 0 || location.key != null || cacheOnly);
//FileLoader.getInstance().loadFile(location, ext, size, size == 0 || location.key != null || cacheOnly);
} else if (imageLocation instanceof TLRPC.Document) {
FileLoader.getInstance().loadFile((TLRPC.Document) imageLocation, true, cacheOnly);
//FileLoader.getInstance().loadFile((TLRPC.Document) imageLocation, true, cacheOnly);
}
} else {
String file = Utilities.MD5(httpLocation);
File cacheDir = FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE);
img.tempFilePath = new File(cacheDir, file + "_temp.jpg");
img.finalFilePath = cacheFile;
img.httpTask = new HttpImageTask(img, size);
httpTasks.add(img.httpTask);
runHttpTasks(false);
//img.httpTask = new HttpImageTask(img, size);
//httpTasks.add(img.httpTask);
//runHttpTasks(false);
}
}
}
@@ -1618,130 +1156,6 @@ public class ImageLoader {
}
}
private void httpFileLoadError(final String location) {
imageLoadQueue.postRunnable(new Runnable() {
@Override
public void run() {
CacheImage img = imageLoadingByUrl.get(location);
if (img == null) {
return;
}
HttpImageTask oldTask = img.httpTask;
img.httpTask = new HttpImageTask(oldTask.cacheImage, oldTask.imageSize);
httpTasks.add(img.httpTask);
runHttpTasks(false);
}
});
}
private void fileDidLoaded(final String location, final File finalFile, final int type) {
imageLoadQueue.postRunnable(new Runnable() {
@Override
public void run() {
ThumbGenerateInfo info = waitingForQualityThumb.get(location);
if (info != null) {
generateThumb(type, finalFile, info.fileLocation, info.filter);
waitingForQualityThumb.remove(location);
}
CacheImage img = imageLoadingByUrl.get(location);
if (img == null) {
return;
}
imageLoadingByUrl.remove(location);
CacheOutTask task = null;
for (int a = 0; a < img.imageReceiverArray.size(); a++) {
ImageReceiver imageReceiver = img.imageReceiverArray.get(a);
CacheImage cacheImage = imageLoadingByKeys.get(img.key);
if (cacheImage == null) {
cacheImage = new CacheImage();
cacheImage.finalFilePath = finalFile;
cacheImage.key = img.key;
cacheImage.httpUrl = img.httpUrl;
cacheImage.thumb = img.thumb;
cacheImage.ext = img.ext;
cacheImage.cacheTask = task = new CacheOutTask(cacheImage);
cacheImage.filter = img.filter;
cacheImage.animatedFile = img.animatedFile;
imageLoadingByKeys.put(cacheImage.key, cacheImage);
}
cacheImage.addImageReceiver(imageReceiver);
}
if (task != null) {
if (img.thumb) {
cacheThumbOutQueue.postRunnable(task);
} else {
cacheOutQueue.postRunnable(task);
}
}
}
});
}
private void fileDidFailedLoad(final String location, int canceled) {
if (canceled == 1) {
return;
}
imageLoadQueue.postRunnable(new Runnable() {
@Override
public void run() {
CacheImage img = imageLoadingByUrl.get(location);
if (img != null) {
img.setImageAndClear(null);
}
}
});
}
private void runHttpTasks(boolean complete) {
if (complete) {
currentHttpTasksCount--;
}
while (currentHttpTasksCount < 4 && !httpTasks.isEmpty()) {
HttpImageTask task = httpTasks.poll();
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null, null, null);
currentHttpTasksCount++;
}
}
private void runHttpFileLoadTasks(final HttpFileTask oldTask, final int reason) {
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
if (oldTask != null) {
currentHttpFileLoadTasksCount--;
}
if (oldTask != null) {
if (reason == 1) {
if (oldTask.canRetry) {
final HttpFileTask newTask = new HttpFileTask(oldTask.url, oldTask.tempFile, oldTask.ext);
Runnable runnable = new Runnable() {
@Override
public void run() {
httpFileLoadTasks.add(newTask);
runHttpFileLoadTasks(null, 0);
}
};
retryHttpsTasks.put(oldTask.url, runnable);
AndroidUtilities.runOnUIThread(runnable, 1000);
} else {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.httpFileDidFailedLoad, oldTask.url);
}
} else if (reason == 2) {
httpFileLoadTasksByKeys.remove(oldTask.url);
File file = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), Utilities.MD5(oldTask.url) + "." + oldTask.ext);
String result = oldTask.tempFile.renameTo(file) ? file.toString() : oldTask.tempFile.toString();
NotificationCenter.getInstance().postNotificationName(NotificationCenter.httpFileDidLoaded, oldTask.url, result);
}
}
while (currentHttpFileLoadTasksCount < 2 && !httpFileLoadTasks.isEmpty()) {
HttpFileTask task = httpFileLoadTasks.poll();
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null, null, null);
currentHttpFileLoadTasksCount++;
}
}
});
}
public static Bitmap loadBitmap(String path, Uri uri, float maxWidth, float maxHeight, boolean useMaxScale) {
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
@@ -1754,7 +1168,7 @@ public class ImageLoader {
try {
path = AndroidUtilities.getPath(uri);
} catch (Throwable e) {
FileLog.e("messenger", e);
}
}
}
@@ -1768,7 +1182,7 @@ public class ImageLoader {
inputStream.close();
inputStream = ApplicationLoader.applicationContext.getContentResolver().openInputStream(uri);
} catch (Throwable e) {
FileLog.e("messenger", e);
return null;
}
}
@@ -1809,7 +1223,7 @@ public class ImageLoader {
break;
}
} catch (Throwable e) {
FileLog.e("messenger", e);
}
}
@@ -1828,7 +1242,7 @@ public class ImageLoader {
}
}
} catch (Throwable e) {
FileLog.e("messenger", e);
ImageLoader.getInstance().clearMemory();
try {
if (b == null) {
@@ -1845,7 +1259,6 @@ public class ImageLoader {
}
}
} catch (Throwable e2) {
FileLog.e("messenger", e2);
}
}
} else if (uri != null) {
@@ -1862,12 +1275,12 @@ public class ImageLoader {
}
}
} catch (Throwable e) {
FileLog.e("messenger", e);
} finally {
try {
inputStream.close();
} catch (Throwable e) {
FileLog.e("messenger", e);
}
}
}
@@ -1970,13 +1383,12 @@ public class ImageLoader {
try {
return scaleAndSaveImageInternal(cacheFile, bitmap, w, h, photoW, photoH, scaleFactor, quality, cache, scaleAnyway);
} catch (Throwable e) {
FileLog.e("messenger", e);
ImageLoader.getInstance().clearMemory();
System.gc();
try {
return scaleAndSaveImageInternal(cacheFile, bitmap, w, h, photoW, photoH, scaleFactor, quality, cache, scaleAnyway);
} catch (Throwable e2) {
FileLog.e("messenger", e2);
return null;
}
}
@@ -2034,7 +1446,7 @@ public class ImageLoader {
writeFile.write(photoSize.bytes);
writeFile.close();
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
TLRPC.TL_photoSize newPhotoSize = new TLRPC.TL_photoSize();
@@ -140,7 +140,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
setImage(path, null, filter, thumb, null, null, size, ext, cacheOnly);
}
public void setImage(String httpUrl, String filter, Drawable thumb, String ext, int size) {
public void setImageByPath(String httpUrl, String filter, Drawable thumb, String ext, int size) {
setImage(null, httpUrl, filter, thumb, null, null, size, ext, true);
}
@@ -371,12 +371,10 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
setImageBackup.ext = currentExt;
setImageBackup.cacheOnly = currentCacheOnly;
}
NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didReplacedPhotoInMemCache);
clearImage();
}
public boolean onAttachedToWindow() {
NotificationCenter.getInstance().addObserver(this, NotificationCenter.didReplacedPhotoInMemCache);
if (setImageBackup != null && (setImageBackup.fileLocation != null || setImageBackup.httpUrl != null || setImageBackup.thumbLocation != null || setImageBackup.thumb != null)) {
setImage(setImageBackup.fileLocation, setImageBackup.httpUrl, setImageBackup.filter, setImageBackup.thumb, setImageBackup.thumbLocation, setImageBackup.thumbFilter, setImageBackup.size, setImageBackup.ext, setImageBackup.cacheOnly);
return true;
@@ -485,7 +483,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
currentThumbKey = null;
}
setImage(currentImageLocation, currentHttpUrl, currentFilter, currentThumb, currentThumbLocation, currentThumbFilter, currentSize, currentExt, currentCacheOnly);
FileLog.e("messenger", e);
}
canvas.restore();
} else {
@@ -530,7 +528,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
currentThumbKey = null;
}
setImage(currentImageLocation, currentHttpUrl, currentFilter, currentThumb, currentThumbLocation, currentThumbFilter, currentSize, currentExt, currentCacheOnly);
FileLog.e("messenger", e);
}
}
@@ -567,7 +565,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
currentThumbKey = null;
}
setImage(currentImageLocation, currentHttpUrl, currentFilter, currentThumb, currentThumbLocation, currentThumbFilter, currentSize, currentExt, currentCacheOnly);
FileLog.e("messenger", e);
}
}
canvas.restore();
@@ -582,7 +580,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
drawable.setAlpha(alpha);
drawable.draw(canvas);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
@@ -664,7 +662,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
checkAlphaAnimation(animationNotReady);
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
return false;
}
@@ -1058,26 +1056,6 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
}
}
}
} else if (id == NotificationCenter.didReplacedPhotoInMemCache) {
String oldKey = (String) args[0];
if (currentKey != null && currentKey.equals(oldKey)) {
currentKey = (String) args[1];
currentImageLocation = (TLRPC.FileLocation) args[2];
}
if (currentThumbKey != null && currentThumbKey.equals(oldKey)) {
currentThumbKey = (String) args[1];
currentThumbLocation = (TLRPC.FileLocation) args[2];
}
if (setImageBackup != null) {
if (currentKey != null && currentKey.equals(oldKey)) {
currentKey = (String) args[1];
currentImageLocation = (TLRPC.FileLocation) args[2];
}
if (currentThumbKey != null && currentThumbKey.equals(oldKey)) {
currentThumbKey = (String) args[1];
currentThumbLocation = (TLRPC.FileLocation) args[2];
}
}
}
}
}
@@ -0,0 +1,114 @@
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see http://www.gnu.org/licenses/ .
*
******************************************************************************/
package com.b44t.messenger;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import java.util.Locale;
public class KeepAliveService extends Service {
static KeepAliveService s_this = null;
@Override
public void onCreate() {
MrMailbox.log_i("DeltaChat", "*** KeepAliveService.onCreate()");
// there's nothing more to do here as all initialisation stuff is already done in
// ApplicationLoader.onCreate() which is called before this broadcast is sended.
s_this = this;
setSelfAsForeground();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// START_STICKY ensured, the service is recreated as soon it is terminted for any reasons.
// as ApplicationLoader.onCreate() is called before a service starts, there is no more to do here,
// the app is just running fine.
MrMailbox.log_i("DeltaChat", "*** KeepAliveService.onStartCommand()");
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
public void onDestroy() {
MrMailbox.log_i("DeltaChat", "*** KeepAliveService.onDestroy()");
// the service will be restarted due to START_STICKY automatically, there's nothing more to do.
}
private void setSelfAsForeground() {
stopForeground(true);
startForeground(FG_NOTIFICATION_ID, createNotification()); // TODO: if we target Android O, we should use startServiceInForeground()
}
static public KeepAliveService getInstance()
{
return s_this; // may be null
}
/* The notification
* A notification is required for a foreground service; and without a foreground service,
* Delta Chat won't get new messages reliable
**********************************************************************************************/
public static final int FG_NOTIFICATION_ID = 4142;
private Notification createNotification()
{
// a notification _must_ contain a small icon, a title and a text, see https://developer.android.com/guide/topics/ui/notifiers/notifications.html#Required
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
if( MrMailbox.isConfigured()!=0) {
builder.setContentTitle(String.format(getString(R.string.PermNotificationTitle), MrMailbox.getConfig("addr", "")));
builder.setContentText(getString(R.string.PermNotificationText));
}
else {
builder.setContentTitle(getString(R.string.AppName));
builder.setContentText(getString(R.string.AccountNotConfigured));
}
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ) {
builder.setPriority(NotificationCompat.PRIORITY_MIN);
}
builder.setWhen(0);
builder.setSmallIcon(R.drawable.notification_permanent);
return builder.build();
}
public void updateForegroundNotification()
{
// update the notification by simply creating a new notification with the same ID, see https://developer.android.com/training/notify-user/managing.html#Updating
NotificationManager nm = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(FG_NOTIFICATION_ID, createNotification());
}
}
@@ -23,36 +23,25 @@
package com.b44t.messenger;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Build;
import android.text.format.DateFormat;
import android.util.Xml;
import android.view.View;
import com.b44t.messenger.time.FastDateFormat;
import org.xmlpull.v1.XmlPullParser;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.TimeZone;
public class LocaleController {
public static boolean isRTL = false;
public static int nameDisplayOrder = 1;
private static boolean is24HourFormat = false;
public FastDateFormat formatterDay;
public FastDateFormat formatterWeek;
public FastDateFormat formatterMonth;
@@ -61,12 +50,7 @@ public class LocaleController {
public FastDateFormat chatDate;
public FastDateFormat chatFullDate;
private Locale currentLocale;
private LocaleInfo currentLocaleInfo;
private LocaleInfo defaultLocalInfo;
private HashMap<String, String> localeValues = new HashMap<>();
private String languageOverride;
private boolean changingConfiguration = false;
private String formattersCreatedFor = "";
private class TimeZoneChangedReceiver extends BroadcastReceiver {
@Override
@@ -82,38 +66,6 @@ public class LocaleController {
}
}
public static class LocaleInfo {
public String name;
public String nameEnglish;
public String shortName;
public String pathToFile;
public String getSaveString() {
return name + "|" + nameEnglish + "|" + shortName + "|" + pathToFile;
}
public static LocaleInfo createWithString(String string) {
if (string == null || string.length() == 0) {
return null;
}
String[] args = string.split("\\|");
if (args.length != 4) {
return null;
}
LocaleInfo localeInfo = new LocaleInfo();
localeInfo.name = args[0];
localeInfo.nameEnglish = args[1];
localeInfo.shortName = args[2];
localeInfo.pathToFile = args[3];
return localeInfo;
}
}
public ArrayList<LocaleInfo> sortedLanguages = new ArrayList<>();
public HashMap<String, LocaleInfo> languagesDict = new HashMap<>();
private ArrayList<LocaleInfo> otherLanguages = new ArrayList<>();
private static volatile LocaleController Instance = null;
public static LocaleController getInstance() {
LocaleController localInstance = Instance;
@@ -129,420 +81,37 @@ public class LocaleController {
}
public LocaleController() {
LocaleInfo localeInfo = new LocaleInfo();
localeInfo.name = "English";
localeInfo.nameEnglish = "English";
localeInfo.shortName = "en";
localeInfo.pathToFile = null;
sortedLanguages.add(localeInfo);
languagesDict.put(localeInfo.shortName, localeInfo);
localeInfo = new LocaleInfo();
localeInfo.name = "Italiano";
localeInfo.nameEnglish = "Italian";
localeInfo.shortName = "it";
localeInfo.pathToFile = null;
sortedLanguages.add(localeInfo);
languagesDict.put(localeInfo.shortName, localeInfo);
localeInfo = new LocaleInfo();
localeInfo.name = "Español";
localeInfo.nameEnglish = "Spanish";
localeInfo.shortName = "es";
sortedLanguages.add(localeInfo);
languagesDict.put(localeInfo.shortName, localeInfo);
localeInfo = new LocaleInfo();
localeInfo.name = "Deutsch";
localeInfo.nameEnglish = "German";
localeInfo.shortName = "de";
localeInfo.pathToFile = null;
sortedLanguages.add(localeInfo);
languagesDict.put(localeInfo.shortName, localeInfo);
localeInfo = new LocaleInfo();
localeInfo.name = "Français";
localeInfo.nameEnglish = "French";
localeInfo.shortName = "fr";
localeInfo.pathToFile = null;
sortedLanguages.add(localeInfo);
languagesDict.put(localeInfo.shortName, localeInfo);
localeInfo = new LocaleInfo();
localeInfo.name = "Nederlands";
localeInfo.nameEnglish = "Dutch";
localeInfo.shortName = "nl";
localeInfo.pathToFile = null;
sortedLanguages.add(localeInfo);
languagesDict.put(localeInfo.shortName, localeInfo);
localeInfo = new LocaleInfo();
localeInfo.name = "Polski";
localeInfo.nameEnglish = "Polish";
localeInfo.shortName = "pl";
localeInfo.pathToFile = null;
sortedLanguages.add(localeInfo);
languagesDict.put(localeInfo.shortName, localeInfo);
localeInfo = new LocaleInfo();
localeInfo.name = "Português";
localeInfo.nameEnglish = "Portuguese";
localeInfo.shortName = "pt";
localeInfo.pathToFile = null;
sortedLanguages.add(localeInfo);
languagesDict.put(localeInfo.shortName, localeInfo);
loadOtherLanguages();
for (LocaleInfo locale : otherLanguages) {
sortedLanguages.add(locale);
languagesDict.put(locale.shortName, locale);
}
Collections.sort(sortedLanguages, new Comparator<LocaleInfo>() {
@Override
public int compare(LocaleController.LocaleInfo o, LocaleController.LocaleInfo o2) {
return o.name.compareTo(o2.name);
}
});
defaultLocalInfo = localeInfo = new LocaleController.LocaleInfo();
localeInfo.name = "System default";
localeInfo.nameEnglish = "System default";
localeInfo.shortName = null;
localeInfo.pathToFile = null;
sortedLanguages.add(0, localeInfo);
is24HourFormat = DateFormat.is24HourFormat(ApplicationLoader.applicationContext);
LocaleInfo currentInfo = null;
boolean override = false;
try {
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE);
String lang = preferences.getString("language", null);
if (lang != null) {
currentInfo = languagesDict.get(lang);
if (currentInfo != null) {
override = true;
}
}
if (currentInfo == null && Locale.getDefault().getLanguage() != null) {
currentInfo = languagesDict.get(Locale.getDefault().getLanguage());
}
if (currentInfo == null) {
currentInfo = languagesDict.get(getLocaleString(Locale.getDefault()));
}
if (currentInfo == null) {
currentInfo = languagesDict.get("en");
}
applyLanguage(currentInfo, override);
} catch (Exception e) {
FileLog.e("messenger", e);
}
recreateFormatters();
try {
IntentFilter timezoneFilter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
ApplicationLoader.applicationContext.registerReceiver(new TimeZoneChangedReceiver(), timezoneFilter);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
private String getLocaleString(Locale locale) {
if (locale == null) {
return "en";
}
String languageCode = locale.getLanguage();
String countryCode = locale.getCountry();
String variantCode = locale.getVariant();
if (languageCode.length() == 0 && countryCode.length() == 0) {
return "en";
}
StringBuilder result = new StringBuilder(11);
result.append(languageCode);
if (countryCode.length() > 0 || variantCode.length() > 0) {
result.append('_');
}
result.append(countryCode);
if (variantCode.length() > 0) {
result.append('_');
}
result.append(variantCode);
return result.toString();
}
public static String getLocaleStringIso639() {
Locale locale = Locale.getDefault();
if (locale == null) {
return "en";
}
String languageCode = locale.getLanguage();
String countryCode = locale.getCountry();
String variantCode = locale.getVariant();
if (languageCode.length() == 0 && countryCode.length() == 0) {
return "en";
}
StringBuilder result = new StringBuilder(11);
result.append(languageCode);
if (countryCode.length() > 0 || variantCode.length() > 0) {
result.append('-');
}
result.append(countryCode);
if (variantCode.length() > 0) {
result.append('_');
}
result.append(variantCode);
return result.toString();
}
private void saveOtherLanguages() {
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("langconfig", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
String locales = "";
for (LocaleInfo localeInfo : otherLanguages) {
String loc = localeInfo.getSaveString();
if (loc != null) {
if (locales.length() != 0) {
locales += "&";
}
locales += loc;
}
}
editor.putString("locales", locales);
editor.apply();
}
public boolean deleteLanguage(LocaleInfo localeInfo) {
if (localeInfo.pathToFile == null) {
return false;
}
if (currentLocaleInfo == localeInfo) {
applyLanguage(defaultLocalInfo, true);
}
otherLanguages.remove(localeInfo);
sortedLanguages.remove(localeInfo);
languagesDict.remove(localeInfo.shortName);
File file = new File(localeInfo.pathToFile);
file.delete();
saveOtherLanguages();
return true;
}
private void loadOtherLanguages() {
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("langconfig", Activity.MODE_PRIVATE);
String locales = preferences.getString("locales", null);
if (locales == null || locales.length() == 0) {
return;
}
String[] localesArr = locales.split("&");
for (String locale : localesArr) {
LocaleInfo localeInfo = LocaleInfo.createWithString(locale);
if (localeInfo != null) {
otherLanguages.add(localeInfo);
}
}
}
private HashMap<String, String> getLocaleFileStrings(File file) {
FileInputStream stream = null;
try {
HashMap<String, String> stringMap = new HashMap<>();
XmlPullParser parser = Xml.newPullParser();
stream = new FileInputStream(file);
parser.setInput(stream, "UTF-8");
int eventType = parser.getEventType();
String name = null;
String value = null;
String attrName = null;
while (eventType != XmlPullParser.END_DOCUMENT) {
if(eventType == XmlPullParser.START_TAG) {
name = parser.getName();
int c = parser.getAttributeCount();
if (c > 0) {
attrName = parser.getAttributeValue(0);
}
} else if(eventType == XmlPullParser.TEXT) {
if (attrName != null) {
value = parser.getText();
if (value != null) {
value = value.trim();
value = value.replace("\\n", "\n");
value = value.replace("\\", "");
}
}
} else if (eventType == XmlPullParser.END_TAG) {
value = null;
attrName = null;
name = null;
}
if (name != null && name.equals("string") && value != null && attrName != null && value.length() != 0 && attrName.length() != 0) {
stringMap.put(attrName, value);
name = null;
value = null;
attrName = null;
}
eventType = parser.next();
}
return stringMap;
} catch (Exception e) {
FileLog.e("messenger", e);
} finally {
try {
if (stream != null) {
stream.close();
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
return new HashMap<>();
}
public void applyLanguage(LocaleInfo localeInfo, boolean override) {
applyLanguage(localeInfo, override, false);
}
public void applyLanguage(LocaleInfo localeInfo, boolean override, boolean fromFile) {
if (localeInfo == null) {
return;
}
try {
Locale newLocale;
if (localeInfo.shortName != null) {
String[] args = localeInfo.shortName.split("_");
if (args.length == 1) {
newLocale = new Locale(localeInfo.shortName);
} else {
newLocale = new Locale(args[0], args[1]);
}
if (newLocale != null) {
if (override) {
languageOverride = localeInfo.shortName;
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("language", localeInfo.shortName);
editor.apply();
}
}
} else {
newLocale = Locale.getDefault();
languageOverride = null;
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.remove("language");
editor.apply();
if (newLocale != null) {
LocaleInfo info = null;
if (newLocale.getLanguage() != null) {
info = languagesDict.get(newLocale.getLanguage());
}
if (info == null) {
info = languagesDict.get(getLocaleString(newLocale));
}
if (info == null) {
newLocale = Locale.US;
}
}
}
if (newLocale != null) {
if (localeInfo.pathToFile == null) {
localeValues.clear();
} else if (!fromFile) {
localeValues = getLocaleFileStrings(new File(localeInfo.pathToFile));
}
currentLocale = newLocale;
currentLocaleInfo = localeInfo;
changingConfiguration = true;
Locale.setDefault(currentLocale);
android.content.res.Configuration config = new android.content.res.Configuration();
config.locale = currentLocale;
ApplicationLoader.applicationContext.getResources().updateConfiguration(config, ApplicationLoader.applicationContext.getResources().getDisplayMetrics());
changingConfiguration = false;
}
} catch (Exception e) {
FileLog.e("messenger", e);
changingConfiguration = false;
}
recreateFormatters();
}
public static String getCurrentLanguageName() {
return getString("LanguageName", R.string.LanguageName);
}
private String getStringInternal(String key, int res) {
String value = localeValues.get(key);
if (value == null) {
try {
value = ApplicationLoader.applicationContext.getString(res);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
if (value == null) {
value = "LOC_ERR:" + key;
}
return value;
}
public static String getString(String key, int res) {
return getInstance().getStringInternal(key, res);
}
public static String formatString(String key, int res, Object... args) {
try {
String value = getInstance().localeValues.get(key);
if (value == null) {
value = ApplicationLoader.applicationContext.getString(res);
}
if (getInstance().currentLocale != null) {
return String.format(getInstance().currentLocale, value, args);
} else {
return String.format(value, args);
}
} catch (Exception e) {
FileLog.e("messenger", e);
return "LOC_ERR: " + key;
}
}
public static String formatStringSimple(String string, Object... args) {
try {
if (getInstance().currentLocale != null) {
return String.format(getInstance().currentLocale, string, args);
} else {
return String.format(string, args);
}
} catch (Exception e) {
FileLog.e("messenger", e);
return "LOC_ERR: " + string;
public void rebuildUiParts()
{
// While android makes sure, the activities are re-translated on locale changes, this is
// not done for the permanent notification "Connected to foo@bar - Waiting for messages ..."
// To re-translate this, it is needed to update the notification (which is a good idea as the
// notification is really permanent and would stuck in the wrong language for weeks otherwise ...)
KeepAliveService kas = KeepAliveService.getInstance();
if( kas != null ) {
kas.updateForegroundNotification();
}
}
public void onDeviceConfigurationChange(Configuration newConfig) {
if (changingConfiguration) {
return;
}
is24HourFormat = DateFormat.is24HourFormat(ApplicationLoader.applicationContext);
if (languageOverride != null) {
LocaleInfo toSet = currentLocaleInfo;
currentLocaleInfo = null;
applyLanguage(toSet, false);
} else {
Locale newLocale = newConfig.locale;
if (newLocale != null) {
String d1 = newLocale.getDisplayName();
String d2 = currentLocale.getDisplayName();
if (d1 != null && d2 != null && !d1.equals(d2)) {
recreateFormatters();
}
currentLocale = newLocale;
}
// rebuild some UI parts after a language change:
// - the permanent notification (Waiting for messages ...) - notfifications are not re-translated after a locale change. This cannot be done without listening to the language-change-envent.
// - some formatters and other cached values, maybe this can be done in other ways, however, it's just working.
Locale newLocale = Locale.getDefault();
if( newLocale!=null && !newLocale.getDisplayName().equals(formattersCreatedFor) ) { // onDeviceConfigurationChange() is also called on screen orientation changes; do not rebuild the locale stuff in these cases
recreateFormatters();
rebuildUiParts(); // this is really needed, see comment in rebuildUiParts()
}
}
@@ -559,7 +128,7 @@ public class LocaleController {
}
return getInstance().chatFullDate.format(date * 1000);
} catch (Exception e) {
FileLog.e("messenger", e);
}
return "LOC_ERR: formatDateChat";
}
@@ -578,25 +147,29 @@ public class LocaleController {
return formatter;
}
public void recreateFormatters() {
Locale locale = currentLocale;
if (locale == null) {
locale = Locale.getDefault();
}
String lang = locale.getLanguage();
if (lang == null) {
lang = "en";
}
isRTL = lang.toLowerCase().equals("ar");
nameDisplayOrder = lang.toLowerCase().equals("ko") ? 2 : 1;
private void recreateFormatters() {
Locale locale = Locale.getDefault();
formatterMonth = createFormatter(locale, getStringInternal("formatterMonth", R.string.formatterMonth), "dd MMM");
formatterYear = createFormatter(locale, getStringInternal("formatterYear", R.string.formatterYear), "dd.MM.yyyy");
chatDate = createFormatter(locale, getStringInternal("chatDate", R.string.chatDate), "d MMMM");
chatFullDate = createFormatter(locale, getStringInternal("chatFullDate", R.string.chatFullDate), "d MMMM yyyy");
formatterWeek = createFormatter(locale, getStringInternal("formatterWeek", R.string.formatterWeek), "EEE");
formatterMonthYear = createFormatter(locale, getStringInternal("formatterMonthYear", R.string.formatterMonthYear), "MMMM yyyy");
formatterDay = createFormatter(lang.toLowerCase().equals("ar") || lang.toLowerCase().equals("ko") ? locale : Locale.US, is24HourFormat ? getStringInternal("formatterDay24H", R.string.formatterDay24H) : getStringInternal("formatterDay12H", R.string.formatterDay12H), is24HourFormat ? "HH:mm" : "h:mm a");
isRTL = false;
if (Build.VERSION.SDK_INT >= 17) {
// ViewCompat.getLayoutDirection() can also be used, however, this function always returns LTR for SDK < 17, too, but requires a additinal view handle
Configuration config = ApplicationLoader.applicationContext.getResources().getConfiguration();
if(config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
isRTL = true;
}
}
boolean is24HourFormat = DateFormat.is24HourFormat(ApplicationLoader.applicationContext);
formatterMonth = createFormatter(locale, ApplicationLoader.applicationContext.getString(R.string.formatterMonth), "dd MMM");
formatterYear = createFormatter(locale, ApplicationLoader.applicationContext.getString(R.string.formatterYear), "dd.MM.yyyy");
chatDate = createFormatter(locale, ApplicationLoader.applicationContext.getString(R.string.chatDate), "d MMMM");
chatFullDate = createFormatter(locale, ApplicationLoader.applicationContext.getString(R.string.chatFullDate), "d MMMM yyyy");
formatterWeek = createFormatter(locale, ApplicationLoader.applicationContext.getString(R.string.formatterWeek), "EEE");
formatterMonthYear = createFormatter(locale, ApplicationLoader.applicationContext.getString(R.string.formatterMonthYear), "MMMM yyyy");
formatterDay = createFormatter(locale, is24HourFormat ? ApplicationLoader.applicationContext.getString(R.string.formatterDay24H) : ApplicationLoader.applicationContext.getString(R.string.formatterDay12H), is24HourFormat ? "HH:mm" : "h:mm a");
formattersCreatedFor = locale.getDisplayName();
}
public static String stringForMessageListDate(long date) {
@@ -621,7 +194,7 @@ public class LocaleController {
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
return "LOC_ERR";
}
@@ -27,10 +27,8 @@ import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
@@ -50,7 +48,6 @@ import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
@@ -84,6 +81,7 @@ import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Semaphore;
public class MediaController implements AudioManager.OnAudioFocusChangeListener, NotificationCenter.NotificationCenterDelegate, SensorEventListener {
private native int startRecord(String path);
@@ -268,7 +266,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
public int wifiDownloadMask = 0;
public int roamingDownloadMask = 0;
private final boolean autoplayGifs = true;
private boolean autoplayGifs = true;
private boolean raiseToSpeak = true;
private boolean directShare = true;
@@ -370,7 +368,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
}
samplesCount = newSamplesCount;
} catch (Exception e) {
FileLog.e("messenger", e);
}
buffer.position(0);
final double amplitude = Math.sqrt(sum / len / 2);
@@ -499,14 +497,14 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
freePlayerBuffers.add(new AudioBuffer(playerBufferSize));
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
try {
sensorManager = (SensorManager) ApplicationLoader.applicationContext.getSystemService(Context.SENSOR_SERVICE);
linearSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION);
gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY);
if (linearSensor == null || gravitySensor == null) {
FileLog.e("messenger", "gravity or linear sensor not found");
//Log.i("DeltaChat", "gravity or linear sensor not found");
accelerometerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
linearSensor = null;
gravitySensor = null;
@@ -515,7 +513,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
PowerManager powerManager = (PowerManager) ApplicationLoader.applicationContext.getSystemService(Context.POWER_SERVICE);
proximityWakeLock = powerManager.newWakeLock(0x00000020, "proximity");
} catch (Exception e) {
FileLog.e("messenger", e);
}
fileBuffer = ByteBuffer.allocateDirect(1920);
recordQueue = new DispatchQueue("recordQueue");
@@ -529,34 +527,18 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
mobileDataDownloadMask = preferences.getInt("mobileDataDownloadMask", AUTODOWNLOAD_MASK_PHOTO | AUTODOWNLOAD_MASK_AUDIO | AUTODOWNLOAD_MASK_MUSIC | AUTODOWNLOAD_MASK_GIF);
wifiDownloadMask = preferences.getInt("wifiDownloadMask", AUTODOWNLOAD_MASK_PHOTO | AUTODOWNLOAD_MASK_AUDIO | AUTODOWNLOAD_MASK_MUSIC | AUTODOWNLOAD_MASK_GIF);
roamingDownloadMask = preferences.getInt("roamingDownloadMask", 0);
//autoplayGifs = preferences.getBoolean("autoplay_gif", true);
autoplayGifs = preferences.getBoolean("autoplay_gif", true);
raiseToSpeak = preferences.getBoolean("raise_to_speak", true);
directShare = preferences.getBoolean("direct_share", true);
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
NotificationCenter.getInstance().addObserver(MediaController.this, NotificationCenter.FileDidFailedLoad);
NotificationCenter.getInstance().addObserver(MediaController.this, NotificationCenter.didReceivedNewMessages);
NotificationCenter.getInstance().addObserver(MediaController.this, NotificationCenter.messagesDeleted);
NotificationCenter.getInstance().addObserver(MediaController.this, NotificationCenter.FileDidLoaded);
NotificationCenter.getInstance().addObserver(MediaController.this, NotificationCenter.FileLoadProgressChanged);
//NotificationCenter.getInstance().addObserver(MediaController.this, NotificationCenter.FileUploadProgressChanged);
//NotificationCenter.getInstance().addObserver(MediaController.this, NotificationCenter.musicDidLoaded);
}
});
BroadcastReceiver networkStateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
checkAutodownloadSettings();
}
};
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
ApplicationLoader.applicationContext.registerReceiver(networkStateReceiver, filter);
checkAutodownloadSettings();
if (Build.VERSION.SDK_INT >= 16) {
mediaProjections = new String[]{
MediaStore.Images.ImageColumns.DATA,
@@ -580,12 +562,12 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
try {
ApplicationLoader.applicationContext.getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, new GalleryObserverExternal());
} catch (Exception e) {
FileLog.e("messenger", e);
}
try {
ApplicationLoader.applicationContext.getContentResolver().registerContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, false, new GalleryObserverInternal());
} catch (Exception e) {
FileLog.e("messenger", e);
}
try {
@@ -611,7 +593,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
mgr.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -657,7 +639,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
audioTrackPlayer.setStereoVolume(volume, volume);
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -668,7 +650,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
progressTimer.cancel();
progressTimer = null;
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
progressTimer = new Timer();
@@ -705,7 +687,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
currentPlayingMessageObject.audioProgressSec = lastProgress / 1000;
NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioProgressDidChanged, currentPlayingMessageObject.getId(), value);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
@@ -723,7 +705,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
progressTimer.cancel();
progressTimer = null;
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
@@ -739,31 +721,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
cancelVideoConvert(null);
}
public void checkAutodownloadSettings() {
/* -- leave this for future use
int currentMask = getCurrentDownloadMask();
if (currentMask == lastCheckMask) {
return;
}
lastCheckMask = currentMask;
... maybe tell the backend what can be downloaded ...
*/
}
/* -- leave this for future use
private int getCurrentDownloadMask() {
if (ConnectionsManager.isConnectedToWiFi()) {
return wifiDownloadMask;
} else if (ConnectionsManager.isRoaming()) {
return roamingDownloadMask;
} else {
return mobileDataDownloadMask;
}
} */
public int generateObserverTag() {
return lastTag++;
}
@@ -835,103 +792,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
@SuppressWarnings("unchecked")
@Override
public void didReceivedNotification(int id, Object... args) {
if (id == NotificationCenter.FileDidFailedLoad) {
listenerInProgress = true;
String fileName = (String) args[0];
loadingFileMessagesObservers.get(fileName);
ArrayList<WeakReference<FileDownloadProgressListener>> arrayList = loadingFileObservers.get(fileName);
if (arrayList != null) {
for (int a = 0; a < arrayList.size(); a++) {
WeakReference<FileDownloadProgressListener> reference = arrayList.get(a);
if (reference.get() != null) {
reference.get().onFailedDownload(fileName);
observersByTag.remove(reference.get().getObserverTag());
}
}
loadingFileObservers.remove(fileName);
}
listenerInProgress = false;
processLaterArrays();
} else if (id == NotificationCenter.FileDidLoaded) {
listenerInProgress = true;
String fileName = (String) args[0];
ArrayList<MessageObject> messageObjects = loadingFileMessagesObservers.get(fileName);
if (messageObjects != null) {
for (int a = 0; a < messageObjects.size(); a++) {
MessageObject messageObject = messageObjects.get(a);
messageObject.mediaExists = true;
}
loadingFileMessagesObservers.remove(fileName);
}
ArrayList<WeakReference<FileDownloadProgressListener>> arrayList = loadingFileObservers.get(fileName);
if (arrayList != null) {
for (int a = 0; a < arrayList.size(); a++) {
WeakReference<FileDownloadProgressListener> reference = arrayList.get(a);
if (reference.get() != null) {
reference.get().onSuccessDownload(fileName);
observersByTag.remove(reference.get().getObserverTag());
}
}
loadingFileObservers.remove(fileName);
}
listenerInProgress = false;
processLaterArrays();
} else if (id == NotificationCenter.FileLoadProgressChanged) {
listenerInProgress = true;
String fileName = (String) args[0];
ArrayList<WeakReference<FileDownloadProgressListener>> arrayList = loadingFileObservers.get(fileName);
if (arrayList != null) {
Float progress = (Float) args[1];
for (WeakReference<FileDownloadProgressListener> reference : arrayList) {
if (reference.get() != null) {
reference.get().onProgressDownload(fileName, progress);
}
}
}
listenerInProgress = false;
processLaterArrays();
}
/*else if (id == NotificationCenter.FileUploadProgressChanged) {
listenerInProgress = true;
String fileName = (String) args[0];
ArrayList<WeakReference<FileDownloadProgressListener>> arrayList = loadingFileObservers.get(fileName);
if (arrayList != null) {
Float progress = (Float) args[1];
Boolean enc = (Boolean) args[2];
for (WeakReference<FileDownloadProgressListener> reference : arrayList) {
if (reference.get() != null) {
reference.get().onProgressUpload(fileName, progress, enc);
}
}
}
listenerInProgress = false;
processLaterArrays();
try {
ArrayList<SendMessagesHelper.DelayedMessage> delayedMessages = SendMessagesHelper.getInstance().getDelayedMessages(fileName);
if (delayedMessages != null) {
for (int a = 0; a < delayedMessages.size(); a++) {
SendMessagesHelper.DelayedMessage delayedMessage = delayedMessages.get(a);
if (delayedMessage.encryptedChat == null) {
long dialog_id = delayedMessage.obj.getDialogId();
Long lastTime = typingTimes.get(dialog_id);
if (lastTime == null || lastTime + 4000 < System.currentTimeMillis()) {
if (MessageObject.isVideoDocument(delayedMessage.documentLocation)) {
MessagesController.getInstance().sendTyping(dialog_id, 5, 0);
} else if (delayedMessage.documentLocation != null) {
MessagesController.getInstance().sendTyping(dialog_id, 3, 0);
} else if (delayedMessage.location != null) {
MessagesController.getInstance().sendTyping(dialog_id, 4, 0);
}
typingTimes.put(dialog_id, System.currentTimeMillis());
}
}
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
} */
else if (id == NotificationCenter.messagesDeleted) {
if (id == NotificationCenter.messagesDeleted) {
ArrayList<Integer> markAsDeletedMessages = (ArrayList<Integer>) args[0];
if (playingMessageObject != null) {
if (markAsDeletedMessages.contains(playingMessageObject.getId())) {
@@ -939,19 +800,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
}
}
}
/*else if (id == NotificationCenter.musicDidLoaded) {
long did = (Long) args[0];
if (playingMessageObject != null && playingMessageObject.isMusic() && playingMessageObject.getDialogId() == did) {
ArrayList<MessageObject> arrayList = (ArrayList<MessageObject>) args[1];
playlist.addAll(0, arrayList);
if (shuffleMusic) {
buildShuffledPlayList();
currentPlaylistNum = 0;
} else {
currentPlaylistNum += arrayList.size();
}
}
}*/
}
private void checkDecoderQueue() {
@@ -1028,7 +876,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
try {
count = audioTrackPlayer.write(buffer.bufferBytes, 0, buffer.size);
} catch (Exception e) {
FileLog.e("messenger", e);
}
buffersWrited++;
@@ -1083,7 +931,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
return;
}
if (event.sensor == proximitySensor) {
FileLog.e("messenger", "proximity changed to " + event.values[0]);
//Log.i("DeltaChat", "proximity changed to " + event.values[0]);
if (lastProximityValue == -100) {
lastProximityValue = event.values[0];
} else if (lastProximityValue != event.values[0]) {
@@ -1148,7 +996,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
raisedToTop = 0;
countLess = 0;
timeSinceRaise = System.currentTimeMillis();
//FileLog.e("messenger", "motion detected");
//Log.i("DeltaChat", "motion detected");
}
}
} else {
@@ -1165,13 +1013,13 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
}
previousAccValue = val;
accelerometerVertical = gravityFast[1] > 2.5f && Math.abs(gravityFast[2]) < 4.0f && /*Math.abs(gravityFast[0]) < 9.0f &&*/ Math.abs(gravityFast[0]) > 1.5f;
//FileLog.e("messenger", accelerometerVertical + " val = " + val + " acc (" + linearAcceleration[0] + ", " + linearAcceleration[1] + ", " + linearAcceleration[2] + ") grav (" + gravityFast[0] + ", " + gravityFast[1] + ", " + gravityFast[2] + ")");
//Log.i("DeltaChat", accelerometerVertical + " val = " + val + " acc (" + linearAcceleration[0] + ", " + linearAcceleration[1] + ", " + linearAcceleration[2] + ") grav (" + gravityFast[0] + ", " + gravityFast[1] + ", " + gravityFast[2] + ")");
}
if (raisedToBack == minCount && accelerometerVertical && proximityTouched && !NotificationsController.getInstance().audioManager.isWiredHeadsetOn()) {
FileLog.e("messenger", "sensor values reached");
//Log.i("DeltaChat", "sensor values reached");
if (playingMessageObject == null && recordStartRunnable == null && recordingAudio == null && !PhotoViewer.getInstance().isVisible() && ApplicationLoader.isScreenOn && !inputFieldHasText && allowStartRecord && raiseChat != null && !callInProgress) {
if (!raiseToEarRecord) {
FileLog.e("messenger", "start record");
//Log.i("DeltaChat", "start record");
useFrontSpeaker = true;
if (!raiseChat.playFirstUnreadVoiceMessage() /*if there is nothing to play, the function checks for record permission*/ ) {
raiseToEarRecord = true;
@@ -1185,7 +1033,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
}
} /*else if (playingMessageObject != null && playingMessageObject.isVoice()) {
if (!useFrontSpeaker) {
FileLog.e("messenger", "start listen");
//Log.i("DeltaChat", "start listen");
if (proximityHasDifferentValues && proximityWakeLock != null && !proximityWakeLock.isHeld()) {
proximityWakeLock.acquire();
}
@@ -1200,7 +1048,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
} else if (proximityTouched) {
/*if (playingMessageObject != null && playingMessageObject.isVoice()) {
if (!useFrontSpeaker) {
FileLog.e("messenger", "start listen by proximity only");
//Log.i("DeltaChat", "start listen by proximity only");
if (proximityHasDifferentValues && proximityWakeLock != null && !proximityWakeLock.isHeld()) {
proximityWakeLock.acquire();
}
@@ -1211,7 +1059,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
}*/
} else if (!proximityTouched) {
if (raiseToEarRecord) {
FileLog.e("messenger", "stop record");
//Log.i("DeltaChat", "stop record");
stopRecording(2);
raiseToEarRecord = false;
ignoreOnPause = false;
@@ -1219,7 +1067,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
proximityWakeLock.release();
}
} else if (useFrontSpeaker) {
FileLog.e("messenger", "stop listen");
//Log.i("DeltaChat", "stop listen");
useFrontSpeaker = false;
startAudioAgain(true);
ignoreOnPause = false;
@@ -1363,18 +1211,18 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
try {
audioPlayer.reset();
} catch (Exception e) {
FileLog.e("messenger", e);
}
try {
audioPlayer.stop();
} catch (Exception e) {
FileLog.e("messenger", e);
}
try {
audioPlayer.release();
audioPlayer = null;
} catch (Exception e) {
FileLog.e("messenger", e);
}
} else if (audioTrackPlayer != null) {
synchronized (playerObjectSync) {
@@ -1382,13 +1230,13 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
audioTrackPlayer.pause();
audioTrackPlayer.flush();
} catch (Exception e) {
FileLog.e("messenger", e);
}
try {
audioTrackPlayer.release();
audioTrackPlayer = null;
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
@@ -1475,7 +1323,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
seekOpusPlayer(progress);
}
} catch (Exception e) {
FileLog.e("messenger", e);
return false;
}
return true;
@@ -1552,7 +1400,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
return true;
}
if (!messageObject.isOut() && messageObject.isContentUnread()) {
MessagesController.getInstance().markMessageContentAsRead(messageObject);
MrMailbox.markMessageContentAsRead(messageObject);
}
boolean notify = !playMusicAgain;
if (playingMessageObject != null) {
@@ -1609,7 +1457,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
});
audioTrackPlayer.play();
} catch (Exception e) {
FileLog.e("messenger", e);
if (audioTrackPlayer != null) {
audioTrackPlayer.release();
audioTrackPlayer = null;
@@ -1640,11 +1488,11 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
try {
audioInfo = AudioInfo.getAudioInfo(cacheFile);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioPlayStateChanged, playingMessageObject != null ? playingMessageObject.getId() : 0);
if (audioPlayer != null) {
audioPlayer.release();
@@ -1678,7 +1526,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
playingMessageObject.audioProgress = 0;
playingMessageObject.audioProgressSec = 0;
NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioProgressDidChanged, playingMessageObject.getId(), 0);
FileLog.e("messenger", e2);
}
} else if (audioTrackPlayer != null) {
if (playingMessageObject.audioProgress == 1) {
@@ -1693,7 +1540,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
seekOpusFile(playingMessageObject.audioProgress);
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
synchronized (playerSync) {
freePlayerBuffers.addAll(usedPlayerBuffers);
@@ -1721,7 +1568,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
try {
audioPlayer.reset();
} catch (Exception e) {
FileLog.e("messenger", e);
}
audioPlayer.stop();
} else if (audioTrackPlayer != null) {
@@ -1729,7 +1576,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
audioTrackPlayer.flush();
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
try {
if (audioPlayer != null) {
@@ -1742,7 +1589,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
stopProgressTimer();
playingMessageObject = null;
@@ -1770,7 +1617,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
isPaused = true;
NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioPlayStateChanged, playingMessageObject.getId());
} catch (Exception e) {
FileLog.e("messenger", e);
isPaused = false;
return false;
}
@@ -1794,7 +1641,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
isPaused = false;
NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioPlayStateChanged, playingMessageObject.getId());
} catch (Exception e) {
FileLog.e("messenger", e);
return false;
}
return true;
@@ -1823,7 +1670,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
v.vibrate(50);
//NotificationsController.getInstance().playRecordSound(); -- was also disabled in Telegram; playing a sound while recording may result in a feedback
} catch (Exception e) {
FileLog.e("messenger", e);
}
recordQueue.postRunnable(recordStartRunnable = new Runnable() {
@@ -1843,12 +1690,12 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
recordingAudio = new TLRPC.TL_document();
recordingAudio.dc_id = Integer.MIN_VALUE;
recordingAudio.id = UserConfig.lastLocalId;
recordingAudio.user_id = UserConfig.getClientUserId();
recordingAudio.user_id = MrContact.MR_CONTACT_ID_SELF;
recordingAudio.mime_type = "audio/ogg";
recordingAudio.thumb = new TLRPC.TL_photoSizeEmpty();
recordingAudio.thumb.type = "s";
UserConfig.lastLocalId--;
UserConfig.saveConfig(false);
UserConfig.saveConfig();
recordingAudioFile = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), FileLoader.getAttachFileName(recordingAudio));
@@ -1873,7 +1720,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
audioRecorder.startRecording();
} catch (Exception e) {
FileLog.e("messenger", e);
recordingAudio = null;
stopRecord();
recordingAudioFile.delete();
@@ -1882,7 +1729,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
audioRecorder.release();
audioRecorder = null;
} catch (Exception e2) {
FileLog.e("messenger", e2);
}
AndroidUtilities.runOnUIThread(new Runnable() {
@@ -1976,7 +1822,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
audioToSend.date = ConnectionsManager.getInstance().getCurrentTime();
audioToSend.date = MrMailbox.getCurrentTime();
audioToSend.size = (int) recordingAudioFileToSend.length();
TLRPC.TL_documentAttributeAudio attributeAudio = new TLRPC.TL_documentAttributeAudio();
attributeAudio.voice = true;
@@ -2005,7 +1851,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
audioRecorder = null;
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
recordingAudio = null;
recordingAudioFile = null;
@@ -2026,7 +1872,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
sendAfterDone = send;
audioRecorder.stop();
} catch (Exception e) {
FileLog.e("messenger", e);
if (recordingAudioFile != null) {
recordingAudioFile.delete();
}
@@ -2038,7 +1884,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
Vibrator v = (Vibrator) ApplicationLoader.applicationContext.getSystemService(Context.VIBRATOR_SERVICE);
v.vibrate(50);
} catch (Exception e) {
FileLog.e("messenger", e);
}
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
@@ -2066,14 +1912,13 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (Exception e2) {
FileLog.e("messenger", e2);
}
}
return false;
@@ -2091,14 +1936,13 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
}
}
} catch (Exception e) {
FileLog.e("messenger", e);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (Exception e2) {
FileLog.e("messenger", e2);
}
}
return false;
@@ -2114,7 +1958,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
} catch (Exception e) {
FileLog.e("messenger", e);
} finally {
if (cursor != null) {
cursor.close();
@@ -2139,7 +1983,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
if (name == null) {
int id = UserConfig.lastLocalId;
UserConfig.lastLocalId--;
UserConfig.saveConfig(false);
UserConfig.saveConfig();
name = String.format(Locale.US, "%d.%s", id, ext);
}
inputStream = ApplicationLoader.applicationContext.getContentResolver().openInputStream(uri);
@@ -2152,27 +1996,24 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
}
return f.getAbsolutePath();
} catch (Exception e) {
FileLog.e("messenger", e);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (Exception e2) {
FileLog.e("messenger", e2);
}
try {
if (output != null) {
output.close();
}
} catch (Exception e2) {
FileLog.e("messenger", e2);
}
}
return null;
}
/*
public void toggleAutoplayGifs() {
autoplayGifs = !autoplayGifs;
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE);
@@ -2180,7 +2021,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
editor.putBoolean("autoplay_gif", autoplayGifs);
editor.apply();
}
*/
public void toogleRaiseToSpeak() {
raiseToSpeak = !raiseToSpeak;
@@ -2249,7 +2089,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
PhotoEntry photoEntry = new PhotoEntry(bucketId, imageId, dateTaken, path, orientation, false);
if (allPhotosAlbum == null) {
allPhotosAlbum = new AlbumEntry(0, LocaleController.getString("AllPhotos", R.string.AllPhotos), photoEntry, false);
allPhotosAlbum = new AlbumEntry(0, ApplicationLoader.applicationContext.getString(R.string.AllPhotos), photoEntry, false);
albumsSorted.add(0, allPhotosAlbum);
}
if (allPhotosAlbum != null) {
@@ -2273,13 +2113,13 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
}
}
} catch (Throwable e) {
FileLog.e("messenger", e);
} finally {
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
@@ -2310,7 +2150,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
PhotoEntry photoEntry = new PhotoEntry(bucketId, imageId, dateTaken, path, 0, true);
if (allVideosAlbum == null) {
allVideosAlbum = new AlbumEntry(0, LocaleController.getString("AllVideo", R.string.AllVideo), photoEntry, true);
allVideosAlbum = new AlbumEntry(0, ApplicationLoader.applicationContext.getString(R.string.AllVideo), photoEntry, true);
videoAlbumsSorted.add(0, allVideosAlbum);
}
if (allVideosAlbum != null) {
@@ -2334,13 +2174,13 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
}
}
} catch (Throwable e) {
FileLog.e("messenger", e);
} finally {
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
@@ -2587,7 +2427,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
th.start();
th.join();
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}).start();
@@ -2614,7 +2454,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
int rotationValue = messageObject.videoEditedInfo.rotationValue;
int originalWidth = messageObject.videoEditedInfo.originalWidth;
int originalHeight = messageObject.videoEditedInfo.originalHeight;
int bitrate = messageObject.videoEditedInfo.bitrate;
int originalBitrate = messageObject.videoEditedInfo.originalBitrate;
int resultBitrate = messageObject.videoEditedInfo.resultBitrate;
int rotateRender = 0;
File cacheFile = new File(messageObject.messageOwner.attachPath);
@@ -2676,7 +2517,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
checkConversionCanceled();
if (resultWidth != originalWidth || resultHeight != originalHeight || rotateRender != 0) {
if (resultBitrate<originalBitrate || resultWidth != originalWidth || resultHeight != originalHeight || rotateRender != 0) {
int videoIndex;
videoIndex = selectTrack(extractor, false);
if (videoIndex >= 0) {
@@ -2720,11 +2561,11 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
} else if (codecName.equals("OMX.TI.DUCATI1.VIDEO.H264E")) {
processorType = PROCESSOR_TYPE_TI;
}
FileLog.e("messenger", "codec = " + codecInfo.getName() + " manufacturer = " + manufacturer + "device = " + Build.MODEL);
//Log.i("DeltaChat", "codec = " + codecInfo.getName() + " manufacturer = " + manufacturer + "device = " + Build.MODEL);
} else {
colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface;
}
FileLog.e("messenger", "colorFormat = " + colorFormat);
//Log.i("DeltaChat", "colorFormat = " + colorFormat);
int resultHeightAligned = resultHeight;
int padding = 0;
@@ -2765,7 +2606,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
MediaFormat outputFormat = MediaFormat.createVideoFormat(MIME_TYPE, resultWidth, resultHeight);
outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate != 0 ? bitrate : 921600);
outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, resultBitrate != 0 ? resultBitrate : 921600);
outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 25);
outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);
if (Build.VERSION.SDK_INT < 18) {
@@ -2916,7 +2757,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat newFormat = decoder.getOutputFormat();
FileLog.e("messenger", "newFormat = " + newFormat);
//Log.i("DeltaChat", "newFormat = " + newFormat);
} else if (decoderStatus < 0) {
throw new RuntimeException("unexpected result from decoder.dequeueOutputBuffer: " + decoderStatus);
} else {
@@ -2935,7 +2776,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
if (startTime > 0 && videoTime == -1) {
if (info.presentationTimeUs < startTime) {
doRender = false;
FileLog.e("messenger", "drop frame startTime = " + startTime + " present time = " + info.presentationTimeUs);
//Log.i("DeltaChat", "drop frame startTime = " + startTime + " present time = " + info.presentationTimeUs);
} else {
videoTime = info.presentationTimeUs;
}
@@ -2947,7 +2788,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
outputSurface.awaitNewImage();
} catch (Exception e) {
errorWait = true;
FileLog.e("messenger", e);
}
if (!errorWait) {
if (Build.VERSION.SDK_INT >= 18) {
@@ -2964,14 +2805,14 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
Utilities.convertVideoFrame(rgbBuf, yuvBuf, colorFormat, resultWidth, resultHeight, padding, swapUV);
encoder.queueInputBuffer(inputBufIndex, 0, bufferSize, info.presentationTimeUs, 0);
} else {
FileLog.e("messenger", "input buffer not available");
//Log.i("DeltaChat", "input buffer not available");
}
}
}
}
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
decoderOutputAvailable = false;
FileLog.e("messenger", "decoder stream end");
//Log.i("DeltaChat", "decoder stream end");
if (Build.VERSION.SDK_INT >= 18) {
encoder.signalEndOfInputStream();
} else {
@@ -2989,7 +2830,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
videoStartTime = videoTime;
}
} catch (Exception e) {
FileLog.e("messenger", e);
error = true;
}
@@ -3023,7 +2864,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
}
} catch (Exception e) {
error = true;
FileLog.e("messenger", e);
} finally {
if (extractor != null) {
extractor.release();
@@ -3032,10 +2873,10 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener,
try {
mediaMuxer.finishMovie(false);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
FileLog.e("messenger", "time = " + (System.currentTimeMillis() - time));
//Log.i("DeltaChat", "time = " + (System.currentTimeMillis() - time));
}
} else {
preferences.edit().putBoolean("isPreviousOk", true).apply();
@@ -95,7 +95,7 @@ public class MessageObject {
textPaint.linkColor = Theme.MSG_LINK_TEXT_COLOR;
}
textPaint.setTextSize(AndroidUtilities.dp(MessagesController.getInstance().fontSize));
textPaint.setTextSize(AndroidUtilities.dp(ApplicationLoader.fontSize));
messageOwner = message;
@@ -106,13 +106,6 @@ public class MessageObject {
setType();
if (messageOwner.message != null && messageOwner.id < 0 && messageOwner.message.length() > 6 && isVideo()) {
videoEditedInfo = new VideoEditedInfo();
if (!videoEditedInfo.parseString(messageOwner.message)) {
videoEditedInfo = null;
}
}
generateCaption();
if (generateLayout) {
messageText = Emoji.replaceEmoji(messageText, textPaint.getFontMetricsInt(), AndroidUtilities.dp(20), false);
@@ -127,7 +120,7 @@ public class MessageObject {
textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(Theme.MSG_TEXT_COLOR);
textPaint.linkColor = Theme.MSG_LINK_TEXT_COLOR;
textPaint.setTextSize(AndroidUtilities.dp(MessagesController.getInstance().fontSize));
textPaint.setTextSize(AndroidUtilities.dp(ApplicationLoader.fontSize));
}
return textPaint;
}
@@ -168,18 +161,7 @@ public class MessageObject {
}
public static boolean isGifDocument(TLRPC.Document document) {
return document != null && document.thumb != null && document.mime_type != null && (document.mime_type.equals("image/gif") || isNewGifDocument(document));
}
public static boolean isNewGifDocument(TLRPC.Document document) {
if (document != null && document.mime_type != null && document.mime_type.equals("video/mp4")) {
for (int a = 0; a < document.attributes.size(); a++) {
if (document.attributes.get(a) instanceof TLRPC.TL_documentAttributeAnimated) {
return true;
}
}
}
return false;
return document != null && document.thumb != null && document.mime_type != null && document.mime_type.equals("image/gif");
}
private void generateThumbs(boolean update) {
@@ -316,7 +298,7 @@ public class MessageObject {
try {
Linkify.addLinks((Spannable) caption, Linkify.WEB_URLS | Linkify.PHONE_NUMBERS | Linkify.EMAIL_ADDRESSES);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
@@ -327,7 +309,7 @@ public class MessageObject {
try {
Linkify.addLinks((Spannable) messageText, Linkify.WEB_URLS | Linkify.PHONE_NUMBERS | Linkify.EMAIL_ADDRESSES);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
@@ -362,7 +344,7 @@ public class MessageObject {
try {
textLayout = new StaticLayout(messageText, textPaint, maxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
} catch (Exception e) {
FileLog.e("messenger", e);
return;
}
@@ -399,7 +381,7 @@ public class MessageObject {
block.height = Math.max(block.height, block.textLayout.getLineBottom(block.textLayout.getLineCount() - 1));
prevOffset = block.textYOffset;
} catch (Exception e) {
FileLog.e("messenger", e);
continue;
}
if (a == blocksCount - 1) {
@@ -407,7 +389,7 @@ public class MessageObject {
try {
textHeight = Math.max(textHeight, (int) (block.textYOffset + block.textLayout.getHeight()));
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
@@ -418,14 +400,14 @@ public class MessageObject {
try {
lastLeft = block.textXOffset = block.textLayout.getLineLeft(currentBlockLinesCount - 1);
} catch (Exception e) {
FileLog.e("messenger", e);
}
float lastLine = 0;
try {
lastLine = block.textLayout.getLineWidth(currentBlockLinesCount - 1);
} catch (Exception e) {
FileLog.e("messenger", e);
}
int linesMaxWidth = (int) Math.ceil(lastLine);
@@ -448,7 +430,7 @@ public class MessageObject {
try {
lineWidth = block.textLayout.getLineWidth(n);
} catch (Exception e) {
FileLog.e("messenger", e);
lineWidth = 0;
}
@@ -459,7 +441,7 @@ public class MessageObject {
try {
lineLeft = block.textLayout.getLineLeft(n);
} catch (Exception e) {
FileLog.e("messenger", e);
lineLeft = 0;
}
@@ -727,10 +709,6 @@ public class MessageObject {
return messageOwner.media instanceof TLRPC.TL_messageMediaDocument && isGifDocument(messageOwner.media.document);
}
public boolean isNewGif() {
return messageOwner.media != null && isNewGifDocument(messageOwner.media.document);
}
public int getDuration() {
TLRPC.Document document;
document = messageOwner.media.document;
@@ -802,11 +780,10 @@ public class MessageObject {
}
}
// EDIT BY MR: we use the attachPath for the normal images
// we use the attachPath for the normal images (bp)
if( (mediaExists || attachPathExists) && messageOwner.created_by_mr ) {
attachPathExists = true;
mediaExists = true;
}
// /EDIT BY MR
}
}
@@ -1,147 +0,0 @@
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2013-2016 Nikolai Kudashov
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see http://www.gnu.org/licenses/ .
*
******************************************************************************/
package com.b44t.messenger;
import android.app.Activity;
import android.content.SharedPreferences;
import com.b44t.ui.SettingsAdvActivity;
import java.util.ArrayList;
public class MessagesController {
public int fontSize;
public static final int UPDATE_MASK_NAME = 1;
public static final int UPDATE_MASK_AVATAR = 2;
public static final int UPDATE_MASK_STATUS = 4;
public static final int UPDATE_MASK_CHAT_AVATAR = 8;
public static final int UPDATE_MASK_CHAT_NAME = 16;
public static final int UPDATE_MASK_CHAT_MEMBERS = 32;
public static final int UPDATE_MASK_SELECT_DIALOG = 512;
public static final int UPDATE_MASK_NEW_MESSAGE = 2048;
public static final int UPDATE_MASK_SEND_STATE = 4096;
private static volatile MessagesController Instance = null;
public static MessagesController getInstance() {
MessagesController localInstance = Instance;
if (localInstance == null) {
synchronized (MessagesController.class) {
localInstance = Instance;
if (localInstance == null) {
Instance = localInstance = new MessagesController();
}
}
}
return localInstance;
}
public MessagesController() {
ImageLoader.getInstance();
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE);
fontSize = preferences.getInt("msg_font_size", SettingsAdvActivity.defMsgFontSize());
}
public TLRPC.User getUser(Integer id) {
// EDIT BY MR - additional information should be loaded as needed by the caller
TLRPC.User u = new TLRPC.User();
u.id = id;
return u;
}
public MediaController.SearchImage saveGif(TLRPC.Document document) {
MediaController.SearchImage searchImage = new MediaController.SearchImage();
searchImage.type = 2;
searchImage.document = document;
searchImage.date = (int) (System.currentTimeMillis() / 1000);
searchImage.id = "" + searchImage.document.id;
ArrayList<MediaController.SearchImage> arrayList = new ArrayList<>();
arrayList.add(searchImage);
//MessagesStorage.getInstance().putWebRecent(arrayList);
/*TLRPC.TL_messages_saveGif req = new TLRPC.TL_messages_saveGif();
req.id = new TLRPC.TL_inputDocument();
req.id.id = searchImage.document.id;
req.id.access_hash = searchImage.document.access_hash;
req.unsave = false;
ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() {
@Override
public void run(TLObject response, TLRPC.TL_error error) {
}
});*/
return searchImage;
}
public void cancelTyping(int action, long dialog_id) {
}
public void sendTyping(final long dialog_id, final int action, int classGuid) {
}
public void markMessageContentAsRead(final MessageObject messageObject) {
}
public void changeChatAvatar(int chat_id, TLRPC.InputFile uploadedAvatar) {
/*TLObject request;
{
TLRPC.TL_messages_editChatPhoto req = new TLRPC.TL_messages_editChatPhoto();
req.chat_id = chat_id;
if (uploadedAvatar != null) {
req.photo = new TLRPC.TL_inputChatUploadedPhoto();
req.photo.file = uploadedAvatar;
req.photo.crop = new TLRPC.TL_inputPhotoCropAuto();
} else {
req.photo = new TLRPC.TL_inputChatPhotoEmpty();
}
request = req;
}*/
/*ConnectionsManager.getInstance().sendRequest(request, new RequestDelegate() {
@Override
public void run(TLObject response, TLRPC.TL_error error) {
if (error != null) {
return;
}
processUpdates((TLRPC.Updates) response, false);
}
}, ConnectionsManager.RequestFlagInvokeAfter);*/
}
public boolean isDialogMuted(long dialog_id) {
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE);
int mute_type = preferences.getInt("notify2_" + dialog_id, 0);
if (mute_type == 2) {
return true;
} else if (mute_type == 3) {
int mute_until = preferences.getInt("notifyuntil_" + dialog_id, 0);
if (mute_until >= ConnectionsManager.getInstance().getCurrentTime()) {
return true;
}
}
return false;
}
}
@@ -45,6 +45,7 @@ public class MrContact {
public String getName() {
return MrContactGetName(m_hContact);
}
public native String getAuthName();
public String getAddr() {
return MrContactGetAddr(m_hContact);
@@ -28,6 +28,22 @@
package com.b44t.messenger;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.util.Log;
import com.b44t.ui.Components.ForegroundDetector;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class MrMailbox {
public static void init () {
@@ -38,53 +54,25 @@ public class MrMailbox {
public native static void close();
public native static String getBlobdir();
public static int configure() {
return MrMailboxConfigure(m_hMailbox);
}
public native static void configureAndConnect();
public native static void configureCancel();
public static int isConfigured() {
return MrMailboxIsConfigured(m_hMailbox);
}
public native static int isConfigured();
public static int connect() {
return MrMailboxConnect(m_hMailbox);
}
public native static void connect();
public native static void disconnect();
public static void disconnect() {
MrMailboxDisconnect(m_hMailbox);
}
public static int fetch() {
return MrMailboxFetch(m_hMailbox);
}
public native static String getErrorDescr();
public static int setConfig(String key, String value) {
return MrMailboxSetConfig(m_hMailbox, key, value);
}
public static String getConfig(String key, String def) {
return MrMailboxGetConfig(m_hMailbox, key, def);
}
public static int getConfigInt(String key, int def) {
return MrMailboxGetConfigInt(m_hMailbox, key, def);
}
public native static void setConfig(String key, String value);
public native static void setConfigInt(String key, int value);
public native static String getConfig(String key, String def);
public native static int getConfigInt(String key, int def);
public native static String getInfo();
public native static String cmdline(String cmd);
public native static void heartbeat();
private static long m_hMailbox = 0; // do not rename this, is used in C-part
private native static long MrMailboxNew (); // returns hMailbox which must be unref'd after usage (Names as mrmailbox_new don't work due to the additional underscore)
private native static int MrMailboxConfigure (long hMailbox);
private native static int MrMailboxIsConfigured (long hMailbox);
private native static int MrMailboxConnect (long hMailbox);
private native static void MrMailboxDisconnect (long hMailbox);
private native static int MrMailboxFetch (long hMailbox);
private native static int MrMailboxSetConfig (long hMailbox, String key, String value); // value may be NULL
private native static String MrMailboxGetConfig (long hMailbox, String key, String def); // def may be NULL, returns empty string as NULL
private native static int MrMailboxGetConfigInt (long hMailbox, String key, int def); // def may be NULL, returns empty string as NULL
private native static long MrMailboxNew(); // returns hMailbox which must be unref'd after usage (Names as mrmailbox_new don't work due to the additional underscore)
// contacts
public native static int[] getKnownContacts(String query);
@@ -130,13 +118,8 @@ public class MrMailbox {
public native static int markseenMsg (int msg_id);
public native static int markseenChat (int chat_id);
public static int getChatIdByContactId (int contact_id) {
return MrMailboxGetChatIdByContactId(m_hMailbox, contact_id);
}
public static int createChatByContactId(int contact_id) {
return MrMailboxCreateChatByContactId(m_hMailbox, contact_id);
}
public native static int getChatIdByContactId (int contact_id);
public native static int createChatByContactId(int contact_id); // returns chat_id
public native static int createGroupChat (String name);
public native static int isContactInChat (int chat_id, int contact_id);
@@ -158,8 +141,6 @@ public class MrMailbox {
private native static long MrMailboxGetChatlist (long hMailbox, String query); // returns hChatlist which must be unref'd after usage
private native static long MrMailboxGetChat (long hMailbox, int chat_id); // return hChat which must be unref'd after usage
private native static int MrMailboxGetChatIdByContactId (long hMailbox, int contact_id);
private native static int MrMailboxCreateChatByContactId(long hMailbox, int contact_id); // returns chat_id
// msgs
@@ -185,27 +166,51 @@ public class MrMailbox {
/* receive events
**********************************************************************************************/
public final static int MR_EVENT_INFO = 100;
public final static int MR_EVENT_WARNING = 300;
public final static int MR_EVENT_ERROR = 400; // INFO and WARNING are blocked in the mrwrapper.c
public final static int MR_EVENT_MSGS_CHANGED = 2000;
public final static int MR_EVENT_INCOMING_MSG = 2005;
public final static int MR_EVENT_MSG_DELIVERED = 2010;
public final static int MR_EVENT_MSG_READ = 2015;
public final static int MR_EVENT_CHAT_MODIFIED = 2020;
public final static int MR_EVENT_CONTACTS_CHANGED = 2030;
public final static int MR_EVENT_CONNECTION_STATE_CHANGED = 2040;
public final static int MR_EVENT_REPORT = 2050;
public final static int MR_EVENT_CONFIGURE_ENDED = 2040;
public final static int MR_EVENT_CONFIGURE_PROGRESS = 2041;
public final static int MR_EVENT_IS_ONLINE = 2080;
public final static int MR_EVENT_GET_STRING = 2091;
public final static int MR_EVENT_GET_QUANTITIY_STRING = 2092;
public final static int MR_EVENT_HTTP_GET = 2100;
public final static int MR_EVENT_WAKE_LOCK = 2110;
public final static int MR_REPORT_ERR_SELF_NOT_IN_GROUP = 1;
public static final Object m_lastErrorLock = new Object();
public static int m_lastErrorCode = 0;
public static String m_lastErrorString = "";
public static boolean m_showNextErrorAsToast = true;
public static long MrCallback(final int event, final long data1, final long data2) // this function is called from within the C-wrapper
{
switch(event) {
case MR_EVENT_CONNECTION_STATE_CHANGED:
case MR_EVENT_CONFIGURE_ENDED:
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.connectionStateChanged, (int)data1);
NotificationCenter.getInstance().postNotificationName(NotificationCenter.configureEnded, (int)data1);
}
});
return 0;
case MR_EVENT_CONFIGURE_PROGRESS:
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.configureProgress, (int)data1);
}
});
return 0;
@@ -254,18 +259,45 @@ public class MrMailbox {
public void run() {
reloadMainChatlist();
NotificationCenter.getInstance().postNotificationName(NotificationCenter.updateInterfaces,
MessagesController.UPDATE_MASK_NAME|MessagesController.UPDATE_MASK_CHAT_NAME|
MessagesController.UPDATE_MASK_CHAT_MEMBERS|MessagesController.UPDATE_MASK_AVATAR);
UPDATE_MASK_NAME|UPDATE_MASK_CHAT_NAME|
UPDATE_MASK_CHAT_MEMBERS|UPDATE_MASK_AVATAR);
}
});
return 0;
case MR_EVENT_REPORT:
case MR_EVENT_INFO:
Log.i("DeltaChat", CPtr2String(data2));
if( BuildConfig.BUILD_TYPE.equals("debug") ) {
FileLog.i("DeltaChat", CPtr2String(data2)); // TODO: Can be removed as soon as notifications are reliable.
}
break;
case MR_EVENT_WARNING:
Log.w("DeltaChat", CPtr2String(data2));
if( BuildConfig.BUILD_TYPE.equals("debug") ) {
FileLog.w("DeltaChat", CPtr2String(data2)); // TODO: Can be removed as soon as notifications are reliable.
}
break;
case MR_EVENT_ERROR:
Log.e("DeltaChat", CPtr2String(data2));
if( BuildConfig.BUILD_TYPE.equals("debug") ) {
FileLog.e("DeltaChat", CPtr2String(data2)); // TODO: Can be removed as soon as notifications are reliable.
}
synchronized (m_lastErrorLock) {
m_lastErrorCode = (int)data1;
m_lastErrorString = CPtr2String(data2);
}
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
if(data1==MR_REPORT_ERR_SELF_NOT_IN_GROUP) {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.errSelfNotInGroup);
synchronized (m_lastErrorLock) {
if( m_showNextErrorAsToast ) {
if(ForegroundDetector.getInstance().isForeground()) {
AndroidUtilities.showHint(ApplicationLoader.applicationContext, m_lastErrorString);
}
}
m_showNextErrorAsToast = true;
}
}
});
@@ -274,6 +306,7 @@ public class MrMailbox {
case MR_EVENT_GET_STRING:
String s = "ErrStrBadId";
switch( (int)data1 ) {
// the string-IDs are defined in the backend; as this is the only place where they're used, there is no benefit in creating an enum or sth. like that.
case 1: s = ApplicationLoader.applicationContext.getString(R.string.NoMessages); break;
case 2: s = ApplicationLoader.applicationContext.getString(R.string.FromSelf); break;
case 3: s = ApplicationLoader.applicationContext.getString(R.string.Draft); break;
@@ -281,7 +314,7 @@ public class MrMailbox {
case 8: s = ApplicationLoader.applicationContext.getString(R.string.Deaddrop); break;
case 9: s = ApplicationLoader.applicationContext.getString(R.string.AttachPhoto); break;
case 10: s = ApplicationLoader.applicationContext.getString(R.string.AttachVideo); break;
case 11: s = ApplicationLoader.applicationContext.getString(R.string.AttachMusic); break;
case 11: s = ApplicationLoader.applicationContext.getString(R.string.Audio); break;
case 12: s = ApplicationLoader.applicationContext.getString(R.string.AttachDocument); break;
case 13: s = ApplicationLoader.applicationContext.getString(R.string.DefaultStatusText); break;
case 14: s = ApplicationLoader.applicationContext.getString(R.string.MsgNewGroupDraft); break;
@@ -290,16 +323,58 @@ public class MrMailbox {
case 17: s = ApplicationLoader.applicationContext.getString(R.string.MsgMemberAddedToGroup); break;
case 18: s = ApplicationLoader.applicationContext.getString(R.string.MsgMemberRemovedFromToGroup); break;
case 19: s = ApplicationLoader.applicationContext.getString(R.string.MsgGroupLeft); break;
case 20: s = ApplicationLoader.applicationContext.getString(R.string.Error); break;
case 21: s = ApplicationLoader.applicationContext.getString(R.string.ErrSelfNotInGroup); break;
case 22: s = ApplicationLoader.applicationContext.getString(R.string.NoNetwork); break;
case 23: s = ApplicationLoader.applicationContext.getString(R.string.AttachGif); break;
}
return String2CPtr(s);
case MR_EVENT_GET_QUANTITIY_STRING:
String sp = "ErrQtyStrBadId";
switch( (int)data1 ) {
// the string-IDs are defined in the backend; as this is the only place where they're used, there is no benefit in creating an enum or sth. like that.
case 4: sp = ApplicationLoader.applicationContext.getResources().getQuantityString(R.plurals.Members, (int)data2, (int)data2); break;
case 6: sp = ApplicationLoader.applicationContext.getResources().getQuantityString(R.plurals.Contacts, (int)data2, (int)data2); break;
}
return String2CPtr(sp);
case MR_EVENT_IS_ONLINE:
return ApplicationLoader.isNetworkOnline()? 1 : 0;
case MR_EVENT_HTTP_GET:
String httpContent = null;
try {
URL url = new URL(CPtr2String(data1));
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
try {
InputStream inputStream = new BufferedInputStream(urlConnection.getInputStream());
BufferedReader r = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder total = new StringBuilder();
String line;
while ((line = r.readLine()) != null) {
total.append(line).append('\n');
}
httpContent = total.toString();
} finally {
urlConnection.disconnect();
}
}
catch(Exception e) {}
return String2CPtr(httpContent);
case MR_EVENT_WAKE_LOCK:
if( data1 != 0 ) {
ApplicationLoader.backendWakeLock.acquire();
}
else {
if( !ApplicationLoader.wakeupWakeLock.isHeld()) {
ApplicationLoader.wakeupWakeLock.acquire(1 * 1000); /* make sure, subsequent release/acquires do not make the CPU sleep */
}
ApplicationLoader.backendWakeLock.release();
}
return 0;
}
return 0;
}
@@ -308,6 +383,14 @@ public class MrMailbox {
/* additional functions that are not 1:1 available in the backend
**********************************************************************************************/
public static void log_i(String tag, String msg)
{
Log.i(tag, msg);
if( BuildConfig.BUILD_TYPE.equals("debug") ) {
FileLog.i(tag, msg); // TODO: Can be removed as soon as notifications are reliable.
}
}
public static MrChatlist m_currChatlist = new MrChatlist(0);
public native static int getCurrentTime ();
public static void reloadMainChatlist()
@@ -336,7 +419,47 @@ public class MrMailbox {
public static String getInviteText() {
String url = "https://getdelta.org";
String email = getConfig("addr", "");
String text = LocaleController.formatString("InviteText", R.string.InviteText, url, email);
String text = String.format(ApplicationLoader.applicationContext.getString(R.string.InviteText), url, email);
return text;
}
public static TLRPC.User getUser(Integer id) {
TLRPC.User u = new TLRPC.User(); // legacy function, information should be loaded as needed by the caller
u.id = id;
return u;
}
public static void cancelTyping(int action, long dialog_id) {
}
public static void sendTyping(final long dialog_id, final int action, int classGuid) {
}
public static void markMessageContentAsRead(final MessageObject messageObject) {
}
public static boolean isDialogMuted(long dialog_id) {
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE);
int mute_type = preferences.getInt("notify2_" + dialog_id, 0);
if (mute_type == 2) {
return true;
} else if (mute_type == 3) {
int mute_until = preferences.getInt("notifyuntil_" + dialog_id, 0);
if (mute_until >= MrMailbox.getCurrentTime()) {
return true;
}
}
return false;
}
// legacy update masks
public static final int UPDATE_MASK_NAME = 1;
public static final int UPDATE_MASK_AVATAR = 2;
public static final int UPDATE_MASK_STATUS = 4;
public static final int UPDATE_MASK_CHAT_AVATAR = 8;
public static final int UPDATE_MASK_CHAT_NAME = 16;
public static final int UPDATE_MASK_CHAT_MEMBERS = 32;
public static final int UPDATE_MASK_SELECT_DIALOG = 512;
public static final int UPDATE_MASK_NEW_MESSAGE = 2048;
public static final int UPDATE_MASK_SEND_STATE = 4096;
}
@@ -41,6 +41,7 @@ public class MrMsg {
public final static int MR_MSG_UNDEFINED = 0;
public final static int MR_MSG_TEXT = 10;
public final static int MR_MSG_IMAGE = 20;
public final static int MR_MSG_GIF = 21;
public final static int MR_MSG_AUDIO = 40;
public final static int MR_MSG_VOICE = 41;
public final static int MR_MSG_VIDEO = 50;
@@ -195,7 +196,7 @@ public class MrMsg {
ret.message = "<cannot load image>";
}
}
else if( type == MR_MSG_AUDIO || type == MR_MSG_VOICE || type == MR_MSG_VIDEO || type == MR_MSG_FILE ) {
else if( type == MR_MSG_GIF || type == MR_MSG_AUDIO || type == MR_MSG_VOICE || type == MR_MSG_VIDEO || type == MR_MSG_FILE ) {
String path = getParam('f', "");
if( !path.isEmpty()) {
ret.message = "-1"; // may be misused for video editing information
@@ -205,7 +206,18 @@ public class MrMsg {
ret.media.document.file_name = getFilename();
ret.media.document.mr_path = path;
ret.media.document.size = getBytes();
if( type == MR_MSG_AUDIO || type == MR_MSG_VOICE ) {
if( type == MR_MSG_GIF ) {
ret.media.document.mime_type = getParam('m', "image/gif");
TLRPC.PhotoSize size = new TLRPC.PhotoSize();
size.location = new TLRPC.TL_fileLocation();
size.location.mr_path = path;
size.location.local_id = -ret.id;
size.w = getParamInt('w', 320);
size.h = getParamInt('h', 240);
size.type = "s";
ret.media.document.thumb = size;
}
else if( type == MR_MSG_AUDIO || type == MR_MSG_VOICE ) {
TLRPC.TL_documentAttributeAudio attr = new TLRPC.TL_documentAttributeAudio();
attr.voice = type == MR_MSG_VOICE;
attr.duration = getParamInt('d', 0) / 1000;
@@ -215,15 +227,18 @@ public class MrMsg {
File vfile = new File(path);
File tfile = new File(MrMailbox.getBlobdir(), vfile.getName()+"-preview.jpg");
if( !tfile.exists() ) {
Bitmap thumb = ThumbnailUtils.createVideoThumbnail(path, MediaStore.Video.Thumbnails.MINI_KIND);
TLRPC.PhotoSize size = ImageLoader.scaleAndSaveImage(tfile, thumb, 90, 90, 55, false);
size.location.mr_path = tfile.getAbsolutePath();
size.type = "s";
ret.media.document.thumb = size;
try {
Bitmap thumb = ThumbnailUtils.createVideoThumbnail(path, MediaStore.Video.Thumbnails.MINI_KIND);
TLRPC.PhotoSize size = ImageLoader.scaleAndSaveImage(tfile, thumb, 90, 90, 55, false);
size.location.mr_path = tfile.getAbsolutePath();
size.type = "s";
ret.media.document.thumb = size;
setParamInt('w', size.w);
setParamInt('h', size.h);
saveParamToDisk();
setParamInt('w', size.w);
setParamInt('h', size.h);
saveParamToDisk();
}
catch (Exception e) {}
}
else {
TLRPC.PhotoSize size = new TLRPC.PhotoSize();
@@ -238,8 +253,8 @@ public class MrMsg {
TLRPC.TL_documentAttributeVideo attr = new TLRPC.TL_documentAttributeVideo();
attr.duration = getParamInt('d', 0) / 1000;
attr.w = ret.media.document.thumb.w;
attr.h = ret.media.document.thumb.h;
attr.w = getParamInt('w', 320);
attr.h = getParamInt('h', 240);
ret.media.document.attributes.add(attr);
}
else {
@@ -96,7 +96,7 @@ public class MusicPlayerService extends Service implements NotificationCenter.No
remoteControlClient.setTransportControlFlags(RemoteControlClient.FLAG_KEY_MEDIA_PLAY | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE |
RemoteControlClient.FLAG_KEY_MEDIA_STOP | RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | RemoteControlClient.FLAG_KEY_MEDIA_NEXT);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
createNotification(messageObject);
@@ -182,7 +182,7 @@ public class MusicPlayerService extends Service implements NotificationCenter.No
try {
metadataEditor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, audioInfo.getCover());
} catch (Throwable e) {
FileLog.e("messenger", e);
}
}
metadataEditor.apply();
@@ -1,212 +0,0 @@
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2013-2016 Nikolai Kudashov
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see http://www.gnu.org/licenses/ .
*
******************************************************************************/
package com.b44t.messenger;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class NativeLoader {
private final static int LIB_VERSION = 1;
private final static String LIB_NAME = "messenger." + LIB_VERSION;
private final static String LIB_SO_NAME = "lib" + LIB_NAME + ".so";
private final static String LOCALE_LIB_SO_NAME = "lib" + LIB_NAME + "loc.so";
//private String crashPath = "";
private static volatile boolean nativeLoaded = false;
private static File getNativeLibraryDir(Context context) {
File f = null;
if (context != null) {
try {
f = new File((String)ApplicationInfo.class.getField("nativeLibraryDir").get(context.getApplicationInfo()));
} catch (Throwable th) {
th.printStackTrace();
}
}
if (f == null) {
f = new File(context.getApplicationInfo().dataDir, "lib");
}
if (f.isDirectory()) {
return f;
}
return null;
}
private static boolean loadFromZip(Context context, File destDir, File destLocalFile, String folder) {
try {
for (File file : destDir.listFiles()) {
file.delete();
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
ZipFile zipFile = null;
InputStream stream = null;
try {
zipFile = new ZipFile(context.getApplicationInfo().sourceDir);
ZipEntry entry = zipFile.getEntry("lib/" + folder + "/" + LIB_SO_NAME);
if (entry == null) {
throw new Exception("Unable to find file in apk:" + "lib/" + folder + "/" + LIB_NAME);
}
stream = zipFile.getInputStream(entry);
OutputStream out = new FileOutputStream(destLocalFile);
byte[] buf = new byte[4096];
int len;
while ((len = stream.read(buf)) > 0) {
Thread.yield();
out.write(buf, 0, len);
}
out.close();
destLocalFile.setReadable(true, false);
destLocalFile.setExecutable(true, false);
destLocalFile.setWritable(true);
try {
System.load(destLocalFile.getAbsolutePath());
//init(context.getCacheDir().getAbsolutePath(), BuildVars.DEBUG_VERSION);
nativeLoaded = true;
} catch (Error e) {
FileLog.e("messenger", e);
}
return true;
} catch (Exception e) {
FileLog.e("messenger", e);
} finally {
if (stream != null) {
try {
stream.close();
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
if (zipFile != null) {
try {
zipFile.close();
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
return false;
}
public static synchronized void initNativeLibs(Context context) {
if (nativeLoaded) {
return;
}
//String crashDir = context.getCacheDir().getAbsolutePath();
try {
String folder;
try {
if (Build.CPU_ABI.equalsIgnoreCase("armeabi-v7a")) {
folder = "armeabi-v7a";
} else if (Build.CPU_ABI.equalsIgnoreCase("armeabi")) {
folder = "armeabi";
} else if (Build.CPU_ABI.equalsIgnoreCase("x86")) {
folder = "x86";
} else if (Build.CPU_ABI.equalsIgnoreCase("mips")) {
folder = "mips";
} else {
folder = "armeabi";
FileLog.e("messenger", "Unsupported arch: " + Build.CPU_ABI);
}
} catch (Exception e) {
FileLog.e("messenger", e);
folder = "armeabi";
}
String javaArch = System.getProperty("os.arch");
if (javaArch != null && javaArch.contains("686")) {
folder = "x86";
}
File destFile = getNativeLibraryDir(context);
if (destFile != null) {
destFile = new File(destFile, LIB_SO_NAME);
if (destFile.exists()) {
FileLog.d("messenger", "load normal lib");
try {
System.loadLibrary(LIB_NAME);
//init(crashDir, BuildVars.DEBUG_VERSION);
nativeLoaded = true;
return;
} catch (Error e) {
FileLog.e("messenger", e);
}
}
}
File destDir = new File(context.getFilesDir(), "lib");
destDir.mkdirs();
File destLocalFile = new File(destDir, LOCALE_LIB_SO_NAME);
if (destLocalFile.exists()) {
try {
FileLog.d("messenger", "Load local lib");
System.load(destLocalFile.getAbsolutePath());
//init(crashDir, BuildVars.DEBUG_VERSION);
nativeLoaded = true;
return;
} catch (Error e) {
FileLog.e("messenger", e);
}
destLocalFile.delete();
}
FileLog.e("messenger", "Library not found, arch = " + folder);
if (loadFromZip(context, destDir, destLocalFile, folder)) {
return;
}
} catch (Throwable e) {
e.printStackTrace();
}
try {
System.loadLibrary(LIB_NAME);
//init(crashDir, BuildVars.DEBUG_VERSION);
nativeLoaded = true;
} catch (Error e) {
FileLog.e("messenger", e);
}
}
//private static native void init(String path, boolean enableGoglBreakpad); // not needed
//public static native void crash();
}
@@ -31,7 +31,8 @@ public class NotificationCenter {
private static int totalEvents = 1;
public static final int connectionStateChanged = totalEvents++;
public static final int configureEnded = totalEvents++;
public static final int configureProgress = totalEvents++;
public static final int didReceivedNewMessages = totalEvents++;
public static final int updateInterfaces = totalEvents++;
public static final int dialogsNeedReload = totalEvents++;
@@ -43,9 +44,7 @@ public class NotificationCenter {
public static final int chatDidCreated = totalEvents++;
public static final int mediaDidLoaded = totalEvents++;
public static final int mediaCountDidLoaded = totalEvents++;
public static final int dialogPhotosLoaded = totalEvents++;
public static final int notificationsSettingsUpdated = totalEvents++;
//public static final int pushMessagesUpdated = totalEvents++;
public static final int blockedUsersDidLoaded = totalEvents++;
public static final int openedChatChanged = totalEvents++;
public static final int mainUserInfoChanged = totalEvents++;
@@ -54,23 +53,13 @@ public class NotificationCenter {
public static final int didSetPasscode = totalEvents++;
//public static final int screenStateChanged = totalEvents++; -- currently not used, but this may get handy
public static final int stickersDidLoaded = totalEvents++;
public static final int didReplacedPhotoInMemCache = totalEvents++;
//public static final int musicDidLoaded = totalEvents++;
public static final int reloadHints = totalEvents++;
public static final int httpFileDidLoaded = totalEvents++;
public static final int httpFileDidFailedLoad = totalEvents++;
public static final int messageThumbGenerated = totalEvents++;
public static final int wallpapersDidLoaded = totalEvents++;
public static final int closeOtherAppActivities = totalEvents++;
public static final int didUpdatedConnectionState = totalEvents++;
public static final int emojiDidLoaded = totalEvents++;
public static final int FileLoadProgressChanged = totalEvents++;
public static final int FileDidLoaded = totalEvents++;
public static final int FileDidFailedLoad = totalEvents++;
public static final int FilePreparingStarted = totalEvents++;
public static final int FileNewChunkAvailable = totalEvents++;
public static final int FilePreparingFailed = totalEvents++;
@@ -87,8 +76,6 @@ public class NotificationCenter {
public static final int audioDidStarted = totalEvents++;
public static final int audioRouteChanged = totalEvents++;
public static final int errSelfNotInGroup = totalEvents++;
private SparseArray<ArrayList<Object>> observers = new SparseArray<>();
private SparseArray<ArrayList<Object>> removeAfterBroadcast = new SparseArray<>();
private SparseArray<ArrayList<Object>> addAfterBroadcast = new SparseArray<>();
@@ -163,17 +150,18 @@ public class NotificationCenter {
}
public void postNotificationNameInternal(int id, boolean allowDuringAnimation, Object... args) {
if (BuildVars.DEBUG_VERSION) {
/*if (BuildConfig.BUILD_TYPE.equals("debug")) {
if (Thread.currentThread() != ApplicationLoader.applicationHandler.getLooper().getThread()) {
throw new RuntimeException("postNotificationName allowed only from MAIN thread");
}
}
}*/
if (!allowDuringAnimation && animationInProgress) {
DelayedPost delayedPost = new DelayedPost(id, args);
delayedPosts.add(delayedPost);
if (BuildVars.DEBUG_VERSION) {
FileLog.e("messenger", "delay post notification " + id + " with args count = " + args.length);
}
/*if (BuildConfig.BUILD_TYPE.equals("debug")) {
Log.i("DeltaChat", "delay post notification " + id + " with args count = " + args.length);
}*/
return;
}
broadcasting++;
@@ -210,11 +198,11 @@ public class NotificationCenter {
}
public void addObserver(Object observer, int id) {
if (BuildVars.DEBUG_VERSION) {
/*if (BuildConfig.BUILD_TYPE.equals("debug")) {
if (Thread.currentThread() != ApplicationLoader.applicationHandler.getLooper().getThread()) {
throw new RuntimeException("addObserver allowed only from MAIN thread");
}
}
}*/
if (broadcasting != 0) {
ArrayList<Object> arrayList = addAfterBroadcast.get(id);
if (arrayList == null) {
@@ -235,11 +223,11 @@ public class NotificationCenter {
}
public void removeObserver(Object observer, int id) {
if (BuildVars.DEBUG_VERSION) {
/*if (BuildConfig.BUILD_TYPE.equals("debug")) {
if (Thread.currentThread() != ApplicationLoader.applicationHandler.getLooper().getThread()) {
throw new RuntimeException("removeObserver allowed only from MAIN thread");
}
}
}*/
if (broadcasting != 0) {
ArrayList<Object> arrayList = removeAfterBroadcast.get(id);
if (arrayList == null) {
@@ -90,11 +90,13 @@ public class NotificationsController {
protected AudioManager audioManager;
private AlarmManager alarmManager;
public static final int DEF_LED_COLOR = 0xffffffff;
private static volatile NotificationsController Instance = null;
public static NotificationsController getInstance() {
NotificationsController localInstance = Instance;
if (localInstance == null) {
synchronized (MessagesController.class) {
synchronized (NotificationsController.class) {
localInstance = Instance;
if (localInstance == null) {
Instance = localInstance = new NotificationsController();
@@ -113,26 +115,26 @@ public class NotificationsController {
try {
audioManager = (AudioManager) ApplicationLoader.applicationContext.getSystemService(Context.AUDIO_SERVICE);
} catch (Exception e) {
FileLog.e("messenger", e);
}
try {
alarmManager = (AlarmManager) ApplicationLoader.applicationContext.getSystemService(Context.ALARM_SERVICE);
} catch (Exception e) {
FileLog.e("messenger", e);
}
try {
PowerManager pm = (PowerManager) ApplicationLoader.applicationContext.getSystemService(Context.POWER_SERVICE);
notificationDelayWakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "lock");
notificationDelayWakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "notificationDelayWakelock" /*any name*/);
notificationDelayWakelock.setReferenceCounted(false);
} catch (Exception e) {
FileLog.e("messenger", e);
}
notificationDelayRunnable = new Runnable() {
@Override
public void run() {
FileLog.e("messenger", "delay reached");
//Log.i("DeltaChat", "delay reached");
if (!delayedPushMessages.isEmpty()) {
showOrUpdateNotification(true);
delayedPushMessages.clear();
@@ -142,7 +144,7 @@ public class NotificationsController {
notificationDelayWakelock.release();
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
};
@@ -170,7 +172,7 @@ public class NotificationsController {
notificationDelayWakelock.release();
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
setBadge(0);
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Context.MODE_PRIVATE);
@@ -769,12 +771,12 @@ public class NotificationsController {
intent.putExtra("badge_count_class_name", launcherClassName);
ApplicationLoader.applicationContext.sendBroadcast(intent);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
});
} catch (Throwable e) {
FileLog.e("messenger", e);
}
}
});
@@ -819,7 +821,7 @@ public class NotificationsController {
alarmManager.cancel(pintent);
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -838,7 +840,7 @@ public class NotificationsController {
}
}
} catch (Throwable e) {
FileLog.e("messenger", e);
}
return null;
}
@@ -847,7 +849,7 @@ public class NotificationsController {
int notifyOverride = preferences.getInt("notify2_" + dialog_id, 0);
if (notifyOverride == 3) {
int muteUntil = preferences.getInt("notifyuntil_" + dialog_id, 0);
if (muteUntil >= ConnectionsManager.getInstance().getCurrentTime()) {
if (muteUntil >= MrMailbox.getCurrentTime()) {
notifyOverride = 2;
}
}
@@ -876,7 +878,7 @@ public class NotificationsController {
total_unread_count = 0;
setBadge(0);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -889,7 +891,7 @@ public class NotificationsController {
return;
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
try {
@@ -925,23 +927,23 @@ public class NotificationsController {
soundPool.play(soundIn, 1.0f, 1.0f, 1, 0, 1.0f);
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
});
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
/*private void scheduleNotificationDelay(boolean onlineReason) {
try {
FileLog.e("messenger", "delay notification start, onlineReason = " + onlineReason);
//Log.i("DeltaChat", "delay notification start, onlineReason = " + onlineReason);
notificationDelayWakelock.acquire(10000);
AndroidUtilities.cancelRunOnUIThread(notificationDelayRunnable);
AndroidUtilities.runOnUIThread(notificationDelayRunnable, (onlineReason ? 3 * 1000 : 1000));
} catch (Exception e) {
FileLog.e("messenger", e);
showOrUpdateNotification(notifyCheck);
}
}*/
@@ -968,8 +970,6 @@ public class NotificationsController {
}
try {
ConnectionsManager.getInstance().resumeNetworkMaybe();
MessageObject lastMessageObject = pushMessages.get(0);
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Context.MODE_PRIVATE);
int dismissDate = preferences.getInt("dismissDate", 0);
@@ -994,7 +994,7 @@ public class NotificationsController {
boolean notifyDisabled = false;
int needVibrate = 0;
String choosenSoundPath = null;
int ledColor = 0xff00ff00;
int ledColor = DEF_LED_COLOR;
boolean inAppSounds;
boolean inAppVibrate;
//boolean inAppPreview = false;
@@ -1048,7 +1048,7 @@ public class NotificationsController {
}
needVibrate = preferences.getInt("vibrate_group", 0);
priority = preferences.getInt("priority_group", 1);
ledColor = preferences.getInt("GroupLed", 0xff00ff00);
ledColor = preferences.getInt("GroupLed", DEF_LED_COLOR);
} else {
if (choosenSoundPath != null && choosenSoundPath.equals(defaultPath)) {
choosenSoundPath = null;
@@ -1057,10 +1057,10 @@ public class NotificationsController {
}
needVibrate = preferences.getInt("vibrate_messages", 0);
priority = preferences.getInt("priority_messages", 1);
ledColor = preferences.getInt("MessagesLed", 0xff00ff00);
ledColor = preferences.getInt("MessagesLed", DEF_LED_COLOR);
}
if (preferences.contains("color_" + dialog_id)) {
ledColor = preferences.getInt("color_" + dialog_id, 0);
ledColor = preferences.getInt("color_" + dialog_id, 0/*0=off*/);
}
if (priorityOverride != 3) {
@@ -1090,7 +1090,7 @@ public class NotificationsController {
needVibrate = 2;
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
@@ -1274,7 +1274,7 @@ public class NotificationsController {
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -1321,7 +1321,7 @@ public class NotificationsController {
TLRPC.User user = null;
String name;
if (dialog_id > 0) {
user = MessagesController.getInstance().getUser((int)dialog_id);
user = MrMailbox.getUser((int)dialog_id);
if (user == null) {
continue;
}
@@ -1338,7 +1338,7 @@ public class NotificationsController {
if (chat != null) {
name = chat.title;
} else {
name = UserObject.getUserName(user);
name = "ErrName"; // use MrContact.getName(), if really needed
}
/*if (chat != null) {
if (chat.photo != null && chat.photo.photo_small != null && chat.photo.photo_small.volume_id != 0 && chat.photo.photo_small.local_id != 0) {
@@ -1473,7 +1473,7 @@ public class NotificationsController {
return;
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
notificationsQueue.postRunnable(new Runnable() {
@Override
@@ -1502,40 +1502,9 @@ public class NotificationsController {
soundPool.play(soundOut, 1.0f, 1.0f, 1, 0, 1.0f);
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
});
}
public static void updateServerNotificationsSettings(long dialog_id) {
// the following command is needed to reflect the changes in the GUI
NotificationCenter.getInstance().postNotificationName(NotificationCenter.notificationsSettingsUpdated);
/*
if ((int) dialog_id == 0) {
return;
}
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE);
TLRPC.TL_account_updateNotifySettings req = new TLRPC.TL_account_updateNotifySettings();
req.settings = new TLRPC.TL_inputPeerNotifySettings();
req.settings.sound = "default";
int mute_type = preferences.getInt("notify2_" + dialog_id, 0);
if (mute_type == 3) {
req.settings.mute_until = preferences.getInt("notifyuntil_" + dialog_id, 0);
} else {
req.settings.mute_until = mute_type != 2 ? 0 : Integer.MAX_VALUE;
}
req.settings.show_previews = preferences.getBoolean("preview_" + dialog_id, true);
req.settings.silent = preferences.getBoolean("silent_" + dialog_id, false);
req.peer = new TLRPC.TL_inputNotifyPeer();
((TLRPC.TL_inputNotifyPeer) req.peer).peer = MessagesController.getInputPeer((int) dialog_id);
ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() {
@Override
public void run(TLObject response, TLRPC.TL_error error) {
}
});
*/
}
}
@@ -1,58 +0,0 @@
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2013-2016 Nikolai Kudashov
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see http://www.gnu.org/licenses/ .
*
******************************************************************************/
package com.b44t.messenger;
import android.app.Service;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.IBinder;
public class NotificationsService extends Service {
@Override
public void onCreate() {
FileLog.e("messenger", "service started");
ApplicationLoader.postInitApplication();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
public void onDestroy() {
FileLog.e("messenger", "service destroyed");
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", MODE_PRIVATE);
if (preferences.getBoolean("pushService", true)) {
Intent intent = new Intent("com.b44t.start");
sendBroadcast(intent);
}
}
}
@@ -26,18 +26,17 @@ package com.b44t.messenger;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class ScreenReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
FileLog.e("messenger", "screen off");
ConnectionsManager.getInstance().setAppPaused(true, true);
MrMailbox.log_i("DeltaChat", "*** Screen off");
ApplicationLoader.isScreenOn = false;
} else if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
FileLog.e("messenger", "screen on");
ConnectionsManager.getInstance().setAppPaused(false, true);
MrMailbox.log_i("DeltaChat", "*** Screen on");
ApplicationLoader.isScreenOn = true;
}
//NotificationCenter.getInstance().postNotificationName(NotificationCenter.screenStateChanged);
@@ -31,7 +31,6 @@ import android.media.ThumbnailUtils;
import android.net.Uri;
import android.provider.MediaStore;
import android.webkit.MimeTypeMap;
import android.widget.Toast;
import com.b44t.messenger.audioinfo.AudioInfo;
@@ -63,10 +62,6 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
NotificationCenter.getInstance().addObserver(this, NotificationCenter.FilePreparingStarted);
NotificationCenter.getInstance().addObserver(this, NotificationCenter.FileNewChunkAvailable);
NotificationCenter.getInstance().addObserver(this, NotificationCenter.FilePreparingFailed);
NotificationCenter.getInstance().addObserver(this, NotificationCenter.httpFileDidFailedLoad);
NotificationCenter.getInstance().addObserver(this, NotificationCenter.httpFileDidLoaded);
NotificationCenter.getInstance().addObserver(this, NotificationCenter.FileDidLoaded);
NotificationCenter.getInstance().addObserver(this, NotificationCenter.FileDidFailedLoad);
}
@Override
@@ -94,112 +89,6 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
new File(messageObject.messageOwner.attachPath+".increation").delete();
NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesSentOrRead);
}
else if (id == NotificationCenter.httpFileDidLoaded) {
/*
String path = (String) args[0];
ArrayList<DelayedMessage> arr = delayedMessages.get(path);
if (arr != null) {
for (int a = 0; a < arr.size(); a++) {
final DelayedMessage message = arr.get(a);
if (message.type == 0) {
String md5 = Utilities.MD5(message.httpLocation) + "." + ImageLoader.getHttpUrlExtension(message.httpLocation, "file");
final File cacheFile = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), md5);
Utilities.globalQueue.postRunnable(new Runnable() {
@Override
public void run() {
final TLRPC.TL_photo photo = SendMessagesHelper.getInstance().generatePhotoSizes(cacheFile.toString(), null);
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
if (photo != null) {
message.httpLocation = null;
message.obj.messageOwner.media.photo = photo;
message.obj.messageOwner.attachPath = cacheFile.toString();
message.location = photo.sizes.get(photo.sizes.size() - 1).location;
ArrayList<TLRPC.Message> messages = new ArrayList<>();
messages.add(message.obj.messageOwner);
//MessagesStorage.getInstance().putMessages(messages, false, true, false, 0);
performSendDelayedMessage(message);
NotificationCenter.getInstance().postNotificationName(NotificationCenter.updateMessageMedia, message.obj);
} else {
FileLog.e("messenger", "can't load image " + message.httpLocation + " to file " + cacheFile.toString());
//MessagesStorage.getInstance().markMessageAsSendError(message.obj.messageOwner);
message.obj.messageOwner.send_state = MessageObject.MESSAGE_SEND_STATE_SEND_ERROR;
NotificationCenter.getInstance().postNotificationName(NotificationCenter.messageSendError, message.obj.getId());
processSentMessage(message.obj.getId());
}
}
});
}
});
} else if (message.type == 2) {
String md5 = Utilities.MD5(message.httpLocation) + ".gif";
final File cacheFile = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), md5);
Utilities.globalQueue.postRunnable(new Runnable() {
@Override
public void run() {
if (message.documentLocation.thumb.location instanceof TLRPC.TL_fileLocationUnavailable) {
try {
Bitmap bitmap = ImageLoader.loadBitmap(cacheFile.getAbsolutePath(), null, 90, 90, true);
if (bitmap != null) {
message.documentLocation.thumb = ImageLoader.scaleAndSaveImage(bitmap, 90, 90, 55, message.sendEncryptedRequest != null);
bitmap.recycle();
}
} catch (Exception e) {
message.documentLocation.thumb = null;
FileLog.e("messenger", e);
}
if (message.documentLocation.thumb == null) {
message.documentLocation.thumb = new TLRPC.TL_photoSizeEmpty();
message.documentLocation.thumb.type = "s";
}
}
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
message.httpLocation = null;
message.obj.messageOwner.attachPath = cacheFile.toString();
message.location = message.documentLocation.thumb.location;
ArrayList<TLRPC.Message> messages = new ArrayList<>();
messages.add(message.obj.messageOwner);
//MessagesStorage.getInstance().putMessages(messages, false, true, false, 0);
performSendDelayedMessage(message);
NotificationCenter.getInstance().postNotificationName(NotificationCenter.updateMessageMedia, message.obj);
}
});
}
});
}
}
delayedMessages.remove(path);
}
*/
} else if (id == NotificationCenter.FileDidLoaded) {
/*
String path = (String) args[0];
ArrayList<DelayedMessage> arr = delayedMessages.get(path);
if (arr != null) {
for (int a = 0; a < arr.size(); a++) {
performSendDelayedMessage(arr.get(a));
}
delayedMessages.remove(path);
}
*/
} else if (id == NotificationCenter.httpFileDidFailedLoad || id == NotificationCenter.FileDidFailedLoad) {
/*
String path = (String) args[0];
ArrayList<DelayedMessage> arr = delayedMessages.get(path);
if (arr != null) {
for (DelayedMessage message : arr) {
//MessagesStorage.getInstance().markMessageAsSendError(message.obj.messageOwner);
message.obj.messageOwner.send_state = MessageObject.MESSAGE_SEND_STATE_SEND_ERROR;
NotificationCenter.getInstance().postNotificationName(NotificationCenter.messageSendError, message.obj.getId());
processSentMessage(message.obj.getId());
}
delayedMessages.remove(path);
}
*/
}
}
public void sendSticker(TLRPC.Document document, long peer) {
@@ -211,7 +100,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
public void sendMessageContact(int contact_id, int dialog_id) {
MrContact contact = MrMailbox.getContact(contact_id);
String msg = contact.getName();
String msg = contact.getAuthName();
if(msg.isEmpty()) {
msg = contact.getAddr();
}
@@ -343,10 +232,6 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
}
}
public void checkUnsentMessages() {
//MessagesStorage.getInstance().getUnsentMessages(1000);
}
public TLRPC.TL_photo generatePhotoSizes(String path, Uri imageUri) {
Bitmap bitmap = ImageLoader.loadBitmap(path, imageUri, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), true);
if (bitmap == null && AndroidUtilities.getPhotoSize() != 800) {
@@ -367,9 +252,9 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
if (sizes.isEmpty()) {
return null;
} else {
UserConfig.saveConfig(false);
UserConfig.saveConfig();
TLRPC.TL_photo photo = new TLRPC.TL_photo();
photo.date = ConnectionsManager.getInstance().getCurrentTime();
photo.date = MrMailbox.getCurrentTime();
photo.sizes = sizes;
return photo;
}
@@ -438,16 +323,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
}
TLRPC.TL_document document = null;
/*if (!isEncrypted)*/ {
document = null;//(TLRPC.TL_document) MessagesStorage.getInstance().getSentFile(originalPath, !isEncrypted ? 1 : 4);
if (document == null && !path.equals(originalPath) /*&& !isEncrypted*/) {
document = null;//(TLRPC.TL_document) MessagesStorage.getInstance().getSentFile(path + f.length(), !isEncrypted ? 1 : 4);
}
}
if (document == null) {
{
document = new TLRPC.TL_document();
document.id = 0;
document.date = ConnectionsManager.getInstance().getCurrentTime();
document.date = MrMailbox.getCurrentTime();
TLRPC.TL_documentAttributeFilename fileName = new TLRPC.TL_documentAttributeFilename();
fileName.file_name = name;
document.attributes.add(fileName);
@@ -479,7 +358,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
bitmap.recycle();
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
if (document.mime_type.equals("image/webp") && allowSticker) {
@@ -491,7 +370,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
Utilities.loadWebpImage(null, buffer, buffer.limit(), bmOptions, true);
file.close();
} catch (Exception e) {
FileLog.e("messenger", e);
}
if (bmOptions.outWidth != 0 && bmOptions.outHeight != 0 && bmOptions.outWidth <= 800 && bmOptions.outHeight <= 800) {
TLRPC.TL_documentAttributeSticker attributeSticker = new TLRPC.TL_documentAttributeSticker();
@@ -511,16 +390,16 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
}
document.caption = caption;
final HashMap<String, String> params = new HashMap<>();
/*final HashMap<String, String> params = new HashMap<>();
if (originalPath != null) {
params.put("originalPath", originalPath);
}
}*/
final TLRPC.TL_document documentFinal = document;
final String pathFinal = path;
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
SendMessagesHelper.getInstance().sendMessageDocument(documentFinal, null, pathFinal, dialog_id, params);
SendMessagesHelper.getInstance().sendMessageDocument(documentFinal, null, pathFinal, dialog_id, null);
}
});
return true;
@@ -541,44 +420,6 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
prepareSendingDocuments(paths, originalPaths, uris, mine, dialog_id);
}
/*public static void prepareSendingAudioDocuments(final ArrayList<MessageObject> messageObjects, final long dialog_id) {
new Thread(new Runnable() {
@Override
public void run() {
int size = messageObjects.size();
for (int a = 0; a < size; a++) {
final MessageObject messageObject = messageObjects.get(a);
String originalPath = messageObject.messageOwner.attachPath;
final File f = new File(originalPath);
if (originalPath != null) {
originalPath += "audio" + f.length();
}
TLRPC.TL_document document = null;
if (!isEncrypted) {
document = null;//(TLRPC.TL_document) MessagesStorage.getInstance().getSentFile(originalPath, !isEncrypted ? 1 : 4);
}
if (document == null) {
document = null;//(TLRPC.TL_document) messageObject.messageOwner.media.document;
}
final HashMap<String, String> params = new HashMap<>();
if (originalPath != null) {
params.put("originalPath", originalPath);
}
final TLRPC.TL_document documentFinal = document;
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
SendMessagesHelper.getInstance().sendMessageDocument(documentFinal, null, messageObject.messageOwner.attachPath, dialog_id, params);
}
});
}
}
}).start();
}*/
public static void prepareSendingDocuments(final ArrayList<String> paths, final ArrayList<String> originalPaths, final ArrayList<Uri> uris, final String mime, final long dialog_id) {
if (paths == null && originalPaths == null && uris == null || paths != null && originalPaths != null && paths.size() != originalPaths.size()) {
return;
@@ -681,7 +522,6 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
new Thread(new Runnable() {
@Override
public void run() {
final boolean isEncrypted = false;//(int) dialog_id == 0;
ArrayList<String> sendAsDocuments = null;
ArrayList<String> sendAsDocumentsOriginal = null;
@@ -700,7 +540,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
String originalPath = path;
String tempPath = path;
if (tempPath == null && uri != null) {
tempPath = AndroidUtilities.getPath(uri);
tempPath = AndroidUtilities.getPath(uri); // convert uri to path, this may fail
originalPath = uri.toString();
}
@@ -713,6 +553,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
}
isDocument = true;
} else if (tempPath == null && uri != null) {
// if conversion from uri to path fails above, we have a look at the content of the file
if (MediaController.isGif(uri)) {
isDocument = true;
originalPath = uri.toString();
@@ -757,14 +598,14 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
photo.caption = captions.get(a);
}
final TLRPC.TL_photo photoFinal = photo;
final HashMap<String, String> params = new HashMap<>();
/*final HashMap<String, String> params = new HashMap<>();
if (originalPath != null) {
params.put("originalPath", originalPath);
}
}*/
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
SendMessagesHelper.getInstance().sendMessagePhoto(photoFinal, null, dialog_id, params);
SendMessagesHelper.getInstance().sendMessagePhoto(photoFinal, null, dialog_id, null);
}
});
}
@@ -789,20 +630,20 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
if (videoEditedInfo != null || videoPath.endsWith("mp4")) {
String path = videoPath;
String originalPath = videoPath;
File temp = new File(originalPath);
originalPath += temp.length() + "_" + temp.lastModified();
//String originalPath = videoPath;
File temp = new File(videoPath);
/*originalPath += temp.length() + "_" + temp.lastModified();
if (videoEditedInfo != null) {
originalPath += duration + "_" + videoEditedInfo.startTime + "_" + videoEditedInfo.endTime;
if (videoEditedInfo.resultWidth == videoEditedInfo.originalWidth) {
originalPath += "_" + videoEditedInfo.resultWidth;
}
}
}*/
TLRPC.TL_document document = null;
{
document = new TLRPC.TL_document();
document.mime_type = "video/mp4";
UserConfig.saveConfig(false);
UserConfig.saveConfig();
TLRPC.TL_documentAttributeVideo attributeVideo = new TLRPC.TL_documentAttributeVideo();
document.attributes.add(attributeVideo);
if (videoEditedInfo != null) {
@@ -815,10 +656,8 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
attributeVideo.h = height;
}
document.size = (int) estimatedSize;
String fileName = Integer.MIN_VALUE + "_" + UserConfig.lastLocalId + ".mp4";
UserConfig.lastLocalId--;
File cacheFile = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), fileName);
UserConfig.saveConfig(false);
String fileName = temp.getName(); // we could also all videoEditInformation to the filename and re-use already encoded videos this way. however, for the moment, I have no time to check this out (bp)
File cacheFile = AndroidUtilities.getFineFilename(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), fileName);
path = cacheFile.getAbsolutePath();
} else {
if (temp.exists()) {
@@ -844,14 +683,14 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
}
infoObtained = true;
} catch (Exception e) {
FileLog.e("messenger", e);
} finally {
try {
if (mediaMetadataRetriever != null) {
mediaMetadataRetriever.release();
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
if (!infoObtained) {
@@ -864,7 +703,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
mp.release();
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
@@ -878,14 +717,14 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
File tfile = new File(MrMailbox.getBlobdir(), vfile.getName()+"-preview.jpg");
ImageLoader.scaleAndSaveImage(tfile, thumb, 90, 90, 55, false);
final HashMap<String, String> params = new HashMap<>();
/*final HashMap<String, String> params = new HashMap<>();
if (originalPath != null) {
params.put("originalPath", originalPath);
}
}*/
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
SendMessagesHelper.getInstance().sendMessageDocument(videoFinal, videoEditedInfo, finalPath, dialog_id, params);
SendMessagesHelper.getInstance().sendMessageDocument(videoFinal, videoEditedInfo, finalPath, dialog_id, null);
}
});
} else {
@@ -103,23 +103,9 @@ public class TLRPC {
public int duration;
public String mime_type;
public int size;
public final int dc_id = 0;
public int user_id;
}
public static class InputFileLocation extends TLObject {
public long id;
public long access_hash;
public long volume_id;
public int local_id;
}
public static class TL_inputDocumentFileLocation extends InputFileLocation {
}
public static class TL_inputFileLocation extends InputFileLocation {
}
public static class User extends TLObject {
public int id;
public String first_name;
@@ -136,7 +122,6 @@ public class TLRPC {
public int duration;
public int size;
public PhotoSize thumb;
public int dc_id;
public int w;
public int h;
public String mime_type;
@@ -192,7 +177,6 @@ public class TLRPC {
public static class Chat extends TLObject {
public int flags;
public boolean creator;
public int id;
public String title;
public final ChatPhoto photo = null;
@@ -0,0 +1,72 @@
/*******************************************************************************
*
* Messenger Android Frontend
* (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see http://www.gnu.org/licenses/ .
*
******************************************************************************/
package com.b44t.messenger;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
public class TimerReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
// acquire for 3 seconds, this should wake up the threads,
// _if_ there is more to do, the backend acquires an additional wakelock using MR_EVENT_WAKE_LOCK
ApplicationLoader.wakeupWakeLock.acquire(3*1000); // do this first!
MrMailbox.log_i("DeltaChat", "*** TimerReceiver.onReceive()");
// we assume, the IMAP thread is alive. I cannot imagine, the thread was killed with the App being running.
// (if the whole App was killed, the IMAP thread is already started by MrMailbox.connect() if we're here)
// (the thread itself will reconnect to the IMAP server as needed)
// however, it seems as if the threads sleep longer than ususal, check this by calling heartbeat() manually
MrMailbox.heartbeat();
// create the next alarm in about a minute
scheduleNextAlarm();
}
public static void scheduleNextAlarm()
{
try {
Intent intent = new Intent(ApplicationLoader.applicationContext, TimerReceiver.class);
PendingIntent alarmIntent = PendingIntent.getBroadcast(ApplicationLoader.applicationContext, 0, intent, 0);
long triggerAtMillis = System.currentTimeMillis() + 60 * 1000;
AlarmManager alarmManager = (AlarmManager) ApplicationLoader.applicationContext.getSystemService(Activity.ALARM_SERVICE);
if( Build.VERSION.SDK_INT >= 23 ) {
// a simple AlarmManager.set() is no longer send in the new DOZE mode
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, alarmIntent);
}
else {
alarmManager.set(AlarmManager.RTC_WAKEUP, triggerAtMillis, alarmIntent);
}
}
catch(Exception e) { Log.e("DeltaChat", "Cannot create alarm.", e); }
}
}
@@ -31,7 +31,6 @@ import java.io.File;
public class UserConfig {
private static TLRPC.User currentUser;
public static int lastLocalId = -210000;
private final static Object sync = new Object();
public static String passcodeHash = "";
@@ -43,11 +42,7 @@ public class UserConfig {
public static boolean isWaitingForPasscodeEnter;
public static boolean useFingerprint = true;
public static void saveConfig(boolean withFile) {
saveConfig(withFile, null);
}
public static void saveConfig(boolean withFile, File oldFile) {
public static void saveConfig() {
synchronized (sync) {
try {
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("userconfing", Context.MODE_PRIVATE);
@@ -61,128 +56,15 @@ public class UserConfig {
editor.putInt("lastPauseTime", lastPauseTime);
editor.putBoolean("useFingerprint", useFingerprint);
/*
if (currentUser != null) {
if (withFile) {
SerializedData data = new SerializedData();
currentUser.serializeToStream(data);
String userString = Base64.encodeToString(data.toByteArray(), Base64.DEFAULT);
editor.putString("user", userString);
data.cleanup();
}
} else {
editor.remove("user");
}
*/
editor.apply();
if (oldFile != null) {
oldFile.delete();
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
}
public static boolean isClientActivated() {
return true; // EDIT BY MR -- for "real" checking, call MrMailbox.MrMailboxIsConfigured()
/* EDIT BY MR
synchronized (sync) {
return currentUser != null;
}
*/
}
public static int getClientUserId() {
return 1; // we are user #1 by definition
/* EDIT BY MR
synchronized (sync) {
return currentUser != null ? currentUser.id : 0;
}
*/
}
public static TLRPC.User getCurrentUser() {
synchronized (sync) {
if( currentUser==null ) {
currentUser = MrContact.contactId2user(1);
}
return currentUser;
}
}
public static void setCurrentUser(TLRPC.User user) {
synchronized (sync) {
currentUser = MrContact.contactId2user(1); // EDIT BY MR - force the current user to be user #1, normally this function should not be called at all
}
}
public static void loadConfig() {
synchronized (sync) {
/*
final File configFile = new File(ApplicationLoader.getFilesDirFixed(), "user.dat");
if (configFile.exists()) {
try {
SerializedData data = new SerializedData(configFile);
int ver = data.readInt32(false);
if (ver == 1) {
int constructor = data.readInt32(false);
currentUser = TLRPC.User.TLdeserialize(data, constructor, false);
MessagesStorage.lastDateValue = data.readInt32(false);
MessagesStorage.lastPtsValue = data.readInt32(false);
MessagesStorage.lastSeqValue = data.readInt32(false);
registeredForPush = data.readBool(false);
pushString = data.readString(false);
lastSendMessageId = data.readInt32(false);
lastLocalId = data.readInt32(false);
contactsHash = data.readString(false);
data.readString(false);
saveIncomingPhotos = data.readBool(false);
//MessagesStorage.lastQtsValue = data.readInt32(false);
//MessagesStorage.lastSecretVersion = data.readInt32(false);
int val = data.readInt32(false);
//if (val == 1) {
// MessagesStorage.secretPBytes = data.readByteArray(false);
//}
//MessagesStorage.secretG = data.readInt32(false);
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
saveConfig(true, configFile);
}
});
} else if (ver == 2) {
int constructor = data.readInt32(false);
currentUser = TLRPC.User.TLdeserialize(data, constructor, false);
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("userconfing", Context.MODE_PRIVATE);
registeredForPush = preferences.getBoolean("registeredForPush", false);
pushString = preferences.getString("pushString2", "");
lastSendMessageId = preferences.getInt("lastSendMessageId", -210000);
lastLocalId = preferences.getInt("lastLocalId", -210000);
contactsHash = preferences.getString("contactsHash", "");
saveIncomingPhotos = preferences.getBoolean("saveIncomingPhotos", false);
}
if (lastLocalId > -210000) {
lastLocalId = -210000;
}
if (lastSendMessageId > -210000) {
lastSendMessageId = -210000;
}
data.cleanup();
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
saveConfig(true, configFile);
}
});
} catch (Exception e) {
FileLog.e("messenger", e);
}
} else
*/
{
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("userconfing", Context.MODE_PRIVATE);
lastLocalId = preferences.getInt("lastLocalId", -210000);
passcodeHash = preferences.getString("passcodeHash1", "");
@@ -192,26 +74,12 @@ public class UserConfig {
lastPauseTime = preferences.getInt("lastPauseTime", 0);
useFingerprint = preferences.getBoolean("useFingerprint", true);
/*
String user = preferences.getString("user", null);
if (user != null) {
byte[] userBytes = Base64.decode(user, Base64.DEFAULT);
if (userBytes != null) {
SerializedData data = new SerializedData(userBytes);
currentUser = TLRPC.User.TLdeserialize(data, data.readInt32(false), false);
data.cleanup();
}
}
*/
setCurrentUser(null);
String passcodeSaltString = preferences.getString("passcodeSalt", "");
if (passcodeSaltString.length() > 0) {
passcodeSalt = Base64.decode(passcodeSaltString, Base64.DEFAULT);
} else {
passcodeSalt = new byte[0];
}
}
}
}
@@ -228,9 +96,9 @@ public class UserConfig {
System.arraycopy(passcodeBytes, 0, bytes, 16, passcodeBytes.length);
System.arraycopy(passcodeSalt, 0, bytes, passcodeBytes.length + 16, 16);
passcodeHash = Utilities.bytesToHex(Utilities.computeSHA256(bytes, 0, bytes.length));
saveConfig(false);
saveConfig();
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
return result;
@@ -244,7 +112,7 @@ public class UserConfig {
String hash = Utilities.bytesToHex(Utilities.computeSHA256(bytes, 0, bytes.length));
return passcodeHash.equals(hash);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
return false;
@@ -56,7 +56,7 @@ public class Utilities {
sUrandomIn.close();
random.setSeed(buffer);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -67,15 +67,8 @@ public class Utilities {
public native static void calcCDT(ByteBuffer hsvBuffer, int width, int height, ByteBuffer buffer);
public native static boolean loadWebpImage(Bitmap bitmap, ByteBuffer buffer, int len, BitmapFactory.Options options, boolean unpin);
public native static int convertVideoFrame(ByteBuffer src, ByteBuffer dest, int destFormat, int width, int height, int padding, int swap);
//private native static void aesIgeEncryption(ByteBuffer buffer, byte[] key, byte[] iv, boolean encrypt, int offset, int length);
public native static String readlink(String path);
/*
public static void aesIgeEncryption(ByteBuffer buffer, byte[] key, byte[] iv, boolean encrypt, boolean changeIv, int offset, int length) {
aesIgeEncryption(buffer, key, changeIv ? iv : iv.clone(), encrypt, offset, length);
}
*/
public static Integer parseInt(String value) {
if (value == null) {
return 0;
@@ -88,7 +81,7 @@ public class Utilities {
val = Integer.parseInt(num);
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
return val;
}
@@ -105,19 +98,11 @@ public class Utilities {
val = Long.parseLong(num);
}
} catch (Exception e) {
FileLog.e("messenger", e);
}
return val;
}
public static String parseIntToString(String value) {
Matcher matcher = pattern.matcher(value);
if (matcher.find()) {
return matcher.group(0);
}
return null;
}
public static String bytesToHex(byte[] bytes) {
if (bytes == null) {
return "";
@@ -132,138 +117,17 @@ public class Utilities {
return new String(hexChars);
}
public static byte[] hexToBytes(String hex) {
if (hex == null) {
return null;
}
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16));
}
return data;
}
public static boolean isGoodPrime(byte[] prime, int g) {
if (!(g >= 2 && g <= 7)) {
return false;
}
if (prime.length != 256 || prime[0] >= 0) {
return false;
}
BigInteger dhBI = new BigInteger(1, prime);
if (g == 2) { // p mod 8 = 7 for g = 2;
BigInteger res = dhBI.mod(BigInteger.valueOf(8));
if (res.intValue() != 7) {
return false;
}
} else if (g == 3) { // p mod 3 = 2 for g = 3;
BigInteger res = dhBI.mod(BigInteger.valueOf(3));
if (res.intValue() != 2) {
return false;
}
} else if (g == 5) { // p mod 5 = 1 or 4 for g = 5;
BigInteger res = dhBI.mod(BigInteger.valueOf(5));
int val = res.intValue();
if (val != 1 && val != 4) {
return false;
}
} else if (g == 6) { // p mod 24 = 19 or 23 for g = 6;
BigInteger res = dhBI.mod(BigInteger.valueOf(24));
int val = res.intValue();
if (val != 19 && val != 23) {
return false;
}
} else if (g == 7) { // p mod 7 = 3, 5 or 6 for g = 7.
BigInteger res = dhBI.mod(BigInteger.valueOf(7));
int val = res.intValue();
if (val != 3 && val != 5 && val != 6) {
return false;
}
}
String hex = bytesToHex(prime);
if (hex.equals("C71CAEB9C6B1C9048E6C522F70F13F73980D40238E3E21C14934D037563D930F48198A0AA7C14058229493D22530F4DBFA336F6E0AC925139543AED44CCE7C3720FD51F69458705AC68CD4FE6B6B13ABDC9746512969328454F18FAF8C595F642477FE96BB2A941D5BCD1D4AC8CC49880708FA9B378E3C4F3A9060BEE67CF9A4A4A695811051907E162753B56B0F6B410DBA74D8A84B2A14B3144E0EF1284754FD17ED950D5965B4B9DD46582DB1178D169C6BC465B0D6FF9CA3928FEF5B9AE4E418FC15E83EBEA0F87FA9FF5EED70050DED2849F47BF959D956850CE929851F0D8115F635B105EE2E4E15D04B2454BF6F4FADF034B10403119CD8E3B92FCC5B")) {
return true;
}
BigInteger dhBI2 = dhBI.subtract(BigInteger.valueOf(1)).divide(BigInteger.valueOf(2));
return !(!dhBI.isProbablePrime(30) || !dhBI2.isProbablePrime(30));
}
public static boolean isGoodGaAndGb(BigInteger g_a, BigInteger p) {
return !(g_a.compareTo(BigInteger.valueOf(1)) != 1 || g_a.compareTo(p.subtract(BigInteger.valueOf(1))) != -1);
}
public static boolean arraysEquals(byte[] arr1, int offset1, byte[] arr2, int offset2) {
if (arr1 == null || arr2 == null || offset1 < 0 || offset2 < 0 || arr1.length - offset1 != arr2.length - offset2 || arr1.length - offset1 < 0 || arr2.length - offset2 < 0) {
return false;
}
boolean result = true;
for (int a = offset1; a < arr1.length; a++) {
if (arr1[a + offset1] != arr2[a + offset2]) {
result = false;
}
}
return result;
}
public static byte[] computeSHA1(byte[] convertme, int offset, int len) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(convertme, offset, len);
return md.digest();
} catch (Exception e) {
FileLog.e("messenger", e);
}
return new byte[20];
}
public static byte[] computeSHA1(ByteBuffer convertme, int offset, int len) {
int oldp = convertme.position();
int oldl = convertme.limit();
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
convertme.position(offset);
convertme.limit(len);
md.update(convertme);
return md.digest();
} catch (Exception e) {
FileLog.e("messenger", e);
} finally {
convertme.limit(oldl);
convertme.position(oldp);
}
return new byte[20];
}
public static byte[] computeSHA1(ByteBuffer convertme) {
return computeSHA1(convertme, 0, convertme.limit());
}
public static byte[] computeSHA1(byte[] convertme) {
return computeSHA1(convertme, 0, convertme.length);
}
public static byte[] computeSHA256(byte[] convertme, int offset, int len) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(convertme, offset, len);
return md.digest();
} catch (Exception e) {
FileLog.e("messenger", e);
}
return null;
}
public static long bytesToLong(byte[] bytes) {
return ((long) bytes[7] << 56) + (((long) bytes[6] & 0xFF) << 48) + (((long) bytes[5] & 0xFF) << 40) + (((long) bytes[4] & 0xFF) << 32)
+ (((long) bytes[3] & 0xFF) << 24) + (((long) bytes[2] & 0xFF) << 16) + (((long) bytes[1] & 0xFF) << 8) + ((long) bytes[0] & 0xFF);
}
public static String MD5(String md5) {
if (md5 == null) {
return null;
@@ -277,7 +141,7 @@ public class Utilities {
}
return sb.toString();
} catch (java.security.NoSuchAlgorithmException e) {
FileLog.e("messenger", e);
}
return null;
}
@@ -23,50 +23,15 @@
package com.b44t.messenger;
import java.util.Locale;
public class VideoEditedInfo {
public long startTime;
public long endTime;
public int rotationValue;
public int originalWidth;
public int originalHeight;
public int originalBitrate;
public int resultWidth;
public int resultHeight;
public int bitrate;
public int resultBitrate;
public String originalPath;
public String getString() {
return String.format(Locale.US, "-1_%d_%d_%d_%d_%d_%d_%d_%d_%s", startTime, endTime, rotationValue, originalWidth, originalHeight, bitrate, resultWidth, resultHeight, originalPath);
}
public boolean parseString(String string) {
if (string.length() < 6) {
return false;
}
try {
String args[] = string.split("_");
if (args.length >= 10) {
startTime = Long.parseLong(args[1]);
endTime = Long.parseLong(args[2]);
rotationValue = Integer.parseInt(args[3]);
originalWidth = Integer.parseInt(args[4]);
originalHeight = Integer.parseInt(args[5]);
bitrate = Integer.parseInt(args[6]);
resultWidth = Integer.parseInt(args[7]);
resultHeight = Integer.parseInt(args[8]);
for (int a = 9; a < args.length; a++) {
if (originalPath == null) {
originalPath = args[a];
} else {
originalPath += "_" + args[a];
}
}
}
return true;
} catch (Exception e) {
FileLog.e("messenger", e);
}
return false;
}
}
@@ -28,12 +28,13 @@ import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.RemoteInput;
import android.util.Log;
public class WearReplyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
ApplicationLoader.postInitApplication();
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput == null) {
return;
@@ -28,7 +28,6 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import com.b44t.messenger.FileLog;
import com.b44t.ui.LaunchActivity;
public class Browser {
@@ -55,7 +54,7 @@ public class Browser {
intent.putExtra(android.provider.Browser.EXTRA_APPLICATION_ID, context.getPackageName());
context.startActivity(intent);
} catch (Exception e) {
FileLog.e("messenger", e);
}
}
@@ -1,86 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
/**
* A {@link FrameLayout} that resizes itself to match a specified aspect ratio.
*/
public final class AspectRatioFrameLayout extends FrameLayout {
/**
* The {@link FrameLayout} will not resize itself if the fractional difference between its natural
* aspect ratio and the requested aspect ratio falls below this threshold.
* <p>
* This tolerance allows the view to occupy the whole of the screen when the requested aspect
* ratio is very close, but not exactly equal to, the aspect ratio of the screen. This may reduce
* the number of view layers that need to be composited by the underlying system, which can help
* to reduce power consumption.
*/
private static final float MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f;
private float videoAspectRatio;
public AspectRatioFrameLayout(Context context) {
super(context);
}
public AspectRatioFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Set the aspect ratio that this view should satisfy.
*
* @param widthHeightRatio The width to height ratio.
*/
public void setAspectRatio(float widthHeightRatio) {
if (this.videoAspectRatio != widthHeightRatio) {
this.videoAspectRatio = widthHeightRatio;
requestLayout();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (videoAspectRatio == 0) {
// Aspect ratio not set.
return;
}
int width = getMeasuredWidth();
int height = getMeasuredHeight();
float viewAspectRatio = (float) width / height;
float aspectDeformation = videoAspectRatio / viewAspectRatio - 1;
if (Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION) {
// We're within the allowed tolerance.
return;
}
if (aspectDeformation > 0) {
height = (int) (width / videoAspectRatio);
} else {
width = (int) (height * videoAspectRatio);
}
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
}
}
@@ -1,33 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import java.io.IOException;
/**
* Thrown when a live playback falls behind the available media window.
*/
public final class BehindLiveWindowException extends IOException {
public BehindLiveWindowException() {
super();
}
public BehindLiveWindowException(String message) {
super(message);
}
}
@@ -1,146 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.util.Util;
import android.media.AudioFormat;
import android.media.MediaCodec;
import android.media.MediaExtractor;
/**
* Defines constants that are generally useful throughout the library.
*/
public final class C {
/**
* Represents an unknown microsecond time or duration.
*/
public static final long UNKNOWN_TIME_US = -1L;
/**
* Represents a microsecond duration whose exact value is unknown, but which should match the
* longest of some other known durations.
*/
public static final long MATCH_LONGEST_US = -2L;
/**
* The number of microseconds in one second.
*/
public static final long MICROS_PER_SECOND = 1000000L;
/**
* Represents an unbounded length of data.
*/
public static final int LENGTH_UNBOUNDED = -1;
/**
* The name of the UTF-8 charset.
*/
public static final String UTF8_NAME = "UTF-8";
/**
* @see MediaCodec#CRYPTO_MODE_AES_CTR
*/
@SuppressWarnings("InlinedApi")
public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;
/**
* @see AudioFormat#ENCODING_INVALID
*/
public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID;
/**
* @see AudioFormat#ENCODING_PCM_8BIT
*/
public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT;
/**
* @see AudioFormat#ENCODING_PCM_16BIT
*/
public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT;
/**
* PCM encoding with 24 bits per sample.
*/
public static final int ENCODING_PCM_24BIT = 0x80000000;
/**
* PCM encoding with 32 bits per sample.
*/
public static final int ENCODING_PCM_32BIT = 0x40000000;
/**
* @see AudioFormat#ENCODING_AC3
*/
@SuppressWarnings("InlinedApi")
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
/**
* @see AudioFormat#ENCODING_E_AC3
*/
@SuppressWarnings("InlinedApi")
public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
/**
* @see AudioFormat#ENCODING_DTS
*/
@SuppressWarnings("InlinedApi")
public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS;
/**
* @see AudioFormat#ENCODING_DTS_HD
*/
@SuppressWarnings("InlinedApi")
public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD;
/**
* @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND
*/
@SuppressWarnings({"InlinedApi", "deprecation"})
public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23
? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
/**
* @see MediaExtractor#SAMPLE_FLAG_SYNC
*/
@SuppressWarnings("InlinedApi")
public static final int SAMPLE_FLAG_SYNC = MediaExtractor.SAMPLE_FLAG_SYNC;
/**
* @see MediaExtractor#SAMPLE_FLAG_ENCRYPTED
*/
@SuppressWarnings("InlinedApi")
public static final int SAMPLE_FLAG_ENCRYPTED = MediaExtractor.SAMPLE_FLAG_ENCRYPTED;
/**
* Indicates that a sample should be decoded but not rendered.
*/
public static final int SAMPLE_FLAG_DECODE_ONLY = 0x8000000;
/**
* A return value for methods where the end of an input was encountered.
*/
public static final int RESULT_END_OF_INPUT = -1;
/**
* A return value for methods where the length of parsed data exceeds the maximum length allowed.
*/
public static final int RESULT_MAX_LENGTH_EXCEEDED = -2;
private C() {}
}
@@ -1,62 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
/**
* Maintains codec event counts, for debugging purposes only.
* <p>
* Counters should be written from the playback thread only. Counters may be read from any thread.
* To ensure that the counter values are correctly reflected between threads, users of this class
* should invoke {@link #ensureUpdated()} prior to reading and after writing.
*/
public final class CodecCounters {
public int codecInitCount;
public int codecReleaseCount;
public int inputBufferCount;
public int outputFormatChangedCount;
public int outputBuffersChangedCount;
public int renderedOutputBufferCount;
public int skippedOutputBufferCount;
public int droppedOutputBufferCount;
public int maxConsecutiveDroppedOutputBufferCount;
/**
* Should be invoked from the playback thread after the counters have been updated. Should also
* be invoked from any other thread that wishes to read the counters, before reading. These calls
* ensure that counter updates are made visible to the reading threads.
*/
public synchronized void ensureUpdated() {
// Do nothing. The use of synchronized ensures a memory barrier should another thread also
// call this method.
}
public String getDebugString() {
ensureUpdated();
StringBuilder builder = new StringBuilder();
builder.append("cic:").append(codecInitCount);
builder.append(" crc:").append(codecReleaseCount);
builder.append(" ibc:").append(inputBufferCount);
builder.append(" ofc:").append(outputFormatChangedCount);
builder.append(" obc:").append(outputBuffersChangedCount);
builder.append(" ren:").append(renderedOutputBufferCount);
builder.append(" sob:").append(skippedOutputBufferCount);
builder.append(" dob:").append(droppedOutputBufferCount);
builder.append(" mcdob:").append(maxConsecutiveDroppedOutputBufferCount);
return builder.toString();
}
}
@@ -1,116 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.util.Util;
import android.annotation.TargetApi;
import android.media.MediaExtractor;
/**
* Compatibility wrapper around {@link android.media.MediaCodec.CryptoInfo}.
*/
public final class CryptoInfo {
/**
* @see android.media.MediaCodec.CryptoInfo#iv
*/
public byte[] iv;
/**
* @see android.media.MediaCodec.CryptoInfo#key
*/
public byte[] key;
/**
* @see android.media.MediaCodec.CryptoInfo#mode
*/
public int mode;
/**
* @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData
*/
public int[] numBytesOfClearData;
/**
* @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData
*/
public int[] numBytesOfEncryptedData;
/**
* @see android.media.MediaCodec.CryptoInfo#numSubSamples
*/
public int numSubSamples;
private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo;
public CryptoInfo() {
frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : null;
}
/**
* @see android.media.MediaCodec.CryptoInfo#set(int, int[], int[], byte[], byte[], int)
*/
public void set(int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData,
byte[] key, byte[] iv, int mode) {
this.numSubSamples = numSubSamples;
this.numBytesOfClearData = numBytesOfClearData;
this.numBytesOfEncryptedData = numBytesOfEncryptedData;
this.key = key;
this.iv = iv;
this.mode = mode;
if (Util.SDK_INT >= 16) {
updateFrameworkCryptoInfoV16();
}
}
/**
* Equivalent to {@link MediaExtractor#getSampleCryptoInfo(android.media.MediaCodec.CryptoInfo)}.
*
* @param extractor The extractor from which to retrieve the crypto information.
*/
@TargetApi(16)
public void setFromExtractorV16(MediaExtractor extractor) {
extractor.getSampleCryptoInfo(frameworkCryptoInfo);
numSubSamples = frameworkCryptoInfo.numSubSamples;
numBytesOfClearData = frameworkCryptoInfo.numBytesOfClearData;
numBytesOfEncryptedData = frameworkCryptoInfo.numBytesOfEncryptedData;
key = frameworkCryptoInfo.key;
iv = frameworkCryptoInfo.iv;
mode = frameworkCryptoInfo.mode;
}
/**
* Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
* <p>
* Successive calls to this method on a single {@link CryptoInfo} will return the same instance.
* Changes to the {@link CryptoInfo} will be reflected in the returned object. The return object
* should not be modified directly.
*
* @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
*/
@TargetApi(16)
public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() {
return frameworkCryptoInfo;
}
@TargetApi(16)
private android.media.MediaCodec.CryptoInfo newFrameworkCryptoInfoV16() {
return new android.media.MediaCodec.CryptoInfo();
}
@TargetApi(16)
private void updateFrameworkCryptoInfoV16() {
frameworkCryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, key, iv,
mode);
}
}
@@ -1,69 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.util.Util;
import android.annotation.TargetApi;
import android.media.MediaCodecInfo.CodecCapabilities;
/**
* Contains information about a media decoder.
*/
@TargetApi(16)
public final class DecoderInfo {
/**
* The name of the decoder.
* <p>
* May be passed to {@link android.media.MediaCodec#createByCodecName(String)} to create an
* instance of the decoder.
*/
public final String name;
/**
* {@link CodecCapabilities} for this decoder.
*/
public final CodecCapabilities capabilities;
/**
* Whether the decoder supports seamless resolution switches.
*
* @see android.media.MediaCodecInfo.CodecCapabilities#isFeatureSupported(String)
* @see android.media.MediaCodecInfo.CodecCapabilities#FEATURE_AdaptivePlayback
*/
public final boolean adaptive;
/**
* @param name The name of the decoder.
* @param capabilities {@link CodecCapabilities} of the decoder.
*/
/* package */ DecoderInfo(String name, CodecCapabilities capabilities) {
this.name = name;
this.capabilities = capabilities;
this.adaptive = isAdaptive(capabilities);
}
private static boolean isAdaptive(CodecCapabilities capabilities) {
return capabilities != null && Util.SDK_INT >= 19 && isAdaptiveV19(capabilities);
}
@TargetApi(19)
private static boolean isAdaptiveV19(CodecCapabilities capabilities) {
return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback);
}
}
@@ -1,278 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.upstream.Allocator;
import com.b44t.messenger.exoplayer.upstream.NetworkLock;
import android.os.Handler;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* A {@link LoadControl} implementation that allows loads to continue in a sequence that prevents
* any loader from getting too far ahead or behind any of the other loaders.
* <p>
* Loads are scheduled so as to fill the available buffer space as rapidly as possible. Once the
* duration of buffered media and the buffer utilization both exceed respective thresholds, the
* control switches to a draining state during which no loads are permitted to start. During
* draining periods, resources such as the device radio have an opportunity to switch into low
* power modes. The control reverts back to the loading state when either the duration of buffered
* media or the buffer utilization fall below respective thresholds.
* <p>
* This implementation of {@link LoadControl} integrates with {@link NetworkLock}, by registering
* itself as a task with priority {@link NetworkLock#STREAMING_PRIORITY} during loading periods,
* and unregistering itself during draining periods.
*/
public final class DefaultLoadControl implements LoadControl {
/**
* Interface definition for a callback to be notified of {@link DefaultLoadControl} events.
*/
public interface EventListener {
/**
* Invoked when the control transitions from a loading to a draining state, or vice versa.
*
* @param loading Whether the control is now in a loading state.
*/
void onLoadingChanged(boolean loading);
}
public static final int DEFAULT_LOW_WATERMARK_MS = 15000;
public static final int DEFAULT_HIGH_WATERMARK_MS = 30000;
public static final float DEFAULT_LOW_BUFFER_LOAD = 0.2f;
public static final float DEFAULT_HIGH_BUFFER_LOAD = 0.8f;
private static final int ABOVE_HIGH_WATERMARK = 0;
private static final int BETWEEN_WATERMARKS = 1;
private static final int BELOW_LOW_WATERMARK = 2;
private final Allocator allocator;
private final List<Object> loaders;
private final HashMap<Object, LoaderState> loaderStates;
private final Handler eventHandler;
private final EventListener eventListener;
private final long lowWatermarkUs;
private final long highWatermarkUs;
private final float lowBufferLoad;
private final float highBufferLoad;
private int targetBufferSize;
private long maxLoadStartPositionUs;
private int bufferState;
private boolean fillingBuffers;
private boolean streamingPrioritySet;
/**
* Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
*
* @param allocator The {@link Allocator} used by the loader.
*/
public DefaultLoadControl(Allocator allocator) {
this(allocator, null, null);
}
/**
* Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
*
* @param allocator The {@link Allocator} used by the loader.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
*/
public DefaultLoadControl(Allocator allocator, Handler eventHandler,
EventListener eventListener) {
this(allocator, eventHandler, eventListener, DEFAULT_LOW_WATERMARK_MS,
DEFAULT_HIGH_WATERMARK_MS, DEFAULT_LOW_BUFFER_LOAD, DEFAULT_HIGH_BUFFER_LOAD);
}
/**
* Constructs a new instance.
*
* @param allocator The {@link Allocator} used by the loader.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param lowWatermarkMs The minimum duration of media that can be buffered for the control to
* be in the draining state. If less media is buffered, then the control will transition to
* the filling state.
* @param highWatermarkMs The minimum duration of media that can be buffered for the control to
* transition from filling to draining.
* @param lowBufferLoad The minimum fraction of the buffer that must be utilized for the control
* to be in the draining state. If the utilization is lower, then the control will transition
* to the filling state.
* @param highBufferLoad The minimum fraction of the buffer that must be utilized for the control
* to transition from the loading state to the draining state.
*/
public DefaultLoadControl(Allocator allocator, Handler eventHandler, EventListener eventListener,
int lowWatermarkMs, int highWatermarkMs, float lowBufferLoad, float highBufferLoad) {
this.allocator = allocator;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
this.loaders = new ArrayList<>();
this.loaderStates = new HashMap<>();
this.lowWatermarkUs = lowWatermarkMs * 1000L;
this.highWatermarkUs = highWatermarkMs * 1000L;
this.lowBufferLoad = lowBufferLoad;
this.highBufferLoad = highBufferLoad;
}
@Override
public void register(Object loader, int bufferSizeContribution) {
loaders.add(loader);
loaderStates.put(loader, new LoaderState(bufferSizeContribution));
targetBufferSize += bufferSizeContribution;
}
@Override
public void unregister(Object loader) {
loaders.remove(loader);
LoaderState state = loaderStates.remove(loader);
targetBufferSize -= state.bufferSizeContribution;
updateControlState();
}
@Override
public void trimAllocator() {
allocator.trim(targetBufferSize);
}
@Override
public Allocator getAllocator() {
return allocator;
}
@Override
public boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs,
boolean loading) {
// Update the loader state.
int loaderBufferState = getLoaderBufferState(playbackPositionUs, nextLoadPositionUs);
LoaderState loaderState = loaderStates.get(loader);
boolean loaderStateChanged = loaderState.bufferState != loaderBufferState
|| loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading;
if (loaderStateChanged) {
loaderState.bufferState = loaderBufferState;
loaderState.nextLoadPositionUs = nextLoadPositionUs;
loaderState.loading = loading;
}
// Update the buffer state.
int currentBufferSize = allocator.getTotalBytesAllocated();
int bufferState = getBufferState(currentBufferSize);
boolean bufferStateChanged = this.bufferState != bufferState;
if (bufferStateChanged) {
this.bufferState = bufferState;
}
// If either of the individual states have changed, update the shared control state.
if (loaderStateChanged || bufferStateChanged) {
updateControlState();
}
return currentBufferSize < targetBufferSize && nextLoadPositionUs != -1
&& nextLoadPositionUs <= maxLoadStartPositionUs;
}
private int getLoaderBufferState(long playbackPositionUs, long nextLoadPositionUs) {
if (nextLoadPositionUs == -1) {
return ABOVE_HIGH_WATERMARK;
} else {
long timeUntilNextLoadPosition = nextLoadPositionUs - playbackPositionUs;
return timeUntilNextLoadPosition > highWatermarkUs ? ABOVE_HIGH_WATERMARK :
timeUntilNextLoadPosition < lowWatermarkUs ? BELOW_LOW_WATERMARK :
BETWEEN_WATERMARKS;
}
}
private int getBufferState(int currentBufferSize) {
float bufferLoad = (float) currentBufferSize / targetBufferSize;
return bufferLoad > highBufferLoad ? ABOVE_HIGH_WATERMARK
: bufferLoad < lowBufferLoad ? BELOW_LOW_WATERMARK
: BETWEEN_WATERMARKS;
}
private void updateControlState() {
boolean loading = false;
boolean haveNextLoadPosition = false;
int highestState = bufferState;
for (int i = 0; i < loaders.size(); i++) {
LoaderState loaderState = loaderStates.get(loaders.get(i));
loading |= loaderState.loading;
haveNextLoadPosition |= loaderState.nextLoadPositionUs != -1;
highestState = Math.max(highestState, loaderState.bufferState);
}
fillingBuffers = !loaders.isEmpty() && (loading || haveNextLoadPosition)
&& (highestState == BELOW_LOW_WATERMARK
|| (highestState == BETWEEN_WATERMARKS && fillingBuffers));
if (fillingBuffers && !streamingPrioritySet) {
NetworkLock.instance.add(NetworkLock.STREAMING_PRIORITY);
streamingPrioritySet = true;
notifyLoadingChanged(true);
} else if (!fillingBuffers && streamingPrioritySet && !loading) {
NetworkLock.instance.remove(NetworkLock.STREAMING_PRIORITY);
streamingPrioritySet = false;
notifyLoadingChanged(false);
}
maxLoadStartPositionUs = -1;
if (fillingBuffers) {
for (int i = 0; i < loaders.size(); i++) {
Object loader = loaders.get(i);
LoaderState loaderState = loaderStates.get(loader);
long loaderTime = loaderState.nextLoadPositionUs;
if (loaderTime != -1
&& (maxLoadStartPositionUs == -1 || loaderTime < maxLoadStartPositionUs)) {
maxLoadStartPositionUs = loaderTime;
}
}
}
}
private void notifyLoadingChanged(final boolean loading) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onLoadingChanged(loading);
}
});
}
}
private static class LoaderState {
public final int bufferSizeContribution;
public int bufferState;
public boolean loading;
public long nextLoadPositionUs;
public LoaderState(int bufferSizeContribution) {
this.bufferSizeContribution = bufferSizeContribution;
bufferState = ABOVE_HIGH_WATERMARK;
loading = false;
nextLoadPositionUs = -1;
}
}
}
@@ -1,77 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
/**
* A {@link TrackRenderer} that does nothing.
* <p>
* This renderer returns 0 from {@link #getTrackCount()} in order to request that it should be
* ignored. {@link IllegalStateException} is thrown from all other methods documented to indicate
* that they should not be invoked unless the renderer is prepared.
*/
public final class DummyTrackRenderer extends TrackRenderer {
@Override
protected boolean doPrepare(long positionUs) throws ExoPlaybackException {
return true;
}
@Override
protected int getTrackCount() {
return 0;
}
@Override
protected MediaFormat getFormat(int track) {
throw new IllegalStateException();
}
@Override
protected boolean isEnded() {
throw new IllegalStateException();
}
@Override
protected boolean isReady() {
throw new IllegalStateException();
}
@Override
protected void seekTo(long positionUs) {
throw new IllegalStateException();
}
@Override
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) {
throw new IllegalStateException();
}
@Override
protected void maybeThrowError() {
throw new IllegalStateException();
}
@Override
protected long getDurationUs() {
throw new IllegalStateException();
}
@Override
protected long getBufferedPositionUs() {
throw new IllegalStateException();
}
}
@@ -1,51 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
/**
* Thrown when a non-recoverable playback failure occurs.
* <p>
* Where possible, the cause returned by {@link #getCause()} will indicate the reason for failure.
*/
public final class ExoPlaybackException extends Exception {
/**
* True if the cause (i.e. the {@link Throwable} returned by {@link #getCause()}) was only caught
* by a fail-safe at the top level of the player. False otherwise.
*/
public final boolean caughtAtTopLevel;
public ExoPlaybackException(String message) {
super(message);
caughtAtTopLevel = false;
}
public ExoPlaybackException(Throwable cause) {
super(cause);
caughtAtTopLevel = false;
}
public ExoPlaybackException(String message, Throwable cause) {
super(message, cause);
caughtAtTopLevel = false;
}
/* package */ ExoPlaybackException(Throwable cause, boolean caughtAtTopLevel) {
super(cause);
this.caughtAtTopLevel = caughtAtTopLevel;
}
}
@@ -1,415 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import android.os.Looper;
/**
* An extensible media player exposing traditional high-level media player functionality, such as
* the ability to prepare, play, pause and seek.
*
* <p>Topics covered here are:
* <ol>
* <li><a href="#Assumptions">Assumptions and player composition</a>
* <li><a href="#Threading">Threading model</a>
* <li><a href="#State">Player state</a>
* </ol>
*
* <a name="Assumptions"></a>
* <h3>Assumptions and player construction</h3>
*
* <p>The implementation is designed to make no assumptions about (and hence impose no restrictions
* on) the type of the media being played, how and where it is stored, or how it is rendered.
* Rather than implementing the loading and rendering of media directly, {@link ExoPlayer} instead
* delegates this work to one or more {@link TrackRenderer}s, which are injected when the player
* is prepared. Hence {@link ExoPlayer} is capable of loading and playing any media for which a
* {@link TrackRenderer} implementation can be provided.
*
* <p>{@link MediaCodecAudioTrackRenderer} and {@link MediaCodecVideoTrackRenderer} can be used for
* the common cases of rendering audio and video. These components in turn require an
* <i>upstream</i> {@link SampleSource} to be injected through their constructors, where upstream
* is defined to denote a component that is closer to the source of the media. This pattern of
* upstream dependency injection is actively encouraged, since it means that the functionality of
* the player is built up through the composition of components that can easily be exchanged for
* alternate implementations. For example a {@link SampleSource} implementation may require a
* further upstream data loading component to be injected through its constructor, with different
* implementations enabling the loading of data from various sources.
*
* <a name="Threading"></a>
* <h3>Threading model</h3>
*
* <p>The figure below shows the {@link ExoPlayer} threading model.</p>
* <p align="center"><img src="../../../../../images/exoplayer_threading_model.png"
* alt="MediaPlayer state diagram"
* border="0"/></p>
*
* <ul>
* <li>It is recommended that instances are created and accessed from a single application thread.
* An application's main thread is ideal. Accessing an instance from multiple threads is
* discouraged, however if an application does wish to do this then it may do so provided that it
* ensures accesses are synchronized.
* </li>
* <li>Registered {@link Listener}s are invoked on the thread that created the {@link ExoPlayer}
* instance.</li>
* <li>An internal playback thread is responsible for managing playback and invoking the
* {@link TrackRenderer}s in order to load and play the media.</li>
* <li>{@link TrackRenderer} implementations (or any upstream components that they depend on) may
* use additional background threads (e.g. to load data). These are implementation specific.</li>
* </ul>
*
* <a name="State"></a>
* <h3>Player state</h3>
*
* <p>The components of an {@link ExoPlayer}'s state can be divided into two distinct groups. State
* accessed by {@link #getSelectedTrack(int)} and {@link #getPlayWhenReady()} is only ever
* changed by invoking the player's methods, and are never changed as a result of operations that
* have been performed asynchronously by the playback thread. In contrast, the playback state
* accessed by {@link #getPlaybackState()} is only ever changed as a result of operations
* completing on the playback thread, as illustrated below.</p>
* <p align="center"><img src="../../../../../images/exoplayer_state.png"
* alt="ExoPlayer state"
* border="0"/></p>
*
* <p>The possible playback state transitions are shown below. Transitions can be triggered either
* by changes in the state of the {@link TrackRenderer}s being used, or as a result of
* {@link #prepare(TrackRenderer[])}, {@link #stop()} or {@link #release()} being invoked.</p>
* <p align="center"><img src="../../../../../images/exoplayer_playbackstate.png"
* alt="ExoPlayer playback state transitions"
* border="0"/></p>
*/
public interface ExoPlayer {
/**
* A factory for instantiating ExoPlayer instances.
*/
public static final class Factory {
/**
* The default minimum duration of data that must be buffered for playback to start or resume
* following a user action such as a seek.
*/
public static final int DEFAULT_MIN_BUFFER_MS = 2500;
/**
* The default minimum duration of data that must be buffered for playback to resume
* after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and
* not due to a user action such as starting playback or seeking).
*/
public static final int DEFAULT_MIN_REBUFFER_MS = 5000;
private Factory() {}
/**
* Obtains an {@link ExoPlayer} instance.
* <p>
* Must be invoked from a thread that has an associated {@link Looper}.
*
* @param rendererCount The number of {@link TrackRenderer}s that will be passed to
* {@link #prepare(TrackRenderer[])}.
* @param minBufferMs A minimum duration of data that must be buffered for playback to start
* or resume following a user action such as a seek.
* @param minRebufferMs A minimum duration of data that must be buffered for playback to resume
* after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and
* not due to a user action such as starting playback or seeking).
*/
public static ExoPlayer newInstance(int rendererCount, int minBufferMs, int minRebufferMs) {
return new ExoPlayerImpl(rendererCount, minBufferMs, minRebufferMs);
}
/**
* Obtains an {@link ExoPlayer} instance.
* <p>
* Must be invoked from a thread that has an associated {@link Looper}.
*
* @param rendererCount The number of {@link TrackRenderer}s that will be passed to
* {@link #prepare(TrackRenderer[])}.
*/
public static ExoPlayer newInstance(int rendererCount) {
return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, DEFAULT_MIN_REBUFFER_MS);
}
}
/**
* Interface definition for a callback to be notified of changes in player state.
*/
public interface Listener {
/**
* Invoked when the value returned from either {@link ExoPlayer#getPlayWhenReady()} or
* {@link ExoPlayer#getPlaybackState()} changes.
*
* @param playWhenReady Whether playback will proceed when ready.
* @param playbackState One of the {@code STATE} constants defined in the {@link ExoPlayer}
* interface.
*/
void onPlayerStateChanged(boolean playWhenReady, int playbackState);
/**
* Invoked when the current value of {@link ExoPlayer#getPlayWhenReady()} has been reflected
* by the internal playback thread.
* <p>
* An invocation of this method will shortly follow any call to
* {@link ExoPlayer#setPlayWhenReady(boolean)} that changes the state. If multiple calls are
* made in rapid succession, then this method will be invoked only once, after the final state
* has been reflected.
*/
void onPlayWhenReadyCommitted();
/**
* Invoked when an error occurs. The playback state will transition to
* {@link ExoPlayer#STATE_IDLE} immediately after this method is invoked. The player instance
* can still be used, and {@link ExoPlayer#release()} must still be called on the player should
* it no longer be required.
*
* @param error The error.
*/
void onPlayerError(ExoPlaybackException error);
}
/**
* A component of an {@link ExoPlayer} that can receive messages on the playback thread.
* <p>
* Messages can be delivered to a component via {@link ExoPlayer#sendMessage} and
* {@link ExoPlayer#blockingSendMessage}.
*/
public interface ExoPlayerComponent {
/**
* Handles a message delivered to the component. Invoked on the playback thread.
*
* @param messageType An integer identifying the type of message.
* @param message The message object.
* @throws ExoPlaybackException If an error occurred whilst handling the message.
*/
void handleMessage(int messageType, Object message) throws ExoPlaybackException;
}
/**
* The player is neither prepared or being prepared.
*/
public static final int STATE_IDLE = 1;
/**
* The player is being prepared.
*/
public static final int STATE_PREPARING = 2;
/**
* The player is prepared but not able to immediately play from the current position. The cause
* is {@link TrackRenderer} specific, but this state typically occurs when more data needs
* to be buffered for playback to start.
*/
public static final int STATE_BUFFERING = 3;
/**
* The player is prepared and able to immediately play from the current position. The player will
* be playing if {@link #getPlayWhenReady()} returns true, and paused otherwise.
*/
public static final int STATE_READY = 4;
/**
* The player has finished playing the media.
*/
public static final int STATE_ENDED = 5;
/**
* A value that can be passed as the second argument to {@link #setSelectedTrack(int, int)} to
* disable the renderer.
*/
public static final int TRACK_DISABLED = -1;
/**
* A value that can be passed as the second argument to {@link #setSelectedTrack(int, int)} to
* select the default track.
*/
public static final int TRACK_DEFAULT = 0;
/**
* Represents an unknown time or duration.
*/
public static final long UNKNOWN_TIME = -1;
/**
* Gets the {@link Looper} associated with the playback thread.
*
* @return The {@link Looper} associated with the playback thread.
*/
public Looper getPlaybackLooper();
/**
* Register a listener to receive events from the player. The listener's methods will be invoked
* on the thread that was used to construct the player.
*
* @param listener The listener to register.
*/
public void addListener(Listener listener);
/**
* Unregister a listener. The listener will no longer receive events from the player.
*
* @param listener The listener to unregister.
*/
public void removeListener(Listener listener);
/**
* Returns the current state of the player.
*
* @return One of the {@code STATE} constants defined in this interface.
*/
public int getPlaybackState();
/**
* Prepares the player for playback.
*
* @param renderers The {@link TrackRenderer}s to use. The number of renderers must match the
* value that was passed to the {@link ExoPlayer.Factory#newInstance} method.
*/
public void prepare(TrackRenderer... renderers);
/**
* Returns the number of tracks exposed by the specified renderer.
*
* @param rendererIndex The index of the renderer.
* @return The number of tracks.
*/
public int getTrackCount(int rendererIndex);
/**
* Returns the format of a track.
*
* @param rendererIndex The index of the renderer.
* @param trackIndex The index of the track.
* @return The format of the track.
*/
public MediaFormat getTrackFormat(int rendererIndex, int trackIndex);
/**
* Selects a track for the specified renderer.
*
* @param rendererIndex The index of the renderer.
* @param trackIndex The index of the track. A negative value or a value greater than or equal to
* the renderer's track count will disable the renderer.
*/
public void setSelectedTrack(int rendererIndex, int trackIndex);
/**
* Returns the index of the currently selected track for the specified renderer.
*
* @param rendererIndex The index of the renderer.
* @return The selected track. A negative value or a value greater than or equal to the renderer's
* track count indicates that the renderer is disabled.
*/
public int getSelectedTrack(int rendererIndex);
/**
* Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
* If the player is already in this state, then this method can be used to pause and resume
* playback.
*
* @param playWhenReady Whether playback should proceed when ready.
*/
public void setPlayWhenReady(boolean playWhenReady);
/**
* Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
*
* @return Whether playback will proceed when ready.
*/
public boolean getPlayWhenReady();
/**
* Whether the current value of {@link ExoPlayer#getPlayWhenReady()} has been reflected by the
* internal playback thread.
*
* @return True if the current value has been reflected. False otherwise.
*/
public boolean isPlayWhenReadyCommitted();
/**
* Seeks to a position specified in milliseconds.
*
* @param positionMs The seek position.
*/
public void seekTo(long positionMs);
/**
* Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
* is to pause playback.
* <p>
* Calling this method will cause the playback state to transition to
* {@link ExoPlayer#STATE_IDLE}. The player instance can still be used, and
* {@link ExoPlayer#release()} must still be called on the player if it's no longer required.
* <p>
* Calling this method does not reset the playback position. If this player instance will be used
* to play another video from its start, then {@code seekTo(0)} should be called after stopping
* the player and before preparing it for the next video.
*/
public void stop();
/**
* Releases the player. This method must be called when the player is no longer required.
* <p>
* The player must not be used after calling this method.
*/
public void release();
/**
* Sends a message to a specified component. The message is delivered to the component on the
* playback thread. If the component throws a {@link ExoPlaybackException}, then it is
* propagated out of the player as an error.
*
* @param target The target to which the message should be delivered.
* @param messageType An integer that can be used to identify the type of the message.
* @param message The message object.
*/
public void sendMessage(ExoPlayerComponent target, int messageType, Object message);
/**
* Blocking variant of {@link #sendMessage(ExoPlayerComponent, int, Object)} that does not return
* until after the message has been delivered.
*
* @param target The target to which the message should be delivered.
* @param messageType An integer that can be used to identify the type of the message.
* @param message The message object.
*/
public void blockingSendMessage(ExoPlayerComponent target, int messageType, Object message);
/**
* Gets the duration of the track in milliseconds.
*
* @return The duration of the track in milliseconds, or {@link ExoPlayer#UNKNOWN_TIME} if the
* duration is not known.
*/
public long getDuration();
/**
* Gets the current playback position in milliseconds.
*
* @return The current playback position in milliseconds.
*/
public long getCurrentPosition();
/**
* Gets an estimate of the absolute position in milliseconds up to which data is buffered.
*
* @return An estimate of the absolute position in milliseconds up to which data is buffered,
* or {@link ExoPlayer#UNKNOWN_TIME} if no estimate is available.
*/
public long getBufferedPosition();
/**
* Gets an estimate of the percentage into the media up to which data is buffered.
*
* @return An estimate of the percentage into the media up to which data is buffered. 0 if the
* duration of the media is not known or if no estimate is available.
*/
public int getBufferedPercentage();
}
@@ -1,230 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import java.util.Arrays;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Concrete implementation of {@link ExoPlayer}.
*/
/* package */ final class ExoPlayerImpl implements ExoPlayer {
private static final String TAG = "ExoPlayerImpl";
private final Handler eventHandler;
private final ExoPlayerImplInternal internalPlayer;
private final CopyOnWriteArraySet<Listener> listeners;
private final MediaFormat[][] trackFormats;
private final int[] selectedTrackIndices;
private boolean playWhenReady;
private int playbackState;
private int pendingPlayWhenReadyAcks;
/**
* Constructs an instance. Must be invoked from a thread that has an associated {@link Looper}.
*
* @param rendererCount The number of {@link TrackRenderer}s that will be passed to
* {@link #prepare(TrackRenderer[])}.
* @param minBufferMs A minimum duration of data that must be buffered for playback to start
* or resume following a user action such as a seek.
* @param minRebufferMs A minimum duration of data that must be buffered for playback to resume
* after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and
* not due to a user action such as starting playback or seeking).
*/
@SuppressLint("HandlerLeak")
public ExoPlayerImpl(int rendererCount, int minBufferMs, int minRebufferMs) {
Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION);
this.playWhenReady = false;
this.playbackState = STATE_IDLE;
this.listeners = new CopyOnWriteArraySet<>();
this.trackFormats = new MediaFormat[rendererCount][];
this.selectedTrackIndices = new int[rendererCount];
eventHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
ExoPlayerImpl.this.handleEvent(msg);
}
};
internalPlayer = new ExoPlayerImplInternal(eventHandler, playWhenReady, selectedTrackIndices,
minBufferMs, minRebufferMs);
}
@Override
public Looper getPlaybackLooper() {
return internalPlayer.getPlaybackLooper();
}
@Override
public void addListener(Listener listener) {
listeners.add(listener);
}
@Override
public void removeListener(Listener listener) {
listeners.remove(listener);
}
@Override
public int getPlaybackState() {
return playbackState;
}
@Override
public void prepare(TrackRenderer... renderers) {
Arrays.fill(trackFormats, null);
internalPlayer.prepare(renderers);
}
@Override
public int getTrackCount(int rendererIndex) {
return trackFormats[rendererIndex] != null ? trackFormats[rendererIndex].length : 0;
}
@Override
public MediaFormat getTrackFormat(int rendererIndex, int trackIndex) {
return trackFormats[rendererIndex][trackIndex];
}
@Override
public void setSelectedTrack(int rendererIndex, int trackIndex) {
if (selectedTrackIndices[rendererIndex] != trackIndex) {
selectedTrackIndices[rendererIndex] = trackIndex;
internalPlayer.setRendererSelectedTrack(rendererIndex, trackIndex);
}
}
@Override
public int getSelectedTrack(int rendererIndex) {
return selectedTrackIndices[rendererIndex];
}
@Override
public void setPlayWhenReady(boolean playWhenReady) {
if (this.playWhenReady != playWhenReady) {
this.playWhenReady = playWhenReady;
pendingPlayWhenReadyAcks++;
internalPlayer.setPlayWhenReady(playWhenReady);
for (Listener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, playbackState);
}
}
}
@Override
public boolean getPlayWhenReady() {
return playWhenReady;
}
@Override
public boolean isPlayWhenReadyCommitted() {
return pendingPlayWhenReadyAcks == 0;
}
@Override
public void seekTo(long positionMs) {
internalPlayer.seekTo(positionMs);
}
@Override
public void stop() {
internalPlayer.stop();
}
@Override
public void release() {
internalPlayer.release();
eventHandler.removeCallbacksAndMessages(null);
}
@Override
public void sendMessage(ExoPlayerComponent target, int messageType, Object message) {
internalPlayer.sendMessage(target, messageType, message);
}
@Override
public void blockingSendMessage(ExoPlayerComponent target, int messageType, Object message) {
internalPlayer.blockingSendMessage(target, messageType, message);
}
@Override
public long getDuration() {
return internalPlayer.getDuration();
}
@Override
public long getCurrentPosition() {
return internalPlayer.getCurrentPosition();
}
@Override
public long getBufferedPosition() {
return internalPlayer.getBufferedPosition();
}
@Override
public int getBufferedPercentage() {
long bufferedPosition = getBufferedPosition();
long duration = getDuration();
return bufferedPosition == ExoPlayer.UNKNOWN_TIME || duration == ExoPlayer.UNKNOWN_TIME ? 0
: (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
}
// Not private so it can be called from an inner class without going through a thunk method.
/* package */ void handleEvent(Message msg) {
switch (msg.what) {
case ExoPlayerImplInternal.MSG_PREPARED: {
System.arraycopy(msg.obj, 0, trackFormats, 0, trackFormats.length);
playbackState = msg.arg1;
for (Listener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, playbackState);
}
break;
}
case ExoPlayerImplInternal.MSG_STATE_CHANGED: {
playbackState = msg.arg1;
for (Listener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, playbackState);
}
break;
}
case ExoPlayerImplInternal.MSG_SET_PLAY_WHEN_READY_ACK: {
pendingPlayWhenReadyAcks--;
if (pendingPlayWhenReadyAcks == 0) {
for (Listener listener : listeners) {
listener.onPlayWhenReadyCommitted();
}
}
break;
}
case ExoPlayerImplInternal.MSG_ERROR: {
ExoPlaybackException exception = (ExoPlaybackException) msg.obj;
for (Listener listener : listeners) {
listener.onPlayerError(exception);
}
break;
}
}
}
}
@@ -1,668 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.ExoPlayer.ExoPlayerComponent;
import com.b44t.messenger.exoplayer.util.Assertions;
import com.b44t.messenger.exoplayer.util.PriorityHandlerThread;
import com.b44t.messenger.exoplayer.util.TraceUtil;
import com.b44t.messenger.exoplayer.util.Util;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.SystemClock;
import android.util.Log;
import android.util.Pair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Implements the internal behavior of {@link ExoPlayerImpl}.
*/
/* package */ final class ExoPlayerImplInternal implements Handler.Callback {
private static final String TAG = "ExoPlayerImplInternal";
// External messages
public static final int MSG_PREPARED = 1;
public static final int MSG_STATE_CHANGED = 2;
public static final int MSG_SET_PLAY_WHEN_READY_ACK = 3;
public static final int MSG_ERROR = 4;
// Internal messages
private static final int MSG_PREPARE = 1;
private static final int MSG_INCREMENTAL_PREPARE = 2;
private static final int MSG_SET_PLAY_WHEN_READY = 3;
private static final int MSG_STOP = 4;
private static final int MSG_RELEASE = 5;
private static final int MSG_SEEK_TO = 6;
private static final int MSG_DO_SOME_WORK = 7;
private static final int MSG_SET_RENDERER_SELECTED_TRACK = 8;
private static final int MSG_CUSTOM = 9;
private static final int PREPARE_INTERVAL_MS = 10;
private static final int RENDERING_INTERVAL_MS = 10;
private static final int IDLE_INTERVAL_MS = 1000;
private final Handler handler;
private final HandlerThread internalPlaybackThread;
private final Handler eventHandler;
private final StandaloneMediaClock standaloneMediaClock;
private final AtomicInteger pendingSeekCount;
private final List<TrackRenderer> enabledRenderers;
private final MediaFormat[][] trackFormats;
private final int[] selectedTrackIndices;
private final long minBufferUs;
private final long minRebufferUs;
private TrackRenderer[] renderers;
private TrackRenderer rendererMediaClockSource;
private MediaClock rendererMediaClock;
private boolean released;
private boolean playWhenReady;
private boolean rebuffering;
private int state;
private int customMessagesSent = 0;
private int customMessagesProcessed = 0;
private long lastSeekPositionMs;
private long elapsedRealtimeUs;
private volatile long durationUs;
private volatile long positionUs;
private volatile long bufferedPositionUs;
public ExoPlayerImplInternal(Handler eventHandler, boolean playWhenReady,
int[] selectedTrackIndices, int minBufferMs, int minRebufferMs) {
this.eventHandler = eventHandler;
this.playWhenReady = playWhenReady;
this.minBufferUs = minBufferMs * 1000L;
this.minRebufferUs = minRebufferMs * 1000L;
this.selectedTrackIndices = Arrays.copyOf(selectedTrackIndices, selectedTrackIndices.length);
this.state = ExoPlayer.STATE_IDLE;
this.durationUs = TrackRenderer.UNKNOWN_TIME_US;
this.bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US;
standaloneMediaClock = new StandaloneMediaClock();
pendingSeekCount = new AtomicInteger();
enabledRenderers = new ArrayList<>(selectedTrackIndices.length);
trackFormats = new MediaFormat[selectedTrackIndices.length][];
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
// not normally change to this priority" is incorrect.
internalPlaybackThread = new PriorityHandlerThread("ExoPlayerImplInternal:Handler",
Process.THREAD_PRIORITY_AUDIO);
internalPlaybackThread.start();
handler = new Handler(internalPlaybackThread.getLooper(), this);
}
public Looper getPlaybackLooper() {
return internalPlaybackThread.getLooper();
}
public long getCurrentPosition() {
return pendingSeekCount.get() > 0 ? lastSeekPositionMs : (positionUs / 1000);
}
public long getBufferedPosition() {
return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME
: bufferedPositionUs / 1000;
}
public long getDuration() {
return durationUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME
: durationUs / 1000;
}
public void prepare(TrackRenderer... renderers) {
handler.obtainMessage(MSG_PREPARE, renderers).sendToTarget();
}
public void setPlayWhenReady(boolean playWhenReady) {
handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget();
}
public void seekTo(long positionMs) {
lastSeekPositionMs = positionMs;
pendingSeekCount.incrementAndGet();
handler.obtainMessage(MSG_SEEK_TO, Util.getTopInt(positionMs),
Util.getBottomInt(positionMs)).sendToTarget();
}
public void stop() {
handler.sendEmptyMessage(MSG_STOP);
}
public void setRendererSelectedTrack(int rendererIndex, int trackIndex) {
handler.obtainMessage(MSG_SET_RENDERER_SELECTED_TRACK, rendererIndex, trackIndex)
.sendToTarget();
}
public void sendMessage(ExoPlayerComponent target, int messageType, Object message) {
customMessagesSent++;
handler.obtainMessage(MSG_CUSTOM, messageType, 0, Pair.create(target, message)).sendToTarget();
}
public synchronized void blockingSendMessage(ExoPlayerComponent target, int messageType,
Object message) {
if (released) {
Log.w(TAG, "Sent message(" + messageType + ") after release. Message ignored.");
return;
}
int messageNumber = customMessagesSent++;
handler.obtainMessage(MSG_CUSTOM, messageType, 0, Pair.create(target, message)).sendToTarget();
while (customMessagesProcessed <= messageNumber) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public synchronized void release() {
if (released) {
return;
}
handler.sendEmptyMessage(MSG_RELEASE);
while (!released) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
internalPlaybackThread.quit();
}
@Override
public boolean handleMessage(Message msg) {
try {
switch (msg.what) {
case MSG_PREPARE: {
prepareInternal((TrackRenderer[]) msg.obj);
return true;
}
case MSG_INCREMENTAL_PREPARE: {
incrementalPrepareInternal();
return true;
}
case MSG_SET_PLAY_WHEN_READY: {
setPlayWhenReadyInternal(msg.arg1 != 0);
return true;
}
case MSG_DO_SOME_WORK: {
doSomeWork();
return true;
}
case MSG_SEEK_TO: {
seekToInternal(Util.getLong(msg.arg1, msg.arg2));
return true;
}
case MSG_STOP: {
stopInternal();
return true;
}
case MSG_RELEASE: {
releaseInternal();
return true;
}
case MSG_CUSTOM: {
sendMessageInternal(msg.arg1, msg.obj);
return true;
}
case MSG_SET_RENDERER_SELECTED_TRACK: {
setRendererSelectedTrackInternal(msg.arg1, msg.arg2);
return true;
}
default:
return false;
}
} catch (ExoPlaybackException e) {
Log.e(TAG, "Internal track renderer error.", e);
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
stopInternal();
return true;
} catch (RuntimeException e) {
Log.e(TAG, "Internal runtime error.", e);
eventHandler.obtainMessage(MSG_ERROR, new ExoPlaybackException(e, true)).sendToTarget();
stopInternal();
return true;
}
}
private void setState(int state) {
if (this.state != state) {
this.state = state;
eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget();
}
}
private void prepareInternal(TrackRenderer[] renderers) throws ExoPlaybackException {
resetInternal();
this.renderers = renderers;
Arrays.fill(trackFormats, null);
setState(ExoPlayer.STATE_PREPARING);
incrementalPrepareInternal();
}
private void incrementalPrepareInternal() throws ExoPlaybackException {
long operationStartTimeMs = SystemClock.elapsedRealtime();
boolean prepared = true;
for (int rendererIndex = 0; rendererIndex < renderers.length; rendererIndex++) {
TrackRenderer renderer = renderers[rendererIndex];
if (renderer.getState() == TrackRenderer.STATE_UNPREPARED) {
int state = renderer.prepare(positionUs);
if (state == TrackRenderer.STATE_UNPREPARED) {
renderer.maybeThrowError();
prepared = false;
}
}
}
if (!prepared) {
// We're still waiting for some sources to be prepared.
scheduleNextOperation(MSG_INCREMENTAL_PREPARE, operationStartTimeMs, PREPARE_INTERVAL_MS);
return;
}
long durationUs = 0;
boolean allRenderersEnded = true;
boolean allRenderersReadyOrEnded = true;
for (int rendererIndex = 0; rendererIndex < renderers.length; rendererIndex++) {
TrackRenderer renderer = renderers[rendererIndex];
int rendererTrackCount = renderer.getTrackCount();
MediaFormat[] rendererTrackFormats = new MediaFormat[rendererTrackCount];
for (int trackIndex = 0; trackIndex < rendererTrackCount; trackIndex++) {
rendererTrackFormats[trackIndex] = renderer.getFormat(trackIndex);
}
trackFormats[rendererIndex] = rendererTrackFormats;
if (rendererTrackCount > 0) {
if (durationUs == TrackRenderer.UNKNOWN_TIME_US) {
// We've already encountered a track for which the duration is unknown, so the media
// duration is unknown regardless of the duration of this track.
} else {
long trackDurationUs = renderer.getDurationUs();
if (trackDurationUs == TrackRenderer.UNKNOWN_TIME_US) {
durationUs = TrackRenderer.UNKNOWN_TIME_US;
} else if (trackDurationUs == TrackRenderer.MATCH_LONGEST_US) {
// Do nothing.
} else {
durationUs = Math.max(durationUs, trackDurationUs);
}
}
int trackIndex = selectedTrackIndices[rendererIndex];
if (0 <= trackIndex && trackIndex < rendererTrackFormats.length) {
enableRenderer(renderer, trackIndex, false);
allRenderersEnded = allRenderersEnded && renderer.isEnded();
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
}
}
}
this.durationUs = durationUs;
if (allRenderersEnded
&& (durationUs == TrackRenderer.UNKNOWN_TIME_US || durationUs <= positionUs)) {
// We don't expect this case, but handle it anyway.
state = ExoPlayer.STATE_ENDED;
} else {
state = allRenderersReadyOrEnded ? ExoPlayer.STATE_READY : ExoPlayer.STATE_BUFFERING;
}
// Fire an event indicating that the player has been prepared, passing the initial state and
// renderer track information.
eventHandler.obtainMessage(MSG_PREPARED, state, 0, trackFormats).sendToTarget();
// Start the renderers if required, and schedule the first piece of work.
if (playWhenReady && state == ExoPlayer.STATE_READY) {
startRenderers();
}
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
private void enableRenderer(TrackRenderer renderer, int trackIndex, boolean joining)
throws ExoPlaybackException {
renderer.enable(trackIndex, positionUs, joining);
enabledRenderers.add(renderer);
MediaClock mediaClock = renderer.getMediaClock();
if (mediaClock != null) {
Assertions.checkState(rendererMediaClock == null);
rendererMediaClock = mediaClock;
rendererMediaClockSource = renderer;
}
}
private boolean rendererReadyOrEnded(TrackRenderer renderer) {
if (renderer.isEnded()) {
return true;
}
if (!renderer.isReady()) {
return false;
}
if (state == ExoPlayer.STATE_READY) {
return true;
}
long rendererDurationUs = renderer.getDurationUs();
long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs;
return minBufferDurationUs <= 0
|| rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US
|| rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US
|| rendererBufferedPositionUs >= positionUs + minBufferDurationUs
|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST_US
&& rendererBufferedPositionUs >= rendererDurationUs);
}
private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
try {
rebuffering = false;
this.playWhenReady = playWhenReady;
if (!playWhenReady) {
stopRenderers();
updatePositionUs();
} else {
if (state == ExoPlayer.STATE_READY) {
startRenderers();
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
} else if (state == ExoPlayer.STATE_BUFFERING) {
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
}
} finally {
eventHandler.obtainMessage(MSG_SET_PLAY_WHEN_READY_ACK).sendToTarget();
}
}
private void startRenderers() throws ExoPlaybackException {
rebuffering = false;
standaloneMediaClock.start();
for (int i = 0; i < enabledRenderers.size(); i++) {
enabledRenderers.get(i).start();
}
}
private void stopRenderers() throws ExoPlaybackException {
standaloneMediaClock.stop();
for (int i = 0; i < enabledRenderers.size(); i++) {
ensureStopped(enabledRenderers.get(i));
}
}
private void updatePositionUs() {
if (rendererMediaClock != null && enabledRenderers.contains(rendererMediaClockSource)
&& !rendererMediaClockSource.isEnded()) {
positionUs = rendererMediaClock.getPositionUs();
standaloneMediaClock.setPositionUs(positionUs);
} else {
positionUs = standaloneMediaClock.getPositionUs();
}
elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
}
private void doSomeWork() throws ExoPlaybackException {
TraceUtil.beginSection("doSomeWork");
long operationStartTimeMs = SystemClock.elapsedRealtime();
long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME_US ? durationUs
: Long.MAX_VALUE;
boolean allRenderersEnded = true;
boolean allRenderersReadyOrEnded = true;
updatePositionUs();
for (int i = 0; i < enabledRenderers.size(); i++) {
TrackRenderer renderer = enabledRenderers.get(i);
// TODO: Each renderer should return the maximum delay before which it wishes to be
// invoked again. The minimum of these values should then be used as the delay before the next
// invocation of this method.
renderer.doSomeWork(positionUs, elapsedRealtimeUs);
allRenderersEnded = allRenderersEnded && renderer.isEnded();
// Determine whether the renderer is ready (or ended). If it's not, throw an error that's
// preventing the renderer from making progress, if such an error exists.
boolean rendererReadyOrEnded = rendererReadyOrEnded(renderer);
if (!rendererReadyOrEnded) {
renderer.maybeThrowError();
}
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded;
if (bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {
// We've already encountered a track for which the buffered position is unknown. Hence the
// media buffer position unknown regardless of the buffered position of this track.
} else {
long rendererDurationUs = renderer.getDurationUs();
long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {
bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US;
} else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US
|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST_US
&& rendererBufferedPositionUs >= rendererDurationUs)) {
// This track is fully buffered.
} else {
bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
}
}
}
this.bufferedPositionUs = bufferedPositionUs;
if (allRenderersEnded
&& (durationUs == TrackRenderer.UNKNOWN_TIME_US || durationUs <= positionUs)) {
setState(ExoPlayer.STATE_ENDED);
stopRenderers();
} else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded) {
setState(ExoPlayer.STATE_READY);
if (playWhenReady) {
startRenderers();
}
} else if (state == ExoPlayer.STATE_READY && !allRenderersReadyOrEnded) {
rebuffering = playWhenReady;
setState(ExoPlayer.STATE_BUFFERING);
stopRenderers();
}
handler.removeMessages(MSG_DO_SOME_WORK);
if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) {
scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, RENDERING_INTERVAL_MS);
} else if (!enabledRenderers.isEmpty()) {
scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, IDLE_INTERVAL_MS);
}
TraceUtil.endSection();
}
private void scheduleNextOperation(int operationType, long thisOperationStartTimeMs,
long intervalMs) {
long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs;
long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime();
if (nextOperationDelayMs <= 0) {
handler.sendEmptyMessage(operationType);
} else {
handler.sendEmptyMessageDelayed(operationType, nextOperationDelayMs);
}
}
private void seekToInternal(long positionMs) throws ExoPlaybackException {
try {
if (positionMs == (positionUs / 1000)) {
// Seek is to the current position. Do nothing.
return;
}
rebuffering = false;
positionUs = positionMs * 1000;
standaloneMediaClock.stop();
standaloneMediaClock.setPositionUs(positionUs);
if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) {
return;
}
for (int i = 0; i < enabledRenderers.size(); i++) {
TrackRenderer renderer = enabledRenderers.get(i);
ensureStopped(renderer);
renderer.seekTo(positionUs);
}
setState(ExoPlayer.STATE_BUFFERING);
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
} finally {
pendingSeekCount.decrementAndGet();
}
}
private void stopInternal() {
resetInternal();
setState(ExoPlayer.STATE_IDLE);
}
private void releaseInternal() {
resetInternal();
setState(ExoPlayer.STATE_IDLE);
synchronized (this) {
released = true;
notifyAll();
}
}
private void resetInternal() {
handler.removeMessages(MSG_DO_SOME_WORK);
handler.removeMessages(MSG_INCREMENTAL_PREPARE);
rebuffering = false;
standaloneMediaClock.stop();
if (renderers == null) {
return;
}
for (int i = 0; i < renderers.length; i++) {
TrackRenderer renderer = renderers[i];
stopAndDisable(renderer);
release(renderer);
}
renderers = null;
rendererMediaClock = null;
rendererMediaClockSource = null;
enabledRenderers.clear();
}
private void stopAndDisable(TrackRenderer renderer) {
try {
ensureDisabled(renderer);
} catch (ExoPlaybackException e) {
// There's nothing we can do.
Log.e(TAG, "Stop failed.", e);
} catch (RuntimeException e) {
// Ditto.
Log.e(TAG, "Stop failed.", e);
}
}
private void release(TrackRenderer renderer) {
try {
renderer.release();
} catch (ExoPlaybackException e) {
// There's nothing we can do.
Log.e(TAG, "Release failed.", e);
} catch (RuntimeException e) {
// Ditto.
Log.e(TAG, "Release failed.", e);
}
}
private <T> void sendMessageInternal(int what, Object obj)
throws ExoPlaybackException {
try {
@SuppressWarnings("unchecked")
Pair<ExoPlayerComponent, Object> targetAndMessage = (Pair<ExoPlayerComponent, Object>) obj;
targetAndMessage.first.handleMessage(what, targetAndMessage.second);
if (state != ExoPlayer.STATE_IDLE && state != ExoPlayer.STATE_PREPARING) {
// The message may have caused something to change that now requires us to do work.
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
} finally {
synchronized (this) {
customMessagesProcessed++;
notifyAll();
}
}
}
private void setRendererSelectedTrackInternal(int rendererIndex, int trackIndex)
throws ExoPlaybackException {
if (selectedTrackIndices[rendererIndex] == trackIndex) {
return;
}
selectedTrackIndices[rendererIndex] = trackIndex;
if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) {
return;
}
TrackRenderer renderer = renderers[rendererIndex];
int rendererState = renderer.getState();
if (rendererState == TrackRenderer.STATE_UNPREPARED
|| rendererState == TrackRenderer.STATE_RELEASED
|| renderer.getTrackCount() == 0) {
return;
}
boolean isEnabled = rendererState == TrackRenderer.STATE_ENABLED
|| rendererState == TrackRenderer.STATE_STARTED;
boolean shouldEnable = 0 <= trackIndex && trackIndex < trackFormats[rendererIndex].length;
if (isEnabled) {
// The renderer is currently enabled. We need to disable it, so that we can either re-enable
// it with the newly selected track (if shouldEnable is true) or because we want to leave it
// disabled (if shouldEnable is false).
if (!shouldEnable && renderer == rendererMediaClockSource) {
// We've been using rendererMediaClockSource to advance the current position, but it's being
// disabled and won't be re-enabled. Sync standaloneMediaClock so that it can take over
// timing responsibilities.
standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs());
}
ensureDisabled(renderer);
enabledRenderers.remove(renderer);
}
if (shouldEnable) {
// Re-enable the renderer with the newly selected track.
boolean playing = playWhenReady && state == ExoPlayer.STATE_READY;
// Consider as joining if the renderer was previously disabled, but not when switching tracks.
boolean joining = !isEnabled && playing;
enableRenderer(renderer, trackIndex, joining);
if (playing) {
renderer.start();
}
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
}
private void ensureStopped(TrackRenderer renderer) throws ExoPlaybackException {
if (renderer.getState() == TrackRenderer.STATE_STARTED) {
renderer.stop();
}
}
private void ensureDisabled(TrackRenderer renderer) throws ExoPlaybackException {
ensureStopped(renderer);
if (renderer.getState() == TrackRenderer.STATE_ENABLED) {
renderer.disable();
if (renderer == rendererMediaClockSource) {
rendererMediaClock = null;
rendererMediaClockSource = null;
}
}
}
}
@@ -1,51 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
/**
* Information about the ExoPlayer library.
*/
public final class ExoPlayerLibraryInfo {
/**
* The version of the library, expressed as a string.
*/
public static final String VERSION = "1.5.8";
/**
* The version of the library, expressed as an integer.
* <p>
* Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
* corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
* integer version 123045006 (123-045-006).
*/
public static final int VERSION_INT = 1005008;
/**
* Whether the library was compiled with {@link com.b44t.messenger.exoplayer.util.Assertions}
* checks enabled.
*/
public static final boolean ASSERTIONS_ENABLED = true;
/**
* Whether the library was compiled with {@link com.b44t.messenger.exoplayer.util.TraceUtil}
* trace enabled.
*/
public static final boolean TRACE_ENABLED = true;
private ExoPlayerLibraryInfo() {}
}
@@ -1,349 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.SampleSource.SampleSourceReader;
import com.b44t.messenger.exoplayer.drm.DrmInitData;
import com.b44t.messenger.exoplayer.drm.DrmInitData.SchemeInitData;
import com.b44t.messenger.exoplayer.extractor.ExtractorSampleSource;
import com.b44t.messenger.exoplayer.extractor.mp4.PsshAtomUtil;
import com.b44t.messenger.exoplayer.util.Assertions;
import com.b44t.messenger.exoplayer.util.MimeTypes;
import com.b44t.messenger.exoplayer.util.Util;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.MediaExtractor;
import android.net.Uri;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Map;
import java.util.UUID;
/**
* Extracts samples from a stream using Android's {@link MediaExtractor}.
* <p>
* Warning - This class is marked as deprecated because there are known device specific issues
* associated with its use, including playbacks not starting, playbacks stuttering and other
* miscellaneous failures. For mp4, m4a, mp3, webm, mkv, mpeg-ts, ogg, wav and aac playbacks it is
* strongly recommended to use {@link ExtractorSampleSource} instead. Where this is not possible
* this class can still be used, but please be aware of the associated risks. Playing container
* formats for which an ExoPlayer extractor does not yet exist (e.g. avi) is a valid use case of
* this class.
* <p>
* Over time we hope to enhance {@link ExtractorSampleSource} to support more formats, and hence
* make use of this class unnecessary.
*/
// TODO: This implementation needs to be fixed so that its methods are non-blocking (either
// through use of a background thread, or through changes to the framework's MediaExtractor API).
@Deprecated
@TargetApi(16)
public final class FrameworkSampleSource implements SampleSource, SampleSourceReader {
private static final int ALLOWED_FLAGS_MASK = C.SAMPLE_FLAG_SYNC | C.SAMPLE_FLAG_ENCRYPTED;
private static final int TRACK_STATE_DISABLED = 0;
private static final int TRACK_STATE_ENABLED = 1;
private static final int TRACK_STATE_FORMAT_SENT = 2;
// Parameters for a Uri data source.
private final Context context;
private final Uri uri;
private final Map<String, String> headers;
// Parameters for a FileDescriptor data source.
private final FileDescriptor fileDescriptor;
private final long fileDescriptorOffset;
private final long fileDescriptorLength;
private IOException preparationError;
private MediaExtractor extractor;
private MediaFormat[] trackFormats;
private boolean prepared;
private int remainingReleaseCount;
private int[] trackStates;
private boolean[] pendingDiscontinuities;
private long lastSeekPositionUs;
private long pendingSeekPositionUs;
/**
* Instantiates a new sample extractor reading from the specified {@code uri}.
*
* @param context Context for resolving {@code uri}.
* @param uri The content URI from which to extract data.
* @param headers Headers to send with requests for data.
*/
public FrameworkSampleSource(Context context, Uri uri, Map<String, String> headers) {
Assertions.checkState(Util.SDK_INT >= 16);
this.context = Assertions.checkNotNull(context);
this.uri = Assertions.checkNotNull(uri);
this.headers = headers;
fileDescriptor = null;
fileDescriptorOffset = 0;
fileDescriptorLength = 0;
}
/**
* Instantiates a new sample extractor reading from the specified seekable {@code fileDescriptor}.
* The caller is responsible for releasing the file descriptor.
*
* @param fileDescriptor File descriptor from which to read.
* @param fileDescriptorOffset The offset in bytes where the data to be extracted starts.
* @param fileDescriptorLength The length in bytes of the data to be extracted.
*/
public FrameworkSampleSource(FileDescriptor fileDescriptor, long fileDescriptorOffset,
long fileDescriptorLength) {
Assertions.checkState(Util.SDK_INT >= 16);
this.fileDescriptor = Assertions.checkNotNull(fileDescriptor);
this.fileDescriptorOffset = fileDescriptorOffset;
this.fileDescriptorLength = fileDescriptorLength;
context = null;
uri = null;
headers = null;
}
@Override
public SampleSourceReader register() {
remainingReleaseCount++;
return this;
}
@Override
public boolean prepare(long positionUs) {
if (!prepared) {
if (preparationError != null) {
return false;
}
extractor = new MediaExtractor();
try {
if (context != null) {
extractor.setDataSource(context, uri, headers);
} else {
extractor.setDataSource(fileDescriptor, fileDescriptorOffset, fileDescriptorLength);
}
} catch (IOException e) {
preparationError = e;
return false;
}
trackStates = new int[extractor.getTrackCount()];
pendingDiscontinuities = new boolean[trackStates.length];
trackFormats = new MediaFormat[trackStates.length];
for (int i = 0; i < trackStates.length; i++) {
trackFormats[i] = createMediaFormat(extractor.getTrackFormat(i));
}
prepared = true;
}
return true;
}
@Override
public int getTrackCount() {
Assertions.checkState(prepared);
return trackStates.length;
}
@Override
public MediaFormat getFormat(int track) {
Assertions.checkState(prepared);
return trackFormats[track];
}
@Override
public void enable(int track, long positionUs) {
Assertions.checkState(prepared);
Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED);
trackStates[track] = TRACK_STATE_ENABLED;
extractor.selectTrack(track);
seekToUsInternal(positionUs, positionUs != 0);
}
@Override
public boolean continueBuffering(int track, long positionUs) {
// MediaExtractor takes care of buffering and blocks until it has samples, so we can always
// return true here. Although note that the blocking behavior is itself as bug, as per the
// TODO further up this file. This method will need to return something else as part of fixing
// the TODO.
return true;
}
@Override
public long readDiscontinuity(int track) {
if (pendingDiscontinuities[track]) {
pendingDiscontinuities[track] = false;
return lastSeekPositionUs;
}
return NO_DISCONTINUITY;
}
@Override
public int readData(int track, long positionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder) {
Assertions.checkState(prepared);
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
if (pendingDiscontinuities[track]) {
return NOTHING_READ;
}
if (trackStates[track] != TRACK_STATE_FORMAT_SENT) {
formatHolder.format = trackFormats[track];
formatHolder.drmInitData = Util.SDK_INT >= 18 ? getDrmInitDataV18() : null;
trackStates[track] = TRACK_STATE_FORMAT_SENT;
return FORMAT_READ;
}
int extractorTrackIndex = extractor.getSampleTrackIndex();
if (extractorTrackIndex == track) {
if (sampleHolder.data != null) {
int offset = sampleHolder.data.position();
sampleHolder.size = extractor.readSampleData(sampleHolder.data, offset);
sampleHolder.data.position(offset + sampleHolder.size);
} else {
sampleHolder.size = 0;
}
sampleHolder.timeUs = extractor.getSampleTime();
sampleHolder.flags = extractor.getSampleFlags() & ALLOWED_FLAGS_MASK;
if (sampleHolder.isEncrypted()) {
sampleHolder.cryptoInfo.setFromExtractorV16(extractor);
}
pendingSeekPositionUs = C.UNKNOWN_TIME_US;
extractor.advance();
return SAMPLE_READ;
} else {
return extractorTrackIndex < 0 ? END_OF_STREAM : NOTHING_READ;
}
}
@Override
public void disable(int track) {
Assertions.checkState(prepared);
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
extractor.unselectTrack(track);
pendingDiscontinuities[track] = false;
trackStates[track] = TRACK_STATE_DISABLED;
}
@Override
public void maybeThrowError() throws IOException {
if (preparationError != null) {
throw preparationError;
}
}
@Override
public void seekToUs(long positionUs) {
Assertions.checkState(prepared);
seekToUsInternal(positionUs, false);
}
@Override
public long getBufferedPositionUs() {
Assertions.checkState(prepared);
long bufferedDurationUs = extractor.getCachedDuration();
if (bufferedDurationUs == -1) {
return TrackRenderer.UNKNOWN_TIME_US;
} else {
long sampleTime = extractor.getSampleTime();
return sampleTime == -1 ? TrackRenderer.END_OF_TRACK_US : sampleTime + bufferedDurationUs;
}
}
@Override
public void release() {
Assertions.checkState(remainingReleaseCount > 0);
if (--remainingReleaseCount == 0 && extractor != null) {
extractor.release();
extractor = null;
}
}
@TargetApi(18)
private DrmInitData getDrmInitDataV18() {
// MediaExtractor only supports psshInfo for MP4, so it's ok to hard code the mimeType here.
Map<UUID, byte[]> psshInfo = extractor.getPsshInfo();
if (psshInfo == null || psshInfo.isEmpty()) {
return null;
}
DrmInitData.Mapped drmInitData = new DrmInitData.Mapped();
for (UUID uuid : psshInfo.keySet()) {
byte[] psshAtom = PsshAtomUtil.buildPsshAtom(uuid, psshInfo.get(uuid));
drmInitData.put(uuid, new SchemeInitData(MimeTypes.VIDEO_MP4, psshAtom));
}
return drmInitData;
}
private void seekToUsInternal(long positionUs, boolean force) {
// Unless forced, avoid duplicate calls to the underlying extractor's seek method in the case
// that there have been no interleaving calls to readSample.
if (force || pendingSeekPositionUs != positionUs) {
lastSeekPositionUs = positionUs;
pendingSeekPositionUs = positionUs;
extractor.seekTo(positionUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
for (int i = 0; i < trackStates.length; ++i) {
if (trackStates[i] != TRACK_STATE_DISABLED) {
pendingDiscontinuities[i] = true;
}
}
}
}
@SuppressLint("InlinedApi")
private static MediaFormat createMediaFormat(android.media.MediaFormat format) {
String mimeType = format.getString(android.media.MediaFormat.KEY_MIME);
String language = getOptionalStringV16(format, android.media.MediaFormat.KEY_LANGUAGE);
int maxInputSize = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE);
int width = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_WIDTH);
int height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT);
int rotationDegrees = getOptionalIntegerV16(format, "rotation-degrees");
int channelCount = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT);
int sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE);
int encoderDelay = getOptionalIntegerV16(format, "encoder-delay");
int encoderPadding = getOptionalIntegerV16(format, "encoder-padding");
ArrayList<byte[]> initializationData = new ArrayList<>();
for (int i = 0; format.containsKey("csd-" + i); i++) {
ByteBuffer buffer = format.getByteBuffer("csd-" + i);
byte[] data = new byte[buffer.limit()];
buffer.get(data);
initializationData.add(data);
buffer.flip();
}
long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION)
? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
int pcmEncoding = MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT
: MediaFormat.NO_VALUE;
MediaFormat mediaFormat = new MediaFormat(null, mimeType, MediaFormat.NO_VALUE, maxInputSize,
durationUs, width, height, rotationDegrees, MediaFormat.NO_VALUE, channelCount, sampleRate,
language, MediaFormat.OFFSET_SAMPLE_RELATIVE, initializationData, false,
MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, pcmEncoding, encoderDelay, encoderPadding);
mediaFormat.setFrameworkFormatV16(format);
return mediaFormat;
}
@TargetApi(16)
private static final String getOptionalStringV16(android.media.MediaFormat format, String key) {
return format.containsKey(key) ? format.getString(key) : null;
}
@TargetApi(16)
private static final int getOptionalIntegerV16(android.media.MediaFormat format, String key) {
return format.containsKey(key) ? format.getInteger(key) : MediaFormat.NO_VALUE;
}
}
@@ -1,75 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.upstream.Allocator;
/**
* Coordinates multiple loaders of time series data.
*/
public interface LoadControl {
/**
* Registers a loader.
*
* @param loader The loader being registered.
* @param bufferSizeContribution For instances whose {@link Allocator} maintains a pool of memory
* for the purpose of satisfying allocation requests, this is a hint indicating the loader's
* desired contribution to the size of the pool, in bytes.
*/
void register(Object loader, int bufferSizeContribution);
/**
* Unregisters a loader.
*
* @param loader The loader being unregistered.
*/
void unregister(Object loader);
/**
* Gets the {@link Allocator} that loaders should use to obtain memory allocations into which
* data can be loaded.
*
* @return The {@link Allocator} to use.
*/
Allocator getAllocator();
/**
* Hints to the control that it should consider trimming any unused memory being held in order
* to satisfy allocation requests.
* <p>
* This method is typically invoked by a recently unregistered loader, once it has released all
* of its allocations back to the {@link Allocator}.
*/
void trimAllocator();
/**
* Invoked by a loader to update the control with its current state.
* <p>
* This method must be called by a registered loader whenever its state changes. This is true
* even if the registered loader does not itself wish to start its next load (since the state of
* the loader will still affect whether other registered loaders are allowed to proceed).
*
* @param loader The loader invoking the update.
* @param playbackPositionUs The loader's playback position.
* @param nextLoadPositionUs The loader's next load position. -1 if finished, failed, or if the
* next load position is not yet known.
* @param loading Whether the loader is currently loading data.
* @return True if the loader is allowed to start its next load. False otherwise.
*/
boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs, boolean loading);
}
@@ -1,28 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
/**
* Tracks the progression of media time.
*/
public interface MediaClock {
/**
* @return The current media position in microseconds.
*/
long getPositionUs();
}
@@ -1,486 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.MediaCodecUtil.DecoderQueryException;
import com.b44t.messenger.exoplayer.audio.AudioCapabilities;
import com.b44t.messenger.exoplayer.audio.AudioTrack;
import com.b44t.messenger.exoplayer.drm.DrmSessionManager;
import com.b44t.messenger.exoplayer.util.MimeTypes;
import android.annotation.TargetApi;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.media.PlaybackParams;
import android.media.audiofx.Virtualizer;
import android.os.Handler;
import android.os.SystemClock;
import java.nio.ByteBuffer;
/**
* Decodes and renders audio using {@link MediaCodec} and {@link android.media.AudioTrack}.
*/
@TargetApi(16)
public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implements MediaClock {
/**
* Interface definition for a callback to be notified of {@link MediaCodecAudioTrackRenderer}
* events.
*/
public interface EventListener extends MediaCodecTrackRenderer.EventListener {
/**
* Invoked when an {@link AudioTrack} fails to initialize.
*
* @param e The corresponding exception.
*/
void onAudioTrackInitializationError(AudioTrack.InitializationException e);
/**
* Invoked when an {@link AudioTrack} write fails.
*
* @param e The corresponding exception.
*/
void onAudioTrackWriteError(AudioTrack.WriteException e);
/**
* Invoked when an {@link AudioTrack} underrun occurs.
*
* @param bufferSize The size of the {@link AudioTrack}'s buffer, in bytes.
* @param bufferSizeMs The size of the {@link AudioTrack}'s buffer, in milliseconds, if it is
* configured for PCM output. -1 if it is configured for passthrough output, as the buffered
* media can have a variable bitrate so the duration may be unknown.
* @param elapsedSinceLastFeedMs The time since the {@link AudioTrack} was last fed data.
*/
void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
}
/**
* The type of a message that can be passed to an instance of this class via
* {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object
* should be a {@link Float} with 0 being silence and 1 being unity gain.
*/
public static final int MSG_SET_VOLUME = 1;
/**
* The type of a message that can be passed to an instance of this class via
* {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object
* should be a {@link android.media.PlaybackParams}, which will be used to configure the
* underlying {@link android.media.AudioTrack}. The message object should not be modified by the
* caller after it has been passed
*/
public static final int MSG_SET_PLAYBACK_PARAMS = 2;
private final EventListener eventListener;
private final AudioTrack audioTrack;
private boolean passthroughEnabled;
private android.media.MediaFormat passthroughMediaFormat;
private int pcmEncoding;
private int audioSessionId;
private long currentPositionUs;
private boolean allowPositionDiscontinuity;
private boolean audioTrackHasData;
private long lastFeedElapsedRealtimeMs;
/**
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector.
*/
public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector) {
this(source, mediaCodecSelector, null, true);
}
/**
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required.
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
* For example a media file may start with a short clear region so as to allow playback to
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
*/
public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector,
DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) {
this(source, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null);
}
/**
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
*/
public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector,
Handler eventHandler, EventListener eventListener) {
this(source, mediaCodecSelector, null, true, eventHandler, eventListener);
}
/**
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required.
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
* For example a media file may start with a short clear region so as to allow playback to
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
*/
public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector,
DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
Handler eventHandler, EventListener eventListener) {
this(source, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
eventListener, null, AudioManager.STREAM_MUSIC);
}
/**
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required.
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
* For example a media file may start with a short clear region so as to allow playback to
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param streamType The type of audio stream for the {@link AudioTrack}.
*/
public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector,
DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
Handler eventHandler, EventListener eventListener, AudioCapabilities audioCapabilities,
int streamType) {
this (new SampleSource[] {source}, mediaCodecSelector, drmSessionManager,
playClearSamplesWithoutKeys, eventHandler, eventListener, audioCapabilities, streamType);
}
/**
* @param sources The upstream sources from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required.
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
* For example a media file may start with a short clear region so as to allow playback to
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param streamType The type of audio stream for the {@link AudioTrack}.
*/
public MediaCodecAudioTrackRenderer(SampleSource[] sources, MediaCodecSelector mediaCodecSelector,
DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
Handler eventHandler, EventListener eventListener, AudioCapabilities audioCapabilities,
int streamType) {
super(sources, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
eventListener);
this.eventListener = eventListener;
this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
this.audioTrack = new AudioTrack(audioCapabilities, streamType);
}
@Override
protected boolean handlesTrack(MediaCodecSelector mediaCodecSelector, MediaFormat mediaFormat)
throws DecoderQueryException {
String mimeType = mediaFormat.mimeType;
return MimeTypes.isAudio(mimeType) && (MimeTypes.AUDIO_UNKNOWN.equals(mimeType)
|| (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null)
|| mediaCodecSelector.getDecoderInfo(mimeType, false) != null);
}
@Override
protected DecoderInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector, String mimeType,
boolean requiresSecureDecoder) throws DecoderQueryException {
if (allowPassthrough(mimeType)) {
DecoderInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo();
if (passthroughDecoderInfo != null) {
passthroughEnabled = true;
return passthroughDecoderInfo;
}
}
passthroughEnabled = false;
return super.getDecoderInfo(mediaCodecSelector, mimeType, requiresSecureDecoder);
}
/**
* Returns whether encoded audio passthrough should be used for playing back the input format.
* This implementation returns true if the {@link AudioTrack}'s audio capabilities indicate that
* passthrough is supported.
*
* @param mimeType The type of input media.
* @return True if passthrough playback should be used. False otherwise.
*/
protected boolean allowPassthrough(String mimeType) {
return audioTrack.isPassthroughSupported(mimeType);
}
@Override
protected void configureCodec(MediaCodec codec, boolean codecIsAdaptive,
android.media.MediaFormat format, android.media.MediaCrypto crypto) {
String mimeType = format.getString(android.media.MediaFormat.KEY_MIME);
if (passthroughEnabled) {
// Override the MIME type used to configure the codec if we are using a passthrough decoder.
format.setString(android.media.MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW);
codec.configure(format, null, crypto, 0);
format.setString(android.media.MediaFormat.KEY_MIME, mimeType);
passthroughMediaFormat = format;
} else {
codec.configure(format, null, crypto, 0);
passthroughMediaFormat = null;
}
}
@Override
protected MediaClock getMediaClock() {
return this;
}
@Override
protected void onInputFormatChanged(MediaFormatHolder holder) throws ExoPlaybackException {
super.onInputFormatChanged(holder);
// If the input format is anything other than PCM then we assume that the audio decoder will
// output 16-bit PCM.
pcmEncoding = MimeTypes.AUDIO_RAW.equals(holder.format.mimeType) ? holder.format.pcmEncoding
: C.ENCODING_PCM_16BIT;
}
@Override
protected void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat outputFormat) {
boolean passthrough = passthroughMediaFormat != null;
String mimeType = passthrough
? passthroughMediaFormat.getString(android.media.MediaFormat.KEY_MIME)
: MimeTypes.AUDIO_RAW;
android.media.MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat;
int channelCount = format.getInteger(android.media.MediaFormat.KEY_CHANNEL_COUNT);
int sampleRate = format.getInteger(android.media.MediaFormat.KEY_SAMPLE_RATE);
audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding);
}
/**
* Invoked when the audio session id becomes known. Once the id is known it will not change
* (and hence this method will not be invoked again) unless the renderer is disabled and then
* subsequently re-enabled.
* <p>
* The default implementation is a no-op. One reason for overriding this method would be to
* instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For
* this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()}
* (if not before).
*
* @param audioSessionId The audio session id.
*/
protected void onAudioSessionId(int audioSessionId) {
// Do nothing.
}
@Override
protected void onStarted() {
super.onStarted();
audioTrack.play();
}
@Override
protected void onStopped() {
audioTrack.pause();
super.onStopped();
}
@Override
protected boolean isEnded() {
return super.isEnded() && !audioTrack.hasPendingData();
}
@Override
protected boolean isReady() {
return audioTrack.hasPendingData() || super.isReady();
}
@Override
public long getPositionUs() {
long newCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded());
if (newCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) {
currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs
: Math.max(currentPositionUs, newCurrentPositionUs);
allowPositionDiscontinuity = false;
}
return currentPositionUs;
}
@Override
protected void onDisabled() throws ExoPlaybackException {
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
try {
audioTrack.release();
} finally {
super.onDisabled();
}
}
@Override
protected void onDiscontinuity(long positionUs) throws ExoPlaybackException {
super.onDiscontinuity(positionUs);
audioTrack.reset();
currentPositionUs = positionUs;
allowPositionDiscontinuity = true;
}
@Override
protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip)
throws ExoPlaybackException {
if (passthroughEnabled && (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// Discard output buffers from the passthrough (raw) decoder containing codec specific data.
codec.releaseOutputBuffer(bufferIndex, false);
return true;
}
if (shouldSkip) {
codec.releaseOutputBuffer(bufferIndex, false);
codecCounters.skippedOutputBufferCount++;
audioTrack.handleDiscontinuity();
return true;
}
if (!audioTrack.isInitialized()) {
// Initialize the AudioTrack now.
try {
if (audioSessionId != AudioTrack.SESSION_ID_NOT_SET) {
audioTrack.initialize(audioSessionId);
} else {
audioSessionId = audioTrack.initialize();
onAudioSessionId(audioSessionId);
}
audioTrackHasData = false;
} catch (AudioTrack.InitializationException e) {
notifyAudioTrackInitializationError(e);
throw new ExoPlaybackException(e);
}
if (getState() == TrackRenderer.STATE_STARTED) {
audioTrack.play();
}
} else {
// Check for AudioTrack underrun.
boolean audioTrackHadData = audioTrackHasData;
audioTrackHasData = audioTrack.hasPendingData();
if (audioTrackHadData && !audioTrackHasData && getState() == TrackRenderer.STATE_STARTED) {
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
long bufferSizeUs = audioTrack.getBufferSizeUs();
long bufferSizeMs = bufferSizeUs == C.UNKNOWN_TIME_US ? -1 : bufferSizeUs / 1000;
notifyAudioTrackUnderrun(audioTrack.getBufferSize(), bufferSizeMs, elapsedSinceLastFeedMs);
}
}
int handleBufferResult;
try {
handleBufferResult = audioTrack.handleBuffer(
buffer, bufferInfo.offset, bufferInfo.size, bufferInfo.presentationTimeUs);
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
} catch (AudioTrack.WriteException e) {
notifyAudioTrackWriteError(e);
throw new ExoPlaybackException(e);
}
// If we are out of sync, allow currentPositionUs to jump backwards.
if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
handleAudioTrackDiscontinuity();
allowPositionDiscontinuity = true;
}
// Release the buffer if it was consumed.
if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
codec.releaseOutputBuffer(bufferIndex, false);
codecCounters.renderedOutputBufferCount++;
return true;
}
return false;
}
@Override
protected void onOutputStreamEnded() {
audioTrack.handleEndOfStream();
}
protected void handleAudioTrackDiscontinuity() {
// Do nothing
}
@Override
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
switch (messageType) {
case MSG_SET_VOLUME:
audioTrack.setVolume((Float) message);
break;
case MSG_SET_PLAYBACK_PARAMS:
audioTrack.setPlaybackParams((PlaybackParams) message);
break;
default:
super.handleMessage(messageType, message);
break;
}
}
private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onAudioTrackInitializationError(e);
}
});
}
}
private void notifyAudioTrackWriteError(final AudioTrack.WriteException e) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onAudioTrackWriteError(e);
}
});
}
}
private void notifyAudioTrackUnderrun(final int bufferSize, final long bufferSizeMs,
final long elapsedSinceLastFeedMs) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
});
}
}
}
@@ -1,64 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.MediaCodecUtil.DecoderQueryException;
import android.media.MediaCodec;
/**
* Selector of {@link MediaCodec} instances.
*/
public interface MediaCodecSelector {
/**
* Default implementation of {@link MediaCodecSelector}.
*/
MediaCodecSelector DEFAULT = new MediaCodecSelector() {
@Override
public DecoderInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
throws DecoderQueryException {
return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder);
}
@Override
public DecoderInfo getPassthroughDecoderInfo() throws DecoderQueryException {
return MediaCodecUtil.getPassthroughDecoderInfo();
}
};
/**
* Selects a decoder to instantiate for a given mime type.
*
* @param mimeType The mime type for which a decoder is required.
* @param requiresSecureDecoder Whether a secure decoder is required.
* @return A {@link DecoderInfo} describing the decoder, or null if no suitable decoder exists.
* @throws DecoderQueryException Thrown if there was an error querying decoders.
*/
DecoderInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
throws DecoderQueryException;
/**
* Selects a decoder to instantiate for audio passthrough.
*
* @return A {@link DecoderInfo} describing the decoder, or null if no suitable decoder exists.
* @throws DecoderQueryException Thrown if there was an error querying decoders.
*/
DecoderInfo getPassthroughDecoderInfo() throws DecoderQueryException;
}
@@ -1,501 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.util.Assertions;
import com.b44t.messenger.exoplayer.util.MimeTypes;
import com.b44t.messenger.exoplayer.util.Util;
import android.annotation.TargetApi;
import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.CodecProfileLevel;
import android.media.MediaCodecList;
import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A utility class for querying the available codecs.
*/
@TargetApi(16)
public final class MediaCodecUtil {
/**
* Thrown when an error occurs querying the device for its underlying media capabilities.
* <p>
* Such failures are not expected in normal operation and are normally temporary (e.g. if the
* mediaserver process has crashed and is yet to restart).
*/
public static class DecoderQueryException extends IOException {
private DecoderQueryException(Throwable cause) {
super("Failed to query underlying media codecs", cause);
}
}
private static final String TAG = "MediaCodecUtil";
private static final DecoderInfo PASSTHROUGH_DECODER_INFO =
new DecoderInfo("OMX.google.raw.decoder", null);
private static final Map<CodecKey, List<DecoderInfo>> decoderInfosCache = new HashMap<>();
// Lazily initialized.
private static int maxH264DecodableFrameSize = -1;
private MediaCodecUtil() {}
/**
* Optional call to warm the codec cache for a given mime type.
* <p>
* Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean)}.
*
* @param mimeType The mime type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false
* unless secure decryption really is required.
*/
public static void warmCodec(String mimeType, boolean secure) {
try {
getDecoderInfos(mimeType, secure);
} catch (DecoderQueryException e) {
// Codec warming is best effort, so we can swallow the exception.
Log.e(TAG, "Codec warming failed", e);
}
}
/**
* Gets information about a decoder suitable for audio passthrough.
**
* @return A {@link DecoderInfo} describing the decoder, or null if no suitable decoder exists.
*/
public static DecoderInfo getPassthroughDecoderInfo() {
// TODO: Return null if the raw decoder doesn't exist.
return PASSTHROUGH_DECODER_INFO;
}
/**
* Get information about the preferred decoder for a given mime type.
*
* @param mimeType The mime type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false
* unless secure decryption really is required.
* @return A {@link DecoderInfo} describing the decoder, or null if no suitable decoder exists.
*/
public static DecoderInfo getDecoderInfo(String mimeType, boolean secure)
throws DecoderQueryException {
List<DecoderInfo> decoderInfos = getDecoderInfos(mimeType, secure);
return decoderInfos.isEmpty() ? null : decoderInfos.get(0);
}
/**
* Returns all @{link DecoderInfo}s for a given mime type, in the order given by
* {@link MediaCodecList}.
*
* @param mimeType The mime type.
* @param secure Whether the decoders are required to support secure decryption. Always pass false
* unless secure decryption really is required.
* @return A list of all @{link DecoderInfo}s for the given mime type. May be empty if no suitable
* decoders exist.
*/
public static synchronized List<DecoderInfo> getDecoderInfos(String mimeType, boolean secure)
throws DecoderQueryException {
CodecKey key = new CodecKey(mimeType, secure);
List<DecoderInfo> decoderInfos = decoderInfosCache.get(key);
if (decoderInfos != null) {
return decoderInfos;
}
MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21
? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16();
decoderInfos = getDecoderInfosInternal(key, mediaCodecList);
if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) {
// Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the
// legacy path. We also try this path on API levels 22 and 23 as a defensive measure.
mediaCodecList = new MediaCodecListCompatV16();
decoderInfos = getDecoderInfosInternal(key, mediaCodecList);
if (!decoderInfos.isEmpty()) {
Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType
+ ". Assuming: " + decoderInfos.get(0).name);
}
}
decoderInfos = Collections.unmodifiableList(decoderInfos);
decoderInfosCache.put(key, decoderInfos);
return decoderInfos;
}
private static List<DecoderInfo> getDecoderInfosInternal(
CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException {
try {
List<DecoderInfo> decoderInfos = new ArrayList<>();
String mimeType = key.mimeType;
int numberOfCodecs = mediaCodecList.getCodecCount();
boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit();
// Note: MediaCodecList is sorted by the framework such that the best decoders come first.
for (int i = 0; i < numberOfCodecs; i++) {
MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i);
String codecName = codecInfo.getName();
if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit)) {
for (String supportedType : codecInfo.getSupportedTypes()) {
if (supportedType.equalsIgnoreCase(mimeType)) {
try {
CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType);
boolean secure = mediaCodecList.isSecurePlaybackSupported(mimeType, capabilities);
if ((secureDecodersExplicit && key.secure == secure)
|| (!secureDecodersExplicit && !key.secure)) {
decoderInfos.add(new DecoderInfo(codecName, capabilities));
} else if (!secureDecodersExplicit && secure) {
decoderInfos.add(new DecoderInfo(codecName + ".secure", capabilities));
// It only makes sense to have one synthesized secure decoder, return immediately.
return decoderInfos;
}
} catch (Exception e) {
if (Util.SDK_INT <= 23 && !decoderInfos.isEmpty()) {
// Suppress error querying secondary codec capabilities up to API level 23.
Log.e(TAG, "Skipping codec " + codecName + " (failed to query capabilities)");
} else {
throw e;
}
}
}
}
}
}
return decoderInfos;
} catch (Exception e) {
// If the underlying mediaserver is in a bad state, we may catch an IllegalStateException
// or an IllegalArgumentException here.
throw new DecoderQueryException(e);
}
}
/**
* Returns whether the specified codec is usable for decoding on the current device.
*/
private static boolean isCodecUsableDecoder(MediaCodecInfo info, String name,
boolean secureDecodersExplicit) {
if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) {
return false;
}
// Work around broken audio decoders.
if (Util.SDK_INT < 21
&& ("CIPAACDecoder".equals(name))
|| "CIPMP3Decoder".equals(name)
|| "CIPVorbisDecoder".equals(name)
|| "AACDecoder".equals(name)
|| "MP3Decoder".equals(name)) {
return false;
}
// Work around https://github.com/google/ExoPlayer/issues/398
if (Util.SDK_INT < 18 && "OMX.SEC.MP3.Decoder".equals(name)) {
return false;
}
// Work around https://github.com/google/ExoPlayer/issues/1528
if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name)
&& "a70".equals(Util.DEVICE)) {
return false;
}
// Work around an issue where creating a particular MP3 decoder on some devices on platform API
// version 16 crashes mediaserver.
if (Util.SDK_INT == 16
&& "OMX.qcom.audio.decoder.mp3".equals(name)
&& ("dlxu".equals(Util.DEVICE) // HTC Butterfly
|| "protou".equals(Util.DEVICE) // HTC Desire X
|| "C6602".equals(Util.DEVICE) // Sony Xperia Z
|| "C6603".equals(Util.DEVICE)
|| "C6606".equals(Util.DEVICE)
|| "C6616".equals(Util.DEVICE)
|| "L36h".equals(Util.DEVICE)
|| "SO-02E".equals(Util.DEVICE))) {
return false;
}
// Work around an issue where large timestamps are not propagated correctly.
if (Util.SDK_INT == 16
&& "OMX.qcom.audio.decoder.aac".equals(name)
&& ("C1504".equals(Util.DEVICE) // Sony Xperia E
|| "C1505".equals(Util.DEVICE)
|| "C1604".equals(Util.DEVICE) // Sony Xperia E dual
|| "C1605".equals(Util.DEVICE))) {
return false;
}
// Work around an issue where the VP8 decoder on Samsung Galaxy S3/S4 Mini does not render
// video.
if (Util.SDK_INT <= 19 && Util.DEVICE != null
&& (Util.DEVICE.startsWith("d2") || Util.DEVICE.startsWith("serrano"))
&& "samsung".equals(Util.MANUFACTURER) && name.equals("OMX.SEC.vp8.dec")) {
return false;
}
return true;
}
/**
* Tests whether the device advertises it can decode video of a given type at a specified width
* and height.
* <p>
* Must not be called if the device SDK version is less than 21.
*
* @param mimeType The mime type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false
* unless secure decryption really is required.
* @param width Width in pixels.
* @param height Height in pixels.
* @return Whether the decoder advertises support of the given size.
*/
@TargetApi(21)
public static boolean isSizeSupportedV21(String mimeType, boolean secure, int width,
int height) throws DecoderQueryException {
Assertions.checkState(Util.SDK_INT >= 21);
MediaCodecInfo.VideoCapabilities videoCapabilities = getVideoCapabilitiesV21(mimeType, secure);
return videoCapabilities != null && videoCapabilities.isSizeSupported(width, height);
}
/**
* Tests whether the device advertises it can decode video of a given type at a specified
* width, height, and frame rate.
* <p>
* Must not be called if the device SDK version is less than 21.
*
* @param mimeType The mime type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false
* unless secure decryption really is required.
* @param width Width in pixels.
* @param height Height in pixels.
* @param frameRate Frame rate in frames per second.
* @return Whether the decoder advertises support of the given size and frame rate.
*/
@TargetApi(21)
public static boolean isSizeAndRateSupportedV21(String mimeType, boolean secure,
int width, int height, double frameRate) throws DecoderQueryException {
Assertions.checkState(Util.SDK_INT >= 21);
MediaCodecInfo.VideoCapabilities videoCapabilities = getVideoCapabilitiesV21(mimeType, secure);
return videoCapabilities != null
&& videoCapabilities.areSizeAndRateSupported(width, height, frameRate);
}
/**
* @param profile An AVC profile constant from {@link CodecProfileLevel}.
* @param level An AVC profile level from {@link CodecProfileLevel}.
* @return Whether the specified profile is supported at the specified level.
* @deprecated Prefer {@link #getDecoderInfos(String, boolean)} for new code.
*/
@Deprecated
public static boolean isH264ProfileSupported(int profile, int level)
throws DecoderQueryException {
DecoderInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false);
if (decoderInfo == null) {
return false;
}
for (CodecProfileLevel profileLevel : decoderInfo.capabilities.profileLevels) {
if (profileLevel.profile == profile && profileLevel.level >= level) {
return true;
}
}
return false;
}
/**
* @return the maximum frame size for an H264 stream that can be decoded on the device.
*/
public static int maxH264DecodableFrameSize() throws DecoderQueryException {
if (maxH264DecodableFrameSize == -1) {
int result = 0;
DecoderInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false);
if (decoderInfo != null) {
for (CodecProfileLevel profileLevel : decoderInfo.capabilities.profileLevels) {
result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result);
}
// We assume support for at least 360p.
result = Math.max(result, 480 * 360);
}
maxH264DecodableFrameSize = result;
}
return maxH264DecodableFrameSize;
}
@TargetApi(21)
private static MediaCodecInfo.VideoCapabilities getVideoCapabilitiesV21(String mimeType,
boolean secure) throws DecoderQueryException {
DecoderInfo decoderInfo = getDecoderInfo(mimeType, secure);
return decoderInfo == null ? null : decoderInfo.capabilities.getVideoCapabilities();
}
/**
* Conversion values taken from ISO 14496-10 Table A-1.
*
* @param avcLevel one of CodecProfileLevel.AVCLevel* constants.
* @return maximum frame size that can be decoded by a decoder with the specified avc level
* (or {@code -1} if the level is not recognized)
*/
private static int avcLevelToMaxFrameSize(int avcLevel) {
switch (avcLevel) {
case CodecProfileLevel.AVCLevel1: return 99 * 16 * 16;
case CodecProfileLevel.AVCLevel1b: return 99 * 16 * 16;
case CodecProfileLevel.AVCLevel12: return 396 * 16 * 16;
case CodecProfileLevel.AVCLevel13: return 396 * 16 * 16;
case CodecProfileLevel.AVCLevel2: return 396 * 16 * 16;
case CodecProfileLevel.AVCLevel21: return 792 * 16 * 16;
case CodecProfileLevel.AVCLevel22: return 1620 * 16 * 16;
case CodecProfileLevel.AVCLevel3: return 1620 * 16 * 16;
case CodecProfileLevel.AVCLevel31: return 3600 * 16 * 16;
case CodecProfileLevel.AVCLevel32: return 5120 * 16 * 16;
case CodecProfileLevel.AVCLevel4: return 8192 * 16 * 16;
case CodecProfileLevel.AVCLevel41: return 8192 * 16 * 16;
case CodecProfileLevel.AVCLevel42: return 8704 * 16 * 16;
case CodecProfileLevel.AVCLevel5: return 22080 * 16 * 16;
case CodecProfileLevel.AVCLevel51: return 36864 * 16 * 16;
default: return -1;
}
}
private interface MediaCodecListCompat {
/**
* The number of codecs in the list.
*/
int getCodecCount();
/**
* The info at the specified index in the list.
*
* @param index The index.
*/
MediaCodecInfo getCodecInfoAt(int index);
/**
* @return Returns whether secure decoders are explicitly listed, if present.
*/
boolean secureDecodersExplicit();
/**
* Whether secure playback is supported for the given {@link CodecCapabilities}, which should
* have been obtained from a {@link MediaCodecInfo} obtained from this list.
*/
boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities);
}
@TargetApi(21)
private static final class MediaCodecListCompatV21 implements MediaCodecListCompat {
private final int codecKind;
private MediaCodecInfo[] mediaCodecInfos;
public MediaCodecListCompatV21(boolean includeSecure) {
codecKind = includeSecure ? MediaCodecList.ALL_CODECS : MediaCodecList.REGULAR_CODECS;
}
@Override
public int getCodecCount() {
ensureMediaCodecInfosInitialized();
return mediaCodecInfos.length;
}
@Override
public MediaCodecInfo getCodecInfoAt(int index) {
ensureMediaCodecInfosInitialized();
return mediaCodecInfos[index];
}
@Override
public boolean secureDecodersExplicit() {
return true;
}
@Override
public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) {
return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback);
}
private void ensureMediaCodecInfosInitialized() {
if (mediaCodecInfos == null) {
mediaCodecInfos = new MediaCodecList(codecKind).getCodecInfos();
}
}
}
@SuppressWarnings("deprecation")
private static final class MediaCodecListCompatV16 implements MediaCodecListCompat {
@Override
public int getCodecCount() {
return MediaCodecList.getCodecCount();
}
@Override
public MediaCodecInfo getCodecInfoAt(int index) {
return MediaCodecList.getCodecInfoAt(index);
}
@Override
public boolean secureDecodersExplicit() {
return false;
}
@Override
public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) {
// Secure decoders weren't explicitly listed prior to API level 21. We assume that a secure
// H264 decoder exists.
return MimeTypes.VIDEO_H264.equals(mimeType);
}
}
private static final class CodecKey {
public final String mimeType;
public final boolean secure;
public CodecKey(String mimeType, boolean secure) {
this.mimeType = mimeType;
this.secure = secure;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((mimeType == null) ? 0 : mimeType.hashCode());
result = prime * result + (secure ? 1231 : 1237);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || obj.getClass() != CodecKey.class) {
return false;
}
CodecKey other = (CodecKey) obj;
return TextUtils.equals(mimeType, other.mimeType) && secure == other.secure;
}
}
}
@@ -1,611 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.MediaCodecUtil.DecoderQueryException;
import com.b44t.messenger.exoplayer.drm.DrmSessionManager;
import com.b44t.messenger.exoplayer.util.MimeTypes;
import com.b44t.messenger.exoplayer.util.TraceUtil;
import com.b44t.messenger.exoplayer.util.Util;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaCrypto;
import android.os.Handler;
import android.os.SystemClock;
import android.view.Surface;
import android.view.TextureView;
import java.nio.ByteBuffer;
/**
* Decodes and renders video using {@link MediaCodec}.
*/
@TargetApi(16)
public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
/**
* Interface definition for a callback to be notified of {@link MediaCodecVideoTrackRenderer}
* events.
*/
public interface EventListener extends MediaCodecTrackRenderer.EventListener {
/**
* Invoked to report the number of frames dropped by the renderer. Dropped frames are reported
* whenever the renderer is stopped having dropped frames, and optionally, whenever the count
* reaches a specified threshold whilst the renderer is started.
*
* @param count The number of dropped frames.
* @param elapsed The duration in milliseconds over which the frames were dropped. This
* duration is timed from when the renderer was started or from when dropped frames were
* last reported (whichever was more recent), and not from when the first of the reported
* drops occurred.
*/
void onDroppedFrames(int count, long elapsed);
/**
* Invoked each time there's a change in the size of the video being rendered.
*
* @param width The video width in pixels.
* @param height The video height in pixels.
* @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise
* rotation in degrees that the application should apply for the video for it to be rendered
* in the correct orientation. This value will always be zero on API levels 21 and above,
* since the renderer will apply all necessary rotations internally. On earlier API levels
* this is not possible. Applications that use {@link TextureView} can apply the rotation by
* calling {@link TextureView#setTransform}. Applications that do not expect to encounter
* rotated videos can safely ignore this parameter.
* @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case
* of square pixels this will be equal to 1.0. Different values are indicative of anamorphic
* content.
*/
void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio);
/**
* Invoked when a frame is rendered to a surface for the first time following that surface
* having been set as the target for the renderer.
*
* @param surface The surface to which a first frame has been rendered.
*/
void onDrawnToSurface(Surface surface);
}
// TODO: Use MediaFormat constants if these get exposed through the API. See
// [Internal: b/14127601].
private static final String KEY_CROP_LEFT = "crop-left";
private static final String KEY_CROP_RIGHT = "crop-right";
private static final String KEY_CROP_BOTTOM = "crop-bottom";
private static final String KEY_CROP_TOP = "crop-top";
/**
* The type of a message that can be passed to an instance of this class via
* {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object
* should be the target {@link Surface}, or null.
*/
public static final int MSG_SET_SURFACE = 1;
private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper;
private final EventListener eventListener;
private final long allowedJoiningTimeUs;
private final int videoScalingMode;
private final int maxDroppedFrameCountToNotify;
private Surface surface;
private boolean reportedDrawnToSurface;
private boolean renderedFirstFrame;
private long joiningDeadlineUs;
private long droppedFrameAccumulationStartTimeMs;
private int droppedFrameCount;
private int consecutiveDroppedFrameCount;
private int pendingRotationDegrees;
private float pendingPixelWidthHeightRatio;
private int currentWidth;
private int currentHeight;
private int currentUnappliedRotationDegrees;
private float currentPixelWidthHeightRatio;
private int lastReportedWidth;
private int lastReportedHeight;
private int lastReportedUnappliedRotationDegrees;
private float lastReportedPixelWidthHeightRatio;
/**
* @param context A context.
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector.
* @param videoScalingMode The scaling mode to pass to
* {@link MediaCodec#setVideoScalingMode(int)}.
*/
public MediaCodecVideoTrackRenderer(Context context, SampleSource source,
MediaCodecSelector mediaCodecSelector, int videoScalingMode) {
this(context, source, mediaCodecSelector, videoScalingMode, 0);
}
/**
* @param context A context.
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector.
* @param videoScalingMode The scaling mode to pass to
* {@link MediaCodec#setVideoScalingMode(int)}.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
*/
public MediaCodecVideoTrackRenderer(Context context, SampleSource source,
MediaCodecSelector mediaCodecSelector, int videoScalingMode, long allowedJoiningTimeMs) {
this(context, source, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, null,
-1);
}
/**
* @param context A context.
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector.
* @param videoScalingMode The scaling mode to pass to
* {@link MediaCodec#setVideoScalingMode(int)}.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between
* invocations of {@link EventListener#onDroppedFrames(int, long)}.
*/
public MediaCodecVideoTrackRenderer(Context context, SampleSource source,
MediaCodecSelector mediaCodecSelector, int videoScalingMode, long allowedJoiningTimeMs,
Handler eventHandler, EventListener eventListener, int maxDroppedFrameCountToNotify) {
this(context, source, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, false,
eventHandler, eventListener, maxDroppedFrameCountToNotify);
}
/**
* @param context A context.
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector.
* @param videoScalingMode The scaling mode to pass to
* {@link MediaCodec#setVideoScalingMode(int)}.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required.
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
* For example a media file may start with a short clear region so as to allow playback to
* begin in parallel with key acquisision. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between
* invocations of {@link EventListener#onDroppedFrames(int, long)}.
*/
public MediaCodecVideoTrackRenderer(Context context, SampleSource source,
MediaCodecSelector mediaCodecSelector, int videoScalingMode, long allowedJoiningTimeMs,
DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
Handler eventHandler, EventListener eventListener, int maxDroppedFrameCountToNotify) {
super(source, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
eventListener);
this.frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context);
this.videoScalingMode = videoScalingMode;
this.allowedJoiningTimeUs = allowedJoiningTimeMs * 1000;
this.eventListener = eventListener;
this.maxDroppedFrameCountToNotify = maxDroppedFrameCountToNotify;
joiningDeadlineUs = -1;
currentWidth = -1;
currentHeight = -1;
currentPixelWidthHeightRatio = -1;
pendingPixelWidthHeightRatio = -1;
lastReportedWidth = -1;
lastReportedHeight = -1;
lastReportedPixelWidthHeightRatio = -1;
}
@Override
protected boolean handlesTrack(MediaCodecSelector mediaCodecSelector, MediaFormat mediaFormat)
throws DecoderQueryException {
String mimeType = mediaFormat.mimeType;
return MimeTypes.isVideo(mimeType) && (MimeTypes.VIDEO_UNKNOWN.equals(mimeType)
|| mediaCodecSelector.getDecoderInfo(mimeType, false) != null);
}
@Override
protected void onEnabled(int track, long positionUs, boolean joining)
throws ExoPlaybackException {
super.onEnabled(track, positionUs, joining);
if (joining && allowedJoiningTimeUs > 0) {
joiningDeadlineUs = SystemClock.elapsedRealtime() * 1000L + allowedJoiningTimeUs;
}
frameReleaseTimeHelper.enable();
}
@Override
protected void onDiscontinuity(long positionUs) throws ExoPlaybackException {
super.onDiscontinuity(positionUs);
renderedFirstFrame = false;
consecutiveDroppedFrameCount = 0;
joiningDeadlineUs = -1;
}
@Override
protected boolean isReady() {
if (super.isReady() && (renderedFirstFrame || !codecInitialized()
|| getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL)) {
// Ready. If we were joining then we've now joined, so clear the joining deadline.
joiningDeadlineUs = -1;
return true;
} else if (joiningDeadlineUs == -1) {
// Not joining.
return false;
} else if (SystemClock.elapsedRealtime() * 1000 < joiningDeadlineUs) {
// Joining and still within the joining deadline.
return true;
} else {
// The joining deadline has been exceeded. Give up and clear the deadline.
joiningDeadlineUs = -1;
return false;
}
}
@Override
protected void onStarted() {
super.onStarted();
droppedFrameCount = 0;
droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
}
@Override
protected void onStopped() {
joiningDeadlineUs = -1;
maybeNotifyDroppedFrameCount();
super.onStopped();
}
@Override
protected void onDisabled() throws ExoPlaybackException {
currentWidth = -1;
currentHeight = -1;
currentPixelWidthHeightRatio = -1;
pendingPixelWidthHeightRatio = -1;
lastReportedWidth = -1;
lastReportedHeight = -1;
lastReportedPixelWidthHeightRatio = -1;
frameReleaseTimeHelper.disable();
super.onDisabled();
}
@Override
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
if (messageType == MSG_SET_SURFACE) {
setSurface((Surface) message);
} else {
super.handleMessage(messageType, message);
}
}
/**
* @param surface The surface to set.
* @throws ExoPlaybackException
*/
private void setSurface(Surface surface) throws ExoPlaybackException {
if (this.surface == surface) {
return;
}
this.surface = surface;
this.reportedDrawnToSurface = false;
int state = getState();
if (state == TrackRenderer.STATE_ENABLED || state == TrackRenderer.STATE_STARTED) {
releaseCodec();
maybeInitCodec();
}
}
@Override
protected boolean shouldInitCodec() {
return super.shouldInitCodec() && surface != null && surface.isValid();
}
// Override configureCodec to provide the surface.
@Override
protected void configureCodec(MediaCodec codec, boolean codecIsAdaptive,
android.media.MediaFormat format, MediaCrypto crypto) {
maybeSetMaxInputSize(format, codecIsAdaptive);
codec.configure(format, surface, crypto, 0);
}
@Override
protected void onInputFormatChanged(MediaFormatHolder holder) throws ExoPlaybackException {
super.onInputFormatChanged(holder);
pendingPixelWidthHeightRatio = holder.format.pixelWidthHeightRatio == MediaFormat.NO_VALUE ? 1
: holder.format.pixelWidthHeightRatio;
pendingRotationDegrees = holder.format.rotationDegrees == MediaFormat.NO_VALUE ? 0
: holder.format.rotationDegrees;
}
/**
* @return True if the first frame has been rendered (playback has not necessarily begun).
*/
protected final boolean haveRenderedFirstFrame() {
return renderedFirstFrame;
}
@Override
protected void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat outputFormat) {
boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT)
&& outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM)
&& outputFormat.containsKey(KEY_CROP_TOP);
currentWidth = hasCrop
? outputFormat.getInteger(KEY_CROP_RIGHT) - outputFormat.getInteger(KEY_CROP_LEFT) + 1
: outputFormat.getInteger(android.media.MediaFormat.KEY_WIDTH);
currentHeight = hasCrop
? outputFormat.getInteger(KEY_CROP_BOTTOM) - outputFormat.getInteger(KEY_CROP_TOP) + 1
: outputFormat.getInteger(android.media.MediaFormat.KEY_HEIGHT);
currentPixelWidthHeightRatio = pendingPixelWidthHeightRatio;
if (Util.SDK_INT >= 21) {
// On API level 21 and above the decoder applies the rotation when rendering to the surface.
// Hence currentUnappliedRotation should always be 0. For 90 and 270 degree rotations, we need
// to flip the width, height and pixel aspect ratio to reflect the rotation that was applied.
if (pendingRotationDegrees == 90 || pendingRotationDegrees == 270) {
int rotatedHeight = currentWidth;
currentWidth = currentHeight;
currentHeight = rotatedHeight;
currentPixelWidthHeightRatio = 1 / currentPixelWidthHeightRatio;
}
} else {
// On API level 20 and below the decoder does not apply the rotation.
currentUnappliedRotationDegrees = pendingRotationDegrees;
}
// Must be applied each time the output format changes.
codec.setVideoScalingMode(videoScalingMode);
}
@Override
protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
MediaFormat oldFormat, MediaFormat newFormat) {
return newFormat.mimeType.equals(oldFormat.mimeType)
&& (codecIsAdaptive
|| (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height));
}
@Override
protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) {
if (shouldSkip) {
skipOutputBuffer(codec, bufferIndex);
consecutiveDroppedFrameCount = 0;
return true;
}
if (!renderedFirstFrame) {
if (Util.SDK_INT >= 21) {
renderOutputBufferV21(codec, bufferIndex, System.nanoTime());
} else {
renderOutputBuffer(codec, bufferIndex);
}
consecutiveDroppedFrameCount = 0;
return true;
}
if (getState() != TrackRenderer.STATE_STARTED) {
return false;
}
// Compute how many microseconds it is until the buffer's presentation time.
long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs;
long earlyUs = bufferInfo.presentationTimeUs - positionUs - elapsedSinceStartOfLoopUs;
// Compute the buffer's desired release time in nanoseconds.
long systemTimeNs = System.nanoTime();
long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000);
// Apply a timestamp adjustment, if there is one.
long adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime(
bufferInfo.presentationTimeUs, unadjustedFrameReleaseTimeNs);
earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;
if (earlyUs < -30000) {
// We're more than 30ms late rendering the frame.
dropOutputBuffer(codec, bufferIndex);
return true;
}
if (Util.SDK_INT >= 21) {
// Let the underlying framework time the release.
if (earlyUs < 50000) {
renderOutputBufferV21(codec, bufferIndex, adjustedReleaseTimeNs);
consecutiveDroppedFrameCount = 0;
return true;
}
} else {
// We need to time the release ourselves.
if (earlyUs < 30000) {
if (earlyUs > 11000) {
// We're a little too early to render the frame. Sleep until the frame can be rendered.
// Note: The 11ms threshold was chosen fairly arbitrarily.
try {
// Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms.
Thread.sleep((earlyUs - 10000) / 1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
renderOutputBuffer(codec, bufferIndex);
consecutiveDroppedFrameCount = 0;
return true;
}
}
// We're either not playing, or it's not time to render the frame yet.
return false;
}
protected void skipOutputBuffer(MediaCodec codec, int bufferIndex) {
TraceUtil.beginSection("skipVideoBuffer");
codec.releaseOutputBuffer(bufferIndex, false);
TraceUtil.endSection();
codecCounters.skippedOutputBufferCount++;
}
protected void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
TraceUtil.beginSection("dropVideoBuffer");
codec.releaseOutputBuffer(bufferIndex, false);
TraceUtil.endSection();
codecCounters.droppedOutputBufferCount++;
droppedFrameCount++;
consecutiveDroppedFrameCount++;
codecCounters.maxConsecutiveDroppedOutputBufferCount = Math.max(consecutiveDroppedFrameCount,
codecCounters.maxConsecutiveDroppedOutputBufferCount);
if (droppedFrameCount == maxDroppedFrameCountToNotify) {
maybeNotifyDroppedFrameCount();
}
}
protected void renderOutputBuffer(MediaCodec codec, int bufferIndex) {
maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("releaseOutputBuffer");
codec.releaseOutputBuffer(bufferIndex, true);
TraceUtil.endSection();
codecCounters.renderedOutputBufferCount++;
renderedFirstFrame = true;
maybeNotifyDrawnToSurface();
}
@TargetApi(21)
protected void renderOutputBufferV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) {
maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("releaseOutputBuffer");
codec.releaseOutputBuffer(bufferIndex, releaseTimeNs);
TraceUtil.endSection();
codecCounters.renderedOutputBufferCount++;
renderedFirstFrame = true;
maybeNotifyDrawnToSurface();
}
@SuppressLint("InlinedApi")
private void maybeSetMaxInputSize(android.media.MediaFormat format, boolean codecIsAdaptive) {
if (format.containsKey(android.media.MediaFormat.KEY_MAX_INPUT_SIZE)) {
// Already set. The source of the format may know better, so do nothing.
return;
}
int maxHeight = format.getInteger(android.media.MediaFormat.KEY_HEIGHT);
if (codecIsAdaptive && format.containsKey(android.media.MediaFormat.KEY_MAX_HEIGHT)) {
maxHeight = Math.max(maxHeight, format.getInteger(android.media.MediaFormat.KEY_MAX_HEIGHT));
}
int maxWidth = format.getInteger(android.media.MediaFormat.KEY_WIDTH);
if (codecIsAdaptive && format.containsKey(android.media.MediaFormat.KEY_MAX_WIDTH)) {
maxWidth = Math.max(maxHeight, format.getInteger(android.media.MediaFormat.KEY_MAX_WIDTH));
}
int maxPixels;
int minCompressionRatio;
switch (format.getString(android.media.MediaFormat.KEY_MIME)) {
case MimeTypes.VIDEO_H263:
case MimeTypes.VIDEO_MP4V:
maxPixels = maxWidth * maxHeight;
minCompressionRatio = 2;
break;
case MimeTypes.VIDEO_H264:
if ("BRAVIA 4K 2015".equals(Util.MODEL)) {
// The Sony BRAVIA 4k TV has input buffers that are too small for the calculated 4k video
// maximum input size, so use the default value.
return;
}
// Round up width/height to an integer number of macroblocks.
maxPixels = ((maxWidth + 15) / 16) * ((maxHeight + 15) / 16) * 16 * 16;
minCompressionRatio = 2;
break;
case MimeTypes.VIDEO_VP8:
// VPX does not specify a ratio so use the values from the platform's SoftVPX.cpp.
maxPixels = maxWidth * maxHeight;
minCompressionRatio = 2;
break;
case MimeTypes.VIDEO_H265:
case MimeTypes.VIDEO_VP9:
maxPixels = maxWidth * maxHeight;
minCompressionRatio = 4;
break;
default:
// Leave the default max input size.
return;
}
// Estimate the maximum input size assuming three channel 4:2:0 subsampled input frames.
int maxInputSize = (maxPixels * 3) / (2 * minCompressionRatio);
format.setInteger(android.media.MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
}
private void maybeNotifyVideoSizeChanged() {
if (eventHandler == null || eventListener == null
|| (lastReportedWidth == currentWidth && lastReportedHeight == currentHeight
&& lastReportedUnappliedRotationDegrees == currentUnappliedRotationDegrees
&& lastReportedPixelWidthHeightRatio == currentPixelWidthHeightRatio)) {
return;
}
// Make final copies to ensure the runnable reports the correct values.
final int currentWidth = this.currentWidth;
final int currentHeight = this.currentHeight;
final int currentUnappliedRotationDegrees = this.currentUnappliedRotationDegrees;
final float currentPixelWidthHeightRatio = this.currentPixelWidthHeightRatio;
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onVideoSizeChanged(currentWidth, currentHeight,
currentUnappliedRotationDegrees, currentPixelWidthHeightRatio);
}
});
// Update the last reported values.
lastReportedWidth = currentWidth;
lastReportedHeight = currentHeight;
lastReportedUnappliedRotationDegrees = currentUnappliedRotationDegrees;
lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio;
}
private void maybeNotifyDrawnToSurface() {
if (eventHandler == null || eventListener == null || reportedDrawnToSurface) {
return;
}
// Make a final copy to ensure the runnable reports the correct surface.
final Surface surface = this.surface;
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onDrawnToSurface(surface);
}
});
// Record that we have reported that the surface has been drawn to.
reportedDrawnToSurface = true;
}
private void maybeNotifyDroppedFrameCount() {
if (eventHandler == null || eventListener == null || droppedFrameCount == 0) {
return;
}
long now = SystemClock.elapsedRealtime();
// Make final copies to ensure the runnable reports the correct values.
final int countToNotify = droppedFrameCount;
final long elapsedToNotify = now - droppedFrameAccumulationStartTimeMs;
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onDroppedFrames(countToNotify, elapsedToNotify);
}
});
// Reset the dropped frame tracking.
droppedFrameCount = 0;
droppedFrameAccumulationStartTimeMs = now;
}
}
@@ -1,502 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.util.Assertions;
import com.b44t.messenger.exoplayer.util.MimeTypes;
import com.b44t.messenger.exoplayer.util.Util;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.os.Parcel;
import android.os.Parcelable;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Defines the format of an elementary media stream.
*/
public final class MediaFormat implements Parcelable {
public static final int NO_VALUE = -1;
/**
* A value for {@link #subsampleOffsetUs} to indicate that subsample timestamps are relative to
* the timestamps of their parent samples.
*/
public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE;
/**
* The identifier for the track represented by the format, or null if unknown or not applicable.
*/
public final String trackId;
/**
* The mime type of the format.
*/
public final String mimeType;
/**
* The average bandwidth in bits per second, or {@link #NO_VALUE} if unknown or not applicable.
*/
public final int bitrate;
/**
* The maximum size of a buffer of data (typically one sample) in the format, or {@link #NO_VALUE}
* if unknown or not applicable.
*/
public final int maxInputSize;
/**
* The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration is unknown, or
* {@link C#MATCH_LONGEST_US} if the duration should match the duration of the longest track whose
* duration is known.
*/
public final long durationUs;
/**
* Initialization data that must be provided to the decoder. Will not be null, but may be empty
* if initialization data is not required.
*/
public final List<byte[]> initializationData;
/**
* Whether the format represents an adaptive track, meaning that the format of the actual media
* data may change (e.g. to adapt to network conditions).
*/
public final boolean adaptive;
// Video specific.
/**
* The width of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable.
*/
public final int width;
/**
* The height of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable.
*/
public final int height;
/**
* For formats that belong to an adaptive video track (either describing the track, or describing
* a specific format within it), this is the maximum width of the video in pixels that will be
* encountered in the stream. Set to {@link #NO_VALUE} if unknown or not applicable.
*/
public final int maxWidth;
/**
* For formats that belong to an adaptive video track (either describing the track, or describing
* a specific format within it), this is the maximum height of the video in pixels that will be
* encountered in the stream. Set to {@link #NO_VALUE} if unknown or not applicable.
*/
public final int maxHeight;
/**
* The clockwise rotation that should be applied to the video for it to be rendered in the correct
* orientation, or {@link #NO_VALUE} if unknown or not applicable. Only 0, 90, 180 and 270 are
* supported.
*/
public final int rotationDegrees;
/**
* The width to height ratio of pixels in the video, or {@link #NO_VALUE} if unknown or not
* applicable.
*/
public final float pixelWidthHeightRatio;
// Audio specific.
/**
* The number of audio channels, or {@link #NO_VALUE} if unknown or not applicable.
*/
public final int channelCount;
/**
* The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable.
*/
public final int sampleRate;
/**
* The encoding for PCM audio streams. If {@link #mimeType} is {@link MimeTypes#AUDIO_RAW} then
* one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT}
* and {@link C#ENCODING_PCM_32BIT}. Set to {@link #NO_VALUE} for other media types.
*/
public final int pcmEncoding;
/**
* The number of samples to trim from the start of the decoded audio stream.
*/
public final int encoderDelay;
/**
* The number of samples to trim from the end of the decoded audio stream.
*/
public final int encoderPadding;
// Text specific.
/**
* The language of the track, or null if unknown or not applicable.
*/
public final String language;
/**
* For samples that contain subsamples, this is an offset that should be added to subsample
* timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are
* relative to the timestamps of their parent samples.
*/
public final long subsampleOffsetUs;
// Lazy-initialized hashcode and framework media format.
private int hashCode;
private android.media.MediaFormat frameworkMediaFormat;
public static MediaFormat createVideoFormat(String trackId, String mimeType, int bitrate,
int maxInputSize, long durationUs, int width, int height, List<byte[]> initializationData) {
return createVideoFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height,
initializationData, NO_VALUE, NO_VALUE);
}
public static MediaFormat createVideoFormat(String trackId, String mimeType, int bitrate,
int maxInputSize, long durationUs, int width, int height, List<byte[]> initializationData,
int rotationDegrees, float pixelWidthHeightRatio) {
return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height,
rotationDegrees, pixelWidthHeightRatio, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE,
initializationData, false, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE);
}
public static MediaFormat createAudioFormat(String trackId, String mimeType, int bitrate,
int maxInputSize, long durationUs, int channelCount, int sampleRate,
List<byte[]> initializationData, String language) {
return createAudioFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, channelCount,
sampleRate, initializationData, language, NO_VALUE);
}
public static MediaFormat createAudioFormat(String trackId, String mimeType, int bitrate,
int maxInputSize, long durationUs, int channelCount, int sampleRate,
List<byte[]> initializationData, String language, int pcmEncoding) {
return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, channelCount, sampleRate, language, OFFSET_SAMPLE_RELATIVE,
initializationData, false, NO_VALUE, NO_VALUE, pcmEncoding, NO_VALUE, NO_VALUE);
}
public static MediaFormat createTextFormat(String trackId, String mimeType, int bitrate,
long durationUs, String language) {
return createTextFormat(trackId, mimeType, bitrate, durationUs, language,
OFFSET_SAMPLE_RELATIVE);
}
public static MediaFormat createTextFormat(String trackId, String mimeType, int bitrate,
long durationUs, String language, long subsampleOffsetUs) {
return new MediaFormat(trackId, mimeType, bitrate, NO_VALUE, durationUs, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, language, subsampleOffsetUs, null, false, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE);
}
public static MediaFormat createImageFormat(String trackId, String mimeType, int bitrate,
long durationUs, List<byte[]> initializationData, String language) {
return new MediaFormat(trackId, mimeType, bitrate, NO_VALUE, durationUs, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, language, OFFSET_SAMPLE_RELATIVE,
initializationData, false, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE);
}
public static MediaFormat createFormatForMimeType(String trackId, String mimeType, int bitrate,
long durationUs) {
return new MediaFormat(trackId, mimeType, bitrate, NO_VALUE, durationUs, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, null, false, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE);
}
public static MediaFormat createId3Format() {
return createFormatForMimeType(null, MimeTypes.APPLICATION_ID3, MediaFormat.NO_VALUE,
C.UNKNOWN_TIME_US);
}
/* package */ MediaFormat(Parcel in) {
trackId = in.readString();
mimeType = in.readString();
bitrate = in.readInt();
maxInputSize = in.readInt();
durationUs = in.readLong();
width = in.readInt();
height = in.readInt();
rotationDegrees = in.readInt();
pixelWidthHeightRatio = in.readFloat();
channelCount = in.readInt();
sampleRate = in.readInt();
language = in.readString();
subsampleOffsetUs = in.readLong();
initializationData = new ArrayList<>();
in.readList(initializationData, null);
adaptive = in.readInt() == 1;
maxWidth = in.readInt();
maxHeight = in.readInt();
pcmEncoding = in.readInt();
encoderDelay = in.readInt();
encoderPadding = in.readInt();
}
/* package */ MediaFormat(String trackId, String mimeType, int bitrate, int maxInputSize,
long durationUs, int width, int height, int rotationDegrees, float pixelWidthHeightRatio,
int channelCount, int sampleRate, String language, long subsampleOffsetUs,
List<byte[]> initializationData, boolean adaptive, int maxWidth, int maxHeight,
int pcmEncoding, int encoderDelay, int encoderPadding) {
this.trackId = trackId;
this.mimeType = Assertions.checkNotEmpty(mimeType);
this.bitrate = bitrate;
this.maxInputSize = maxInputSize;
this.durationUs = durationUs;
this.width = width;
this.height = height;
this.rotationDegrees = rotationDegrees;
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
this.channelCount = channelCount;
this.sampleRate = sampleRate;
this.language = language;
this.subsampleOffsetUs = subsampleOffsetUs;
this.initializationData = initializationData == null ? Collections.<byte[]>emptyList()
: initializationData;
this.adaptive = adaptive;
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
this.pcmEncoding = pcmEncoding;
this.encoderDelay = encoderDelay;
this.encoderPadding = encoderPadding;
}
public MediaFormat copyWithMaxInputSize(int maxInputSize) {
return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height,
rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language,
subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight, pcmEncoding,
encoderDelay, encoderPadding);
}
public MediaFormat copyWithMaxVideoDimensions(int maxWidth, int maxHeight) {
return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height,
rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language,
subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight, pcmEncoding,
encoderDelay, encoderPadding);
}
public MediaFormat copyWithSubsampleOffsetUs(long subsampleOffsetUs) {
return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height,
rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language,
subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight, pcmEncoding,
encoderDelay, encoderPadding);
}
public MediaFormat copyWithDurationUs(long durationUs) {
return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height,
rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language,
subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight, pcmEncoding,
encoderDelay, encoderPadding);
}
public MediaFormat copyWithLanguage(String language) {
return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height,
rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language,
subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight, pcmEncoding,
encoderDelay, encoderPadding);
}
public MediaFormat copyWithFixedTrackInfo(String trackId, int bitrate, int width, int height,
String language) {
return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height,
rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language,
subsampleOffsetUs, initializationData, adaptive, NO_VALUE, NO_VALUE, pcmEncoding,
encoderDelay, encoderPadding);
}
public MediaFormat copyAsAdaptive(String trackId) {
return new MediaFormat(trackId, mimeType, NO_VALUE, NO_VALUE, durationUs, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, null, true, maxWidth,
maxHeight, NO_VALUE, NO_VALUE, NO_VALUE);
}
public MediaFormat copyWithGaplessInfo(int encoderDelay, int encoderPadding) {
return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height,
rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language,
subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight, pcmEncoding,
encoderDelay, encoderPadding);
}
/**
* @return A {@link MediaFormat} representation of this format.
*/
@SuppressLint("InlinedApi")
@TargetApi(16)
public final android.media.MediaFormat getFrameworkMediaFormatV16() {
if (frameworkMediaFormat == null) {
android.media.MediaFormat format = new android.media.MediaFormat();
format.setString(android.media.MediaFormat.KEY_MIME, mimeType);
maybeSetStringV16(format, android.media.MediaFormat.KEY_LANGUAGE, language);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_WIDTH, width);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT, height);
maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_WIDTH, maxWidth);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_HEIGHT, maxHeight);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT, channelCount);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE, sampleRate);
maybeSetIntegerV16(format, "encoder-delay", encoderDelay);
maybeSetIntegerV16(format, "encoder-padding", encoderPadding);
for (int i = 0; i < initializationData.size(); i++) {
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
}
if (durationUs != C.UNKNOWN_TIME_US) {
format.setLong(android.media.MediaFormat.KEY_DURATION, durationUs);
}
frameworkMediaFormat = format;
}
return frameworkMediaFormat;
}
/**
* Sets the framework format returned by {@link #getFrameworkMediaFormatV16()}.
*
* @deprecated This method only exists for FrameworkSampleSource, which is itself deprecated.
* @param format The framework format.
*/
@Deprecated
@TargetApi(16)
/* package */ final void setFrameworkFormatV16(android.media.MediaFormat format) {
frameworkMediaFormat = format;
}
@Override
public String toString() {
return "MediaFormat(" + trackId + ", " + mimeType + ", " + bitrate + ", " + maxInputSize
+ ", " + width + ", " + height + ", " + rotationDegrees + ", " + pixelWidthHeightRatio
+ ", " + channelCount + ", " + sampleRate + ", " + language + ", " + durationUs + ", "
+ adaptive + ", " + maxWidth + ", " + maxHeight + ", " + pcmEncoding + ", " + encoderDelay
+ ", " + encoderPadding + ")";
}
@Override
public int hashCode() {
if (hashCode == 0) {
int result = 17;
result = 31 * result + (trackId == null ? 0 : trackId.hashCode());
result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode());
result = 31 * result + bitrate;
result = 31 * result + maxInputSize;
result = 31 * result + width;
result = 31 * result + height;
result = 31 * result + rotationDegrees;
result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio);
result = 31 * result + (int) durationUs;
result = 31 * result + (adaptive ? 1231 : 1237);
result = 31 * result + maxWidth;
result = 31 * result + maxHeight;
result = 31 * result + channelCount;
result = 31 * result + sampleRate;
result = 31 * result + pcmEncoding;
result = 31 * result + encoderDelay;
result = 31 * result + encoderPadding;
result = 31 * result + (language == null ? 0 : language.hashCode());
result = 31 * result + (int) subsampleOffsetUs;
for (int i = 0; i < initializationData.size(); i++) {
result = 31 * result + Arrays.hashCode(initializationData.get(i));
}
hashCode = result;
}
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
MediaFormat other = (MediaFormat) obj;
if (adaptive != other.adaptive || bitrate != other.bitrate || maxInputSize != other.maxInputSize
|| durationUs != other.durationUs || width != other.width || height != other.height
|| rotationDegrees != other.rotationDegrees
|| pixelWidthHeightRatio != other.pixelWidthHeightRatio
|| maxWidth != other.maxWidth || maxHeight != other.maxHeight
|| channelCount != other.channelCount || sampleRate != other.sampleRate
|| pcmEncoding != other.pcmEncoding || encoderDelay != other.encoderDelay
|| encoderPadding != other.encoderPadding || subsampleOffsetUs != other.subsampleOffsetUs
|| !Util.areEqual(trackId, other.trackId) || !Util.areEqual(language, other.language)
|| !Util.areEqual(mimeType, other.mimeType)
|| initializationData.size() != other.initializationData.size()) {
return false;
}
for (int i = 0; i < initializationData.size(); i++) {
if (!Arrays.equals(initializationData.get(i), other.initializationData.get(i))) {
return false;
}
}
return true;
}
@TargetApi(16)
private static final void maybeSetStringV16(android.media.MediaFormat format, String key,
String value) {
if (value != null) {
format.setString(key, value);
}
}
@TargetApi(16)
private static final void maybeSetIntegerV16(android.media.MediaFormat format, String key,
int value) {
if (value != NO_VALUE) {
format.setInteger(key, value);
}
}
// Parcelable implementation.
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(trackId);
dest.writeString(mimeType);
dest.writeInt(bitrate);
dest.writeInt(maxInputSize);
dest.writeLong(durationUs);
dest.writeInt(width);
dest.writeInt(height);
dest.writeInt(rotationDegrees);
dest.writeFloat(pixelWidthHeightRatio);
dest.writeInt(channelCount);
dest.writeInt(sampleRate);
dest.writeString(language);
dest.writeLong(subsampleOffsetUs);
dest.writeList(initializationData);
dest.writeInt(adaptive ? 1 : 0);
dest.writeInt(maxWidth);
dest.writeInt(maxHeight);
dest.writeInt(pcmEncoding);
dest.writeInt(encoderDelay);
dest.writeInt(encoderPadding);
}
public static final Creator<MediaFormat> CREATOR = new Creator<MediaFormat>() {
@Override
public MediaFormat createFromParcel(Parcel in) {
return new MediaFormat(in);
}
@Override
public MediaFormat[] newArray(int size) {
return new MediaFormat[size];
}
};
}
@@ -1,34 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.drm.DrmInitData;
/**
* Holds a {@link MediaFormat} and corresponding drm scheme initialization data.
*/
public final class MediaFormatHolder {
/**
* The format of the media.
*/
public MediaFormat format;
/**
* Initialization data for drm schemes supported by the media. Null if the media is not encrypted.
*/
public DrmInitData drmInitData;
}
@@ -1,41 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import java.io.IOException;
/**
* Thrown when an error occurs parsing media data.
*/
public class ParserException extends IOException {
public ParserException() {
super();
}
public ParserException(String message) {
super(message);
}
public ParserException(Throwable cause) {
super(cause);
}
public ParserException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -1,153 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import java.nio.ByteBuffer;
/**
* Holds sample data and corresponding metadata.
*/
public final class SampleHolder {
/**
* Disallows buffer replacement.
*/
public static final int BUFFER_REPLACEMENT_MODE_DISABLED = 0;
/**
* Allows buffer replacement using {@link ByteBuffer#allocate(int)}.
*/
public static final int BUFFER_REPLACEMENT_MODE_NORMAL = 1;
/**
* Allows buffer replacement using {@link ByteBuffer#allocateDirect(int)}.
*/
public static final int BUFFER_REPLACEMENT_MODE_DIRECT = 2;
public final CryptoInfo cryptoInfo;
/**
* A buffer holding the sample data.
*/
public ByteBuffer data;
/**
* The size of the sample in bytes.
*/
public int size;
/**
* Flags that accompany the sample. A combination of {@link C#SAMPLE_FLAG_SYNC},
* {@link C#SAMPLE_FLAG_ENCRYPTED} and {@link C#SAMPLE_FLAG_DECODE_ONLY}.
*/
public int flags;
/**
* The time at which the sample should be presented.
*/
public long timeUs;
private final int bufferReplacementMode;
/**
* @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One
* of {@link #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} and
* {@link #BUFFER_REPLACEMENT_MODE_DIRECT}.
*/
public SampleHolder(int bufferReplacementMode) {
this.cryptoInfo = new CryptoInfo();
this.bufferReplacementMode = bufferReplacementMode;
}
/**
* Ensures that {@link #data} is large enough to accommodate a write of a given length at its
* current position.
* <p>
* If the capacity of {@link #data} is sufficient this method does nothing. If the capacity is
* insufficient then an attempt is made to replace {@link #data} with a new {@link ByteBuffer}
* whose capacity is sufficient. Data up to the current position is copied to the new buffer.
*
* @param length The length of the write that must be accommodated, in bytes.
* @throws IllegalStateException If there is insufficient capacity to accommodate the write and
* the buffer replacement mode of the holder is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}.
*/
public void ensureSpaceForWrite(int length) throws IllegalStateException {
if (data == null) {
data = createReplacementBuffer(length);
return;
}
// Check whether the current buffer is sufficient.
int capacity = data.capacity();
int position = data.position();
int requiredCapacity = position + length;
if (capacity >= requiredCapacity) {
return;
}
// Instantiate a new buffer if possible.
ByteBuffer newData = createReplacementBuffer(requiredCapacity);
// Copy data up to the current position from the old buffer to the new one.
if (position > 0) {
data.position(0);
data.limit(position);
newData.put(data);
}
// Set the new buffer.
data = newData;
}
/**
* Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_ENCRYPTED} set.
*/
public boolean isEncrypted() {
return (flags & C.SAMPLE_FLAG_ENCRYPTED) != 0;
}
/**
* Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_DECODE_ONLY} set.
*/
public boolean isDecodeOnly() {
return (flags & C.SAMPLE_FLAG_DECODE_ONLY) != 0;
}
/**
* Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_SYNC} set.
*/
public boolean isSyncFrame() {
return (flags & C.SAMPLE_FLAG_SYNC) != 0;
}
/**
* Clears {@link #data}. Does nothing if {@link #data} is null.
*/
public void clearData() {
if (data != null) {
data.clear();
}
}
private ByteBuffer createReplacementBuffer(int requiredCapacity) {
if (bufferReplacementMode == BUFFER_REPLACEMENT_MODE_NORMAL) {
return ByteBuffer.allocate(requiredCapacity);
} else if (bufferReplacementMode == BUFFER_REPLACEMENT_MODE_DIRECT) {
return ByteBuffer.allocateDirect(requiredCapacity);
} else {
int currentCapacity = data == null ? 0 : data.capacity();
throw new IllegalStateException("Buffer too small (" + currentCapacity + " < "
+ requiredCapacity + ")");
}
}
}
@@ -1,214 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import java.io.IOException;
/**
* A source of media samples.
* <p>
* A {@link SampleSource} may expose one or multiple tracks. The number of tracks and each track's
* media format can be queried using {@link SampleSourceReader#getTrackCount()} and
* {@link SampleSourceReader#getFormat(int)} respectively.
*/
public interface SampleSource {
/**
* The end of stream has been reached.
*/
public static final int END_OF_STREAM = -1;
/**
* Neither a sample nor a format was read in full. This may be because insufficient data is
* buffered upstream. If multiple tracks are enabled, this return value may indicate that the
* next piece of data to be returned from the {@link SampleSource} corresponds to a different
* track than the one for which data was requested.
*/
public static final int NOTHING_READ = -2;
/**
* A sample was read.
*/
public static final int SAMPLE_READ = -3;
/**
* A format was read.
*/
public static final int FORMAT_READ = -4;
/**
* Returned from {@link SampleSourceReader#readDiscontinuity(int)} to indicate no discontinuity.
*/
public static final long NO_DISCONTINUITY = Long.MIN_VALUE;
/**
* A consumer of samples should call this method to register themselves and gain access to the
* source through the returned {@link SampleSourceReader}.
* <p>
* {@link SampleSourceReader#release()} should be called on the returned object when access is no
* longer required.
*
* @return A {@link SampleSourceReader} that provides access to the source.
*/
public SampleSourceReader register();
/**
* An interface providing read access to a {@link SampleSource}.
*/
public interface SampleSourceReader {
/**
* If the source is currently having difficulty preparing or loading samples, then this method
* throws the underlying error. Otherwise does nothing.
*
* @throws IOException The underlying error.
*/
public void maybeThrowError() throws IOException;
/**
* Prepares the source.
* <p>
* Preparation may require reading from the data source (e.g. to determine the available tracks
* and formats). If insufficient data is available then the call will return {@code false}
* rather than block. The method can be called repeatedly until the return value indicates
* success.
*
* @param positionUs The player's current playback position.
* @return True if the source was prepared, false otherwise.
*/
public boolean prepare(long positionUs);
/**
* Returns the number of tracks exposed by the source.
* <p>
* This method should only be called after the source has been prepared.
*
* @return The number of tracks.
*/
public int getTrackCount();
/**
* Returns the format of the specified track.
* <p>
* Note that whilst the format of a track will remain constant, the format of the actual media
* stream may change dynamically. An example of this is where the track is adaptive
* (i.e. @link {@link MediaFormat#adaptive} is true). Hence the track formats returned through
* this method should not be used to configure decoders. Decoder configuration should be
* performed using the formats obtained when reading the media stream through calls to
* {@link #readData(int, long, MediaFormatHolder, SampleHolder)}.
* <p>
* This method should only be called after the source has been prepared.
*
* @param track The track index.
* @return The format of the specified track.
*/
public MediaFormat getFormat(int track);
/**
* Enable the specified track. This allows the track's format and samples to be read from
* {@link #readData(int, long, MediaFormatHolder, SampleHolder)}.
* <p>
* This method should only be called after the source has been prepared, and when the specified
* track is disabled.
*
* @param track The track to enable.
* @param positionUs The player's current playback position.
*/
public void enable(int track, long positionUs);
/**
* Indicates to the source that it should still be buffering data for the specified track.
* <p>
* This method should only be called when the specified track is enabled.
*
* @param track The track to continue buffering.
* @param positionUs The current playback position.
* @return True if the track has available samples, or if the end of the stream has been
* reached. False if more data needs to be buffered for samples to become available.
*/
public boolean continueBuffering(int track, long positionUs);
/**
* Attempts to read a pending discontinuity from the source.
* <p>
* This method should only be called when the specified track is enabled.
*
* @param track The track from which to read.
* @return If a discontinuity was read then the playback position after the discontinuity. Else
* {@link #NO_DISCONTINUITY}.
*/
public long readDiscontinuity(int track);
/**
* Attempts to read a sample or a new format from the source.
* <p>
* This method should only be called when the specified track is enabled.
* <p>
* Note that where multiple tracks are enabled, {@link #NOTHING_READ} may be returned if the
* next piece of data to be read from the {@link SampleSource} corresponds to a different track
* than the one for which data was requested.
* <p>
* This method will always return {@link #NOTHING_READ} in the case that there's a pending
* discontinuity to be read from {@link #readDiscontinuity(int)} for the specified track.
*
* @param track The track from which to read.
* @param positionUs The current playback position.
* @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new
* format.
* @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample.
* If the caller requires the sample data then it must ensure that {@link SampleHolder#data}
* references a valid output buffer.
* @return The result, which can be {@link #SAMPLE_READ}, {@link #FORMAT_READ},
* {@link #NOTHING_READ} or {@link #END_OF_STREAM}.
*/
public int readData(int track, long positionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder);
/**
* Seeks to the specified time in microseconds.
* <p>
* This method should only be called when at least one track is enabled.
*
* @param positionUs The seek position in microseconds.
*/
public void seekToUs(long positionUs);
/**
* Returns an estimate of the position up to which data is buffered.
* <p>
* This method should only be called when at least one track is enabled.
*
* @return An estimate of the absolute position in microseconds up to which data is buffered,
* or {@link TrackRenderer#END_OF_TRACK_US} if data is buffered to the end of the stream,
* or {@link TrackRenderer#UNKNOWN_TIME_US} if no estimate is available.
*/
public long getBufferedPositionUs();
/**
* Disable the specified track.
* <p>
* This method should only be called when the specified track is enabled.
*
* @param track The track to disable.
*/
public void disable(int track);
/**
* Releases the {@link SampleSourceReader}.
* <p>
* This method should be called when access to the {@link SampleSource} is no longer required.
*/
public void release();
}
}
@@ -1,267 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.MediaCodecUtil.DecoderQueryException;
import com.b44t.messenger.exoplayer.SampleSource.SampleSourceReader;
import java.io.IOException;
import java.util.Arrays;
/**
* Base class for {@link TrackRenderer} implementations that render samples obtained from a
* {@link SampleSource}.
*/
public abstract class SampleSourceTrackRenderer extends TrackRenderer {
private final SampleSourceReader[] sources;
private int[] handledSourceIndices;
private int[] handledSourceTrackIndices;
private SampleSourceReader enabledSource;
private int enabledSourceTrackIndex;
private long durationUs;
/**
* @param sources One or more upstream sources from which the renderer can obtain samples.
*/
public SampleSourceTrackRenderer(SampleSource... sources) {
this.sources = new SampleSourceReader[sources.length];
for (int i = 0; i < sources.length; i++) {
this.sources[i] = sources[i].register();
}
}
@Override
protected final boolean doPrepare(long positionUs) throws ExoPlaybackException {
boolean allSourcesPrepared = true;
for (int i = 0; i < sources.length; i++) {
allSourcesPrepared &= sources[i].prepare(positionUs);
}
if (!allSourcesPrepared) {
return false;
}
// The sources are all prepared.
int totalSourceTrackCount = 0;
for (int i = 0; i < sources.length; i++) {
totalSourceTrackCount += sources[i].getTrackCount();
}
long durationUs = 0;
int handledTrackCount = 0;
int[] handledSourceIndices = new int[totalSourceTrackCount];
int[] handledTrackIndices = new int[totalSourceTrackCount];
int sourceCount = sources.length;
for (int sourceIndex = 0; sourceIndex < sourceCount; sourceIndex++) {
SampleSourceReader source = sources[sourceIndex];
int sourceTrackCount = source.getTrackCount();
for (int trackIndex = 0; trackIndex < sourceTrackCount; trackIndex++) {
MediaFormat format = source.getFormat(trackIndex);
boolean handlesTrack;
try {
handlesTrack = handlesTrack(format);
} catch (DecoderQueryException e) {
throw new ExoPlaybackException(e);
}
if (handlesTrack) {
handledSourceIndices[handledTrackCount] = sourceIndex;
handledTrackIndices[handledTrackCount] = trackIndex;
handledTrackCount++;
if (durationUs == TrackRenderer.UNKNOWN_TIME_US) {
// We've already encountered a track for which the duration is unknown, so the media
// duration is unknown regardless of the duration of this track.
} else {
long trackDurationUs = format.durationUs;
if (trackDurationUs == TrackRenderer.UNKNOWN_TIME_US) {
durationUs = TrackRenderer.UNKNOWN_TIME_US;
} else if (trackDurationUs == TrackRenderer.MATCH_LONGEST_US) {
// Do nothing.
} else {
durationUs = Math.max(durationUs, trackDurationUs);
}
}
}
}
}
this.durationUs = durationUs;
this.handledSourceIndices = Arrays.copyOf(handledSourceIndices, handledTrackCount);
this.handledSourceTrackIndices = Arrays.copyOf(handledTrackIndices, handledTrackCount);
return true;
}
@Override
protected void onEnabled(int track, long positionUs, boolean joining)
throws ExoPlaybackException {
positionUs = shiftInputPosition(positionUs);
enabledSource = sources[handledSourceIndices[track]];
enabledSourceTrackIndex = handledSourceTrackIndices[track];
enabledSource.enable(enabledSourceTrackIndex, positionUs);
onDiscontinuity(positionUs);
}
@Override
protected final void seekTo(long positionUs) throws ExoPlaybackException {
positionUs = shiftInputPosition(positionUs);
enabledSource.seekToUs(positionUs);
checkForDiscontinuity(positionUs);
}
@Override
protected final void doSomeWork(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException {
positionUs = shiftInputPosition(positionUs);
boolean sourceIsReady = enabledSource.continueBuffering(enabledSourceTrackIndex, positionUs);
positionUs = checkForDiscontinuity(positionUs);
doSomeWork(positionUs, elapsedRealtimeUs, sourceIsReady);
}
@Override
protected long getBufferedPositionUs() {
return enabledSource.getBufferedPositionUs();
}
@Override
protected long getDurationUs() {
return durationUs;
}
@Override
protected void maybeThrowError() throws ExoPlaybackException {
if (enabledSource != null) {
maybeThrowError(enabledSource);
} else {
int sourceCount = sources.length;
for (int i = 0; i < sourceCount; i++) {
maybeThrowError(sources[i]);
}
}
}
@Override
protected void onDisabled() throws ExoPlaybackException {
enabledSource.disable(enabledSourceTrackIndex);
enabledSource = null;
}
@Override
protected void onReleased() throws ExoPlaybackException {
int sourceCount = sources.length;
for (int i = 0; i < sourceCount; i++) {
sources[i].release();
}
}
@Override
protected final int getTrackCount() {
return handledSourceTrackIndices.length;
}
@Override
protected final MediaFormat getFormat(int track) {
SampleSourceReader source = sources[handledSourceIndices[track]];
return source.getFormat(handledSourceTrackIndices[track]);
}
/**
* Shifts positions passed to {@link #onEnabled(int, long, boolean)}, {@link #seekTo(long)} and
* {@link #doSomeWork(long, long)}.
* <p>
* The default implementation does not modify the position. Except in very specific cases,
* subclasses should not override this method.
*
* @param positionUs The position in microseconds.
* @return The adjusted position in microseconds.
*/
protected long shiftInputPosition(long positionUs) {
return positionUs;
}
// Methods to be called by subclasses.
/**
* Reads from the enabled upstream source.
*
* @param positionUs The current playback position.
* @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new format.
* @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample.
* If the caller requires the sample data then it must ensure that {@link SampleHolder#data}
* references a valid output buffer.
* @return The result, which can be {@link SampleSource#SAMPLE_READ},
* {@link SampleSource#FORMAT_READ}, {@link SampleSource#NOTHING_READ} or
* {@link SampleSource#END_OF_STREAM}.
*/
protected final int readSource(long positionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder) {
return enabledSource.readData(enabledSourceTrackIndex, positionUs, formatHolder, sampleHolder);
}
// Abstract methods.
/**
* Returns whether this renderer is capable of handling the provided track.
*
* @param mediaFormat The format of the track.
* @return True if the renderer can handle the track, false otherwise.
* @throws DecoderQueryException Thrown if there was an error querying decoders.
*/
protected abstract boolean handlesTrack(MediaFormat mediaFormat) throws DecoderQueryException;
/**
* Invoked when a discontinuity is encountered. Also invoked when the renderer is enabled, for
* convenience.
*
* @param positionUs The playback position after the discontinuity, or the position at which
* the renderer is being enabled.
* @throws ExoPlaybackException If an error occurs handling the discontinuity.
*/
protected abstract void onDiscontinuity(long positionUs) throws ExoPlaybackException;
/**
* Called by {@link #doSomeWork(long, long)}.
*
* @param positionUs The current media time in microseconds, measured at the start of the
* current iteration of the rendering loop.
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
* measured at the start of the current iteration of the rendering loop.
* @param sourceIsReady The result of the most recent call to
* {@link SampleSourceReader#continueBuffering(int, long)}.
* @throws ExoPlaybackException If an error occurs.
* @throws ExoPlaybackException
*/
protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady)
throws ExoPlaybackException;
// Private methods.
private long checkForDiscontinuity(long positionUs) throws ExoPlaybackException {
long discontinuityPositionUs = enabledSource.readDiscontinuity(enabledSourceTrackIndex);
if (discontinuityPositionUs != SampleSource.NO_DISCONTINUITY) {
onDiscontinuity(discontinuityPositionUs);
return discontinuityPositionUs;
}
return positionUs;
}
private void maybeThrowError(SampleSourceReader source) throws ExoPlaybackException {
try {
source.maybeThrowError();
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
}
}
@@ -1,301 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.SampleSource.SampleSourceReader;
import com.b44t.messenger.exoplayer.upstream.DataSource;
import com.b44t.messenger.exoplayer.upstream.DataSpec;
import com.b44t.messenger.exoplayer.upstream.Loader;
import com.b44t.messenger.exoplayer.upstream.Loader.Loadable;
import com.b44t.messenger.exoplayer.util.Assertions;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
import java.io.IOException;
import java.util.Arrays;
/**
* A {@link SampleSource} that loads the data at a given {@link Uri} as a single sample.
*/
public final class SingleSampleSource implements SampleSource, SampleSourceReader, Loader.Callback,
Loadable {
/**
* Interface definition for a callback to be notified of {@link SingleSampleSource} events.
*/
public interface EventListener {
/**
* Invoked when an error occurs loading media data.
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param e The cause of the failure.
*/
void onLoadError(int sourceId, IOException e);
}
/**
* The default minimum number of times to retry loading data prior to failing.
*/
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
/**
* The initial size of the allocation used to hold the sample data.
*/
private static final int INITIAL_SAMPLE_SIZE = 1;
private static final int STATE_SEND_FORMAT = 0;
private static final int STATE_SEND_SAMPLE = 1;
private static final int STATE_END_OF_STREAM = 2;
private final Uri uri;
private final DataSource dataSource;
private final MediaFormat format;
private final int minLoadableRetryCount;
private final Handler eventHandler;
private final EventListener eventListener;
private final int eventSourceId;
private int state;
private byte[] sampleData;
private int sampleSize;
private long pendingDiscontinuityPositionUs;
private boolean loadingFinished;
private Loader loader;
private IOException currentLoadableException;
private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp;
public SingleSampleSource(Uri uri, DataSource dataSource, MediaFormat format) {
this(uri, dataSource, format, DEFAULT_MIN_LOADABLE_RETRY_COUNT);
}
public SingleSampleSource(Uri uri, DataSource dataSource, MediaFormat format,
int minLoadableRetryCount) {
this(uri, dataSource, format, minLoadableRetryCount, null, null, 0);
}
public SingleSampleSource(Uri uri, DataSource dataSource, MediaFormat format,
int minLoadableRetryCount, Handler eventHandler, EventListener eventListener,
int eventSourceId) {
this.uri = uri;
this.dataSource = dataSource;
this.format = format;
this.minLoadableRetryCount = minLoadableRetryCount;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
this.eventSourceId = eventSourceId;
sampleData = new byte[INITIAL_SAMPLE_SIZE];
}
@Override
public SampleSourceReader register() {
return this;
}
@Override
public boolean prepare(long positionUs) {
if (loader == null) {
loader = new Loader("Loader:" + format.mimeType);
}
return true;
}
@Override
public int getTrackCount() {
return 1;
}
@Override
public MediaFormat getFormat(int track) {
return format;
}
@Override
public void enable(int track, long positionUs) {
state = STATE_SEND_FORMAT;
pendingDiscontinuityPositionUs = NO_DISCONTINUITY;
clearCurrentLoadableException();
maybeStartLoading();
}
@Override
public boolean continueBuffering(int track, long positionUs) {
maybeStartLoading();
return loadingFinished;
}
@Override
public void maybeThrowError() throws IOException {
if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) {
throw currentLoadableException;
}
}
@Override
public long readDiscontinuity(int track) {
long discontinuityPositionUs = pendingDiscontinuityPositionUs;
pendingDiscontinuityPositionUs = NO_DISCONTINUITY;
return discontinuityPositionUs;
}
@Override
public int readData(int track, long positionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder) {
if (state == STATE_END_OF_STREAM) {
return END_OF_STREAM;
} else if (state == STATE_SEND_FORMAT) {
formatHolder.format = format;
state = STATE_SEND_SAMPLE;
return FORMAT_READ;
}
Assertions.checkState(state == STATE_SEND_SAMPLE);
if (!loadingFinished) {
return NOTHING_READ;
} else {
sampleHolder.timeUs = 0;
sampleHolder.size = sampleSize;
sampleHolder.flags = C.SAMPLE_FLAG_SYNC;
sampleHolder.ensureSpaceForWrite(sampleHolder.size);
sampleHolder.data.put(sampleData, 0, sampleSize);
state = STATE_END_OF_STREAM;
return SAMPLE_READ;
}
}
@Override
public void seekToUs(long positionUs) {
if (state == STATE_END_OF_STREAM) {
pendingDiscontinuityPositionUs = positionUs;
state = STATE_SEND_SAMPLE;
}
}
@Override
public long getBufferedPositionUs() {
return loadingFinished ? TrackRenderer.END_OF_TRACK_US : 0;
}
@Override
public void disable(int track) {
state = STATE_END_OF_STREAM;
}
@Override
public void release() {
if (loader != null) {
loader.release();
loader = null;
}
}
// Private methods.
private void maybeStartLoading() {
if (loadingFinished || state == STATE_END_OF_STREAM || loader.isLoading()) {
return;
}
if (currentLoadableException != null) {
long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp;
if (elapsedMillis < getRetryDelayMillis(currentLoadableExceptionCount)) {
return;
}
currentLoadableException = null;
}
loader.startLoading(this, this);
}
private void clearCurrentLoadableException() {
currentLoadableException = null;
currentLoadableExceptionCount = 0;
}
private long getRetryDelayMillis(long errorCount) {
return Math.min((errorCount - 1) * 1000, 5000);
}
// Loader.Callback implementation.
@Override
public void onLoadCompleted(Loadable loadable) {
loadingFinished = true;
clearCurrentLoadableException();
}
@Override
public void onLoadCanceled(Loadable loadable) {
// Never happens.
}
@Override
public void onLoadError(Loadable loadable, IOException e) {
currentLoadableException = e;
currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
notifyLoadError(e);
maybeStartLoading();
}
// Loadable implementation.
@Override
public void cancelLoad() {
// Never happens.
}
@Override
public boolean isLoadCanceled() {
return false;
}
@Override
public void load() throws IOException, InterruptedException {
// We always load from the beginning, so reset the sampleSize to 0.
sampleSize = 0;
try {
// Create and open the input.
dataSource.open(new DataSpec(uri));
// Load the sample data.
int result = 0;
while (result != C.RESULT_END_OF_INPUT) {
sampleSize += result;
if (sampleSize == sampleData.length) {
sampleData = Arrays.copyOf(sampleData, sampleData.length * 2);
}
result = dataSource.read(sampleData, sampleSize, sampleData.length - sampleSize);
}
} finally {
dataSource.close();
}
}
private void notifyLoadError(final IOException e) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onLoadError(eventSourceId, e);
}
});
}
}
}
@@ -1,76 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import android.os.SystemClock;
/**
* A standalone {@link MediaClock}. The clock can be started, stopped and its time can be set and
* retrieved. When started, this clock is based on {@link SystemClock#elapsedRealtime()}.
*/
/* package */ final class StandaloneMediaClock implements MediaClock {
private boolean started;
/**
* The media time when the clock was last set or stopped.
*/
private long positionUs;
/**
* The difference between {@link SystemClock#elapsedRealtime()} and {@link #positionUs}
* when the clock was last set or started.
*/
private long deltaUs;
/**
* Starts the clock. Does nothing if the clock is already started.
*/
public void start() {
if (!started) {
started = true;
deltaUs = elapsedRealtimeMinus(positionUs);
}
}
/**
* Stops the clock. Does nothing if the clock is already stopped.
*/
public void stop() {
if (started) {
positionUs = elapsedRealtimeMinus(deltaUs);
started = false;
}
}
/**
* @param timeUs The position to set in microseconds.
*/
public void setPositionUs(long timeUs) {
this.positionUs = timeUs;
deltaUs = elapsedRealtimeMinus(timeUs);
}
@Override
public long getPositionUs() {
return started ? elapsedRealtimeMinus(deltaUs) : positionUs;
}
private long elapsedRealtimeMinus(long toSubtractUs) {
return SystemClock.elapsedRealtime() * 1000 - toSubtractUs;
}
}
@@ -1,203 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.util.Clock;
import android.os.SystemClock;
/**
* A container to store a start and end time in microseconds.
*/
public interface TimeRange {
/**
* Whether the range is static, meaning repeated calls to {@link #getCurrentBoundsMs(long[])}
* or {@link #getCurrentBoundsUs(long[])} will return identical results.
*
* @return Whether the range is static.
*/
public boolean isStatic();
/**
* Returns the start and end times (in milliseconds) of the TimeRange in the provided array,
* or creates a new one.
*
* @param out An array to store the start and end times; can be null.
* @return An array containing the start time (index 0) and end time (index 1) in milliseconds.
*/
public long[] getCurrentBoundsMs(long[] out);
/**
* Returns the start and end times (in microseconds) of the TimeRange in the provided array,
* or creates a new one.
*
* @param out An array to store the start and end times; can be null.
* @return An array containing the start time (index 0) and end time (index 1) in microseconds.
*/
public long[] getCurrentBoundsUs(long[] out);
/**
* A static {@link TimeRange}.
*/
public static final class StaticTimeRange implements TimeRange {
private final long startTimeUs;
private final long endTimeUs;
/**
* @param startTimeUs The beginning of the range.
* @param endTimeUs The end of the range.
*/
public StaticTimeRange(long startTimeUs, long endTimeUs) {
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;
}
@Override
public boolean isStatic() {
return true;
}
@Override
public long[] getCurrentBoundsMs(long[] out) {
out = getCurrentBoundsUs(out);
out[0] /= 1000;
out[1] /= 1000;
return out;
}
@Override
public long[] getCurrentBoundsUs(long[] out) {
if (out == null || out.length < 2) {
out = new long[2];
}
out[0] = startTimeUs;
out[1] = endTimeUs;
return out;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (int) startTimeUs;
result = 31 * result + (int) endTimeUs;
return result;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
StaticTimeRange other = (StaticTimeRange) obj;
return other.startTimeUs == startTimeUs
&& other.endTimeUs == endTimeUs;
}
}
/**
* A dynamic {@link TimeRange}.
*/
public static final class DynamicTimeRange implements TimeRange {
private final long minStartTimeUs;
private final long maxEndTimeUs;
private final long elapsedRealtimeAtStartUs;
private final long bufferDepthUs;
private final Clock systemClock;
/**
* @param minStartTimeUs A lower bound on the beginning of the range.
* @param maxEndTimeUs An upper bound on the end of the range.
* @param elapsedRealtimeAtStartUs The value of {@link SystemClock#elapsedRealtime()},
* multiplied by 1000, corresponding to a media time of zero.
* @param bufferDepthUs The buffer depth of the media, or -1.
* @param systemClock A system clock.
*/
public DynamicTimeRange(long minStartTimeUs, long maxEndTimeUs, long elapsedRealtimeAtStartUs,
long bufferDepthUs, Clock systemClock) {
this.minStartTimeUs = minStartTimeUs;
this.maxEndTimeUs = maxEndTimeUs;
this.elapsedRealtimeAtStartUs = elapsedRealtimeAtStartUs;
this.bufferDepthUs = bufferDepthUs;
this.systemClock = systemClock;
}
@Override
public boolean isStatic() {
return false;
}
@Override
public long[] getCurrentBoundsMs(long[] out) {
out = getCurrentBoundsUs(out);
out[0] /= 1000;
out[1] /= 1000;
return out;
}
@Override
public long[] getCurrentBoundsUs(long[] out) {
if (out == null || out.length < 2) {
out = new long[2];
}
// Don't allow the end time to be greater than the total elapsed time.
long currentEndTimeUs = Math.min(maxEndTimeUs,
(systemClock.elapsedRealtime() * 1000) - elapsedRealtimeAtStartUs);
long currentStartTimeUs = minStartTimeUs;
if (bufferDepthUs != -1) {
// Don't allow the start time to be less than the current end time minus the buffer depth.
currentStartTimeUs = Math.max(currentStartTimeUs,
currentEndTimeUs - bufferDepthUs);
}
out[0] = currentStartTimeUs;
out[1] = currentEndTimeUs;
return out;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (int) minStartTimeUs;
result = 31 * result + (int) maxEndTimeUs;
result = 31 * result + (int) elapsedRealtimeAtStartUs;
result = 31 * result + (int) bufferDepthUs;
return result;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
DynamicTimeRange other = (DynamicTimeRange) obj;
return other.minStartTimeUs == minStartTimeUs
&& other.maxEndTimeUs == maxEndTimeUs
&& other.elapsedRealtimeAtStartUs == elapsedRealtimeAtStartUs
&& other.bufferDepthUs == bufferDepthUs;
}
}
}
@@ -1,371 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import com.b44t.messenger.exoplayer.ExoPlayer.ExoPlayerComponent;
import com.b44t.messenger.exoplayer.util.Assertions;
/**
* Renders a single component of media.
*
* <p>Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The player
* will transition its renderers through various states as the overall playback state changes. The
* valid state transitions are shown below, annotated with the methods that are invoked during each
* transition.
* <p align="center"><img src="../../../../../images/trackrenderer_state.png"
* alt="TrackRenderer state transitions"
* border="0"/></p>
*/
public abstract class TrackRenderer implements ExoPlayerComponent {
/**
* Represents an unknown time or duration. Equal to {@link C#UNKNOWN_TIME_US}.
*/
public static final long UNKNOWN_TIME_US = C.UNKNOWN_TIME_US; // -1
/**
* Represents a time or duration that should match the duration of the longest track whose
* duration is known. Equal to {@link C#MATCH_LONGEST_US}.
*/
public static final long MATCH_LONGEST_US = C.MATCH_LONGEST_US; // -2
/**
* Represents the time of the end of the track.
*/
public static final long END_OF_TRACK_US = -3;
/**
* The renderer has been released and should not be used.
*/
protected static final int STATE_RELEASED = -1;
/**
* The renderer has not yet been prepared.
*/
protected static final int STATE_UNPREPARED = 0;
/**
* The renderer has completed necessary preparation. Preparation may include, for example,
* reading the header of a media file to determine the track format and duration.
* <p>
* The renderer should not hold scarce or expensive system resources (e.g. media decoders) and
* should not be actively buffering media data when in this state.
*/
protected static final int STATE_PREPARED = 1;
/**
* The renderer is enabled. It should either be ready to be started, or be actively working
* towards this state (e.g. a renderer in this state will typically hold any resources that it
* requires, such as media decoders, and will have buffered or be buffering any media data that
* is required to start playback).
*/
protected static final int STATE_ENABLED = 2;
/**
* The renderer is started. Calls to {@link #doSomeWork(long, long)} should cause the media to be
* rendered.
*/
protected static final int STATE_STARTED = 3;
private int state;
/**
* If the renderer advances its own playback position then this method returns a corresponding
* {@link MediaClock}. If provided, the player will use the returned {@link MediaClock} as its
* source of time during playback. A player may have at most one renderer that returns a
* {@link MediaClock} from this method.
*
* @return The {@link MediaClock} tracking the playback position of the renderer, or null.
*/
protected MediaClock getMediaClock() {
return null;
}
/**
* Returns the current state of the renderer.
*
* @return The current state (one of the STATE_* constants).
*/
protected final int getState() {
return state;
}
/**
* Prepares the renderer. This method is non-blocking, and hence it may be necessary to call it
* more than once in order to transition the renderer into the prepared state.
*
* @param positionUs The player's current playback position.
* @return The current state (one of the STATE_* constants), for convenience.
* @throws ExoPlaybackException If an error occurs.
*/
/* package */ final int prepare(long positionUs) throws ExoPlaybackException {
Assertions.checkState(state == STATE_UNPREPARED);
state = doPrepare(positionUs) ? STATE_PREPARED : STATE_UNPREPARED;
return state;
}
/**
* Invoked to make progress when the renderer is in the {@link #STATE_UNPREPARED} state. This
* method will be called repeatedly until {@code true} is returned.
* <p>
* This method should return quickly, and should not block if the renderer is currently unable to
* make any useful progress.
*
* @param positionUs The player's current playback position.
* @return True if the renderer is now prepared. False otherwise.
* @throws ExoPlaybackException If an error occurs.
*/
protected abstract boolean doPrepare(long positionUs) throws ExoPlaybackException;
/**
* Returns the number of tracks exposed by the renderer.
* <p>
* This method may be called when the renderer is in the following states:
* {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED}
*
* @return The number of tracks.
*/
protected abstract int getTrackCount();
/**
* Returns the format of the specified track.
* <p>
* This method may be called when the renderer is in the following states:
* {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED}
*
* @param track The track index.
* @return The format of the specified track.
*/
protected abstract MediaFormat getFormat(int track);
/**
* Enable the renderer for a specified track.
*
* @param track The track for which the renderer is being enabled.
* @param positionUs The player's current position.
* @param joining Whether this renderer is being enabled to join an ongoing playback.
* @throws ExoPlaybackException If an error occurs.
*/
/* package */ final void enable(int track, long positionUs, boolean joining)
throws ExoPlaybackException {
Assertions.checkState(state == STATE_PREPARED);
state = STATE_ENABLED;
onEnabled(track, positionUs, joining);
}
/**
* Called when the renderer is enabled.
* <p>
* The default implementation is a no-op.
*
* @param track The track for which the renderer is being enabled.
* @param positionUs The player's current position.
* @param joining Whether this renderer is being enabled to join an ongoing playback.
* @throws ExoPlaybackException If an error occurs.
*/
protected void onEnabled(int track, long positionUs, boolean joining)
throws ExoPlaybackException {
// Do nothing.
}
/**
* Starts the renderer, meaning that calls to {@link #doSomeWork(long, long)} will cause the
* track to be rendered.
*
* @throws ExoPlaybackException If an error occurs.
*/
/* package */ final void start() throws ExoPlaybackException {
Assertions.checkState(state == STATE_ENABLED);
state = STATE_STARTED;
onStarted();
}
/**
* Called when the renderer is started.
* <p>
* The default implementation is a no-op.
*
* @throws ExoPlaybackException If an error occurs.
*/
protected void onStarted() throws ExoPlaybackException {
// Do nothing.
}
/**
* Stops the renderer.
*
* @throws ExoPlaybackException If an error occurs.
*/
/* package */ final void stop() throws ExoPlaybackException {
Assertions.checkState(state == STATE_STARTED);
state = STATE_ENABLED;
onStopped();
}
/**
* Called when the renderer is stopped.
* <p>
* The default implementation is a no-op.
*
* @throws ExoPlaybackException If an error occurs.
*/
protected void onStopped() throws ExoPlaybackException {
// Do nothing.
}
/**
* Disable the renderer.
*
* @throws ExoPlaybackException If an error occurs.
*/
/* package */ final void disable() throws ExoPlaybackException {
Assertions.checkState(state == STATE_ENABLED);
state = STATE_PREPARED;
onDisabled();
}
/**
* Called when the renderer is disabled.
* <p>
* The default implementation is a no-op.
*
* @throws ExoPlaybackException If an error occurs.
*/
protected void onDisabled() throws ExoPlaybackException {
// Do nothing.
}
/**
* Releases the renderer.
*
* @throws ExoPlaybackException If an error occurs.
*/
/* package */ final void release() throws ExoPlaybackException {
Assertions.checkState(state != STATE_ENABLED
&& state != STATE_STARTED
&& state != STATE_RELEASED);
state = STATE_RELEASED;
onReleased();
}
/**
* Called when the renderer is released.
* <p>
* The default implementation is a no-op.
*
* @throws ExoPlaybackException If an error occurs.
*/
protected void onReleased() throws ExoPlaybackException {
// Do nothing.
}
/**
* Whether the renderer is ready for the {@link ExoPlayer} instance to transition to
* {@link ExoPlayer#STATE_ENDED}. The player will make this transition as soon as {@code true} is
* returned by all of its {@link TrackRenderer}s.
* <p>
* This method may be called when the renderer is in the following states:
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
*
* @return Whether the renderer is ready for the player to transition to the ended state.
*/
protected abstract boolean isEnded();
/**
* Whether the renderer is able to immediately render media from the current position.
* <p>
* If the renderer is in the {@link #STATE_STARTED} state then returning true indicates that the
* renderer has everything that it needs to continue playback. Returning false indicates that
* the player should pause until the renderer is ready.
* <p>
* If the renderer is in the {@link #STATE_ENABLED} state then returning true indicates that the
* renderer is ready for playback to be started. Returning false indicates that it is not.
* <p>
* This method may be called when the renderer is in the following states:
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
*
* @return True if the renderer is ready to render media. False otherwise.
*/
protected abstract boolean isReady();
/**
* Invoked to make progress when the renderer is in the {@link #STATE_ENABLED} or
* {@link #STATE_STARTED} states.
* <p>
* If the renderer's state is {@link #STATE_STARTED}, then repeated calls to this method should
* cause the media track to be rendered. If the state is {@link #STATE_ENABLED}, then repeated
* calls should make progress towards getting the renderer into a position where it is ready to
* render the track.
* <p>
* This method should return quickly, and should not block if the renderer is currently unable to
* make any useful progress.
* <p>
* This method may be called when the renderer is in the following states:
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
*
* @param positionUs The current media time in microseconds, measured at the start of the
* current iteration of the rendering loop.
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
* measured at the start of the current iteration of the rendering loop.
* @throws ExoPlaybackException If an error occurs.
*/
protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException;
/**
* Throws an error that's preventing the renderer from making progress or buffering more data at
* this point in time.
*
* @throws ExoPlaybackException An error that's preventing the renderer from making progress or
* buffering more data.
*/
protected abstract void maybeThrowError() throws ExoPlaybackException;
/**
* Returns the duration of the media being rendered.
* <p>
* This method may be called when the renderer is in the following states:
* {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED}
*
* @return The duration of the track in microseconds, or {@link #MATCH_LONGEST_US} if
* the track's duration should match that of the longest track whose duration is known, or
* or {@link #UNKNOWN_TIME_US} if the duration is not known.
*/
protected abstract long getDurationUs();
/**
* Returns an estimate of the absolute position in microseconds up to which data is buffered.
* <p>
* This method may be called when the renderer is in the following states:
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
*
* @return An estimate of the absolute position in microseconds up to which data is buffered,
* or {@link #END_OF_TRACK_US} if the track is fully buffered, or {@link #UNKNOWN_TIME_US} if
* no estimate is available.
*/
protected abstract long getBufferedPositionUs();
/**
* Seeks to a specified time in the track.
* <p>
* This method may be called when the renderer is in the following states:
* {@link #STATE_ENABLED}
*
* @param positionUs The desired playback position in microseconds.
* @throws ExoPlaybackException If an error occurs.
*/
protected abstract void seekTo(long positionUs) throws ExoPlaybackException;
@Override
public void handleMessage(int what, Object object) throws ExoPlaybackException {
// Do nothing.
}
}
@@ -1,305 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.view.Choreographer;
import android.view.Choreographer.FrameCallback;
import android.view.WindowManager;
/**
* Makes a best effort to adjust frame release timestamps for a smoother visual result.
*/
@TargetApi(16)
public final class VideoFrameReleaseTimeHelper {
private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500;
private static final long MAX_ALLOWED_DRIFT_NS = 20000000;
private static final long VSYNC_OFFSET_PERCENTAGE = 80;
private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6;
private final VSyncSampler vsyncSampler;
private final boolean useDefaultDisplayVsync;
private final long vsyncDurationNs;
private final long vsyncOffsetNs;
private long lastFramePresentationTimeUs;
private long adjustedLastFrameTimeNs;
private long pendingAdjustedFrameTimeNs;
private boolean haveSync;
private long syncUnadjustedReleaseTimeNs;
private long syncFramePresentationTimeNs;
private long frameCount;
/**
* Constructs an instance that smoothes frame release but does not snap release to the default
* display's vsync signal.
*/
public VideoFrameReleaseTimeHelper() {
this(-1, false);
}
/**
* Constructs an instance that smoothes frame release and snaps release to the default display's
* vsync signal.
*
* @param context A context from which information about the default display can be retrieved.
*/
public VideoFrameReleaseTimeHelper(Context context) {
this(getDefaultDisplayRefreshRate(context), true);
}
private VideoFrameReleaseTimeHelper(float defaultDisplayRefreshRate,
boolean useDefaultDisplayVsync) {
this.useDefaultDisplayVsync = useDefaultDisplayVsync;
if (useDefaultDisplayVsync) {
vsyncSampler = VSyncSampler.getInstance();
vsyncDurationNs = (long) (1000000000d / defaultDisplayRefreshRate);
vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100;
} else {
vsyncSampler = null;
vsyncDurationNs = -1;
vsyncOffsetNs = -1;
}
}
/**
* Enables the helper.
*/
public void enable() {
haveSync = false;
if (useDefaultDisplayVsync) {
vsyncSampler.addObserver();
}
}
/**
* Disables the helper.
*/
public void disable() {
if (useDefaultDisplayVsync) {
vsyncSampler.removeObserver();
}
}
/**
* Called to make a fine-grained adjustment to a frame release time.
*
* @param framePresentationTimeUs The frame's media presentation time, in microseconds.
* @param unadjustedReleaseTimeNs The frame's unadjusted release time, in nanoseconds and in
* the same time base as {@link System#nanoTime()}.
* @return An adjusted release time for the frame, in nanoseconds and in the same time base as
* {@link System#nanoTime()}.
*/
public long adjustReleaseTime(long framePresentationTimeUs, long unadjustedReleaseTimeNs) {
long framePresentationTimeNs = framePresentationTimeUs * 1000;
// Until we know better, the adjustment will be a no-op.
long adjustedFrameTimeNs = framePresentationTimeNs;
long adjustedReleaseTimeNs = unadjustedReleaseTimeNs;
if (haveSync) {
// See if we've advanced to the next frame.
if (framePresentationTimeUs != lastFramePresentationTimeUs) {
frameCount++;
adjustedLastFrameTimeNs = pendingAdjustedFrameTimeNs;
}
if (frameCount >= MIN_FRAMES_FOR_ADJUSTMENT) {
// We're synced and have waited the required number of frames to apply an adjustment.
// Calculate the average frame time across all the frames we've seen since the last sync.
// This will typically give us a frame rate at a finer granularity than the frame times
// themselves (which often only have millisecond granularity).
long averageFrameDurationNs = (framePresentationTimeNs - syncFramePresentationTimeNs)
/ frameCount;
// Project the adjusted frame time forward using the average.
long candidateAdjustedFrameTimeNs = adjustedLastFrameTimeNs + averageFrameDurationNs;
if (isDriftTooLarge(candidateAdjustedFrameTimeNs, unadjustedReleaseTimeNs)) {
haveSync = false;
} else {
adjustedFrameTimeNs = candidateAdjustedFrameTimeNs;
adjustedReleaseTimeNs = syncUnadjustedReleaseTimeNs + adjustedFrameTimeNs
- syncFramePresentationTimeNs;
}
} else {
// We're synced but haven't waited the required number of frames to apply an adjustment.
// Check drift anyway.
if (isDriftTooLarge(framePresentationTimeNs, unadjustedReleaseTimeNs)) {
haveSync = false;
}
}
}
// If we need to sync, do so now.
if (!haveSync) {
syncFramePresentationTimeNs = framePresentationTimeNs;
syncUnadjustedReleaseTimeNs = unadjustedReleaseTimeNs;
frameCount = 0;
haveSync = true;
onSynced();
}
lastFramePresentationTimeUs = framePresentationTimeUs;
pendingAdjustedFrameTimeNs = adjustedFrameTimeNs;
if (vsyncSampler == null || vsyncSampler.sampledVsyncTimeNs == 0) {
return adjustedReleaseTimeNs;
}
// Find the timestamp of the closest vsync. This is the vsync that we're targeting.
long snappedTimeNs = closestVsync(adjustedReleaseTimeNs,
vsyncSampler.sampledVsyncTimeNs, vsyncDurationNs);
// Apply an offset so that we release before the target vsync, but after the previous one.
return snappedTimeNs - vsyncOffsetNs;
}
protected void onSynced() {
// Do nothing.
}
private boolean isDriftTooLarge(long frameTimeNs, long releaseTimeNs) {
long elapsedFrameTimeNs = frameTimeNs - syncFramePresentationTimeNs;
long elapsedReleaseTimeNs = releaseTimeNs - syncUnadjustedReleaseTimeNs;
return Math.abs(elapsedReleaseTimeNs - elapsedFrameTimeNs) > MAX_ALLOWED_DRIFT_NS;
}
private static long closestVsync(long releaseTime, long sampledVsyncTime, long vsyncDuration) {
long vsyncCount = (releaseTime - sampledVsyncTime) / vsyncDuration;
long snappedTimeNs = sampledVsyncTime + (vsyncDuration * vsyncCount);
long snappedBeforeNs;
long snappedAfterNs;
if (releaseTime <= snappedTimeNs) {
snappedBeforeNs = snappedTimeNs - vsyncDuration;
snappedAfterNs = snappedTimeNs;
} else {
snappedBeforeNs = snappedTimeNs;
snappedAfterNs = snappedTimeNs + vsyncDuration;
}
long snappedAfterDiff = snappedAfterNs - releaseTime;
long snappedBeforeDiff = releaseTime - snappedBeforeNs;
return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs;
}
private static float getDefaultDisplayRefreshRate(Context context) {
WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
return manager.getDefaultDisplay().getRefreshRate();
}
/**
* Manages the lifecycle of a single {@link Choreographer} to be shared among all
* {@link VideoFrameReleaseTimeHelper} instances. This is done to avoid a bug fixed in platform
* API version 23 that causes resource leakage. See [Internal: b/12455729].
*/
private static final class VSyncSampler implements FrameCallback, Handler.Callback {
public volatile long sampledVsyncTimeNs;
private static final int CREATE_CHOREOGRAPHER = 0;
private static final int MSG_ADD_OBSERVER = 1;
private static final int MSG_REMOVE_OBSERVER = 2;
private static final VSyncSampler INSTANCE = new VSyncSampler();
private final Handler handler;
private final HandlerThread choreographerOwnerThread;
private Choreographer choreographer;
private int observerCount;
public static VSyncSampler getInstance() {
return INSTANCE;
}
private VSyncSampler() {
choreographerOwnerThread = new HandlerThread("ChoreographerOwner:Handler");
choreographerOwnerThread.start();
handler = new Handler(choreographerOwnerThread.getLooper(), this);
handler.sendEmptyMessage(CREATE_CHOREOGRAPHER);
}
/**
* Tells the {@link VSyncSampler} that there is a new {@link VideoFrameReleaseTimeHelper}
* instance observing the currentSampledVsyncTimeNs value. As a consequence, if necessary, it
* will register itself as a {@code doFrame} callback listener.
*/
public void addObserver() {
handler.sendEmptyMessage(MSG_ADD_OBSERVER);
}
/**
* Counterpart of {@code addNewObservingHelper}. This method should be called once the observer
* no longer needs to read {@link #sampledVsyncTimeNs}
*/
public void removeObserver() {
handler.sendEmptyMessage(MSG_REMOVE_OBSERVER);
}
@Override
public void doFrame(long vsyncTimeNs) {
sampledVsyncTimeNs = vsyncTimeNs;
choreographer.postFrameCallbackDelayed(this, CHOREOGRAPHER_SAMPLE_DELAY_MILLIS);
}
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case CREATE_CHOREOGRAPHER: {
createChoreographerInstanceInternal();
return true;
}
case MSG_ADD_OBSERVER: {
addObserverInternal();
return true;
}
case MSG_REMOVE_OBSERVER: {
removeObserverInternal();
return true;
}
default: {
return false;
}
}
}
private void createChoreographerInstanceInternal() {
choreographer = Choreographer.getInstance();
}
private void addObserverInternal() {
observerCount++;
if (observerCount == 1) {
choreographer.postFrameCallback(this);
}
}
private void removeObserverInternal() {
observerCount--;
if (observerCount == 0) {
choreographer.removeFrameCallback(this);
sampledVsyncTimeNs = 0;
}
}
}
}
@@ -1,122 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer.audio;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioFormat;
import android.media.AudioManager;
import java.util.Arrays;
/**
* Represents the set of audio formats a device is capable of playing back.
*/
@TargetApi(21)
public final class AudioCapabilities {
/**
* The minimum audio capabilities supported by all devices.
*/
public static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES =
new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, 2);
/**
* Gets the current audio capabilities. Note that to be notified when audio capabilities change,
* you can create an instance of {@link AudioCapabilitiesReceiver} and register a listener.
*
* @param context Context for receiving the initial broadcast.
* @return Current audio capabilities for the device.
*/
@SuppressWarnings("InlinedApi")
public static AudioCapabilities getCapabilities(Context context) {
return getCapabilities(
context.registerReceiver(null, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG)));
}
@SuppressLint("InlinedApi")
/* package */ static AudioCapabilities getCapabilities(Intent intent) {
if (intent == null || intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0) == 0) {
return DEFAULT_AUDIO_CAPABILITIES;
}
return new AudioCapabilities(intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS),
intent.getIntExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, 0));
}
private final int[] supportedEncodings;
private final int maxChannelCount;
/**
* Constructs new audio capabilities based on a set of supported encodings and a maximum channel
* count.
*
* @param supportedEncodings Supported audio encodings from {@link android.media.AudioFormat}'s
* {@code ENCODING_*} constants.
* @param maxChannelCount The maximum number of audio channels that can be played simultaneously.
*/
/* package */ AudioCapabilities(int[] supportedEncodings, int maxChannelCount) {
if (supportedEncodings != null) {
this.supportedEncodings = Arrays.copyOf(supportedEncodings, supportedEncodings.length);
Arrays.sort(this.supportedEncodings);
} else {
this.supportedEncodings = new int[0];
}
this.maxChannelCount = maxChannelCount;
}
/**
* Returns whether this device supports playback of the specified audio {@code encoding}.
*
* @param encoding One of {@link android.media.AudioFormat}'s {@code ENCODING_*} constants.
* @return Whether this device supports playback the specified audio {@code encoding}.
*/
public boolean supportsEncoding(int encoding) {
return Arrays.binarySearch(supportedEncodings, encoding) >= 0;
}
/** Returns the maximum number of channels the device can play at the same time. */
public int getMaxChannelCount() {
return maxChannelCount;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof AudioCapabilities)) {
return false;
}
AudioCapabilities audioCapabilities = (AudioCapabilities) other;
return Arrays.equals(supportedEncodings, audioCapabilities.supportedEncodings)
&& maxChannelCount == audioCapabilities.maxChannelCount;
}
@Override
public int hashCode() {
return maxChannelCount + 31 * Arrays.hashCode(supportedEncodings);
}
@Override
public String toString() {
return "AudioCapabilities[maxChannelCount=" + maxChannelCount
+ ", supportedEncodings=" + Arrays.toString(supportedEncodings) + "]";
}
}
@@ -1,104 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer.audio;
import com.b44t.messenger.exoplayer.util.Assertions;
import com.b44t.messenger.exoplayer.util.Util;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
/**
* Notifies a listener when the audio playback capabilities change. Call {@link #register} to start
* (or resume) receiving notifications, and {@link #unregister} to stop.
*/
public final class AudioCapabilitiesReceiver {
/**
* Listener notified when audio capabilities change.
*/
public interface Listener {
/**
* Called when the audio capabilities change.
*
* @param audioCapabilities Current audio capabilities for the device.
*/
void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities);
}
private final Context context;
private final Listener listener;
private final BroadcastReceiver receiver;
/* package */ AudioCapabilities audioCapabilities;
/**
* Constructs a new audio capabilities receiver.
*
* @param context Context for registering to receive broadcasts.
* @param listener Listener to notify when audio capabilities change.
*/
public AudioCapabilitiesReceiver(Context context, Listener listener) {
this.context = Assertions.checkNotNull(context);
this.listener = Assertions.checkNotNull(listener);
this.receiver = Util.SDK_INT >= 21 ? new HdmiAudioPlugBroadcastReceiver() : null;
}
/**
* Registers to notify the listener when audio capabilities change. The current capabilities will
* be returned. It is important to call {@link #unregister} so that the listener can be garbage
* collected.
*
* @return Current audio capabilities for the device.
*/
@SuppressWarnings("InlinedApi")
public AudioCapabilities register() {
Intent stickyIntent = receiver == null ? null
: context.registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG));
audioCapabilities = AudioCapabilities.getCapabilities(stickyIntent);
return audioCapabilities;
}
/**
* Unregisters to stop notifying the listener when audio capabilities change.
*/
public void unregister() {
if (receiver != null) {
context.unregisterReceiver(receiver);
}
}
private final class HdmiAudioPlugBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!isInitialStickyBroadcast()) {
AudioCapabilities newAudioCapabilities = AudioCapabilities.getCapabilities(intent);
if (!newAudioCapabilities.equals(audioCapabilities)) {
audioCapabilities = newAudioCapabilities;
listener.onAudioCapabilitiesChanged(newAudioCapabilities);
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,104 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer.chunk;
import com.b44t.messenger.exoplayer.C;
import com.b44t.messenger.exoplayer.SampleSource;
import com.b44t.messenger.exoplayer.SampleSource.SampleSourceReader;
import java.io.IOException;
/**
* Interface for callbacks to be notified of chunk based {@link SampleSource} events.
*/
public interface BaseChunkSampleSourceEventListener {
/**
* Invoked when an upstream load is started.
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param length The length of the data being loaded in bytes, or {@link C#LENGTH_UNBOUNDED} if
* the length of the data is not known in advance.
* @param type The type of the data being loaded.
* @param trigger The reason for the data being loaded.
* @param format The particular format to which this data corresponds, or null if the data being
* loaded does not correspond to a format.
* @param mediaStartTimeMs The media time of the start of the data being loaded, or -1 if this
* load is for initialization data.
* @param mediaEndTimeMs The media time of the end of the data being loaded, or -1 if this
* load is for initialization data.
*/
void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
long mediaStartTimeMs, long mediaEndTimeMs);
/**
* Invoked when the current load operation completes.
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param bytesLoaded The number of bytes that were loaded.
* @param type The type of the loaded data.
* @param trigger The reason for the data being loaded.
* @param format The particular format to which this data corresponds, or null if the loaded data
* does not correspond to a format.
* @param mediaStartTimeMs The media time of the start of the loaded data, or -1 if this load was
* for initialization data.
* @param mediaEndTimeMs The media time of the end of the loaded data, or -1 if this load was for
* initialization data.
* @param elapsedRealtimeMs {@code elapsedRealtime} timestamp of when the load finished.
* @param loadDurationMs Amount of time taken to load the data.
*/
void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs);
/**
* Invoked when the current upstream load operation is canceled.
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param bytesLoaded The number of bytes that were loaded prior to the cancellation.
*/
void onLoadCanceled(int sourceId, long bytesLoaded);
/**
* Invoked when an error occurs loading media data.
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param e The cause of the failure.
*/
void onLoadError(int sourceId, IOException e);
/**
* Invoked when data is removed from the back of the buffer, typically so that it can be
* re-buffered using a different representation.
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param mediaStartTimeMs The media time of the start of the discarded data.
* @param mediaEndTimeMs The media time of the end of the discarded data.
*/
void onUpstreamDiscarded(int sourceId, long mediaStartTimeMs, long mediaEndTimeMs);
/**
* Invoked when the downstream format changes (i.e. when the format being supplied to the
* caller of {@link SampleSourceReader#readData} changes).
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param format The format.
* @param trigger The trigger specified in the corresponding upstream load, as specified by the
* {@link ChunkSource}.
* @param mediaTimeMs The media time at which the change occurred.
*/
void onDownstreamFormatChanged(int sourceId, Format format, int trigger, long mediaTimeMs);
}
@@ -1,107 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer.chunk;
import com.b44t.messenger.exoplayer.MediaFormat;
import com.b44t.messenger.exoplayer.drm.DrmInitData;
import com.b44t.messenger.exoplayer.extractor.DefaultTrackOutput;
import com.b44t.messenger.exoplayer.upstream.DataSource;
import com.b44t.messenger.exoplayer.upstream.DataSpec;
/**
* A base implementation of {@link MediaChunk}, for chunks that contain a single track.
* <p>
* Loaded samples are output to a {@link DefaultTrackOutput}.
*/
public abstract class BaseMediaChunk extends MediaChunk {
/**
* Whether {@link #getMediaFormat()} and {@link #getDrmInitData()} can be called at any time to
* obtain the chunk's media format and drm initialization data. If false, these methods are only
* guaranteed to return correct data after the first sample data has been output from the chunk.
*/
public final boolean isMediaFormatFinal;
private DefaultTrackOutput output;
private int firstSampleIndex;
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param trigger The reason for this chunk being selected.
* @param format The format of the stream to which this chunk belongs.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param chunkIndex The index of the chunk.
* @param isMediaFormatFinal True if {@link #getMediaFormat()} and {@link #getDrmInitData()} can
* be called at any time to obtain the media format and drm initialization data. False if
* these methods are only guaranteed to return correct data after the first sample data has
* been output from the chunk.
* @param parentId Identifier for a parent from which this chunk originates.
*/
public BaseMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
long startTimeUs, long endTimeUs, int chunkIndex, boolean isMediaFormatFinal, int parentId) {
super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, parentId);
this.isMediaFormatFinal = isMediaFormatFinal;
}
/**
* Initializes the chunk for loading, setting the {@link DefaultTrackOutput} that will receive
* samples as they are loaded.
*
* @param output The output that will receive the loaded samples.
*/
public void init(DefaultTrackOutput output) {
this.output = output;
this.firstSampleIndex = output.getWriteIndex();
}
/**
* Returns the index of the first sample in the output that was passed to
* {@link #init(DefaultTrackOutput)} that will originate from this chunk.
*/
public final int getFirstSampleIndex() {
return firstSampleIndex;
}
/**
* Gets the {@link MediaFormat} corresponding to the chunk.
* <p>
* See {@link #isMediaFormatFinal} for information about when this method is guaranteed to return
* correct data.
*
* @return The {@link MediaFormat} corresponding to this chunk.
*/
public abstract MediaFormat getMediaFormat();
/**
* Gets the {@link DrmInitData} corresponding to the chunk.
* <p>
* See {@link #isMediaFormatFinal} for information about when this method is guaranteed to return
* correct data.
*
* @return The {@link DrmInitData} corresponding to this chunk.
*/
public abstract DrmInitData getDrmInitData();
/**
* Returns the output most recently passed to {@link #init(DefaultTrackOutput)}.
*/
protected final DefaultTrackOutput getOutput() {
return output;
}
}
@@ -1,135 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer.chunk;
import com.b44t.messenger.exoplayer.upstream.DataSource;
import com.b44t.messenger.exoplayer.upstream.DataSpec;
import com.b44t.messenger.exoplayer.upstream.Loader.Loadable;
import com.b44t.messenger.exoplayer.util.Assertions;
/**
* An abstract base class for {@link Loadable} implementations that load chunks of data required
* for the playback of streams.
*/
public abstract class Chunk implements Loadable {
/**
* Value of {@link #type} for chunks containing unspecified data.
*/
public static final int TYPE_UNSPECIFIED = 0;
/**
* Value of {@link #type} for chunks containing media data.
*/
public static final int TYPE_MEDIA = 1;
/**
* Value of {@link #type} for chunks containing media initialization data.
*/
public static final int TYPE_MEDIA_INITIALIZATION = 2;
/**
* Value of {@link #type} for chunks containing drm related data.
*/
public static final int TYPE_DRM = 3;
/**
* Value of {@link #type} for chunks containing manifest or playlist data.
*/
public static final int TYPE_MANIFEST = 4;
/**
* Implementations may define custom {@link #type} codes greater than or equal to this value.
*/
public static final int TYPE_CUSTOM_BASE = 10000;
/**
* Value of {@link #trigger} for a load whose reason is unspecified.
*/
public static final int TRIGGER_UNSPECIFIED = 0;
/**
* Value of {@link #trigger} for a load triggered by an initial format selection.
*/
public static final int TRIGGER_INITIAL = 1;
/**
* Value of {@link #trigger} for a load triggered by a user initiated format selection.
*/
public static final int TRIGGER_MANUAL = 2;
/**
* Value of {@link #trigger} for a load triggered by an adaptive format selection.
*/
public static final int TRIGGER_ADAPTIVE = 3;
/**
* Value of {@link #trigger} for a load triggered whilst in a trick play mode.
*/
public static final int TRIGGER_TRICK_PLAY = 4;
/**
* Implementations may define custom {@link #trigger} codes greater than or equal to this value.
*/
public static final int TRIGGER_CUSTOM_BASE = 10000;
/**
* Value of {@link #parentId} if no parent id need be specified.
*/
public static final int NO_PARENT_ID = -1;
/**
* The type of the chunk. For reporting only.
*/
public final int type;
/**
* The reason why the chunk was generated. For reporting only.
*/
public final int trigger;
/**
* The format associated with the data being loaded, or null if the data being loaded is not
* associated with a specific format.
*/
public final Format format;
/**
* The {@link DataSpec} that defines the data to be loaded.
*/
public final DataSpec dataSpec;
/**
* Optional identifier for a parent from which this chunk originates.
*/
public final int parentId;
protected final DataSource dataSource;
/**
* @param dataSource The source from which the data should be loaded.
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
* {@link Integer#MAX_VALUE}.
* @param type See {@link #type}.
* @param trigger See {@link #trigger}.
* @param format See {@link #format}.
* @param parentId See {@link #parentId}.
*/
public Chunk(DataSource dataSource, DataSpec dataSpec, int type, int trigger, Format format,
int parentId) {
this.dataSource = Assertions.checkNotNull(dataSource);
this.dataSpec = Assertions.checkNotNull(dataSpec);
this.type = type;
this.trigger = trigger;
this.format = format;
this.parentId = parentId;
}
/**
* Gets the number of bytes that have been loaded.
*
* @return The number of bytes that have been loaded.
*/
public abstract long bytesLoaded();
}
@@ -1,145 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer.chunk;
import com.b44t.messenger.exoplayer.MediaFormat;
import com.b44t.messenger.exoplayer.drm.DrmInitData;
import com.b44t.messenger.exoplayer.extractor.Extractor;
import com.b44t.messenger.exoplayer.extractor.ExtractorInput;
import com.b44t.messenger.exoplayer.extractor.ExtractorOutput;
import com.b44t.messenger.exoplayer.extractor.SeekMap;
import com.b44t.messenger.exoplayer.extractor.TrackOutput;
import com.b44t.messenger.exoplayer.util.Assertions;
import com.b44t.messenger.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/**
* An {@link Extractor} wrapper for loading chunks containing a single track.
* <p>
* The wrapper allows switching of the {@link SingleTrackOutput} that receives parsed data.
*/
public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput {
/**
* Receives stream level data extracted by the wrapped {@link Extractor}.
*/
public interface SingleTrackOutput extends TrackOutput {
/**
* @see ExtractorOutput#seekMap(SeekMap)
*/
void seekMap(SeekMap seekMap);
/**
* @see ExtractorOutput#drmInitData(DrmInitData)
*/
void drmInitData(DrmInitData drmInitData);
}
private final Extractor extractor;
private boolean extractorInitialized;
private SingleTrackOutput output;
// Accessed only on the loader thread.
private boolean seenTrack;
/**
* @param extractor The extractor to wrap.
*/
public ChunkExtractorWrapper(Extractor extractor) {
this.extractor = extractor;
}
/**
* Initializes the extractor to output to the provided {@link SingleTrackOutput}, and configures
* it to receive data from a new chunk.
*
* @param output The {@link SingleTrackOutput} that will receive the parsed data.
*/
public void init(SingleTrackOutput output) {
this.output = output;
if (!extractorInitialized) {
extractor.init(this);
extractorInitialized = true;
} else {
extractor.seek();
}
}
/**
* Reads from the provided {@link ExtractorInput}.
*
* @param input The {@link ExtractorInput} from which to read.
* @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}.
* @throws IOException If an error occurred reading from the source.
* @throws InterruptedException If the thread was interrupted.
*/
public int read(ExtractorInput input) throws IOException, InterruptedException {
int result = extractor.read(input, null);
Assertions.checkState(result != Extractor.RESULT_SEEK);
return result;
}
// ExtractorOutput implementation.
@Override
public TrackOutput track(int id) {
Assertions.checkState(!seenTrack);
seenTrack = true;
return this;
}
@Override
public void endTracks() {
Assertions.checkState(seenTrack);
}
@Override
public void seekMap(SeekMap seekMap) {
output.seekMap(seekMap);
}
@Override
public void drmInitData(DrmInitData drmInitData) {
output.drmInitData(drmInitData);
}
// TrackOutput implementation.
@Override
public void format(MediaFormat format) {
output.format(format);
}
@Override
public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
throws IOException, InterruptedException {
return output.sampleData(input, length, allowEndOfInput);
}
@Override
public void sampleData(ParsableByteArray data, int length) {
output.sampleData(data, length);
}
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
output.sampleMetadata(timeUs, flags, size, offset, encryptionKey);
}
}
@@ -1,53 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer.chunk;
/**
* Holds a chunk operation, which consists of a either:
* <ul>
* <li>The number of {@link MediaChunk}s that should be retained on the queue ({@link #queueSize})
* together with the next {@link Chunk} to load ({@link #chunk}). {@link #chunk} may be null if the
* next chunk cannot be provided yet.</li>
* <li>A flag indicating that the end of the stream has been reached ({@link #endOfStream}).</li>
* </ul>
*/
public final class ChunkOperationHolder {
/**
* The number of {@link MediaChunk}s to retain in a queue.
*/
public int queueSize;
/**
* The chunk.
*/
public Chunk chunk;
/**
* Indicates that the end of the stream has been reached.
*/
public boolean endOfStream;
/**
* Clears the holder.
*/
public void clear() {
queueSize = 0;
chunk = null;
endOfStream = false;
}
}
@@ -1,688 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer.chunk;
import com.b44t.messenger.exoplayer.C;
import com.b44t.messenger.exoplayer.LoadControl;
import com.b44t.messenger.exoplayer.MediaFormat;
import com.b44t.messenger.exoplayer.MediaFormatHolder;
import com.b44t.messenger.exoplayer.SampleHolder;
import com.b44t.messenger.exoplayer.SampleSource;
import com.b44t.messenger.exoplayer.SampleSource.SampleSourceReader;
import com.b44t.messenger.exoplayer.TrackRenderer;
import com.b44t.messenger.exoplayer.extractor.DefaultTrackOutput;
import com.b44t.messenger.exoplayer.upstream.Loader;
import com.b44t.messenger.exoplayer.upstream.Loader.Loadable;
import com.b44t.messenger.exoplayer.util.Assertions;
import android.os.Handler;
import android.os.SystemClock;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* A {@link SampleSource} that loads media in {@link Chunk}s, which are themselves obtained from a
* {@link ChunkSource}.
*/
public class ChunkSampleSource implements SampleSource, SampleSourceReader, Loader.Callback {
/**
* Interface definition for a callback to be notified of {@link ChunkSampleSource} events.
*/
public interface EventListener extends BaseChunkSampleSourceEventListener {}
/**
* The default minimum number of times to retry loading data prior to failing.
*/
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
protected final DefaultTrackOutput sampleQueue;
private static final int STATE_IDLE = 0;
private static final int STATE_INITIALIZED = 1;
private static final int STATE_PREPARED = 2;
private static final int STATE_ENABLED = 3;
private static final long NO_RESET_PENDING = Long.MIN_VALUE;
private final int eventSourceId;
private final LoadControl loadControl;
private final ChunkSource chunkSource;
private final ChunkOperationHolder currentLoadableHolder;
private final LinkedList<BaseMediaChunk> mediaChunks;
private final List<BaseMediaChunk> readOnlyMediaChunks;
private final int bufferSizeContribution;
private final Handler eventHandler;
private final EventListener eventListener;
private final int minLoadableRetryCount;
private int state;
private long downstreamPositionUs;
private long lastSeekPositionUs;
private long pendingResetPositionUs;
private long lastPerformedBufferOperation;
private boolean pendingDiscontinuity;
private Loader loader;
private boolean loadingFinished;
private IOException currentLoadableException;
private int enabledTrackCount;
private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp;
private long currentLoadStartTimeMs;
private MediaFormat downstreamMediaFormat;
private Format downstreamFormat;
/**
* @param chunkSource A {@link ChunkSource} from which chunks to load are obtained.
* @param loadControl Controls when the source is permitted to load data.
* @param bufferSizeContribution The contribution of this source to the media buffer, in bytes.
*/
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
int bufferSizeContribution) {
this(chunkSource, loadControl, bufferSizeContribution, null, null, 0);
}
/**
* @param chunkSource A {@link ChunkSource} from which chunks to load are obtained.
* @param loadControl Controls when the source is permitted to load data.
* @param bufferSizeContribution The contribution of this source to the media buffer, in bytes.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param eventSourceId An identifier that gets passed to {@code eventListener} methods.
*/
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
int bufferSizeContribution, Handler eventHandler, EventListener eventListener,
int eventSourceId) {
this(chunkSource, loadControl, bufferSizeContribution, eventHandler, eventListener,
eventSourceId, DEFAULT_MIN_LOADABLE_RETRY_COUNT);
}
/**
* @param chunkSource A {@link ChunkSource} from which chunks to load are obtained.
* @param loadControl Controls when the source is permitted to load data.
* @param bufferSizeContribution The contribution of this source to the media buffer, in bytes.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param eventSourceId An identifier that gets passed to {@code eventListener} methods.
* @param minLoadableRetryCount The minimum number of times that the source should retry a load
* before propagating an error.
*/
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
int bufferSizeContribution, Handler eventHandler, EventListener eventListener,
int eventSourceId, int minLoadableRetryCount) {
this.chunkSource = chunkSource;
this.loadControl = loadControl;
this.bufferSizeContribution = bufferSizeContribution;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
this.eventSourceId = eventSourceId;
this.minLoadableRetryCount = minLoadableRetryCount;
currentLoadableHolder = new ChunkOperationHolder();
mediaChunks = new LinkedList<>();
readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
sampleQueue = new DefaultTrackOutput(loadControl.getAllocator());
state = STATE_IDLE;
pendingResetPositionUs = NO_RESET_PENDING;
}
@Override
public SampleSourceReader register() {
Assertions.checkState(state == STATE_IDLE);
state = STATE_INITIALIZED;
return this;
}
@Override
public boolean prepare(long positionUs) {
Assertions.checkState(state == STATE_INITIALIZED || state == STATE_PREPARED);
if (state == STATE_PREPARED) {
return true;
} else if (!chunkSource.prepare()) {
return false;
}
if (chunkSource.getTrackCount() > 0) {
loader = new Loader("Loader:" + chunkSource.getFormat(0).mimeType);
}
state = STATE_PREPARED;
return true;
}
@Override
public int getTrackCount() {
Assertions.checkState(state == STATE_PREPARED || state == STATE_ENABLED);
return chunkSource.getTrackCount();
}
@Override
public MediaFormat getFormat(int track) {
Assertions.checkState(state == STATE_PREPARED || state == STATE_ENABLED);
return chunkSource.getFormat(track);
}
@Override
public void enable(int track, long positionUs) {
Assertions.checkState(state == STATE_PREPARED);
Assertions.checkState(enabledTrackCount++ == 0);
state = STATE_ENABLED;
chunkSource.enable(track);
loadControl.register(this, bufferSizeContribution);
downstreamFormat = null;
downstreamMediaFormat = null;
downstreamPositionUs = positionUs;
lastSeekPositionUs = positionUs;
pendingDiscontinuity = false;
restartFrom(positionUs);
}
@Override
public void disable(int track) {
Assertions.checkState(state == STATE_ENABLED);
Assertions.checkState(--enabledTrackCount == 0);
state = STATE_PREPARED;
try {
chunkSource.disable(mediaChunks);
} finally {
loadControl.unregister(this);
if (loader.isLoading()) {
loader.cancelLoading();
} else {
sampleQueue.clear();
mediaChunks.clear();
clearCurrentLoadable();
loadControl.trimAllocator();
}
}
}
@Override
public boolean continueBuffering(int track, long positionUs) {
Assertions.checkState(state == STATE_ENABLED);
downstreamPositionUs = positionUs;
chunkSource.continueBuffering(positionUs);
updateLoadControl();
return loadingFinished || !sampleQueue.isEmpty();
}
@Override
public long readDiscontinuity(int track) {
if (pendingDiscontinuity) {
pendingDiscontinuity = false;
return lastSeekPositionUs;
}
return NO_DISCONTINUITY;
}
@Override
public int readData(int track, long positionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder) {
Assertions.checkState(state == STATE_ENABLED);
downstreamPositionUs = positionUs;
if (pendingDiscontinuity || isPendingReset()) {
return NOTHING_READ;
}
boolean haveSamples = !sampleQueue.isEmpty();
BaseMediaChunk currentChunk = mediaChunks.getFirst();
while (haveSamples && mediaChunks.size() > 1
&& mediaChunks.get(1).getFirstSampleIndex() <= sampleQueue.getReadIndex()) {
mediaChunks.removeFirst();
currentChunk = mediaChunks.getFirst();
}
Format format = currentChunk.format;
if (!format.equals(downstreamFormat)) {
notifyDownstreamFormatChanged(format, currentChunk.trigger, currentChunk.startTimeUs);
}
downstreamFormat = format;
if (haveSamples || currentChunk.isMediaFormatFinal) {
MediaFormat mediaFormat = currentChunk.getMediaFormat();
if (!mediaFormat.equals(downstreamMediaFormat)) {
formatHolder.format = mediaFormat;
formatHolder.drmInitData = currentChunk.getDrmInitData();
downstreamMediaFormat = mediaFormat;
return FORMAT_READ;
}
// If mediaFormat and downstreamMediaFormat are equal but different objects then the equality
// check above will have been expensive, comparing the fields in each format. We update
// downstreamMediaFormat here so that referential equality can be cheaply established during
// subsequent calls.
downstreamMediaFormat = mediaFormat;
}
if (!haveSamples) {
if (loadingFinished) {
return END_OF_STREAM;
}
return NOTHING_READ;
}
if (sampleQueue.getSample(sampleHolder)) {
boolean decodeOnly = sampleHolder.timeUs < lastSeekPositionUs;
sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
onSampleRead(currentChunk, sampleHolder);
return SAMPLE_READ;
}
return NOTHING_READ;
}
@Override
public void seekToUs(long positionUs) {
Assertions.checkState(state == STATE_ENABLED);
long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs;
downstreamPositionUs = positionUs;
lastSeekPositionUs = positionUs;
if (currentPositionUs == positionUs) {
return;
}
// If we're not pending a reset, see if we can seek within the sample queue.
boolean seekInsideBuffer = !isPendingReset() && sampleQueue.skipToKeyframeBefore(positionUs);
if (seekInsideBuffer) {
// We succeeded. All we need to do is discard any chunks that we've moved past.
boolean haveSamples = !sampleQueue.isEmpty();
while (haveSamples && mediaChunks.size() > 1
&& mediaChunks.get(1).getFirstSampleIndex() <= sampleQueue.getReadIndex()) {
mediaChunks.removeFirst();
}
} else {
// We failed, and need to restart.
restartFrom(positionUs);
}
// Either way, we need to send a discontinuity to the downstream components.
pendingDiscontinuity = true;
}
@Override
public void maybeThrowError() throws IOException {
if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) {
throw currentLoadableException;
} else if (currentLoadableHolder.chunk == null) {
chunkSource.maybeThrowError();
}
}
@Override
public long getBufferedPositionUs() {
Assertions.checkState(state == STATE_ENABLED);
if (isPendingReset()) {
return pendingResetPositionUs;
} else if (loadingFinished) {
return TrackRenderer.END_OF_TRACK_US;
} else {
long largestParsedTimestampUs = sampleQueue.getLargestParsedTimestampUs();
return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs
: largestParsedTimestampUs;
}
}
@Override
public void release() {
Assertions.checkState(state != STATE_ENABLED);
if (loader != null) {
loader.release();
loader = null;
}
state = STATE_IDLE;
}
@Override
public void onLoadCompleted(Loadable loadable) {
long now = SystemClock.elapsedRealtime();
long loadDurationMs = now - currentLoadStartTimeMs;
Chunk currentLoadable = currentLoadableHolder.chunk;
chunkSource.onChunkLoadCompleted(currentLoadable);
if (isMediaChunk(currentLoadable)) {
BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable;
notifyLoadCompleted(currentLoadable.bytesLoaded(), mediaChunk.type, mediaChunk.trigger,
mediaChunk.format, mediaChunk.startTimeUs, mediaChunk.endTimeUs, now, loadDurationMs);
} else {
notifyLoadCompleted(currentLoadable.bytesLoaded(), currentLoadable.type,
currentLoadable.trigger, currentLoadable.format, -1, -1, now, loadDurationMs);
}
clearCurrentLoadable();
updateLoadControl();
}
@Override
public void onLoadCanceled(Loadable loadable) {
Chunk currentLoadable = currentLoadableHolder.chunk;
notifyLoadCanceled(currentLoadable.bytesLoaded());
clearCurrentLoadable();
if (state == STATE_ENABLED) {
restartFrom(pendingResetPositionUs);
} else {
sampleQueue.clear();
mediaChunks.clear();
clearCurrentLoadable();
loadControl.trimAllocator();
}
}
@Override
public void onLoadError(Loadable loadable, IOException e) {
currentLoadableException = e;
currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
notifyLoadError(e);
chunkSource.onChunkLoadError(currentLoadableHolder.chunk, e);
updateLoadControl();
}
/**
* Called when a sample has been read. Can be used to perform any modifications necessary before
* the sample is returned.
*
* @param mediaChunk The chunk from which the sample was obtained.
* @param sampleHolder Holds the read sample.
*/
protected void onSampleRead(MediaChunk mediaChunk, SampleHolder sampleHolder) {
// Do nothing.
}
private void restartFrom(long positionUs) {
pendingResetPositionUs = positionUs;
loadingFinished = false;
if (loader.isLoading()) {
loader.cancelLoading();
} else {
sampleQueue.clear();
mediaChunks.clear();
clearCurrentLoadable();
updateLoadControl();
}
}
private void clearCurrentLoadable() {
currentLoadableHolder.chunk = null;
clearCurrentLoadableException();
}
private void clearCurrentLoadableException() {
currentLoadableException = null;
currentLoadableExceptionCount = 0;
}
private void updateLoadControl() {
long now = SystemClock.elapsedRealtime();
long nextLoadPositionUs = getNextLoadPositionUs();
boolean isBackedOff = currentLoadableException != null;
boolean loadingOrBackedOff = loader.isLoading() || isBackedOff;
// If we're not loading or backed off, evaluate the operation if (a) we don't have the next
// chunk yet and we're not finished, or (b) if the last evaluation was over 2000ms ago.
if (!loadingOrBackedOff && ((currentLoadableHolder.chunk == null && nextLoadPositionUs != -1)
|| (now - lastPerformedBufferOperation > 2000))) {
// Perform the evaluation.
lastPerformedBufferOperation = now;
doChunkOperation();
boolean chunksDiscarded = discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
// Update the next load position as appropriate.
if (currentLoadableHolder.chunk == null) {
// Set loadPosition to -1 to indicate that we don't have anything to load.
nextLoadPositionUs = -1;
} else if (chunksDiscarded) {
// Chunks were discarded, so we need to re-evaluate the load position.
nextLoadPositionUs = getNextLoadPositionUs();
}
}
// Update the control with our current state, and determine whether we're the next loader.
boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs,
loadingOrBackedOff);
if (isBackedOff) {
long elapsedMillis = now - currentLoadableExceptionTimestamp;
if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
resumeFromBackOff();
}
return;
}
if (!loader.isLoading() && nextLoader) {
maybeStartLoading();
}
}
/**
* Gets the next load time, assuming that the next load starts where the previous chunk ended (or
* from the pending reset time, if there is one).
*/
private long getNextLoadPositionUs() {
if (isPendingReset()) {
return pendingResetPositionUs;
} else {
return loadingFinished ? -1 : mediaChunks.getLast().endTimeUs;
}
}
/**
* Resumes loading.
* <p>
* If the {@link ChunkSource} returns a chunk equivalent to the backed off chunk B, then the
* loading of B will be resumed. In all other cases B will be discarded and the new chunk will
* be loaded.
*/
private void resumeFromBackOff() {
currentLoadableException = null;
Chunk backedOffChunk = currentLoadableHolder.chunk;
if (!isMediaChunk(backedOffChunk)) {
doChunkOperation();
discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
if (currentLoadableHolder.chunk == backedOffChunk) {
// Chunk was unchanged. Resume loading.
loader.startLoading(backedOffChunk, this);
} else {
// Chunk was changed. Notify that the existing load was canceled.
notifyLoadCanceled(backedOffChunk.bytesLoaded());
// Start loading the replacement.
maybeStartLoading();
}
return;
}
if (backedOffChunk == mediaChunks.getFirst()) {
// We're not able to clear the first media chunk, so we have no choice but to continue
// loading it.
loader.startLoading(backedOffChunk, this);
return;
}
// The current loadable is the last media chunk. Remove it before we invoke the chunk source,
// and add it back again afterwards.
BaseMediaChunk removedChunk = mediaChunks.removeLast();
Assertions.checkState(backedOffChunk == removedChunk);
doChunkOperation();
mediaChunks.add(removedChunk);
if (currentLoadableHolder.chunk == backedOffChunk) {
// Chunk was unchanged. Resume loading.
loader.startLoading(backedOffChunk, this);
} else {
// Chunk was changed. Notify that the existing load was canceled.
notifyLoadCanceled(backedOffChunk.bytesLoaded());
// This call will remove and release at least one chunk from the end of mediaChunks. Since
// the current loadable is the last media chunk, it is guaranteed to be removed.
discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
clearCurrentLoadableException();
maybeStartLoading();
}
}
private void maybeStartLoading() {
Chunk currentLoadable = currentLoadableHolder.chunk;
if (currentLoadable == null) {
// Nothing to load.
return;
}
currentLoadStartTimeMs = SystemClock.elapsedRealtime();
if (isMediaChunk(currentLoadable)) {
BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable;
mediaChunk.init(sampleQueue);
mediaChunks.add(mediaChunk);
if (isPendingReset()) {
pendingResetPositionUs = NO_RESET_PENDING;
}
notifyLoadStarted(mediaChunk.dataSpec.length, mediaChunk.type, mediaChunk.trigger,
mediaChunk.format, mediaChunk.startTimeUs, mediaChunk.endTimeUs);
} else {
notifyLoadStarted(currentLoadable.dataSpec.length, currentLoadable.type,
currentLoadable.trigger, currentLoadable.format, -1, -1);
}
loader.startLoading(currentLoadable, this);
}
/**
* Sets up the {@link #currentLoadableHolder}, passes it to the chunk source to cause it to be
* updated with the next operation, and updates {@link #loadingFinished} if the end of the stream
* is reached.
*/
private void doChunkOperation() {
currentLoadableHolder.endOfStream = false;
currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
chunkSource.getChunkOperation(readOnlyMediaChunks,
pendingResetPositionUs != NO_RESET_PENDING ? pendingResetPositionUs : downstreamPositionUs,
currentLoadableHolder);
loadingFinished = currentLoadableHolder.endOfStream;
}
/**
* Discard upstream media chunks until the queue length is equal to the length specified.
*
* @param queueLength The desired length of the queue.
* @return True if chunks were discarded. False otherwise.
*/
private boolean discardUpstreamMediaChunks(int queueLength) {
if (mediaChunks.size() <= queueLength) {
return false;
}
long startTimeUs = 0;
long endTimeUs = mediaChunks.getLast().endTimeUs;
BaseMediaChunk removed = null;
while (mediaChunks.size() > queueLength) {
removed = mediaChunks.removeLast();
startTimeUs = removed.startTimeUs;
loadingFinished = false;
}
sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex());
notifyUpstreamDiscarded(startTimeUs, endTimeUs);
return true;
}
private boolean isMediaChunk(Chunk chunk) {
return chunk instanceof BaseMediaChunk;
}
private boolean isPendingReset() {
return pendingResetPositionUs != NO_RESET_PENDING;
}
private long getRetryDelayMillis(long errorCount) {
return Math.min((errorCount - 1) * 1000, 5000);
}
protected final long usToMs(long timeUs) {
return timeUs / 1000;
}
private void notifyLoadStarted(final long length, final int type, final int trigger,
final Format format, final long mediaStartTimeUs, final long mediaEndTimeUs) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onLoadStarted(eventSourceId, length, type, trigger, format,
usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs));
}
});
}
}
private void notifyLoadCompleted(final long bytesLoaded, final int type, final int trigger,
final Format format, final long mediaStartTimeUs, final long mediaEndTimeUs,
final long elapsedRealtimeMs, final long loadDurationMs) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onLoadCompleted(eventSourceId, bytesLoaded, type, trigger, format,
usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs);
}
});
}
}
private void notifyLoadCanceled(final long bytesLoaded) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onLoadCanceled(eventSourceId, bytesLoaded);
}
});
}
}
private void notifyLoadError(final IOException e) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onLoadError(eventSourceId, e);
}
});
}
}
private void notifyUpstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onUpstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs),
usToMs(mediaEndTimeUs));
}
});
}
}
private void notifyDownstreamFormatChanged(final Format format, final int trigger,
final long positionUs) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onDownstreamFormatChanged(eventSourceId, format, trigger,
usToMs(positionUs));
}
});
}
}
}
@@ -1,139 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer.chunk;
import com.b44t.messenger.exoplayer.MediaFormat;
import java.io.IOException;
import java.util.List;
/**
* A provider of {@link Chunk}s for a {@link ChunkSampleSource} to load.
*/
/*
* TODO: Share more state between this interface and {@link ChunkSampleSource}. In particular
* implementations of this class needs to know about errors, and should be more tightly integrated
* into the process of resuming loading of a chunk after an error occurs.
*/
public interface ChunkSource {
/**
* If the source is currently having difficulty preparing or providing chunks, then this method
* throws the underlying error. Otherwise does nothing.
*
* @throws IOException The underlying error.
*/
void maybeThrowError() throws IOException;
/**
* Prepares the source.
* <p>
* The method can be called repeatedly until the return value indicates success.
*
* @return True if the source was prepared, false otherwise.
*/
boolean prepare();
/**
* Returns the number of tracks exposed by the source.
* <p>
* This method should only be called after the source has been prepared.
*
* @return The number of tracks.
*/
int getTrackCount();
/**
* Gets the format of the specified track.
* <p>
* This method should only be called after the source has been prepared.
*
* @param track The track index.
* @return The format of the track.
*/
MediaFormat getFormat(int track);
/**
* Enable the source for the specified track.
* <p>
* This method should only be called after the source has been prepared, and when the source is
* disabled.
*
* @param track The track index.
*/
void enable(int track);
/**
* Indicates to the source that it should still be checking for updates to the stream.
* <p>
* This method should only be called when the source is enabled.
*
* @param playbackPositionUs The current playback position.
*/
void continueBuffering(long playbackPositionUs);
/**
* Updates the provided {@link ChunkOperationHolder} to contain the next operation that should
* be performed by the calling {@link ChunkSampleSource}.
* <p>
* This method should only be called when the source is enabled.
*
* @param queue A representation of the currently buffered {@link MediaChunk}s.
* @param playbackPositionUs The current playback position. If the queue is empty then this
* parameter is the position from which playback is expected to start (or restart) and hence
* should be interpreted as a seek position.
* @param out A holder for the next operation, whose {@link ChunkOperationHolder#endOfStream} is
* initially set to false, whose {@link ChunkOperationHolder#queueSize} is initially equal to
* the length of the queue, and whose {@link ChunkOperationHolder#chunk} is initially equal to
* null or a {@link Chunk} previously supplied by the {@link ChunkSource} that the caller has
* not yet finished loading. In the latter case the chunk can either be replaced or left
* unchanged. Note that leaving the chunk unchanged is both preferred and more efficient than
* replacing it with a new but identical chunk.
*/
void getChunkOperation(List<? extends MediaChunk> queue, long playbackPositionUs,
ChunkOperationHolder out);
/**
* Invoked when the {@link ChunkSampleSource} has finished loading a chunk obtained from this
* source.
* <p>
* This method should only be called when the source is enabled.
*
* @param chunk The chunk whose load has been completed.
*/
void onChunkLoadCompleted(Chunk chunk);
/**
* Invoked when the {@link ChunkSampleSource} encounters an error loading a chunk obtained from
* this source.
* <p>
* This method should only be called when the source is enabled.
*
* @param chunk The chunk whose load encountered the error.
* @param e The error.
*/
void onChunkLoadError(Chunk chunk, Exception e);
/**
* Disables the source.
* <p>
* This method should only be called when the source is enabled.
*
* @param queue A representation of the currently buffered {@link MediaChunk}s.
*/
void disable(List<? extends MediaChunk> queue);
}
@@ -1,191 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.b44t.messenger.exoplayer.chunk;
import com.b44t.messenger.exoplayer.MediaFormat;
import com.b44t.messenger.exoplayer.chunk.ChunkExtractorWrapper.SingleTrackOutput;
import com.b44t.messenger.exoplayer.drm.DrmInitData;
import com.b44t.messenger.exoplayer.extractor.DefaultExtractorInput;
import com.b44t.messenger.exoplayer.extractor.Extractor;
import com.b44t.messenger.exoplayer.extractor.ExtractorInput;
import com.b44t.messenger.exoplayer.extractor.SeekMap;
import com.b44t.messenger.exoplayer.upstream.DataSource;
import com.b44t.messenger.exoplayer.upstream.DataSpec;
import com.b44t.messenger.exoplayer.util.ParsableByteArray;
import com.b44t.messenger.exoplayer.util.Util;
import java.io.IOException;
/**
* A {@link BaseMediaChunk} that uses an {@link Extractor} to parse sample data.
*/
public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackOutput {
private final ChunkExtractorWrapper extractorWrapper;
private final long sampleOffsetUs;
private final int adaptiveMaxWidth;
private final int adaptiveMaxHeight;
private MediaFormat mediaFormat;
private DrmInitData drmInitData;
private volatile int bytesLoaded;
private volatile boolean loadCanceled;
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param trigger The reason for this chunk being selected.
* @param format The format of the stream to which this chunk belongs.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param chunkIndex The index of the chunk.
* @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor.
* @param extractorWrapper A wrapped extractor to use for parsing the data.
* @param mediaFormat The {@link MediaFormat} of the chunk, if known. May be null if the data is
* known to define its own format.
* @param adaptiveMaxWidth If this chunk contains video and is part of an adaptive playback, this
* is the maximum width of the video in pixels that will be encountered during the playback.
* {@link MediaFormat#NO_VALUE} otherwise.
* @param adaptiveMaxHeight If this chunk contains video and is part of an adaptive playback, this
* is the maximum height of the video in pixels that will be encountered during the playback.
* {@link MediaFormat#NO_VALUE} otherwise.
* @param drmInitData The {@link DrmInitData} for the chunk. Null if the media is not drm
* protected. May also be null if the data is known to define its own initialization data.
* @param isMediaFormatFinal True if {@code mediaFormat} and {@code drmInitData} are known to be
* correct and final. False if the data may define its own format or initialization data.
* @param parentId Identifier for a parent from which this chunk originates.
*/
public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
long startTimeUs, long endTimeUs, int chunkIndex, long sampleOffsetUs,
ChunkExtractorWrapper extractorWrapper, MediaFormat mediaFormat, int adaptiveMaxWidth,
int adaptiveMaxHeight, DrmInitData drmInitData, boolean isMediaFormatFinal, int parentId) {
super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex,
isMediaFormatFinal, parentId);
this.extractorWrapper = extractorWrapper;
this.sampleOffsetUs = sampleOffsetUs;
this.adaptiveMaxWidth = adaptiveMaxWidth;
this.adaptiveMaxHeight = adaptiveMaxHeight;
this.mediaFormat = getAdjustedMediaFormat(mediaFormat, sampleOffsetUs, adaptiveMaxWidth,
adaptiveMaxHeight);
this.drmInitData = drmInitData;
}
@Override
public final long bytesLoaded() {
return bytesLoaded;
}
@Override
public final MediaFormat getMediaFormat() {
return mediaFormat;
}
@Override
public final DrmInitData getDrmInitData() {
return drmInitData;
}
// SingleTrackOutput implementation.
@Override
public final void seekMap(SeekMap seekMap) {
// Do nothing.
}
@Override
public final void drmInitData(DrmInitData drmInitData) {
this.drmInitData = drmInitData;
}
@Override
public final void format(MediaFormat mediaFormat) {
this.mediaFormat = getAdjustedMediaFormat(mediaFormat, sampleOffsetUs, adaptiveMaxWidth,
adaptiveMaxHeight);
}
@Override
public final int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
throws IOException, InterruptedException {
return getOutput().sampleData(input, length, allowEndOfInput);
}
@Override
public final void sampleData(ParsableByteArray data, int length) {
getOutput().sampleData(data, length);
}
@Override
public final void sampleMetadata(long timeUs, int flags, int size, int offset,
byte[] encryptionKey) {
getOutput().sampleMetadata(timeUs + sampleOffsetUs, flags, size, offset, encryptionKey);
}
// Loadable implementation.
@Override
public final void cancelLoad() {
loadCanceled = true;
}
@Override
public final boolean isLoadCanceled() {
return loadCanceled;
}
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public final void load() throws IOException, InterruptedException {
DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
try {
// Create and open the input.
ExtractorInput input = new DefaultExtractorInput(dataSource,
loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
if (bytesLoaded == 0) {
// Set the target to ourselves.
extractorWrapper.init(this);
}
// Load and parse the sample data.
try {
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractorWrapper.read(input);
}
} finally {
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
}
} finally {
dataSource.close();
}
}
// Private methods.
private static MediaFormat getAdjustedMediaFormat(MediaFormat format, long sampleOffsetUs,
int adaptiveMaxWidth, int adaptiveMaxHeight) {
if (format == null) {
return null;
}
if (sampleOffsetUs != 0 && format.subsampleOffsetUs != MediaFormat.OFFSET_SAMPLE_RELATIVE) {
format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs);
}
if (adaptiveMaxWidth != MediaFormat.NO_VALUE || adaptiveMaxHeight != MediaFormat.NO_VALUE) {
format = format.copyWithMaxVideoDimensions(adaptiveMaxWidth, adaptiveMaxHeight);
}
return format;
}
}

Some files were not shown because too many files have changed in this diff Show More