Browse Source

Merge pull request #559 from TacoTheDank/master

Lots of code cleanup
pull/565/head
Eric Kok 4 years ago committed by GitHub
parent
commit
6f07529b6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      .editorconfig
  2. 107
      README.md
  3. 2
      app/build.gradle
  4. 19
      app/src/full/res/values/bools.xml
  5. 6
      app/src/full/res/values/strings.xml
  6. 19
      app/src/lite/res/values/bools.xml
  7. 6
      app/src/lite/res/values/strings.xml
  8. 628
      app/src/main/AndroidManifest.xml
  9. 3
      app/src/main/java/androidx/preference/PreferenceManagerBinder.java
  10. 280
      app/src/main/java/org/transdroid/core/app/search/SearchHelper.java
  11. 186
      app/src/main/java/org/transdroid/core/app/search/SearchResult.java
  12. 65
      app/src/main/java/org/transdroid/core/app/search/SearchSite.java
  13. 1467
      app/src/main/java/org/transdroid/core/app/settings/ApplicationSettings.java
  14. 194
      app/src/main/java/org/transdroid/core/app/settings/NotificationSettings.java
  15. 188
      app/src/main/java/org/transdroid/core/app/settings/RssfeedSetting.java
  16. 596
      app/src/main/java/org/transdroid/core/app/settings/ServerSetting.java
  17. 630
      app/src/main/java/org/transdroid/core/app/settings/SettingsPersistence.java
  18. 3
      app/src/main/java/org/transdroid/core/app/settings/SettingsUtils.java
  19. 84
      app/src/main/java/org/transdroid/core/app/settings/SystemSettings.java
  20. 91
      app/src/main/java/org/transdroid/core/app/settings/WebsearchSetting.java
  21. 619
      app/src/main/java/org/transdroid/core/gui/DetailsActivity.java
  22. 1217
      app/src/main/java/org/transdroid/core/gui/DetailsFragment.java
  23. 69
      app/src/main/java/org/transdroid/core/gui/ServerPickerDialog.java
  24. 54
      app/src/main/java/org/transdroid/core/gui/ServerSelectionView.java
  25. 161
      app/src/main/java/org/transdroid/core/gui/ServerStatusView.java
  26. 32
      app/src/main/java/org/transdroid/core/gui/TorrentTasksExecutor.java
  27. 2485
      app/src/main/java/org/transdroid/core/gui/TorrentsActivity.java
  28. 864
      app/src/main/java/org/transdroid/core/gui/TorrentsFragment.java
  29. 32
      app/src/main/java/org/transdroid/core/gui/TransdroidApp.java
  30. 401
      app/src/main/java/org/transdroid/core/gui/lists/DetailsAdapter.java
  31. 436
      app/src/main/java/org/transdroid/core/gui/lists/LocalTorrent.java
  32. 557
      app/src/main/java/org/transdroid/core/gui/lists/MergeAdapter.java
  33. 36
      app/src/main/java/org/transdroid/core/gui/lists/PiecesMapView.java
  34. 3
      app/src/main/java/org/transdroid/core/gui/lists/SimpleListItem.java
  35. 177
      app/src/main/java/org/transdroid/core/gui/lists/SimpleListItemAdapter.java
  36. 64
      app/src/main/java/org/transdroid/core/gui/lists/SimpleListItemSpinnerAdapter.java
  37. 23
      app/src/main/java/org/transdroid/core/gui/lists/SimpleListItemView.java
  38. 93
      app/src/main/java/org/transdroid/core/gui/lists/SortByListItem.java
  39. 110
      app/src/main/java/org/transdroid/core/gui/lists/TorrentDetailsView.java
  40. 110
      app/src/main/java/org/transdroid/core/gui/lists/TorrentFilePriorityLayout.java
  41. 23
      app/src/main/java/org/transdroid/core/gui/lists/TorrentFileView.java
  42. 179
      app/src/main/java/org/transdroid/core/gui/lists/TorrentProgressBar.java
  43. 131
      app/src/main/java/org/transdroid/core/gui/lists/TorrentStatusLayout.java
  44. 85
      app/src/main/java/org/transdroid/core/gui/lists/TorrentView.java
  45. 91
      app/src/main/java/org/transdroid/core/gui/lists/TorrentsAdapter.java
  46. 158
      app/src/main/java/org/transdroid/core/gui/lists/ViewHolderAdapter.java
  47. 62
      app/src/main/java/org/transdroid/core/gui/log/DatabaseHelper.java
  48. 149
      app/src/main/java/org/transdroid/core/gui/log/ErrorLogEntry.java
  49. 112
      app/src/main/java/org/transdroid/core/gui/log/ErrorLogSender.java
  50. 63
      app/src/main/java/org/transdroid/core/gui/log/Log.java
  51. 54
      app/src/main/java/org/transdroid/core/gui/log/LogUncaughtExceptionHandler.java
  52. 142
      app/src/main/java/org/transdroid/core/gui/navigation/DialogHelper.java
  53. 141
      app/src/main/java/org/transdroid/core/gui/navigation/FilterListAdapter.java
  54. 75
      app/src/main/java/org/transdroid/core/gui/navigation/FilterListItemAdapter.java
  55. 17
      app/src/main/java/org/transdroid/core/gui/navigation/FilterListItemView.java
  56. 40
      app/src/main/java/org/transdroid/core/gui/navigation/FilterSeparatorView.java
  57. 219
      app/src/main/java/org/transdroid/core/gui/navigation/Label.java
  58. 40
      app/src/main/java/org/transdroid/core/gui/navigation/NavigationFilter.java
  59. 490
      app/src/main/java/org/transdroid/core/gui/navigation/NavigationHelper.java
  60. 3
      app/src/main/java/org/transdroid/core/gui/navigation/RefreshableActivity.java
  61. 201
      app/src/main/java/org/transdroid/core/gui/navigation/SelectionManagerMode.java
  62. 179
      app/src/main/java/org/transdroid/core/gui/navigation/SelectionModificationSpinner.java
  63. 111
      app/src/main/java/org/transdroid/core/gui/navigation/SetLabelDialog.java
  64. 58
      app/src/main/java/org/transdroid/core/gui/navigation/SetStorageLocationDialog.java
  65. 55
      app/src/main/java/org/transdroid/core/gui/navigation/SetTrackersDialog.java
  66. 153
      app/src/main/java/org/transdroid/core/gui/navigation/SetTransferRatesDialog.java
  67. 270
      app/src/main/java/org/transdroid/core/gui/navigation/StatusType.java
  68. 191
      app/src/main/java/org/transdroid/core/gui/remoterss/RemoteRssFragment.java
  69. 31
      app/src/main/java/org/transdroid/core/gui/remoterss/RemoteRssItemView.java
  70. 85
      app/src/main/java/org/transdroid/core/gui/remoterss/RemoteRssItemsAdapter.java
  71. 725
      app/src/main/java/org/transdroid/core/gui/rss/RssFeedsActivity.java
  72. 124
      app/src/main/java/org/transdroid/core/gui/rss/RssFeedsFragment.java
  73. 67
      app/src/main/java/org/transdroid/core/gui/rss/RssItemsActivity.java
  74. 355
      app/src/main/java/org/transdroid/core/gui/rss/RssItemsFragment.java
  75. 131
      app/src/main/java/org/transdroid/core/gui/rss/RssfeedLoader.java
  76. 59
      app/src/main/java/org/transdroid/core/gui/rss/RssfeedView.java
  77. 91
      app/src/main/java/org/transdroid/core/gui/rss/RssfeedsAdapter.java
  78. 69
      app/src/main/java/org/transdroid/core/gui/rss/RssitemStatusLayout.java
  79. 27
      app/src/main/java/org/transdroid/core/gui/rss/RssitemView.java
  80. 91
      app/src/main/java/org/transdroid/core/gui/rss/RssitemsAdapter.java
  81. 99
      app/src/main/java/org/transdroid/core/gui/search/BarcodeHelper.java
  82. 70
      app/src/main/java/org/transdroid/core/gui/search/FilePickerHelper.java
  83. 554
      app/src/main/java/org/transdroid/core/gui/search/SearchActivity.java
  84. 17
      app/src/main/java/org/transdroid/core/gui/search/SearchHistoryProvider.java
  85. 41
      app/src/main/java/org/transdroid/core/gui/search/SearchResultView.java
  86. 91
      app/src/main/java/org/transdroid/core/gui/search/SearchResultsAdapter.java
  87. 326
      app/src/main/java/org/transdroid/core/gui/search/SearchResultsFragment.java
  88. 22
      app/src/main/java/org/transdroid/core/gui/search/SearchSetting.java
  89. 17
      app/src/main/java/org/transdroid/core/gui/search/SearchSettingSelectionView.java
  90. 51
      app/src/main/java/org/transdroid/core/gui/search/SearchSettingsDropDownAdapter.java
  91. 39
      app/src/main/java/org/transdroid/core/gui/search/SearchSiteView.java
  92. 91
      app/src/main/java/org/transdroid/core/gui/search/SearchSitesAdapter.java
  93. 88
      app/src/main/java/org/transdroid/core/gui/search/SendIntentHelper.java
  94. 62
      app/src/main/java/org/transdroid/core/gui/search/UrlEntryDialog.java
  95. 47
      app/src/main/java/org/transdroid/core/gui/settings/AboutDialog.java
  96. 47
      app/src/main/java/org/transdroid/core/gui/settings/ChangelogDialog.java
  97. 143
      app/src/main/java/org/transdroid/core/gui/settings/HelpSettingsActivity.java
  98. 70
      app/src/main/java/org/transdroid/core/gui/settings/InterceptableEditTextPreference.java
  99. 388
      app/src/main/java/org/transdroid/core/gui/settings/KeyBoundPreferencesActivity.java
  100. 413
      app/src/main/java/org/transdroid/core/gui/settings/MainSettingsActivity.java
  101. Some files were not shown because too many files have changed in this diff Show More

9
.editorconfig

@ -0,0 +1,9 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

107
README.md

@ -1,44 +1,65 @@
Transdroid Transdroid
========== ==========
[www.transdroid.org](http://www.transdroid.org) [www.transdroid.org](https://www.transdroid.org/)
[Twitter](https://twitter.com/transdroid) - [transdroid@2312.nl](transdroid@2312.nl) [Twitter](https://twitter.com/transdroid) - [transdroid@2312.nl](transdroid@2312.nl)
"Manage your torrents from your Android device" Manage torrents from your Android device.
<a href="https://transdroid.org/latest" target="_blank"> <a href="https://transdroid.org/latest">
<img src="https://transdroid.org/images/getontransdroid.png" alt="Get it on transdroid.org" height="80"/></a> <img src="https://transdroid.org/images/getontransdroid.png"
<a href="https://f-droid.org/repository/browse/?fdid=org.transdroid.full" target="_blank"> alt="Get it on transdroid.org"
<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="80"/></a> height="80">
<a href="https://play.google.com/store/apps/details?id=org.transdroid.lite" target="_blank"> </a>
<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="80"/></a> <a href="https://f-droid.org/packages/org.transdroid.full/">
<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
<img src="http://2312.nl/images/screenshot_transdroid_main.png" alt="Screen shot of the main torrents listing screen" width="280" /> alt="Get it on F-Droid"
height="80">
</a>
Manage your torrents from your Android device with Transdroid. All popular clients are supported: µTorrent, Transmission, rTorrent, Vuze, Deluge, BitTorrent 6, qBittorrent and many more. You can view and manage the running torrents and individual files. Adding is easy via the integrated search or RSS feeds (full version required). Monitor progress using the home screen widget or background alarm service. <a href="https://play.google.com/store/apps/details?id=org.transdroid.lite">
<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"
alt="Get it on Google Play"
height="80">
</a>
<img src="https://2312.nl/images/screenshot_transdroid_main.png" alt="Screen shot of the main torrents listing screen" width="280" />
Manage your torrents from your Android device with Transdroid.
All popular clients are supported: µTorrent, Transmission, rTorrent, Vuze, Deluge, BitTorrent 6, qBittorrent, and many more.
You can view and manage running torrents and individual files.
Adding is easy via the integrated search or RSS feeds (full version required).
Monitor progress using the home screen widget or background alarm service.
Contributions Contributions
============= =============
Code and design contributions are very welcome. You might want to contact me via social networks (G+, Twitter) or e-mail first. Please note all code will be GNU GPL v3 licensed. Code and design contributions are very welcome.
You might want to contact me via social networks (Twitter) or e-mail first.
Please note that all code will be licensed in GNU GPLv3.
Please respect the coding standards for easier merging. master contains the current release version of Transdroid while dev contains the active development version. However, larger, new features are developed in their own branch. Please respect the coding standards for easier merging.
`master` contains the current release version of Transdroid while `dev` contains the active development version.
However, larger and new features will be developed in their own branch.
Code structure Code structure
============== ==============
Starting with version 2.3.0, Transdroid is developed in Android Studio, fully integrating with the Gradle build system. It is (since version 2.5.0) compiled against Android 5.1 (API level 22) and (since version 2.2.0) supporting ICS (API level 15) and up only. To support lite (Transdrone, specially for the Play Store) and full (Transdroid) versions of the app, build flavours are defined in gradle, which contain version-specific resources. Dependencies are managed via JCentral et al. in the app's build.gradle file. Starting with version 2.3.0, Transdroid is developed in Android Studio, fully integrating with the Gradle build system.
It is (since version 2.5.18) compiled against Android 10 (API level 29) and (since version 2.2.0) supporting Android ICS (API level 15) and up only.
To support lite (Transdrone, specially for the Play Store) and full (Transdroid) versions of the app, build flavours are defined in gradle, which contain version-specific resources.
Dependencies are managed via JCentral et al. in the app's build.gradle file.
Developed By Developed By
============ ============
Designed and developed by [Eric Kok](eric@2312.nl) of [2312 development](http://2312.nl). Contributions by various others (see commit log). Designed and developed by [Eric Kok](eric@2312.nl) of [2312 development](https://2312.nl/).
Contributions by various others (see commit log).
License License
======= =======
Copyright 2010-2018 Eric Kok et al. Copyright 2010-2020 Eric Kok et al.
Transdroid is free software: you can redistribute it and/or modify Transdroid is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -51,39 +72,51 @@ License
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with Transdroid. If not, see <http://www.gnu.org/licenses/>. along with Transdroid. If not, see <https://www.gnu.org/licenses/>.
Some code/libraries/resources are used in the project: Some code/libraries/resources are used in the project:
* [Android Jetpack (AndroidX)](https://developer.android.com/jetpack)
The Android Open Source Project
Apache License, Version 2.0
* [AndroidAnnotations](http://androidannotations.org/) * [AndroidAnnotations](http://androidannotations.org/)
Pierre-Yves Ricau (eBusinessInformations) et al. Pierre-Yves Ricau (eBusinessInformations) et al.
Apache License, Version 2.0 Apache License, Version 2.0
* [ActionBar-PullToRefresh](https://github.com/chrisbanes/ActionBar-PullToRefresh) * [ORMLite](https://github.com/j256/ormlite-core) and [ORMLite Android](https://github.com/j256/ormlite-android)
Chris Banes Gray Watson
ISC License
* [Android Universal Image Loader](https://github.com/nostra13/Android-Universal-Image-Loader)
Sergey Tarasevich
Apache License, Version 2.0 Apache License, Version 2.0
* [Crouton](https://github.com/keyboardsurfer/Crouton) * [FloatingActionButton](https://github.com/zendesk/android-floating-action-button)
Code: Benjamin Weiss (Neofonie Mobile Gmbh) et al. Oleksandr Melnykov, Zendesk
Idea: Cyril Mottier
Apache License, Version 2.0 Apache License, Version 2.0
* [Base16Encoder](http://openjpa.apache.org/) * [Snackbar](https://github.com/nispok/snackbar)
William Mora
MIT License
* [Java implementation of Rencode](https://github.com/aegnor/rencode-java)
Daniel Dimovski
MIT License
* [OpenJPA's Base16Encoder](https://github.com/apache/openjpa)
Marc Prud'hommeaux Marc Prud'hommeaux
Apache OpenJPA Apache OpenJPA
* MultipartEntity * [Base64](http://iharder.sourceforge.net/current/java/base64/)
Apache Software Foundation
Apache License, Version 2.0
* RssParser ([learning-android](http://github.com/digitalspaghetti/learning-android))
Tane Piper
Public Domain
* [Base64](http://iharder.net/base64)
Robert Harder Robert Harder
Public Domain Public Domain
* [aXMLRPC](https://github.com/timroes/aXMLRPC) * [aXMLRPC](https://github.com/gturri/aXMLRPC)
Tim Roes Tim Roes
MIT License MIT License
* [Material Dialogs](https://github.com/afollestad/material-dialogs)
Aidan Follestad
Apache License, Version 2.0
* [Android-Job](https://github.com/evernote/android-job)
Evernote Corporation
Apache License, Version 2.0
* [android-ColorPickerPreference](https://github.com/attenzione/android-ColorPickerPreference) * [android-ColorPickerPreference](https://github.com/attenzione/android-ColorPickerPreference)
Daniel Nilsson and Sergey Margaritov Daniel Nilsson and Sergey Margaritov
Apache License, Version 2.0 Apache License, Version 2.0
* [Funnel icon](http://thenounproject.com/noun/funnel/#icon-No5608) * RssParser ([learning-android](https://github.com/tanepiper/learning-android))
Tane Piper
Public Domain
* [Funnel icon](https://thenounproject.com/term/funnel/5608/)
Naomi Atkinson from The Noun Project Naomi Atkinson from The Noun Project
Creative Commons Attribution 3.0 Creative Commons Attribution 3.0

2
app/build.gradle

@ -79,7 +79,7 @@ dependencies {
// Android support // Android support
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.1.0'
// Other // Other

19
app/src/full/res/values/bools.xml

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!--
<!--
Copyright 2010-2018 Eric Kok et al. Copyright 2010-2018 Eric Kok et al.
Transdroid is free software: you can redistribute it and/or modify Transdroid is free software: you can redistribute it and/or modify
@ -17,13 +16,13 @@
--> -->
<resources> <resources>
<!-- Used to enable checking for app updates in the background --> <!-- Used to enable checking for app updates in the background -->
<bool name="updatecheck_available">true</bool> <bool name="updatecheck_available">true</bool>
<!-- Used to enable the search UI --> <!-- Used to enable the search UI -->
<bool name="search_available">true</bool> <bool name="search_available">true</bool>
<!-- Used to enable the RSS UI and background service --> <!-- Used to enable the RSS UI and background service -->
<bool name="rss_available">true</bool> <bool name="rss_available">true</bool>
<!-- Used to allow adding of seedboxes via easy server setup --> <!-- Used to allow adding of seedboxes via easy server setup -->
<bool name="seedboxes_available">true</bool> <bool name="seedboxes_available">true</bool>
</resources> </resources>

6
app/src/full/res/values/strings.xml

@ -16,9 +16,9 @@
--> -->
<resources> <resources>
<string name="app_name" translatable="false">Transdroid</string> <string name="app_name" translatable="false">Transdroid</string>
<string name="donate_text">Donate with PayPal</string> <string name="donate_text">Donate with PayPal</string>
<string name="donate_url">https://paypal.me/erickoknl</string> <string name="donate_url">https://paypal.me/erickoknl</string>
</resources> </resources>

19
app/src/lite/res/values/bools.xml

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!--
<!--
Copyright 2010-2018 Eric Kok et al. Copyright 2010-2018 Eric Kok et al.
Transdroid is free software: you can redistribute it and/or modify Transdroid is free software: you can redistribute it and/or modify
@ -17,13 +16,13 @@
--> -->
<resources> <resources>
<!-- Used to enable checking for app updates in the background --> <!-- Used to enable checking for app updates in the background -->
<bool name="updatecheck_available">false</bool> <bool name="updatecheck_available">false</bool>
<!-- Used to enable the search UI --> <!-- Used to enable the search UI -->
<bool name="search_available">false</bool> <bool name="search_available">false</bool>
<!-- Used to enable the RSS UI and background service --> <!-- Used to enable the RSS UI and background service -->
<bool name="rss_available">false</bool> <bool name="rss_available">false</bool>
<!-- Used to allow adding of seedboxes via easy server setup --> <!-- Used to allow adding of seedboxes via easy server setup -->
<bool name="seedboxes_available">true</bool> <bool name="seedboxes_available">true</bool>
</resources> </resources>

6
app/src/lite/res/values/strings.xml

@ -16,9 +16,9 @@
--> -->
<resources> <resources>
<string name="app_name" translatable="false">Transdrone</string> <string name="app_name" translatable="false">Transdrone</string>
<string name="donate_text"></string> <string name="donate_text" />
<string name="donate_url"></string> <string name="donate_url" />
</resources> </resources>

628
app/src/main/AndroidManifest.xml

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!--
<!--
Copyright 2010-2018 Eric Kok et al. Copyright 2010-2018 Eric Kok et al.
Transdroid is free software: you can redistribute it and/or modify Transdroid is free software: you can redistribute it and/or modify
@ -16,318 +15,317 @@
along with Transdroid. If not, see <http://www.gnu.org/licenses/>. along with Transdroid. If not, see <http://www.gnu.org/licenses/>.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="org.transdroid" > package="org.transdroid">
<uses-sdk /> <uses-sdk />
<supports-screens <supports-screens
android:anyDensity="true" android:anyDensity="true"
android:largeScreens="true" android:largeScreens="true"
android:normalScreens="true" android:normalScreens="true"
android:smallScreens="true" android:smallScreens="true"
android:xlargeScreens="true" /> android:xlargeScreens="true" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<!-- To check for an active connection --> <!-- To check for an active connection -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- To check currently connected wifi network name --> <!-- To check currently connected wifi network name -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- To start rss and torrents background check services --> <!-- To start rss and torrents background check services -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<!-- To export settings file to external storage --> <!-- To export settings file to external storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-feature <uses-feature
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
android:required="false" /> android:required="false" />
<uses-feature <uses-feature
android:name="android.software.leanback" android:name="android.software.leanback"
android:required="false" /> android:required="false" />
<application <application
android:name=".core.gui.TransdroidApp_" android:name=".core.gui.TransdroidApp_"
android:allowBackup="true" android:allowBackup="true"
android:hardwareAccelerated="true" android:banner="@drawable/banner"
android:icon="@drawable/ic_launcher" android:hardwareAccelerated="true"
android:banner="@drawable/banner" android:icon="@drawable/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.AppCompat" android:supportsRtl="true"
android:usesCleartextTraffic="true"> android:theme="@style/Theme.AppCompat"
android:usesCleartextTraffic="true">
<uses-library
android:name="org.apache.http.legacy" <uses-library
android:required="false" /> android:name="org.apache.http.legacy"
android:required="false" />
<!-- Main activities -->
<activity <!-- Main activities -->
android:name="org.transdroid.core.gui.TorrentsActivity_" <activity
android:allowTaskReparenting="true" android:name="org.transdroid.core.gui.TorrentsActivity_"
android:label="@string/app_name" android:allowTaskReparenting="true"
android:launchMode="singleTop" android:label="@string/app_name"
android:theme="@style/TransdroidTheme" android:launchMode="singleTop"
android:windowSoftInputMode="stateHidden" > android:theme="@style/TransdroidTheme"
<intent-filter> android:windowSoftInputMode="stateHidden">
<action android:name="android.intent.action.MAIN" /> <intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<intent-filter> </intent-filter>
<action android:name="android.intent.action.SEARCH" /> <intent-filter>
</intent-filter> <action android:name="android.intent.action.SEARCH" />
<intent-filter> </intent-filter>
<action android:name="org.transdroid.ADD_MULTIPLE" /> <intent-filter>
<action android:name="org.transdroid.ADD_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter> <category android:name="android.intent.category.DEFAULT" />
<intent-filter> </intent-filter>
<action android:name="org.transdroid.START_SERVER" /> <intent-filter>
<action android:name="org.transdroid.START_SERVER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter> <category android:name="android.intent.category.DEFAULT" />
<intent-filter> </intent-filter>
<action android:name="android.intent.action.VIEW" /> <intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*" <data
android:mimeType="application/x-bittorrent" android:host="*"
android:scheme="http" /> android:mimeType="application/x-bittorrent"
</intent-filter> android:scheme="http" />
<intent-filter> </intent-filter>
<action android:name="android.intent.action.VIEW" /> <intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*" <data
android:pathPattern=".*\\.torrent" android:host="*"
android:scheme="http" /> android:pathPattern=".*\\.torrent"
</intent-filter> android:scheme="http" />
<intent-filter> </intent-filter>
<action android:name="android.intent.action.VIEW" /> <intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*" <data
android:mimeType="application/x-bittorrent" android:host="*"
android:scheme="https" /> android:mimeType="application/x-bittorrent"
</intent-filter> android:scheme="https" />
<intent-filter> </intent-filter>
<action android:name="android.intent.action.VIEW" /> <intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*" <data
android:pathPattern=".*\\.torrent" android:host="*"
android:scheme="https" /> android:pathPattern=".*\\.torrent"
</intent-filter> android:scheme="https" />
<intent-filter> </intent-filter>
<action android:name="android.intent.action.VIEW" /> <intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*" <data
android:mimeType="application/x-bittorrent" android:host="*"
android:scheme="file" /> android:mimeType="application/x-bittorrent"
</intent-filter> android:scheme="file" />
<intent-filter> </intent-filter>
<action android:name="android.intent.action.VIEW" /> <intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*" <data
android:pathPattern=".*\\.torrent" android:host="*"
android:scheme="file" /> android:pathPattern=".*\\.torrent"
</intent-filter> android:scheme="file" />
<intent-filter> </intent-filter>
<action android:name="android.intent.action.VIEW" /> <intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*" <data
android:mimeType="application/x-bittorrent" android:host="*"
android:scheme="content" /> android:mimeType="application/x-bittorrent"
</intent-filter> android:scheme="content" />
<intent-filter> </intent-filter>
<action android:name="android.intent.action.VIEW" /> <intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*" <data
android:pathPattern=".*\\.torrent" android:host="*"
android:scheme="content" /> android:pathPattern=".*\\.torrent"
</intent-filter> android:scheme="content" />
<intent-filter> </intent-filter>
<action android:name="android.intent.action.VIEW" /> <intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="magnet" />
</intent-filter> <data android:scheme="magnet" />
</intent-filter>
<meta-data
android:name="android.app.default_searchable" <meta-data
android:value="org.transdroid.core.gui.search.SearchActivity_" /> android:name="android.app.default_searchable"
</activity> android:value="org.transdroid.core.gui.search.SearchActivity_" />
<activity </activity>
android:name="org.transdroid.core.gui.DetailsActivity_" <activity
android:theme="@style/TransdroidTheme" android:name="org.transdroid.core.gui.DetailsActivity_"
android:uiOptions="splitActionBarWhenNarrow" > android:theme="@style/TransdroidTheme"
</activity> android:uiOptions="splitActionBarWhenNarrow" />
<!-- Settings screens --> <!-- Settings screens -->
<activity <activity
android:name="org.transdroid.core.gui.settings.MainSettingsActivity_" android:name="org.transdroid.core.gui.settings.MainSettingsActivity_"
android:theme="@style/TransdroidTheme.Settings" /> android:theme="@style/TransdroidTheme.Settings" />
<activity <activity
android:name="org.transdroid.core.gui.settings.ServerSettingsActivity_" android:name="org.transdroid.core.gui.settings.ServerSettingsActivity_"
android:theme="@style/TransdroidTheme.Settings" /> android:theme="@style/TransdroidTheme.Settings" />
<activity <activity
android:name="org.transdroid.core.gui.settings.WebsearchSettingsActivity_" android:name="org.transdroid.core.gui.settings.WebsearchSettingsActivity_"
android:theme="@style/TransdroidTheme.Settings" /> android:theme="@style/TransdroidTheme.Settings" />
<activity <activity
android:name="org.transdroid.core.gui.settings.RssfeedSettingsActivity_" android:name="org.transdroid.core.gui.settings.RssfeedSettingsActivity_"
android:theme="@style/TransdroidTheme.Settings" /> android:theme="@style/TransdroidTheme.Settings" />
<activity <activity
android:name="org.transdroid.core.gui.settings.NotificationSettingsActivity_" android:name="org.transdroid.core.gui.settings.NotificationSettingsActivity_"
android:theme="@style/TransdroidTheme.Settings" /> android:theme="@style/TransdroidTheme.Settings" />
<activity <activity
android:name="org.transdroid.core.gui.settings.SystemSettingsActivity_" android:name="org.transdroid.core.gui.settings.SystemSettingsActivity_"
android:theme="@style/TransdroidTheme.Settings" /> android:theme="@style/TransdroidTheme.Settings" />
<activity <activity
android:name="org.transdroid.core.gui.settings.HelpSettingsActivity_" android:name="org.transdroid.core.gui.settings.HelpSettingsActivity_"
android:theme="@style/TransdroidTheme.Settings" /> android:theme="@style/TransdroidTheme.Settings" />
<activity <activity
android:name="org.transdroid.core.gui.navigation.DialogHelper_" android:name="org.transdroid.core.gui.navigation.DialogHelper_"
android:theme="@style/TransdroidTheme.Settings" /> android:theme="@style/TransdroidTheme.Settings" />
<!-- Seedbox settings --> <!-- Seedbox settings -->
<activity <activity
android:name="org.transdroid.core.seedbox.DediseedboxSettingsActivity_" android:name="org.transdroid.core.seedbox.DediseedboxSettingsActivity_"
android:theme="@style/TransdroidTheme.Settings" /> android:theme="@style/TransdroidTheme.Settings" />
<activity <activity
android:name="org.transdroid.core.seedbox.SeedstuffSettingsActivity_" android:name="org.transdroid.core.seedbox.SeedstuffSettingsActivity_"
android:theme="@style/TransdroidTheme.Settings" /> android:theme="@style/TransdroidTheme.Settings" />
<activity <activity
android:name="org.transdroid.core.seedbox.XirvikSharedSettingsActivity_" android:name="org.transdroid.core.seedbox.XirvikSharedSettingsActivity_"
android:theme="@style/TransdroidTheme.Settings" /> android:theme="@style/TransdroidTheme.Settings" />
<activity <activity
android:name="org.transdroid.core.seedbox.XirvikSemiSettingsActivity_" android:name="org.transdroid.core.seedbox.XirvikSemiSettingsActivity_"
android:theme="@style/TransdroidTheme.Settings" /> android:theme="@style/TransdroidTheme.Settings" />
<activity <activity
android:name="org.transdroid.core.seedbox.XirvikDediSettingsActivity_" android:name="org.transdroid.core.seedbox.XirvikDediSettingsActivity_"
android:theme="@style/TransdroidTheme.Settings" /> android:theme="@style/TransdroidTheme.Settings" />
<!-- Search --> <!-- Search -->
<activity <activity
android:name="org.transdroid.core.gui.search.SearchActivity_" android:name="org.transdroid.core.gui.search.SearchActivity_"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="@string/search_torrentsearch" android:label="@string/search_torrentsearch"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@style/TransdroidTheme" > android:theme="@style/TransdroidTheme">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEARCH" /> <action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.app.searchable" android:name="android.app.searchable"
android:resource="@xml/searchable" /> android:resource="@xml/searchable" />
<meta-data <meta-data
android:name="android.app.default_searchable" android:name="android.app.default_searchable"
android:value="org.transdroid.core.gui.search.SearchActivity_" /> android:value="org.transdroid.core.gui.search.SearchActivity_" />
</activity> </activity>
<provider <provider
android:name="org.transdroid.core.gui.search.SearchHistoryProvider" android:name="org.transdroid.core.gui.search.SearchHistoryProvider"
android:authorities="@string/search_history_authority" android:authorities="@string/search_history_authority"
android:exported="false" /> android:exported="false" />
<!-- RSS --> <!-- RSS -->
<activity <activity
android:name="org.transdroid.core.gui.rss.RssFeedsActivity_" android:name="org.transdroid.core.gui.rss.RssFeedsActivity_"
android:label="@string/rss_feeds" android:label="@string/rss_feeds"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/TransdroidTheme" /> android:theme="@style/TransdroidTheme" />
<activity <activity
android:name="org.transdroid.core.gui.rss.RssItemsActivity_" android:name="org.transdroid.core.gui.rss.RssItemsActivity_"
android:label="@string/rss_feeds" android:label="@string/rss_feeds"
android:theme="@style/TransdroidTheme" /> android:theme="@style/TransdroidTheme" />
<receiver android:name="org.transdroid.core.service.BootReceiver_" > <receiver android:name="org.transdroid.core.service.BootReceiver_">
<intent-filter> <intent-filter>
<action <action
android:name="android.intent.action.BOOT_COMPLETED" android:name="android.intent.action.BOOT_COMPLETED"
android:value="android.intent.action.BOOT_COMPLETED" /> android:value="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<service <service
android:name="org.transdroid.core.service.ControlService_" android:name="org.transdroid.core.service.ControlService_"
android:exported="true" android:exported="true"
tools:ignore="ExportedService" > tools:ignore="ExportedService">
<intent-filter> <intent-filter>
<action android:name="org.transdroid.control.SET_TRANSFER_RATES" /> <action android:name="org.transdroid.control.SET_TRANSFER_RATES" />
<action android:name="org.transdroid.control.PAUSE_ALL" /> <action android:name="org.transdroid.control.PAUSE_ALL" />
<action android:name="org.transdroid.control.RESUME_ALL" /> <action android:name="org.transdroid.control.RESUME_ALL" />
<action android:name="org.transdroid.control.START_ALL" /> <action android:name="org.transdroid.control.START_ALL" />
<action android:name="org.transdroid.control.STOP_ALL" /> <action android:name="org.transdroid.control.STOP_ALL" />
</intent-filter> </intent-filter>
</service> </service>
<!-- Home screen widget --> <!-- Home screen widget -->
<activity <activity
android:name="org.transdroid.core.widget.ListWidgetConfigActivity_" android:name="org.transdroid.core.widget.ListWidgetConfigActivity_"
android:theme="@style/TransdroidTheme.WidgetConfig" > android:theme="@style/TransdroidTheme.WidgetConfig">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter> </intent-filter>
</activity> </activity>
<service <service
android:name="org.transdroid.core.widget.ListWidgetViewsService_" android:name="org.transdroid.core.widget.ListWidgetViewsService_"
android:exported="false" android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS" /> android:permission="android.permission.BIND_REMOTEVIEWS" />
<receiver <receiver android:name="org.transdroid.core.widget.ListWidgetProvider_">
android:name="org.transdroid.core.widget.ListWidgetProvider_"> <intent-filter>
<intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter>
</intent-filter>
<meta-data
<meta-data android:name="android.appwidget.provider"
android:name="android.appwidget.provider" android:resource="@xml/listwidget_info" />
android:resource="@xml/listwidget_info" /> </receiver>
</receiver> </application>
</application>
</manifest> </manifest>

3
app/src/main/java/androidx/preference/PreferenceManagerBinder.java

@ -5,7 +5,8 @@ package androidx.preference;
* around the protected visibility of {@link Preference#onAttachedToHierarchy(PreferenceManager)}. * around the protected visibility of {@link Preference#onAttachedToHierarchy(PreferenceManager)}.
*/ */
public class PreferenceManagerBinder { public class PreferenceManagerBinder {
private PreferenceManagerBinder() {} private PreferenceManagerBinder() {
}
public static void bind(Preference pref, PreferenceManager manager) { public static void bind(Preference pref, PreferenceManager manager) {
pref.onAttachedToHierarchy(manager); pref.onAttachedToHierarchy(manager);

280
app/src/main/java/org/transdroid/core/app/search/SearchHelper.java

@ -16,6 +16,15 @@
*/ */
package org.transdroid.core.app.search; package org.transdroid.core.app.search;
import android.content.ContentProviderClient;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import org.androidannotations.annotations.EBean;
import org.androidannotations.annotations.EBean.Scope;
import org.androidannotations.annotations.RootContext;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
@ -23,146 +32,141 @@ import java.net.URLEncoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.androidannotations.annotations.EBean;
import org.androidannotations.annotations.EBean.Scope;
import org.androidannotations.annotations.RootContext;
import android.content.ContentProviderClient;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
@EBean(scope = Scope.Singleton) @EBean(scope = Scope.Singleton)
public class SearchHelper { public class SearchHelper {
static final int CURSOR_SEARCH_ID = 0; static final int CURSOR_SEARCH_ID = 0;
static final int CURSOR_SEARCH_NAME = 1; static final int CURSOR_SEARCH_NAME = 1;
static final int CURSOR_SEARCH_TORRENTURL = 2; static final int CURSOR_SEARCH_TORRENTURL = 2;
static final int CURSOR_SEARCH_DETAILSURL = 3; static final int CURSOR_SEARCH_DETAILSURL = 3;
static final int CURSOR_SEARCH_SIZE = 4; static final int CURSOR_SEARCH_SIZE = 4;
static final int CURSOR_SEARCH_ADDED = 5; static final int CURSOR_SEARCH_ADDED = 5;
static final int CURSOR_SEARCH_SEEDERS = 6; static final int CURSOR_SEARCH_SEEDERS = 6;
static final int CURSOR_SEARCH_LEECHERS = 7; static final int CURSOR_SEARCH_LEECHERS = 7;
static final int CURSOR_SITE_ID = 0; static final int CURSOR_SITE_ID = 0;
static final int CURSOR_SITE_CODE = 1; static final int CURSOR_SITE_CODE = 1;
static final int CURSOR_SITE_NAME = 2; static final int CURSOR_SITE_NAME = 2;
static final int CURSOR_SITE_RSSURL = 3; static final int CURSOR_SITE_RSSURL = 3;
static final int CURSOR_SITE_ISPRIVATE = 4; static final int CURSOR_SITE_ISPRIVATE = 4;
@RootContext @RootContext
protected Context context; protected Context context;
public enum SearchSortOrder { /**
Combined, BySeeders * Return whether the Torrent Search package is installed and available to query against
} *
* @return True if the available sites can be retrieved from the content provider, false otherwise
/** */
* Return whether the Torrent Search package is installed and available to query against public boolean isTorrentSearchInstalled() {
* @return True if the available sites can be retrieved from the content provider, false otherwise return getAvailableSites() != null;
*/ }
public boolean isTorrentSearchInstalled() {
return getAvailableSites() != null; /**
} * Queries the Torrent Search package for all available in-app search sites. This method is synchronous.
*
/** * @return A list of available search sites as POJOs, or null if the Torrent Search package is not installed
* Queries the Torrent Search package for all available in-app search sites. This method is synchronous. */
* @return A list of available search sites as POJOs, or null if the Torrent Search package is not installed public List<SearchSite> getAvailableSites() {
*/
public List<SearchSite> getAvailableSites() { // Try to access the TorrentSitesProvider of the Torrent Search app
Uri uri = Uri.parse("content://org.transdroid.search.torrentsitesprovider/sites");
// Try to access the TorrentSitesProvider of the Torrent Search app ContentProviderClient test = context.getContentResolver().acquireContentProviderClient(uri);
Uri uri = Uri.parse("content://org.transdroid.search.torrentsitesprovider/sites"); if (test == null) {
ContentProviderClient test = context.getContentResolver().acquireContentProviderClient(uri); // Torrent Search package is not yet installed
if (test == null) { return null;
// Torrent Search package is not yet installed }
return null;
} // Query the available in-app torrent search sites
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
// Query the available in-app torrent search sites if (cursor == null) {
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); // The installed Torrent Search version is corrupt or incompatible
if (cursor == null) { return null;
// The installed Torrent Search version is corrupt or incompatible }
return null; List<SearchSite> sites = new ArrayList<>();
} if (cursor.moveToFirst()) {
List<SearchSite> sites = new ArrayList<>(); do {
if (cursor.moveToFirst()) { // Read the cursor fields into the SearchSite object
do { sites.add(new SearchSite(cursor.getInt(CURSOR_SITE_ID), cursor.getString(CURSOR_SITE_CODE), cursor
// Read the cursor fields into the SearchSite object .getString(CURSOR_SITE_NAME), cursor.getString(CURSOR_SITE_RSSURL),
sites.add(new SearchSite(cursor.getInt(CURSOR_SITE_ID), cursor.getString(CURSOR_SITE_CODE), cursor cursor.getColumnNames().length > 4 && cursor.getInt(CURSOR_SITE_ISPRIVATE) == 1));
.getString(CURSOR_SITE_NAME), cursor.getString(CURSOR_SITE_RSSURL), } while (cursor.moveToNext());
cursor.getColumnNames().length > 4 && cursor.getInt(CURSOR_SITE_ISPRIVATE) == 1)); }
} while (cursor.moveToNext());
} cursor.close();
return sites;
cursor.close();
return sites; }
} /**
* Queries the Torrent Search module to search for torrents on the web. This method is synchronous and should always
/** * be called in a background thread.
* Queries the Torrent Search module to search for torrents on the web. This method is synchronous and should always *
* be called in a background thread. * @param query The search query to pass to the torrent site
* @param query The search query to pass to the torrent site * @param site The site to search, as retrieved from the TorrentSitesProvider, or null if the Torrent Search package
* @param site The site to search, as retrieved from the TorrentSitesProvider, or null if the Torrent Search package * @param sortBy The sort order to request from the torrent site, if supported
* @param sortBy The sort order to request from the torrent site, if supported * @return A list of torrent search results as POJOs, or null if the Torrent Search package is not installed or
* @return A list of torrent search results as POJOs, or null if the Torrent Search package is not installed or * there is no internet connection
* there is no internet connection */
*/ public ArrayList<SearchResult> search(String query, SearchSite site, SearchSortOrder sortBy) {
public ArrayList<SearchResult> search(String query, SearchSite site, SearchSortOrder sortBy) {
// Try to query the TorrentSearchProvider to search for torrents on the web
// Try to query the TorrentSearchProvider to search for torrents on the web Uri uri = Uri.parse("content://org.transdroid.search.torrentsearchprovider/search/" + query);
Uri uri = Uri.parse("content://org.transdroid.search.torrentsearchprovider/search/" + query); Cursor cursor;
Cursor cursor; if (site == null) {
if (site == null) { // If no explicit site was supplied, rely on the Torrent Search package's default
// If no explicit site was supplied, rely on the Torrent Search package's default cursor = context.getContentResolver().query(uri, null, null, null, sortBy.name());
cursor = context.getContentResolver().query(uri, null, null, null, sortBy.name()); } else {
} else { cursor = context.getContentResolver().query(uri, null, "SITE = ?", new String[]{site.getKey()},
cursor = context.getContentResolver().query(uri, null, "SITE = ?", new String[] { site.getKey() }, sortBy.name());
sortBy.name()); }
} if (cursor == null) {
if (cursor == null) { // The content provider could not load any content (for example when there is no connection)
// The content provider could not load any content (for example when there is no connection) return null;
return null; }
} if (cursor.moveToFirst()) {
if (cursor.moveToFirst()) { ArrayList<SearchResult> results = new ArrayList<>();
ArrayList<SearchResult> results = new ArrayList<>(); do {
do { // Read the cursor fields into the SearchResult object
// Read the cursor fields into the SearchResult object results.add(new SearchResult(cursor.getInt(CURSOR_SEARCH_ID), cursor.getString(CURSOR_SEARCH_NAME),
results.add(new SearchResult(cursor.getInt(CURSOR_SEARCH_ID), cursor.getString(CURSOR_SEARCH_NAME), cursor.getString(CURSOR_SEARCH_TORRENTURL), cursor.getString(CURSOR_SEARCH_DETAILSURL), cursor
cursor.getString(CURSOR_SEARCH_TORRENTURL), cursor.getString(CURSOR_SEARCH_DETAILSURL), cursor .getString(CURSOR_SEARCH_SIZE), cursor.getLong(CURSOR_SEARCH_ADDED), cursor
.getString(CURSOR_SEARCH_SIZE), cursor.getLong(CURSOR_SEARCH_ADDED), cursor .getString(CURSOR_SEARCH_SEEDERS), cursor.getString(CURSOR_SEARCH_LEECHERS)));
.getString(CURSOR_SEARCH_SEEDERS), cursor.getString(CURSOR_SEARCH_LEECHERS))); } while (cursor.moveToNext());
} while (cursor.moveToNext()); cursor.close();
cursor.close(); return results;
return results; }
}
// Torrent Search package is not yet installed
// Torrent Search package is not yet installed cursor.close();
cursor.close(); return null;
return null;
}
}
/**
/** * Asks the Torrent Search module to download a torrent file given the provided url, while using the specifics of
* Asks the Torrent Search module to download a torrent file given the provided url, while using the specifics of * the supplied torrent search site to do so. This way the Search Module can take care of user credentials, for
* the supplied torrent search site to do so. This way the Search Module can take care of user credentials, for * example.
* example. *
* @param site The unique key of the search site that this url belongs to, which is used to create a connection * @param site The unique key of the search site that this url belongs to, which is used to create a connection
* specific to this (private) site * specific to this (private) site
* @param url The full url of the torrent to download * @param url The full url of the torrent to download
* @return A file input stream handler that points to the locally downloaded file * @return A file input stream handler that points to the locally downloaded file
* @throws FileNotFoundException Thrown when the requested url could not be downloaded or is not locally available * @throws FileNotFoundException Thrown when the requested url could not be downloaded or is not locally available
*/ */
public InputStream getFile(String site, String url) throws FileNotFoundException { public InputStream getFile(String site, String url) throws FileNotFoundException {
try { try {
Uri uri = Uri.parse("content://org.transdroid.search.torrentsearchprovider/get/" + site + "/" Uri uri = Uri.parse("content://org.transdroid.search.torrentsearchprovider/get/" + site + "/"
+ URLEncoder.encode(url, "UTF-8")); + URLEncoder.encode(url, "UTF-8"));
return context.getContentResolver().openInputStream(uri); return context.getContentResolver().openInputStream(uri);
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
// Ignore // Ignore
return null; return null;
} }
} }
public enum SearchSortOrder {
Combined, BySeeders
}
} }

186
app/src/main/java/org/transdroid/core/app/search/SearchResult.java

@ -16,107 +16,107 @@
*/ */
package org.transdroid.core.app.search; package org.transdroid.core.app.search;
import java.util.Date;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import java.util.Date;
/** /**
* Represents a search result as retrieved by querying the Torrent Search package. * Represents a search result as retrieved by querying the Torrent Search package.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class SearchResult implements Parcelable { public class SearchResult implements Parcelable {
private final int id; public static final Parcelable.Creator<SearchResult> CREATOR = new Parcelable.Creator<SearchResult>() {
private final String name; public SearchResult createFromParcel(Parcel in) {
private final String torrentUrl; return new SearchResult(in);
private final String detailsUrl; }
private final String size;
private final Date addedOn; public SearchResult[] newArray(int size) {
private final String seeders; return new SearchResult[size];
private final String leechers; }
};
public SearchResult(int id, String name, String torrentUrl, String detailsUrl, String size, long addedOnTime, private final int id;
String seeders, String leechers) { private final String name;
this.id = id; private final String torrentUrl;
this.name = name; private final String detailsUrl;
this.torrentUrl = torrentUrl; private final String size;
this.detailsUrl = detailsUrl; private final Date addedOn;
this.size = size; private final String seeders;
this.addedOn = (addedOnTime == -1L) ? null : new Date(addedOnTime); private final String leechers;
this.seeders = seeders;
this.leechers = leechers; public SearchResult(int id, String name, String torrentUrl, String detailsUrl, String size, long addedOnTime,
} String seeders, String leechers) {
this.id = id;
public int getId() { this.name = name;
return id; this.torrentUrl = torrentUrl;
} this.detailsUrl = detailsUrl;
this.size = size;
public String getName() { this.addedOn = (addedOnTime == -1L) ? null : new Date(addedOnTime);
return name; this.seeders = seeders;
} this.leechers = leechers;
}
public String getTorrentUrl() {
return torrentUrl; public SearchResult(Parcel in) {
} id = in.readInt();
name = in.readString();
public String getDetailsUrl() { torrentUrl = in.readString();
return detailsUrl; detailsUrl = in.readString();
} size = in.readString();
long addedOnIn = in.readLong();
public String getSize() { addedOn = addedOnIn == -1 ? null : new Date(addedOnIn);
return size; seeders = in.readString();
} leechers = in.readString();
}
public Date getAddedOn() {
return addedOn; public int getId() {
} return id;
}
public String getSeeders() {
return seeders; public String getName() {
} return name;
}
public String getLeechers() {
return leechers; public String getTorrentUrl() {
} return torrentUrl;
}
@Override
public int describeContents() { public String getDetailsUrl() {
return 0; return detailsUrl;
} }
@Override public String getSize() {
public void writeToParcel(Parcel out, int flags) { return size;
out.writeInt(id); }
out.writeString(name);
out.writeString(torrentUrl); public Date getAddedOn() {
out.writeString(detailsUrl); return addedOn;
out.writeString(size); }
out.writeLong(addedOn == null ? -1 : addedOn.getTime());
out.writeString(seeders); public String getSeeders() {
out.writeString(leechers); return seeders;
} }
public static final Parcelable.Creator<SearchResult> CREATOR = new Parcelable.Creator<SearchResult>() { public String getLeechers() {
public SearchResult createFromParcel(Parcel in) { return leechers;
return new SearchResult(in); }
}
@Override
public SearchResult[] newArray(int size) { public int describeContents() {
return new SearchResult[size]; return 0;
} }
};
@Override
public SearchResult(Parcel in) { public void writeToParcel(Parcel out, int flags) {
id = in.readInt(); out.writeInt(id);
name = in.readString(); out.writeString(name);
torrentUrl = in.readString(); out.writeString(torrentUrl);
detailsUrl = in.readString(); out.writeString(detailsUrl);
size = in.readString(); out.writeString(size);
long addedOnIn = in.readLong(); out.writeLong(addedOn == null ? -1 : addedOn.getTime());
addedOn = addedOnIn == -1 ? null : new Date(addedOnIn); out.writeString(seeders);
seeders = in.readString(); out.writeString(leechers);
leechers = in.readString(); }
}
} }

65
app/src/main/java/org/transdroid/core/app/search/SearchSite.java

@ -21,48 +21,49 @@ import org.transdroid.core.gui.search.SearchSetting;
/** /**
* Represents an available torrent site that can be searched using the Torrent Search package. * Represents an available torrent site that can be searched using the Torrent Search package.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class SearchSite implements SimpleListItem, SearchSetting { public class SearchSite implements SimpleListItem, SearchSetting {
private final int id; private final int id;
private final String key; private final String key;
private final String name; private final String name;
private final String rssFeedUrl; private final String rssFeedUrl;
private final boolean isPrivate; private final boolean isPrivate;
public SearchSite(int id, String key, String name, String rssFeedUrl, boolean isPrivate) { public SearchSite(int id, String key, String name, String rssFeedUrl, boolean isPrivate) {
this.id = id; this.id = id;
this.key = key; this.key = key;
this.name = name; this.name = name;
this.rssFeedUrl = rssFeedUrl; this.rssFeedUrl = rssFeedUrl;
this.isPrivate = isPrivate; this.isPrivate = isPrivate;
} }
public int getId() { public int getId() {
return id; return id;
} }
public String getKey() { public String getKey() {
return key; return key;
} }
@Override @Override
public String getName() { public String getName() {
return name; return name;
} }
public String getRssFeedUrl() { public String getRssFeedUrl() {
return rssFeedUrl; return rssFeedUrl;
} }
@Override @Override
public String getBaseUrl() { public String getBaseUrl() {
return rssFeedUrl; return rssFeedUrl;
} }
public boolean isPrivate() { public boolean isPrivate() {
return isPrivate; return isPrivate;
} }
} }

1467
app/src/main/java/org/transdroid/core/app/settings/ApplicationSettings.java

File diff suppressed because it is too large Load Diff

194
app/src/main/java/org/transdroid/core/app/settings/NotificationSettings.java

@ -30,103 +30,111 @@ import org.transdroid.R;
/** /**
* Allows instantiation of the settings specified in R.xml.pref_notifications. * Allows instantiation of the settings specified in R.xml.pref_notifications.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EBean(scope = Scope.Singleton) @EBean(scope = Scope.Singleton)
public class NotificationSettings { public class NotificationSettings {
private static final long MINIMUM_BACKGROUND_INTERVAL = 900_000; // 15 minutes private static final long MINIMUM_BACKGROUND_INTERVAL = 900_000; // 15 minutes
@RootContext @RootContext
protected Context context; protected Context context;
private SharedPreferences prefs; private SharedPreferences prefs;
protected NotificationSettings(Context context) { protected NotificationSettings(Context context) {
prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs = PreferenceManager.getDefaultSharedPreferences(context);
} }
/** /**
* Whether the background service is enabled and the user wants to receive RSS-related notifications * Whether the background service is enabled and the user wants to receive RSS-related notifications
* @return True if the server should be checked for RSS feed updates *
*/ * @return True if the server should be checked for RSS feed updates
public boolean isEnabledForRss() { */
return prefs.getBoolean("notifications_enabledrss", true); public boolean isEnabledForRss() {
} return prefs.getBoolean("notifications_enabledrss", true);
}
/**
* Whether the background service is enabled and the user wants to receive torrent-related notifications /**
* @return True if the server should be checked for torrent status updates * Whether the background service is enabled and the user wants to receive torrent-related notifications
*/ *
public boolean isEnabledForTorrents() { * @return True if the server should be checked for torrent status updates
return prefs.getBoolean("notifications_enabled", true); */
} public boolean isEnabledForTorrents() {
return prefs.getBoolean("notifications_enabled", true);
private String getRawInverval() { }
return prefs.getString("notifications_interval", "10800");
} private String getRawInverval() {
return prefs.getString("notifications_interval", "10800");
/** }
* Returns the interval between two server checks
* @return The interval, in milliseconds /**
*/ * Returns the interval between two server checks
public Long getInvervalInMilliseconds() { *
return Math.max(Long.parseLong(getRawInverval()) * 1000L, MINIMUM_BACKGROUND_INTERVAL); * @return The interval, in milliseconds
} */
public Long getInvervalInMilliseconds() {
private String getRawSound() { return Math.max(Long.parseLong(getRawInverval()) * 1000L, MINIMUM_BACKGROUND_INTERVAL);
return prefs.getString("notifications_sound", null); }
}
private String getRawSound() {
/** return prefs.getString("notifications_sound", null);
* Returns the sound (ring tone) to play on a new notification, or null if it should not play any }
* @return Either the user-specified sound, null if the user specified 'Silent' or the system default notification sound
*/ /**
public Uri getSound() { * Returns the sound (ring tone) to play on a new notification, or null if it should not play any
String raw = getRawSound(); *
if (raw == null) * @return Either the user-specified sound, null if the user specified 'Silent' or the system default notification sound
return null; */
if (raw.equals("")) public Uri getSound() {
return Settings.System.DEFAULT_NOTIFICATION_URI; String raw = getRawSound();
return Uri.parse(raw); if (raw == null)
} return null;
if (raw.equals(""))
/** return Settings.System.DEFAULT_NOTIFICATION_URI;
* Whether the device should vibrate on a new notification return Uri.parse(raw);
*/ }
public boolean shouldVibrate() {
return prefs.getBoolean("notifications_vibrate", false); /**
} * Whether the device should vibrate on a new notification
*/
/** public boolean shouldVibrate() {
* Returns the default vibrate pattern to use if the user enabled notification vibrations; check return prefs.getBoolean("notifications_vibrate", false);
* {@link #shouldVibrate()}, }
* @return A unique pattern for vibrations in Transdroid
*/ /**
public long[] getDefaultVibratePattern() { * Returns the default vibrate pattern to use if the user enabled notification vibrations; check
return new long[]{100, 100, 200, 300, 400, 700}; // Unique pattern? * {@link #shouldVibrate()},
} *
* @return A unique pattern for vibrations in Transdroid
private int getRawLedColour() { */
return prefs.getInt("notifications_ledcolour", -1); public long[] getDefaultVibratePattern() {
} return new long[]{100, 100, 200, 300, 400, 700}; // Unique pattern?
}
/**
* Returns the LED colour to use on a new notification private int getRawLedColour() {
* @return The integer value of the user-specified or default colour return prefs.getInt("notifications_ledcolour", -1);
*/ }
public int getDesiredLedColour() {
int raw = getRawLedColour(); /**
if (raw <= 0) * Returns the LED colour to use on a new notification
return context.getResources().getColor(R.color.ledgreen); *
return raw; * @return The integer value of the user-specified or default colour
} */
public int getDesiredLedColour() {
/** int raw = getRawLedColour();
* Whether the background service should report to ADW Launcher if (raw <= 0)
* @return True if the user want Transdroid to report to ADW Launcher return context.getResources().getColor(R.color.ledgreen);
*/ return raw;
public boolean shouldReportToAdwLauncher() { }
return prefs.getBoolean("notifications_adwnotify", false);
} /**
* Whether the background service should report to ADW Launcher
*
* @return True if the user want Transdroid to report to ADW Launcher
*/
public boolean shouldReportToAdwLauncher() {
return prefs.getBoolean("notifications_adwnotify", false);
}
} }

188
app/src/main/java/org/transdroid/core/app/settings/RssfeedSetting.java

@ -16,107 +16,111 @@
*/ */
package org.transdroid.core.app.settings; package org.transdroid.core.app.settings;
import java.util.Date; import android.net.Uri;
import android.text.TextUtils;
import org.transdroid.core.gui.lists.SimpleListItem; import org.transdroid.core.gui.lists.SimpleListItem;
import android.net.Uri; import java.util.Date;
import android.text.TextUtils;
/** /**
* Represents a user-specified RSS feed. * Represents a user-specified RSS feed.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class RssfeedSetting implements SimpleListItem { public class RssfeedSetting implements SimpleListItem {
private static final String DEFAULT_NAME = "Default"; private static final String DEFAULT_NAME = "Default";
private final int order; private final int order;
private final String name; private final String name;
private final String url; private final String url;
private final boolean requiresAuth; private final boolean requiresAuth;
private final boolean alarm; private final boolean alarm;
private final String excludeFilter; private final String excludeFilter;
private final String includeFilter; private final String includeFilter;
private Date lastViewed; private final String lastViewedItemUrl;
private final String lastViewedItemUrl; private Date lastViewed;
public RssfeedSetting(int order, String name, String baseUrl, boolean needsAuth, boolean alarm, String excludeFilter, String includeFilter, Date lastViewed, public RssfeedSetting(int order, String name, String baseUrl, boolean needsAuth, boolean alarm, String excludeFilter, String includeFilter, Date lastViewed,
String lastViewedItemUrl) { String lastViewedItemUrl) {
this.order = order; this.order = order;
this.name = name; this.name = name;
this.url = baseUrl; this.url = baseUrl;
this.requiresAuth = needsAuth; this.requiresAuth = needsAuth;
this.alarm = alarm; this.alarm = alarm;
this.excludeFilter = excludeFilter; this.excludeFilter = excludeFilter;
this.includeFilter = includeFilter; this.includeFilter = includeFilter;
this.lastViewed = lastViewed; this.lastViewed = lastViewed;
this.lastViewedItemUrl = lastViewedItemUrl; this.lastViewedItemUrl = lastViewedItemUrl;
} }
public int getOrder() { public int getOrder() {
return order; return order;
} }
@Override @Override
public String getName() { public String getName() {
if (!TextUtils.isEmpty(name)) if (!TextUtils.isEmpty(name))
return name; return name;
if (!TextUtils.isEmpty(url)) { if (!TextUtils.isEmpty(url)) {
String host = Uri.parse(url).getHost(); String host = Uri.parse(url).getHost();
return host == null ? DEFAULT_NAME : host; return host == null ? DEFAULT_NAME : host;
} }
return DEFAULT_NAME; return DEFAULT_NAME;
} }
public String getUrl() { public String getUrl() {
return url; return url;
} }
public boolean requiresExternalAuthentication() { public boolean requiresExternalAuthentication() {
return requiresAuth; return requiresAuth;
} }
public boolean shouldAlarmOnNewItems() { public boolean shouldAlarmOnNewItems() {
return alarm; return alarm;
} }
public String getExcludeFilter() { public String getExcludeFilter() {
return excludeFilter; return excludeFilter;
} }
public String getIncludeFilter() { public String getIncludeFilter() {
return includeFilter; return includeFilter;
} }
/** /**
* Returns the date on which we last checked this feed. Note that this is NOT updated automatically after the * Returns the date on which we last checked this feed. Note that this is NOT updated automatically after the
* settings were loaded from {@link ApplicationSettings}; instead the settings have to be manually loaded again * settings were loaded from {@link ApplicationSettings}; instead the settings have to be manually loaded again
* using {@link ApplicationSettings#getRssfeedSetting(int)}. * using {@link ApplicationSettings#getRssfeedSetting(int)}.
* @return The last new item's URL as URL-encoded string *
*/ * @return The last new item's URL as URL-encoded string
public Date getLastViewed() { */
return this.lastViewed; public Date getLastViewed() {
} return this.lastViewed;
}
/**
* Returns the URL of the item that was the newest last time we checked this feed. Note that this is NOT updated /**
* automatically after the settings were loaded from {@link ApplicationSettings}; instead the settings have to be * Returns the URL of the item that was the newest last time we checked this feed. Note that this is NOT updated
* manually loaded again using {@link ApplicationSettings#getRssfeedSetting(int)}. * automatically after the settings were loaded from {@link ApplicationSettings}; instead the settings have to be
* @return The last new item's URL as URL-encoded string * manually loaded again using {@link ApplicationSettings#getRssfeedSetting(int)}.
*/ *
public String getLastViewedItemUrl() { * @return The last new item's URL as URL-encoded string
return this.lastViewedItemUrl; */
} public String getLastViewedItemUrl() {
return this.lastViewedItemUrl;
/** }
* Returns a nicely formatted identifier containing (a portion of) the feed URL
* @return A string to identify this feed's URL /**
*/ * Returns a nicely formatted identifier containing (a portion of) the feed URL
public String getHumanReadableIdentifier() { *
String host = Uri.parse(url).getHost(); * @return A string to identify this feed's URL
String path = Uri.parse(url).getPath(); */
return (host == null ? null : host + (path == null ? "" : path)); public String getHumanReadableIdentifier() {
} String host = Uri.parse(url).getHost();
String path = Uri.parse(url).getPath();
return (host == null ? null : host + (path == null ? "" : path));
}
} }

596
app/src/main/java/org/transdroid/core/app/settings/ServerSetting.java

@ -29,303 +29,309 @@ import org.transdroid.daemon.OS;
/** /**
* Represents a user-configured remote server. * Represents a user-configured remote server.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class ServerSetting implements SimpleListItem { public class ServerSetting implements SimpleListItem {
private static final String DEFAULT_NAME = "Default"; private static final String DEFAULT_NAME = "Default";
private final int key; private final int key;
private final String name; private final String name;
private final Daemon type; private final Daemon type;
private final String address; private final String address;
private final String localAddress; private final String localAddress;
private final int localPort; private final int localPort;
private final String localNetwork; private final String localNetwork;
private final int port; private final int port;
private final String folder; private final String folder;
private final boolean useAuthentication; private final boolean useAuthentication;
private final String username; private final String username;
private final String password; private final String password;
private final String extraPass; private final String extraPass;
private final OS os; private final OS os;
private final String downloadDir; private final String downloadDir;
private final String ftpUrl; private final String ftpUrl;
private final String ftpPassword; private final String ftpPassword;
private final int timeout; private final int timeout;
private final boolean alarmOnFinishedDownload; private final boolean alarmOnFinishedDownload;
private final boolean alarmOnNewTorrent; private final boolean alarmOnNewTorrent;
private final boolean ssl; private final boolean ssl;
private final boolean localSsl; private final boolean localSsl;
private final boolean sslTrustAll; private final boolean sslTrustAll;
private final String sslTrustKey; private final String sslTrustKey;
private final String excludeFilter; private final String excludeFilter;
private final String includeFilter; private final String includeFilter;
private final boolean isAutoGenerated; private final boolean isAutoGenerated;
/** /**
* Creates a daemon settings instance, providing full connection details * Creates a daemon settings instance, providing full connection details
* @param name A name used to identify this server to the user *
* @param type The server daemon type * @param name A name used to identify this server to the user
* @param address The server domain name or IP address * @param type The server daemon type
* @param localAddress The server domain or IP address when connected to the server's local network * @param address The server domain name or IP address
* @param localPort The port on which the server is running in the server's local network * @param localAddress The server domain or IP address when connected to the server's local network
* @param localNetwork The server's local network SSID * @param localPort The port on which the server is running in the server's local network
* @param port The port on which the server daemon is running * @param localNetwork The server's local network SSID
* @param sslTrustKey The specific key that will be accepted. * @param port The port on which the server daemon is running
* @param folder The server folder (like a virtual sub-folder or an SCGI mount point) * @param sslTrustKey The specific key that will be accepted.
* @param useAuthentication Whether to use basic authentication * @param folder The server folder (like a virtual sub-folder or an SCGI mount point)
* @param username The user name to provide during authentication * @param useAuthentication Whether to use basic authentication
* @param password The password to provide during authentication * @param username The user name to provide during authentication
* @param extraPass The Deluge web interface password * @param password The password to provide during authentication
* @param downloadDir The default download directory (which may also be used as base directory for file paths) * @param extraPass The Deluge web interface password
* @param ftpUrl The partial URL to connect to when requesting FTP-style transfers * @param downloadDir The default download directory (which may also be used as base directory for file paths)
* @param timeout The number of seconds to wait before timing out a connection attempt * @param ftpUrl The partial URL to connect to when requesting FTP-style transfers
* @param isAutoGenerated Whether this setting was generated rather than manually inputed by the user * @param timeout The number of seconds to wait before timing out a connection attempt
*/ * @param isAutoGenerated Whether this setting was generated rather than manually inputed by the user
public ServerSetting(int key, String name, Daemon type, String address, String localAddress, int localPort, String localNetwork, int port, */
boolean ssl, boolean localSsl, boolean sslTrustAll, String sslTrustKey, String folder, boolean useAuthentication, String username, public ServerSetting(int key, String name, Daemon type, String address, String localAddress, int localPort, String localNetwork, int port,
String password, String extraPass, OS os, String downloadDir, String ftpUrl, String ftpPassword, int timeout, boolean ssl, boolean localSsl, boolean sslTrustAll, String sslTrustKey, String folder, boolean useAuthentication, String username,
boolean alarmOnFinishedDownload, boolean alarmOnNewTorrent, String excludeFilter, String includeFilter, String password, String extraPass, OS os, String downloadDir, String ftpUrl, String ftpPassword, int timeout,
boolean isAutoGenerated) { boolean alarmOnFinishedDownload, boolean alarmOnNewTorrent, String excludeFilter, String includeFilter,
this.key = key; boolean isAutoGenerated) {
this.name = name; this.key = key;
this.type = type; this.name = name;
this.address = address; this.type = type;
this.localAddress = localAddress; this.address = address;
this.localPort = localPort; this.localAddress = localAddress;
this.localNetwork = localNetwork; this.localPort = localPort;
this.port = port; this.localNetwork = localNetwork;
this.ssl = ssl; this.port = port;
this.localSsl = localSsl; this.ssl = ssl;
this.sslTrustAll = sslTrustAll; this.localSsl = localSsl;
this.sslTrustKey = sslTrustKey; this.sslTrustAll = sslTrustAll;
this.folder = folder; this.sslTrustKey = sslTrustKey;
this.useAuthentication = useAuthentication; this.folder = folder;
this.username = username; this.useAuthentication = useAuthentication;
this.password = password; this.username = username;
this.extraPass = extraPass; this.password = password;
this.os = os; this.extraPass = extraPass;
this.downloadDir = downloadDir; this.os = os;
this.ftpUrl = ftpUrl; this.downloadDir = downloadDir;
this.ftpPassword = ftpPassword; this.ftpUrl = ftpUrl;
this.timeout = timeout; this.ftpPassword = ftpPassword;
this.alarmOnFinishedDownload = alarmOnFinishedDownload; this.timeout = timeout;
this.alarmOnNewTorrent = alarmOnNewTorrent; this.alarmOnFinishedDownload = alarmOnFinishedDownload;
this.excludeFilter = excludeFilter; this.alarmOnNewTorrent = alarmOnNewTorrent;
this.includeFilter = includeFilter; this.excludeFilter = excludeFilter;
this.isAutoGenerated = isAutoGenerated; this.includeFilter = includeFilter;
} this.isAutoGenerated = isAutoGenerated;
}
@Override
public String getName() { @Override
if (!TextUtils.isEmpty(name)) { public String getName() {
return name; if (!TextUtils.isEmpty(name)) {
} return name;
if (!TextUtils.isEmpty(address)) { }
String host = Uri.parse(address).getHost(); if (!TextUtils.isEmpty(address)) {
return host == null ? DEFAULT_NAME : host; String host = Uri.parse(address).getHost();
} return host == null ? DEFAULT_NAME : host;
return DEFAULT_NAME; }
} return DEFAULT_NAME;
}
public Daemon getType() {
return type; public Daemon getType() {
} return type;
}
public String getAddress() {
return address; public String getAddress() {
} return address;
}
public String getLocalAddress() {
return localAddress; public String getLocalAddress() {
} return localAddress;
}
public int getLocalPort() {
return localPort; public int getLocalPort() {
} return localPort;
}
public String getLocalNetwork() {
return localNetwork; public String getLocalNetwork() {
} return localNetwork;
}
public int getPort() {
return port; public int getPort() {
} return port;
}
public boolean getSsl() {
return ssl; public boolean getSsl() {
} return ssl;
}
public boolean getLocalSsl() {
return localSsl; public boolean getLocalSsl() {
} return localSsl;
}
public boolean getSslTrustAll() {
return sslTrustAll; public boolean getSslTrustAll() {
} return sslTrustAll;
}
public String getSslTrustKey() {
return sslTrustKey; public String getSslTrustKey() {
} return sslTrustKey;
}
public String getFolder() {
return folder; public String getFolder() {
} return folder;
}
public boolean shouldUseAuthentication() {
return useAuthentication; public boolean shouldUseAuthentication() {
} return useAuthentication;
}
public String getUsername() {
return username; public String getUsername() {
} return username;
}
public String getPassword() {
return password; public String getPassword() {
} return password;
}
public String getExtraPassword() {
return extraPass; public String getExtraPassword() {
} return extraPass;
}
public OS getOS() {
return os; public OS getOS() {
} return os;
}
public String getDownloadDir() {
return downloadDir; public String getDownloadDir() {
} return downloadDir;
}
public String getFtpUrl() {
return ftpUrl; public String getFtpUrl() {
} return ftpUrl;
}
public String getFtpPassword() {
return ftpPassword; public String getFtpPassword() {
} return ftpPassword;
}
public int getTimeoutInMilliseconds() {
return timeout * 1000; public int getTimeoutInMilliseconds() {
} return timeout * 1000;
}
public boolean shouldAlarmOnFinishedDownload() {
return alarmOnFinishedDownload; public boolean shouldAlarmOnFinishedDownload() {
} return alarmOnFinishedDownload;
}
public boolean shouldAlarmOnNewTorrent() {
return alarmOnNewTorrent; public boolean shouldAlarmOnNewTorrent() {
} return alarmOnNewTorrent;
}
public String getExcludeFilter() {
return excludeFilter; public String getExcludeFilter() {
} return excludeFilter;
}
public String getIncludeFilter() {
return includeFilter; public String getIncludeFilter() {
} return includeFilter;
}
public boolean isAutoGenerated() {
return isAutoGenerated; public boolean isAutoGenerated() {
} return isAutoGenerated;
}
public int getOrder() {
return this.key; public int getOrder() {
} return this.key;
}
/**
* Returns a string that the user can use to identify the server by internal settings (rather than the name). /**
* @return A human-readable identifier in the form [https://]username@address:port/folder * Returns a string that the user can use to identify the server by internal settings (rather than the name).
*/ *
public String getHumanReadableIdentifier() { * @return A human-readable identifier in the form [https://]username@address:port/folder
if (isAutoGenerated) { */
// Hide the 'implementation details'; just give the username and server public String getHumanReadableIdentifier() {
return (this.shouldUseAuthentication() && !TextUtils.isEmpty(this.getUsername()) ? if (isAutoGenerated) {
this.getUsername() + "@" : "") + getAddress(); // Hide the 'implementation details'; just give the username and server
} return (this.shouldUseAuthentication() && !TextUtils.isEmpty(this.getUsername()) ?
return (this.ssl ? "https://" : "http://") + this.getUsername() + "@" : "") + getAddress();
(this.shouldUseAuthentication() && !TextUtils.isEmpty(this.getUsername()) ? this.getUsername() + "@" : }
"") + getAddress() + ":" + getPort() + return (this.ssl ? "https://" : "http://") +
(Daemon.supportsCustomFolder(getType()) && getFolder() != null ? getFolder() : ""); (this.shouldUseAuthentication() && !TextUtils.isEmpty(this.getUsername()) ? this.getUsername() + "@" :
} "") + getAddress() + ":" + getPort() +
(Daemon.supportsCustomFolder(getType()) && getFolder() != null ? getFolder() : "");
/** }
* Returns a string that acts as a unique identifier for this server, non-depending on the internal storage
* order/index. THis may be used to store additional details about this server elsewhere. It may change if the user /**
* changes server settings, but not with name or notification settings. * Returns a string that acts as a unique identifier for this server, non-depending on the internal storage
* @return A unique identifying string, based primarily on the configured address, port number, SSL settings and * order/index. THis may be used to store additional details about this server elsewhere. It may change if the user
* user name; returns null if the server is not yet fully identifiable (during configuration, for example) * changes server settings, but not with name or notification settings.
*/ *
public String getUniqueIdentifier() { * @return A unique identifying string, based primarily on the configured address, port number, SSL settings and
if (getType() == null || getAddress() == null || getAddress().equals("")) { * user name; returns null if the server is not yet fully identifiable (during configuration, for example)
return null; */
} public String getUniqueIdentifier() {
return getType().toString() + "|" + getHumanReadableIdentifier(); if (getType() == null || getAddress() == null || getAddress().equals("")) {
} return null;
}
@Override return getType().toString() + "|" + getHumanReadableIdentifier();
public boolean equals(Object o) { }
if (o instanceof ServerSetting) {
// Directly compare order numbers/unique keys @Override
return ((ServerSetting) o).getOrder() == this.key; public boolean equals(Object o) {
} else if (o instanceof DaemonSettings) { if (o instanceof ServerSetting) {
// Old-style DaemonSettings objects can be equal if they were constructed from a ServerSettings object: // Directly compare order numbers/unique keys
// idString should reflect the local key/order return ((ServerSetting) o).getOrder() == this.key;
return ((DaemonSettings) o).getIdString().equals(Integer.toString(this.key)); } else if (o instanceof DaemonSettings) {
} // Old-style DaemonSettings objects can be equal if they were constructed from a ServerSettings object:
// Other objects are never equal to this // idString should reflect the local key/order
return false; return ((DaemonSettings) o).getIdString().equals(Integer.toString(this.key));
} }
// Other objects are never equal to this
@Override return false;
public String toString() { }
return getUniqueIdentifier();
} @Override
public String toString() {
/** return getUniqueIdentifier();
* Returns the appropriate daemon adapter to which tasks can be executed, in accordance with this server's settings }
* @param connectedToNetwork The name of the (wifi) network we are currently connected to, or null if this could not
* be determined /**
* @param context A context to access the logger * Returns the appropriate daemon adapter to which tasks can be executed, in accordance with this server's settings
* @return An IDaemonAdapter instance of the specific torrent client daemon type *
*/ * @param connectedToNetwork The name of the (wifi) network we are currently connected to, or null if this could not
public IDaemonAdapter createServerAdapter(String connectedToNetwork, Context context) { * be determined
return type.createAdapter(convertToDaemonSettings(connectedToNetwork, context)); * @param context A context to access the logger
} * @return An IDaemonAdapter instance of the specific torrent client daemon type
*/
/** public IDaemonAdapter createServerAdapter(String connectedToNetwork, Context context) {
* Converts local server settings into an old-style {@link DaemonSettings} object. return type.createAdapter(convertToDaemonSettings(connectedToNetwork, context));
* @param connectedToNetwork The name of the (wifi) network we are currently connected to, or null if this could not }
* be determined
* @param caller A context to access the logger /**
* @return A {@link DaemonSettings} object to execute server commands against * Converts local server settings into an old-style {@link DaemonSettings} object.
*/ *
private DaemonSettings convertToDaemonSettings(String connectedToNetwork, Context caller) { * @param connectedToNetwork The name of the (wifi) network we are currently connected to, or null if this could not
// The local integer key is converted to the idString string. * be determined
// The host name address used is dependent on the network that we are currently connected to (to allow a * @param caller A context to access the logger
// distinct connection IP or host name when connected to a local network). * @return A {@link DaemonSettings} object to execute server commands against
if (!TextUtils.isEmpty(localNetwork)) { */
Log_.getInstance_(caller) private DaemonSettings convertToDaemonSettings(String connectedToNetwork, Context caller) {
.d("ServerSetting", "Creating adapter for " + name + " of type " + type.name() + ": connected to " + // The local integer key is converted to the idString string.
connectedToNetwork + " and configured local network is " + localNetwork); // The host name address used is dependent on the network that we are currently connected to (to allow a
} // distinct connection IP or host name when connected to a local network).
String addressToUse = address; if (!TextUtils.isEmpty(localNetwork)) {
int portToUse = port; Log_.getInstance_(caller)
boolean sslEnable = ssl; .d("ServerSetting", "Creating adapter for " + name + " of type " + type.name() + ": connected to " +
if (!TextUtils.isEmpty(localNetwork) && !TextUtils.isEmpty(localAddress) && connectedToNetwork + " and configured local network is " + localNetwork);
!TextUtils.isEmpty(connectedToNetwork)) { }
String[] localNetworks = localNetwork.split("\\|"); String addressToUse = address;
for (String network : localNetworks) { int portToUse = port;
if (connectedToNetwork.equals(network)) { boolean sslEnable = ssl;
addressToUse = localAddress; if (!TextUtils.isEmpty(localNetwork) && !TextUtils.isEmpty(localAddress) &&
portToUse = localPort; !TextUtils.isEmpty(connectedToNetwork)) {
sslEnable = localSsl; String[] localNetworks = localNetwork.split("\\|");
break; for (String network : localNetworks) {
} if (connectedToNetwork.equals(network)) {
} addressToUse = localAddress;
} portToUse = localPort;
return new DaemonSettings(name, type, addressToUse, portToUse, sslEnable, sslTrustAll, sslTrustKey, folder, sslEnable = localSsl;
useAuthentication, username, password, extraPass, os, downloadDir, ftpUrl, ftpPassword, timeout, break;
alarmOnFinishedDownload, alarmOnNewTorrent, Integer.toString(key), isAutoGenerated); }
} }
}
return new DaemonSettings(name, type, addressToUse, portToUse, sslEnable, sslTrustAll, sslTrustKey, folder,
useAuthentication, username, password, extraPass, os, downloadDir, ftpUrl, ftpPassword, timeout,
alarmOnFinishedDownload, alarmOnNewTorrent, Integer.toString(key), isAutoGenerated);
}
} }

630
app/src/main/java/org/transdroid/core/app/settings/SettingsPersistence.java

@ -44,42 +44,44 @@ import java.io.OutputStream;
@EBean(scope = Scope.Singleton) @EBean(scope = Scope.Singleton)
public class SettingsPersistence { public class SettingsPersistence {
@Bean public static final String DEFAULT_SETTINGS_DIR = Environment.getExternalStorageDirectory().toString()
protected ApplicationSettings applicationSettings; + "/Transdroid/";
@Bean public static final String DEFAULT_SETTINGS_FILENAME = "settings.json";
protected SystemSettings systemSettings; public static final File DEFAULT_SETTINGS_FILE = new File(DEFAULT_SETTINGS_DIR + DEFAULT_SETTINGS_FILENAME);
@Bean
public static final String DEFAULT_SETTINGS_DIR = Environment.getExternalStorageDirectory().toString() protected ApplicationSettings applicationSettings;
+ "/Transdroid/"; @Bean
public static final String DEFAULT_SETTINGS_FILENAME = "settings.json"; protected SystemSettings systemSettings;
public static final File DEFAULT_SETTINGS_FILE = new File(DEFAULT_SETTINGS_DIR + DEFAULT_SETTINGS_FILENAME);
/**
/** * Reads the server, web searches, RSS feed, background service and system settings from a JSON-encoded String, such as when read via a QR code.
* Reads the server, web searches, RSS feed, background service and system settings from a JSON-encoded String, such as when read via a QR code. *
* @param prefs The application-global preferences object to write settings to * @param prefs The application-global preferences object to write settings to
* @param contents The JSON-encoded settings as raw String * @param contents The JSON-encoded settings as raw String
* @throws JSONException Thrown when the file did not contain valid JSON content * @throws JSONException Thrown when the file did not contain valid JSON content
*/ */
public void importSettingsAsString(SharedPreferences prefs, String contents) throws JSONException { public void importSettingsAsString(SharedPreferences prefs, String contents) throws JSONException {
importSettings(prefs, new JSONObject(contents)); importSettings(prefs, new JSONObject(contents));
} }
/** /**
* Synchronously reads the server, web searches, RSS feed, background service and system settings from a file in * Synchronously reads the server, web searches, RSS feed, background service and system settings from a file in
* JSON format. * JSON format.
* @param prefs The application-global preferences object to write settings to *
* @param settingsFile The local file to read the settings from * @param prefs The application-global preferences object to write settings to
* @throws FileNotFoundException Thrown when the settings file doesn't exist or couldn't be read * @param settingsFile The local file to read the settings from
* @throws JSONException Thrown when the file did not contain valid JSON content * @throws FileNotFoundException Thrown when the settings file doesn't exist or couldn't be read
*/ * @throws JSONException Thrown when the file did not contain valid JSON content
public void importSettingsFromFile(SharedPreferences prefs, File settingsFile) throws FileNotFoundException, JSONException { */
public void importSettingsFromFile(SharedPreferences prefs, File settingsFile) throws FileNotFoundException, JSONException {
importSettingsFromStream(prefs, new FileInputStream(settingsFile)); importSettingsFromStream(prefs, new FileInputStream(settingsFile));
} }
/** /**
* Synchronously reads the server, web searches, RSS feed, background service and system settings from a stream (file) in * Synchronously reads the server, web searches, RSS feed, background service and system settings from a stream (file) in
* JSON format. * JSON format.
* @param prefs The application-global preferences object to write settings to *
* @param prefs The application-global preferences object to write settings to
* @param settingsStream The stream to read the settings from * @param settingsStream The stream to read the settings from
* @throws JSONException Thrown when the file did not contain valid JSON content * @throws JSONException Thrown when the file did not contain valid JSON content
*/ */
@ -88,287 +90,289 @@ public class SettingsPersistence {
importSettings(prefs, new JSONObject(raw)); importSettings(prefs, new JSONObject(raw));
} }
public void importSettings(SharedPreferences prefs, JSONObject json) throws JSONException { public void importSettings(SharedPreferences prefs, JSONObject json) throws JSONException {
Editor editor = prefs.edit(); Editor editor = prefs.edit();
// Import servers // Import servers
if (json.has("servers")) { if (json.has("servers")) {
JSONArray servers = json.getJSONArray("servers"); JSONArray servers = json.getJSONArray("servers");
for (int i = 0; i < servers.length(); i++) { for (int i = 0; i < servers.length(); i++) {
JSONObject server = servers.getJSONObject(i); JSONObject server = servers.getJSONObject(i);
String postfix = Integer.toString(applicationSettings.getMaxOfAllServers() + 1 + i); String postfix = Integer.toString(applicationSettings.getMaxOfAllServers() + 1 + i);
if (server.has("name")) if (server.has("name"))
editor.putString("server_name_" + postfix, server.getString("name")); editor.putString("server_name_" + postfix, server.getString("name"));
if (server.has("type")) if (server.has("type"))
editor.putString("server_type_" + postfix, server.getString("type")); editor.putString("server_type_" + postfix, server.getString("type"));
if (server.has("host")) if (server.has("host"))
editor.putString("server_address_" + postfix, server.getString("host")); editor.putString("server_address_" + postfix, server.getString("host"));
if (server.has("local_network")) if (server.has("local_network"))
editor.putString("server_localnetwork_" + postfix, server.getString("local_network")); editor.putString("server_localnetwork_" + postfix, server.getString("local_network"));
if (server.has("local_host")) if (server.has("local_host"))
editor.putString("server_localaddress_" + postfix, server.getString("local_host")); editor.putString("server_localaddress_" + postfix, server.getString("local_host"));
if (server.has("local_port")) if (server.has("local_port"))
editor.putString("server_localport_" + postfix, server.getString("local_port")); editor.putString("server_localport_" + postfix, server.getString("local_port"));
if (server.has("port")) if (server.has("port"))
editor.putString("server_port_" + postfix, server.getString("port")); editor.putString("server_port_" + postfix, server.getString("port"));
if (server.has("ssl")) if (server.has("ssl"))
editor.putBoolean("server_sslenabled_" + postfix, server.getBoolean("ssl")); editor.putBoolean("server_sslenabled_" + postfix, server.getBoolean("ssl"));
if (server.has("local_ssl")) if (server.has("local_ssl"))
editor.putBoolean("server_localsslenabled_" + postfix, server.getBoolean("local_ssl")); editor.putBoolean("server_localsslenabled_" + postfix, server.getBoolean("local_ssl"));
if (server.has("ssl_accept_all")) if (server.has("ssl_accept_all"))
editor.putBoolean("server_ssltrustall_" + postfix, server.getBoolean("ssl_accept_all")); editor.putBoolean("server_ssltrustall_" + postfix, server.getBoolean("ssl_accept_all"));
if (server.has("ssl_trust_key")) if (server.has("ssl_trust_key"))
editor.putString("server_ssltrustkey_" + postfix, server.getString("ssl_trust_key")); editor.putString("server_ssltrustkey_" + postfix, server.getString("ssl_trust_key"));
if (server.has("folder")) if (server.has("folder"))
editor.putString("server_folder_" + postfix, server.getString("folder")); editor.putString("server_folder_" + postfix, server.getString("folder"));
if (server.has("use_auth")) if (server.has("use_auth"))
editor.putBoolean("server_disableauth_" + postfix, !server.getBoolean("use_auth")); editor.putBoolean("server_disableauth_" + postfix, !server.getBoolean("use_auth"));
if (server.has("username")) if (server.has("username"))
editor.putString("server_user_" + postfix, server.getString("username")); editor.putString("server_user_" + postfix, server.getString("username"));
if (server.has("password")) if (server.has("password"))
editor.putString("server_pass_" + postfix, server.getString("password")); editor.putString("server_pass_" + postfix, server.getString("password"));
if (server.has("extra_password")) if (server.has("extra_password"))
editor.putString("server_extrapass_" + postfix, server.getString("extra_password")); editor.putString("server_extrapass_" + postfix, server.getString("extra_password"));
if (server.has("os_type")) if (server.has("os_type"))
editor.putString("server_os_" + postfix, server.getString("os_type")); editor.putString("server_os_" + postfix, server.getString("os_type"));
if (server.has("downloads_dir")) if (server.has("downloads_dir"))
editor.putString("server_downloaddir_" + postfix, server.getString("downloads_dir")); editor.putString("server_downloaddir_" + postfix, server.getString("downloads_dir"));
if (server.has("base_ftp_url")) if (server.has("base_ftp_url"))
editor.putString("server_ftpurl_" + postfix, server.getString("base_ftp_url")); editor.putString("server_ftpurl_" + postfix, server.getString("base_ftp_url"));
if (server.has("ftp_password")) if (server.has("ftp_password"))
editor.putString("server_ftppass_" + postfix, server.getString("ftp_password")); editor.putString("server_ftppass_" + postfix, server.getString("ftp_password"));
if (server.has("server_timeout")) if (server.has("server_timeout"))
editor.putString("server_timeout_" + postfix, server.getString("server_timeout")); editor.putString("server_timeout_" + postfix, server.getString("server_timeout"));
if (server.has("download_alarm")) if (server.has("download_alarm"))
editor.putBoolean("server_alarmfinished_" + postfix, server.getBoolean("download_alarm")); editor.putBoolean("server_alarmfinished_" + postfix, server.getBoolean("download_alarm"));
if (server.has("new_torrent_alarm")) if (server.has("new_torrent_alarm"))
editor.putBoolean("server_alarmnew_" + postfix, server.getBoolean("new_torrent_alarm")); editor.putBoolean("server_alarmnew_" + postfix, server.getBoolean("new_torrent_alarm"));
if (server.has("alarm_filter_exclude")) if (server.has("alarm_filter_exclude"))
editor.putString("server_alarmexclude_" + postfix, server.getString("alarm_filter_exclude")); editor.putString("server_alarmexclude_" + postfix, server.getString("alarm_filter_exclude"));
if (server.has("alarm_filter_include")) if (server.has("alarm_filter_include"))
editor.putString("server_alarminclude_" + postfix, server.getString("alarm_filter_include")); editor.putString("server_alarminclude_" + postfix, server.getString("alarm_filter_include"));
} }
} }
// Import web search sites // Import web search sites
if (json.has("websites")) { if (json.has("websites")) {
JSONArray sites = json.getJSONArray("websites"); JSONArray sites = json.getJSONArray("websites");
for (int i = 0; i < sites.length(); i++) { for (int i = 0; i < sites.length(); i++) {
JSONObject site = sites.getJSONObject(i); JSONObject site = sites.getJSONObject(i);
String postfix = Integer.toString(applicationSettings.getMaxWebsearch() + 1 + i); String postfix = Integer.toString(applicationSettings.getMaxWebsearch() + 1 + i);
if (site.has("name")) if (site.has("name"))
editor.putString("websearch_name_" + postfix, site.getString("name")); editor.putString("websearch_name_" + postfix, site.getString("name"));
if (site.has("url")) if (site.has("url"))
editor.putString("websearch_baseurl_" + postfix, site.getString("url")); editor.putString("websearch_baseurl_" + postfix, site.getString("url"));
if (site.has("cookies")) if (site.has("cookies"))
editor.putString("websearch_cookies_" + postfix, site.getString("cookies")); editor.putString("websearch_cookies_" + postfix, site.getString("cookies"));
} }
} }
// Import RSS feeds // Import RSS feeds
if (json.has("rssfeeds")) { if (json.has("rssfeeds")) {
JSONArray feeds = json.getJSONArray("rssfeeds"); JSONArray feeds = json.getJSONArray("rssfeeds");
for (int i = 0; i < feeds.length(); i++) { for (int i = 0; i < feeds.length(); i++) {
JSONObject feed = feeds.getJSONObject(i); JSONObject feed = feeds.getJSONObject(i);
String postfix = Integer.toString(applicationSettings.getMaxRssfeed() + 1 + i); String postfix = Integer.toString(applicationSettings.getMaxRssfeed() + 1 + i);
if (feed.has("name")) if (feed.has("name"))
editor.putString("rssfeed_name_" + postfix, feed.getString("name")); editor.putString("rssfeed_name_" + postfix, feed.getString("name"));
if (feed.has("url")) if (feed.has("url"))
editor.putString("rssfeed_url_" + postfix, feed.getString("url")); editor.putString("rssfeed_url_" + postfix, feed.getString("url"));
if (feed.has("needs_auth")) if (feed.has("needs_auth"))
editor.putBoolean("rssfeed_reqauth_" + postfix, feed.getBoolean("needs_auth")); editor.putBoolean("rssfeed_reqauth_" + postfix, feed.getBoolean("needs_auth"));
if (feed.has("new_item_alarm")) if (feed.has("new_item_alarm"))
editor.putBoolean("rssfeed_alarmnew_" + postfix, feed.getBoolean("new_item_alarm")); editor.putBoolean("rssfeed_alarmnew_" + postfix, feed.getBoolean("new_item_alarm"));
if (feed.has("alarm_filter_include")) if (feed.has("alarm_filter_include"))
editor.putString("rssfeed_include_" + postfix, feed.getString("alarm_filter_include")); editor.putString("rssfeed_include_" + postfix, feed.getString("alarm_filter_include"));
if (feed.has("alarm_filter_exclude")) if (feed.has("alarm_filter_exclude"))
editor.putString("rssfeed_exclude_" + postfix, feed.getString("alarm_filter_exclude")); editor.putString("rssfeed_exclude_" + postfix, feed.getString("alarm_filter_exclude"));
if (feed.has("last_seen_time")) if (feed.has("last_seen_time"))
editor.putLong("rssfeed_lastviewed_" + postfix, feed.getLong("last_seen_time")); editor.putLong("rssfeed_lastviewed_" + postfix, feed.getLong("last_seen_time"));
if (feed.has("last_seen_item")) if (feed.has("last_seen_item"))
editor.putString("rssfeed_lastvieweditemurl_" + postfix, feed.getString("last_seen_item")); editor.putString("rssfeed_lastvieweditemurl_" + postfix, feed.getString("last_seen_item"));
} }
} }
// Import background service and system settings // Import background service and system settings
if (json.has("alarm_enabled_rss")) if (json.has("alarm_enabled_rss"))
editor.putBoolean("notifications_enabledrss", json.getBoolean("alarm_enabled_rss")); editor.putBoolean("notifications_enabledrss", json.getBoolean("alarm_enabled_rss"));
if (json.has("alarm_enabled_torrents")) if (json.has("alarm_enabled_torrents"))
editor.putBoolean("notifications_enabled", json.getBoolean("alarm_enabled_torrents")); editor.putBoolean("notifications_enabled", json.getBoolean("alarm_enabled_torrents"));
else if (json.has("alarm_enabled")) // Compat else if (json.has("alarm_enabled")) // Compat
editor.putBoolean("notifications_enabled", json.getBoolean("alarm_enabled")); editor.putBoolean("notifications_enabled", json.getBoolean("alarm_enabled"));
if (json.has("alarm_interval")) if (json.has("alarm_interval"))
editor.putString("notifications_interval", json.getString("alarm_interval")); editor.putString("notifications_interval", json.getString("alarm_interval"));
if (json.has("alarm_sound_uri")) if (json.has("alarm_sound_uri"))
editor.putString("notifications_sound", json.getString("alarm_sound_uri")); editor.putString("notifications_sound", json.getString("alarm_sound_uri"));
if (json.has("alarm_vibrate")) if (json.has("alarm_vibrate"))
editor.putBoolean("notifications_vibrate", json.getBoolean("alarm_vibrate")); editor.putBoolean("notifications_vibrate", json.getBoolean("alarm_vibrate"));
if (json.has("alarm_ledcolour")) if (json.has("alarm_ledcolour"))
editor.putInt("notifications_ledcolour", json.getInt("alarm_ledcolour")); editor.putInt("notifications_ledcolour", json.getInt("alarm_ledcolour"));
if (json.has("alarm_adwnotifications")) if (json.has("alarm_adwnotifications"))
editor.putBoolean("notifications_adwnotify", json.getBoolean("alarm_adwnotifications")); editor.putBoolean("notifications_adwnotify", json.getBoolean("alarm_adwnotifications"));
if (json.has("system_dormantasinactive")) if (json.has("system_dormantasinactive"))
editor.putBoolean("system_dormantasinactive", json.getBoolean("system_dormantasinactive")); editor.putBoolean("system_dormantasinactive", json.getBoolean("system_dormantasinactive"));
if (json.has("system_autorefresh")) if (json.has("system_autorefresh"))
editor.putString("system_autorefresh", json.getString("system_autorefresh")); editor.putString("system_autorefresh", json.getString("system_autorefresh"));
if (json.has("system_checkupdates")) if (json.has("system_checkupdates"))
editor.putBoolean("system_checkupdates", json.getBoolean("system_checkupdates")); editor.putBoolean("system_checkupdates", json.getBoolean("system_checkupdates"));
if (json.has("system_usedarktheme")) if (json.has("system_usedarktheme"))
editor.putBoolean("system_usedarktheme", json.getBoolean("system_usedarktheme")); editor.putBoolean("system_usedarktheme", json.getBoolean("system_usedarktheme"));
editor.apply(); editor.apply();
} }
/** /**
* Returns encoded server, web searches, RSS feed, background service and system settings as a JSON data object structure, serialized to a String. * Returns encoded server, web searches, RSS feed, background service and system settings as a JSON data object structure, serialized to a String.
* @param prefs The application-global preferences object to read settings from *
* @throws JSONException Thrown when the JSON content could not be constructed properly * @param prefs The application-global preferences object to read settings from
*/ * @throws JSONException Thrown when the JSON content could not be constructed properly
public String exportSettingsAsString(SharedPreferences prefs) throws JSONException { */
return exportSettings(prefs).toString(); public String exportSettingsAsString(SharedPreferences prefs) throws JSONException {
} return exportSettings(prefs).toString();
}
/**
* Synchronously writes the server, web searches, RSS feed, background service and system settings to a file in JSON /**
* format. * Synchronously writes the server, web searches, RSS feed, background service and system settings to a file in JSON
* @param prefs The application-global preferences object to read settings from * format.
* @param settingsFile The local file to read the settings from *
* @throws JSONException Thrown when the JSON content could not be constructed properly * @param prefs The application-global preferences object to read settings from
* @throws IOException Thrown when the settings file could not be created or written to * @param settingsFile The local file to read the settings from
*/ * @throws JSONException Thrown when the JSON content could not be constructed properly
public void exportSettingsToFile(SharedPreferences prefs, File settingsFile) throws JSONException, IOException { * @throws IOException Thrown when the settings file could not be created or written to
if (settingsFile.exists()) { */
settingsFile.delete(); public void exportSettingsToFile(SharedPreferences prefs, File settingsFile) throws JSONException, IOException {
} if (settingsFile.exists()) {
settingsFile.getParentFile().mkdirs(); settingsFile.delete();
settingsFile.createNewFile(); }
exportSettingsToStream(prefs, new FileOutputStream(settingsFile)); settingsFile.getParentFile().mkdirs();
} settingsFile.createNewFile();
exportSettingsToStream(prefs, new FileOutputStream(settingsFile));
/** }
* Synchronously writes the server, web searches, RSS feed, background service and system settings to a stream (file) in JSON format. The stream
* will be closed regardless of success. /**
* * Synchronously writes the server, web searches, RSS feed, background service and system settings to a stream (file) in JSON format. The stream
* @param prefs The application-global preferences object to read settings from * will be closed regardless of success.
* @param settingsStream The stream to read the settings to *
* @throws JSONException Thrown when the JSON content could not be constructed properly * @param prefs The application-global preferences object to read settings from
* @throws IOException Thrown when the settings file could not be created or written to * @param settingsStream The stream to read the settings to
*/ * @throws JSONException Thrown when the JSON content could not be constructed properly
public void exportSettingsToStream(SharedPreferences prefs, OutputStream settingsStream) throws JSONException, IOException { * @throws IOException Thrown when the settings file could not be created or written to
try { */
JSONObject json = exportSettings(prefs); public void exportSettingsToStream(SharedPreferences prefs, OutputStream settingsStream) throws JSONException, IOException {
settingsStream.write(json.toString(2).getBytes()); try {
} finally { JSONObject json = exportSettings(prefs);
settingsStream.close(); settingsStream.write(json.toString(2).getBytes());
} } finally {
} settingsStream.close();
}
private JSONObject exportSettings(SharedPreferences prefs) throws JSONException { }
// Create a single JSON object that will contain all settings private JSONObject exportSettings(SharedPreferences prefs) throws JSONException {
JSONObject json = new JSONObject();
// Create a single JSON object that will contain all settings
// Convert server settings into JSON JSONObject json = new JSONObject();
JSONArray servers = new JSONArray();
int i = 0; // Convert server settings into JSON
String postfixi = "0"; JSONArray servers = new JSONArray();
while (prefs.contains("server_type_" + postfixi)) { int i = 0;
String postfixi = "0";
JSONObject server = new JSONObject(); while (prefs.contains("server_type_" + postfixi)) {
server.put("name", prefs.getString("server_name_" + postfixi, null));
server.put("type", prefs.getString("server_type_" + postfixi, null)); JSONObject server = new JSONObject();
server.put("host", prefs.getString("server_address_" + postfixi, null)); server.put("name", prefs.getString("server_name_" + postfixi, null));
server.put("local_network", prefs.getString("server_localnetwork_" + postfixi, null)); server.put("type", prefs.getString("server_type_" + postfixi, null));
server.put("local_host", prefs.getString("server_localaddress_" + postfixi, null)); server.put("host", prefs.getString("server_address_" + postfixi, null));
server.put("local_port", prefs.getString("server_localport_" + postfixi, null)); server.put("local_network", prefs.getString("server_localnetwork_" + postfixi, null));
server.put("port", prefs.getString("server_port_" + postfixi, null)); server.put("local_host", prefs.getString("server_localaddress_" + postfixi, null));
server.put("ssl", prefs.getBoolean("server_sslenabled_" + postfixi, false)); server.put("local_port", prefs.getString("server_localport_" + postfixi, null));
server.put("local_ssl", prefs.getBoolean("server_localsslenabled_" + postfixi, false)); server.put("port", prefs.getString("server_port_" + postfixi, null));
server.put("ssl_accept_all", prefs.getBoolean("server_ssltrustall_" + postfixi, false)); server.put("ssl", prefs.getBoolean("server_sslenabled_" + postfixi, false));
server.put("ssl_trust_key", prefs.getString("server_ssltrustkey_" + postfixi, null)); server.put("local_ssl", prefs.getBoolean("server_localsslenabled_" + postfixi, false));
server.put("folder", prefs.getString("server_folder_" + postfixi, null)); server.put("ssl_accept_all", prefs.getBoolean("server_ssltrustall_" + postfixi, false));
server.put("use_auth", !prefs.getBoolean("server_disableauth_" + postfixi, false)); server.put("ssl_trust_key", prefs.getString("server_ssltrustkey_" + postfixi, null));
server.put("username", prefs.getString("server_user_" + postfixi, null)); server.put("folder", prefs.getString("server_folder_" + postfixi, null));
server.put("password", prefs.getString("server_pass_" + postfixi, null)); server.put("use_auth", !prefs.getBoolean("server_disableauth_" + postfixi, false));
server.put("extra_password", prefs.getString("server_extrapass_" + postfixi, null)); server.put("username", prefs.getString("server_user_" + postfixi, null));
server.put("os_type", prefs.getString("server_os_" + postfixi, null)); server.put("password", prefs.getString("server_pass_" + postfixi, null));
server.put("downloads_dir", prefs.getString("server_downloaddir_" + postfixi, null)); server.put("extra_password", prefs.getString("server_extrapass_" + postfixi, null));
server.put("base_ftp_url", prefs.getString("server_ftpurl_" + postfixi, null)); server.put("os_type", prefs.getString("server_os_" + postfixi, null));
server.put("ftp_password", prefs.getString("server_ftppass_" + postfixi, null)); server.put("downloads_dir", prefs.getString("server_downloaddir_" + postfixi, null));
server.put("server_timeout", prefs.getString("server_timeout_" + postfixi, null)); server.put("base_ftp_url", prefs.getString("server_ftpurl_" + postfixi, null));
server.put("download_alarm", prefs.getBoolean("server_alarmfinished_" + postfixi, false)); server.put("ftp_password", prefs.getString("server_ftppass_" + postfixi, null));
server.put("new_torrent_alarm", prefs.getBoolean("server_alarmnew_" + postfixi, false)); server.put("server_timeout", prefs.getString("server_timeout_" + postfixi, null));
server.put("alarm_filter_exclude", prefs.getString("server_alarmexclude_" + postfixi, null)); server.put("download_alarm", prefs.getBoolean("server_alarmfinished_" + postfixi, false));
server.put("alarm_filter_include", prefs.getString("server_alarminclude_" + postfixi, null)); server.put("new_torrent_alarm", prefs.getBoolean("server_alarmnew_" + postfixi, false));
server.put("alarm_filter_exclude", prefs.getString("server_alarmexclude_" + postfixi, null));
servers.put(server); server.put("alarm_filter_include", prefs.getString("server_alarminclude_" + postfixi, null));
i++;
postfixi = Integer.toString(i); servers.put(server);
} i++;
json.put("servers", servers); postfixi = Integer.toString(i);
}
// Convert web search settings into JSON json.put("servers", servers);
JSONArray sites = new JSONArray();
int j = 0; // Convert web search settings into JSON
String postfixj = "0"; JSONArray sites = new JSONArray();
while (prefs.contains("websearch_baseurl_" + postfixj)) { int j = 0;
String postfixj = "0";
JSONObject site = new JSONObject(); while (prefs.contains("websearch_baseurl_" + postfixj)) {
site.put("name", prefs.getString("websearch_name_" + postfixj, null));
site.put("url", prefs.getString("websearch_baseurl_" + postfixj, null)); JSONObject site = new JSONObject();
site.put("cookies", prefs.getString("websearch_cookies_" + postfixj, null)); site.put("name", prefs.getString("websearch_name_" + postfixj, null));
site.put("url", prefs.getString("websearch_baseurl_" + postfixj, null));
sites.put(site); site.put("cookies", prefs.getString("websearch_cookies_" + postfixj, null));
j++;
postfixj = Integer.toString(j); sites.put(site);
} j++;
json.put("websites", sites); postfixj = Integer.toString(j);
}
// Convert RSS feed settings into JSON json.put("websites", sites);
JSONArray feeds = new JSONArray();
int k = 0; // Convert RSS feed settings into JSON
String postfixk = "0"; JSONArray feeds = new JSONArray();
while (prefs.contains("rssfeed_url_" + postfixk)) { int k = 0;
String postfixk = "0";
JSONObject feed = new JSONObject(); while (prefs.contains("rssfeed_url_" + postfixk)) {
feed.put("name", prefs.getString("rssfeed_name_" + postfixk, null));
feed.put("url", prefs.getString("rssfeed_url_" + postfixk, null)); JSONObject feed = new JSONObject();
feed.put("needs_auth", prefs.getBoolean("rssfeed_reqauth_" + postfixk, false)); feed.put("name", prefs.getString("rssfeed_name_" + postfixk, null));
feed.put("new_item_alarm", prefs.getBoolean("rssfeed_alarmnew_" + postfixk, false)); feed.put("url", prefs.getString("rssfeed_url_" + postfixk, null));
feed.put("alarm_filter_exclude", prefs.getString("rssfeed_exclude_" + postfixk, null)); feed.put("needs_auth", prefs.getBoolean("rssfeed_reqauth_" + postfixk, false));
feed.put("alarm_filter_include", prefs.getString("rssfeed_include_" + postfixk, null)); feed.put("new_item_alarm", prefs.getBoolean("rssfeed_alarmnew_" + postfixk, false));
feed.put("last_seen_time", prefs.getLong("rssfeed_lastviewed_" + postfixk, -1)); feed.put("alarm_filter_exclude", prefs.getString("rssfeed_exclude_" + postfixk, null));
feed.put("last_seen_item", prefs.getString("rssfeed_lastvieweditemurl_" + postfixk, null)); feed.put("alarm_filter_include", prefs.getString("rssfeed_include_" + postfixk, null));
feed.put("last_seen_time", prefs.getLong("rssfeed_lastviewed_" + postfixk, -1));
feeds.put(feed); feed.put("last_seen_item", prefs.getString("rssfeed_lastvieweditemurl_" + postfixk, null));
k++;
postfixk = Integer.toString(k); feeds.put(feed);
} k++;
json.put("rssfeeds", feeds); postfixk = Integer.toString(k);
}
// Convert background service and system settings into JSON json.put("rssfeeds", feeds);
json.put("alarm_enabled_rss", prefs.getBoolean("notifications_enabledrss", true));
json.put("alarm_enabled_torrents", prefs.getBoolean("notifications_enabled", true)); // Convert background service and system settings into JSON
json.put("alarm_interval", prefs.getString("notifications_interval", null)); json.put("alarm_enabled_rss", prefs.getBoolean("notifications_enabledrss", true));
json.put("alarm_sound_uri", prefs.getString("notifications_sound", null)); json.put("alarm_enabled_torrents", prefs.getBoolean("notifications_enabled", true));
json.put("alarm_vibrate", prefs.getBoolean("notifications_vibrate", false)); json.put("alarm_interval", prefs.getString("notifications_interval", null));
json.put("alarm_ledcolour", prefs.getInt("notifications_ledcolour", -1)); json.put("alarm_sound_uri", prefs.getString("notifications_sound", null));
json.put("alarm_adwnotifications", prefs.getBoolean("notifications_adwnotify", false)); json.put("alarm_vibrate", prefs.getBoolean("notifications_vibrate", false));
json.put("system_dormantasinactive", prefs.getBoolean("system_dormantasinactive", false)); json.put("alarm_ledcolour", prefs.getInt("notifications_ledcolour", -1));
json.put("system_autorefresh", prefs.getString("system_autorefresh", "0")); json.put("alarm_adwnotifications", prefs.getBoolean("notifications_adwnotify", false));
json.put("system_usedarktheme", prefs.getBoolean("system_usedarktheme", false)); json.put("system_dormantasinactive", prefs.getBoolean("system_dormantasinactive", false));
json.put("system_checkupdates", prefs.getBoolean("system_checkupdates", true)); json.put("system_autorefresh", prefs.getString("system_autorefresh", "0"));
json.put("system_usedarktheme", prefs.getBoolean("system_usedarktheme", false));
return json; json.put("system_checkupdates", prefs.getBoolean("system_checkupdates", true));
} return json;
}
} }

3
app/src/main/java/org/transdroid/core/app/settings/SettingsUtils.java

@ -1,7 +1,6 @@
package org.transdroid.core.app.settings; package org.transdroid.core.app.settings;
import android.content.Context;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
@ -33,6 +32,6 @@ public class SettingsUtils {
return builder; return builder;
} }
return builder.theme(settings.useDarkTheme() ? Theme.DARK: Theme.LIGHT); return builder.theme(settings.useDarkTheme() ? Theme.DARK : Theme.LIGHT);
} }
} }

84
app/src/main/java/org/transdroid/core/app/settings/SystemSettings.java

@ -29,58 +29,62 @@ import java.util.Date;
/** /**
* Allows instantiation of the settings specified in R.xml.pref_system. * Allows instantiation of the settings specified in R.xml.pref_system.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EBean(scope = Scope.Singleton) @EBean(scope = Scope.Singleton)
public class SystemSettings { public class SystemSettings {
@RootContext @RootContext
protected Context context; protected Context context;
private SharedPreferences prefs; private SharedPreferences prefs;
protected SystemSettings(Context context) { protected SystemSettings(Context context) {
prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs = PreferenceManager.getDefaultSharedPreferences(context);
} }
public boolean treatDormantAsInactive() { public boolean treatDormantAsInactive() {
return prefs.getBoolean("system_dormantasinactive", false); return prefs.getBoolean("system_dormantasinactive", false);
} }
/** /**
* Returns the interval in which automatic screen refreshes should be scheduled. * Returns the interval in which automatic screen refreshes should be scheduled.
* @return The selected refresh interval in milliseconds or 0 if automatic refreshes should be disabled *
*/ * @return The selected refresh interval in milliseconds or 0 if automatic refreshes should be disabled
public long getRefreshIntervalMilliseconds() { */
return Integer.parseInt(prefs.getString("system_autorefresh", "0")) * 1000; public long getRefreshIntervalMilliseconds() {
} return Integer.parseInt(prefs.getString("system_autorefresh", "0")) * 1000;
}
public boolean checkForUpdates() { public boolean checkForUpdates() {
return prefs.getBoolean("system_checkupdates", true); return prefs.getBoolean("system_checkupdates", true);
} }
public boolean autoDarkTheme() { public boolean autoDarkTheme() {
return prefs.getBoolean("system_autodarktheme", true); return prefs.getBoolean("system_autodarktheme", true);
} }
public boolean useDarkTheme() { public boolean useDarkTheme() {
return prefs.getBoolean("system_usedarktheme", false); return prefs.getBoolean("system_usedarktheme", false);
} }
/** /**
* Returns the date when we last checked transdroid.org for the latest app version. * Returns the date when we last checked transdroid.org for the latest app version.
* @return The date/time when the {@link org.transdroid.core.service.AppUpdateJob} checked on the server for updates *
*/ * @return The date/time when the {@link org.transdroid.core.service.AppUpdateJob} checked on the server for updates
public Date getLastCheckedForAppUpdates() { */
long lastChecked = prefs.getLong("system_lastappupdatecheck", -1L); public Date getLastCheckedForAppUpdates() {
return lastChecked == -1 ? null : new Date(lastChecked); long lastChecked = prefs.getLong("system_lastappupdatecheck", -1L);
} return lastChecked == -1 ? null : new Date(lastChecked);
}
/** /**
* Stores the date at which was last successfully, fully checked for new updates to the app. * Stores the date at which was last successfully, fully checked for new updates to the app.
* @param lastChecked The date/time at which the {@link org.transdroid.core.service.AppUpdateJob} last checked the server for updates *
*/ * @param lastChecked The date/time at which the {@link org.transdroid.core.service.AppUpdateJob} last checked the server for updates
public void setLastCheckedForAppUpdates(Date lastChecked) { */
prefs.edit().putLong("system_lastappupdatecheck", lastChecked == null ? -1L : lastChecked.getTime()).apply(); public void setLastCheckedForAppUpdates(Date lastChecked) {
} prefs.edit().putLong("system_lastappupdatecheck", lastChecked == null ? -1L : lastChecked.getTime()).apply();
}
} }

91
app/src/main/java/org/transdroid/core/app/settings/WebsearchSetting.java

@ -16,66 +16,67 @@
*/ */
package org.transdroid.core.app.settings; package org.transdroid.core.app.settings;
import org.transdroid.core.gui.lists.SimpleListItem;
import org.transdroid.core.gui.search.SearchSetting;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import org.transdroid.core.gui.lists.SimpleListItem;
import org.transdroid.core.gui.search.SearchSetting;
/** /**
* Represents a user-specified website that can be searched (by starting the browser, rather than in-app) * Represents a user-specified website that can be searched (by starting the browser, rather than in-app)
*
* @author Eric Kok * @author Eric Kok
*/ */
public class WebsearchSetting implements SimpleListItem, SearchSetting { public class WebsearchSetting implements SimpleListItem, SearchSetting {
private static final String DEFAULT_NAME = "Default"; public static final String KEY_PREFIX = "websearch_";
public static final String KEY_PREFIX = "websearch_"; private static final String DEFAULT_NAME = "Default";
private final int order;
private final int order; private final String name;
private final String name; private final String baseUrl;
private final String baseUrl; private final String cookies;
private final String cookies;
public WebsearchSetting(int order, String name, String baseUrl, String cookies) { public WebsearchSetting(int order, String name, String baseUrl, String cookies) {
this.order = order; this.order = order;
this.name = name; this.name = name;
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.cookies = cookies; this.cookies = cookies;
} }
public int getOrder() { public int getOrder() {
return order; return order;
} }
@Override @Override
public String getName() { public String getName() {
if (!TextUtils.isEmpty(name)) if (!TextUtils.isEmpty(name))
return name; return name;
if (!TextUtils.isEmpty(baseUrl)) { if (!TextUtils.isEmpty(baseUrl)) {
String host = Uri.parse(baseUrl).getHost(); String host = Uri.parse(baseUrl).getHost();
return host == null? DEFAULT_NAME: host; return host == null ? DEFAULT_NAME : host;
} }
return DEFAULT_NAME; return DEFAULT_NAME;
} }
public String getBaseUrl() { public String getBaseUrl() {
return baseUrl; return baseUrl;
} }
public String getCookies() { public String getCookies() {
return cookies; return cookies;
} }
public String getKey() { public String getKey() {
return KEY_PREFIX + getOrder(); return KEY_PREFIX + getOrder();
} }
/** /**
* Returns a nicely formatted identifier containing (a portion of) the search base URL * Returns a nicely formatted identifier containing (a portion of) the search base URL
* @return A string to identify this site's search URL *
*/ * @return A string to identify this site's search URL
public String getHumanReadableIdentifier() { */
return Uri.parse(baseUrl).getHost(); public String getHumanReadableIdentifier() {
} return Uri.parse(baseUrl).getHost();
}
} }

619
app/src/main/java/org/transdroid/core/gui/DetailsActivity.java

@ -20,6 +20,7 @@ import android.annotation.TargetApi;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
@ -56,8 +57,6 @@ import org.transdroid.daemon.TorrentFile;
import org.transdroid.daemon.task.DaemonTaskFailureResult; import org.transdroid.daemon.task.DaemonTaskFailureResult;
import org.transdroid.daemon.task.DaemonTaskResult; import org.transdroid.daemon.task.DaemonTaskResult;
import org.transdroid.daemon.task.DaemonTaskSuccessResult; import org.transdroid.daemon.task.DaemonTaskSuccessResult;
import org.transdroid.daemon.task.ToggleSequentialDownloadTask;
import org.transdroid.daemon.task.ToggleFirstLastPieceDownloadTask;
import org.transdroid.daemon.task.ForceRecheckTask; import org.transdroid.daemon.task.ForceRecheckTask;
import org.transdroid.daemon.task.GetFileListTask; import org.transdroid.daemon.task.GetFileListTask;
import org.transdroid.daemon.task.GetFileListTaskSuccessResult; import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
@ -74,6 +73,8 @@ import org.transdroid.daemon.task.SetLabelTask;
import org.transdroid.daemon.task.SetTrackersTask; import org.transdroid.daemon.task.SetTrackersTask;
import org.transdroid.daemon.task.StartTask; import org.transdroid.daemon.task.StartTask;
import org.transdroid.daemon.task.StopTask; import org.transdroid.daemon.task.StopTask;
import org.transdroid.daemon.task.ToggleFirstLastPieceDownloadTask;
import org.transdroid.daemon.task.ToggleSequentialDownloadTask;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -82,318 +83,318 @@ import java.util.List;
* An activity that holds a single torrents details fragment. It is used on devices (i.e. phones) where there is no room to show details in the {@link * An activity that holds a single torrents details fragment. It is used on devices (i.e. phones) where there is no room to show details in the {@link
* TorrentsActivity} directly. Task execution, such as loading of more details and updating file priorities, is performed in this activity via * TorrentsActivity} directly. Task execution, such as loading of more details and updating file priorities, is performed in this activity via
* background methods. * background methods.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EActivity(R.layout.activity_details) @EActivity(R.layout.activity_details)
@OptionsMenu(R.menu.activity_details) @OptionsMenu(R.menu.activity_details)
public class DetailsActivity extends AppCompatActivity implements TorrentTasksExecutor, RefreshableActivity { public class DetailsActivity extends AppCompatActivity implements TorrentTasksExecutor, RefreshableActivity {
@Extra @Extra
@InstanceState @InstanceState
protected Torrent torrent; protected Torrent torrent;
@Extra @Extra
@InstanceState @InstanceState
protected ArrayList<Label> currentLabels; protected ArrayList<Label> currentLabels;
// Settings // Settings
@Bean @Bean
protected Log log; protected Log log;
@Bean @Bean
protected NavigationHelper navigationHelper; protected NavigationHelper navigationHelper;
@Bean @Bean
protected ConnectivityHelper connectivityHelper; protected ConnectivityHelper connectivityHelper;
@Bean @Bean
protected ApplicationSettings applicationSettings; protected ApplicationSettings applicationSettings;
private IDaemonAdapter currentConnection = null; // Details view components
@ViewById
// Details view components protected Toolbar selectionToolbar;
@ViewById @FragmentById(R.id.torrentdetails_fragment)
protected Toolbar selectionToolbar; protected DetailsFragment fragmentDetails;
@FragmentById(R.id.torrentdetails_fragment) private IDaemonAdapter currentConnection = null;
protected DetailsFragment fragmentDetails;
@Override
@Override public void onCreate(Bundle savedInstanceState) {
public void onCreate(Bundle savedInstanceState) { SettingsUtils.applyDayNightTheme(this);
SettingsUtils.applyDayNightTheme(this); super.onCreate(savedInstanceState);
super.onCreate(savedInstanceState); }
}
@AfterViews
@AfterViews protected void init() {
protected void init() {
// We require a torrent to be specified; otherwise close the activity
// We require a torrent to be specified; otherwise close the activity if (torrent == null) {
if (torrent == null) { finish();
finish(); return;
return; }
}
// Simple action bar with up, torrent name as title and refresh button
// Simple action bar with up, torrent name as title and refresh button setSupportActionBar(selectionToolbar);
setSupportActionBar(selectionToolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(torrent.getName()));
getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(torrent.getName()));
// Connect to the last used server
// Connect to the last used server ServerSetting lastUsed = applicationSettings.getLastUsedServer();
ServerSetting lastUsed = applicationSettings.getLastUsedServer(); fragmentDetails.setCurrentServerSettings(lastUsed);
fragmentDetails.setCurrentServerSettings(lastUsed); currentConnection = lastUsed.createServerAdapter(connectivityHelper.getConnectedNetworkName(), this);
currentConnection = lastUsed.createServerAdapter(connectivityHelper.getConnectedNetworkName(), this);
// Show details and load fine stats and torrent files
// Show details and load fine stats and torrent files fragmentDetails.updateTorrent(torrent);
fragmentDetails.updateTorrent(torrent); fragmentDetails.updateLabels(currentLabels);
fragmentDetails.updateLabels(currentLabels);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@TargetApi(Build.VERSION_CODES.HONEYCOMB) @OptionsItem(android.R.id.home)
@OptionsItem(android.R.id.home) protected void navigateUp() {
protected void navigateUp() { TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start(); }
}
@OptionsItem(R.id.action_refresh)
@OptionsItem(R.id.action_refresh) public void refreshScreen() {
public void refreshScreen() { fragmentDetails.updateIsLoading(true, null);
fragmentDetails.updateIsLoading(true, null); refreshTorrent();
refreshTorrent(); refreshTorrentDetails(torrent);
refreshTorrentDetails(torrent); refreshTorrentFiles(torrent);
refreshTorrentFiles(torrent); }
}
@Background
@Background protected void refreshTorrent() {
protected void refreshTorrent() { DaemonTaskResult result = RetrieveTask.create(currentConnection).execute(log);
DaemonTaskResult result = RetrieveTask.create(currentConnection).execute(log); if (result instanceof RetrieveTaskSuccessResult) {
if (result instanceof RetrieveTaskSuccessResult) { onTorrentsRetrieved(((RetrieveTaskSuccessResult) result).getTorrents(), ((RetrieveTaskSuccessResult) result).getLabels());
onTorrentsRetrieved(((RetrieveTaskSuccessResult) result).getTorrents(), ((RetrieveTaskSuccessResult) result).getLabels()); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, true);
onCommunicationError((DaemonTaskFailureResult) result, true); }
} }
}
@Background
@Background public void refreshTorrentDetails(Torrent torrent) {
public void refreshTorrentDetails(Torrent torrent) { if (currentConnection == null) return;
if (currentConnection == null) return; if (!Daemon.supportsFineDetails(torrent.getDaemon())) {
if (!Daemon.supportsFineDetails(torrent.getDaemon())) { return;
return; }
} DaemonTaskResult result = GetTorrentDetailsTask.create(currentConnection, torrent).execute(log);
DaemonTaskResult result = GetTorrentDetailsTask.create(currentConnection, torrent).execute(log); if (result instanceof GetTorrentDetailsTaskSuccessResult) {
if (result instanceof GetTorrentDetailsTaskSuccessResult) { onTorrentDetailsRetrieved(torrent, ((GetTorrentDetailsTaskSuccessResult) result).getTorrentDetails());
onTorrentDetailsRetrieved(torrent, ((GetTorrentDetailsTaskSuccessResult) result).getTorrentDetails()); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, false);
onCommunicationError((DaemonTaskFailureResult) result, false); }
} }
}
@Background
@Background public void refreshTorrentFiles(Torrent torrent) {
public void refreshTorrentFiles(Torrent torrent) { if (currentConnection == null) return;
if (currentConnection == null) return; if (!Daemon.supportsFileListing(torrent.getDaemon())) {
if (!Daemon.supportsFileListing(torrent.getDaemon())) { return;
return; }
} DaemonTaskResult result = GetFileListTask.create(currentConnection, torrent).execute(log);
DaemonTaskResult result = GetFileListTask.create(currentConnection, torrent).execute(log); if (result instanceof GetFileListTaskSuccessResult) {
if (result instanceof GetFileListTaskSuccessResult) { onTorrentFilesRetrieved(torrent, ((GetFileListTaskSuccessResult) result).getFiles());
onTorrentFilesRetrieved(torrent, ((GetFileListTaskSuccessResult) result).getFiles()); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, false);
onCommunicationError((DaemonTaskFailureResult) result, false); }
} }
}
@Background
@Background @Override
@Override public void resumeTorrent(Torrent torrent) {
public void resumeTorrent(Torrent torrent) { if (currentConnection == null) return;
if (currentConnection == null) return; torrent.mimicResume();
torrent.mimicResume(); DaemonTaskResult result = ResumeTask.create(currentConnection, torrent).execute(log);
DaemonTaskResult result = ResumeTask.create(currentConnection, torrent).execute(log); if (result instanceof DaemonTaskSuccessResult) {
if (result instanceof DaemonTaskSuccessResult) { onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_resumed, torrent.getName()));
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_resumed, torrent.getName())); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, false);
onCommunicationError((DaemonTaskFailureResult) result, false); }
} }
}
@Background
@Background @Override
@Override public void pauseTorrent(Torrent torrent) {
public void pauseTorrent(Torrent torrent) { torrent.mimicPause();
torrent.mimicPause(); DaemonTaskResult result = PauseTask.create(currentConnection, torrent).execute(log);
DaemonTaskResult result = PauseTask.create(currentConnection, torrent).execute(log); if (result instanceof DaemonTaskSuccessResult) {
if (result instanceof DaemonTaskSuccessResult) { onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_paused, torrent.getName()));
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_paused, torrent.getName())); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, false);
onCommunicationError((DaemonTaskFailureResult) result, false); }
} }
}
@Background
@Background @Override
@Override public void startTorrent(Torrent torrent, boolean forced) {
public void startTorrent(Torrent torrent, boolean forced) { torrent.mimicStart();
torrent.mimicStart(); DaemonTaskResult result = StartTask.create(currentConnection, torrent, forced).execute(log);
DaemonTaskResult result = StartTask.create(currentConnection, torrent, forced).execute(log); if (result instanceof DaemonTaskSuccessResult) {
if (result instanceof DaemonTaskSuccessResult) { onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_started, torrent.getName()));
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_started, torrent.getName())); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, false);
onCommunicationError((DaemonTaskFailureResult) result, false); }
} }
}
@Background
@Background @Override
@Override public void stopTorrent(Torrent torrent) {
public void stopTorrent(Torrent torrent) { torrent.mimicStop();
torrent.mimicStop(); DaemonTaskResult result = StopTask.create(currentConnection, torrent).execute(log);
DaemonTaskResult result = StopTask.create(currentConnection, torrent).execute(log); if (result instanceof DaemonTaskSuccessResult) {
if (result instanceof DaemonTaskSuccessResult) { onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_stopped, torrent.getName()));
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_stopped, torrent.getName())); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, false);
onCommunicationError((DaemonTaskFailureResult) result, false); }
} }
}
@Background
@Background @Override
@Override public void removeTorrent(Torrent torrent, boolean withData) {
public void removeTorrent(Torrent torrent, boolean withData) { DaemonTaskResult result = RemoveTask.create(currentConnection, torrent, withData).execute(log);
DaemonTaskResult result = RemoveTask.create(currentConnection, torrent, withData).execute(log); if (result instanceof DaemonTaskSuccessResult) {
if (result instanceof DaemonTaskSuccessResult) { // Close the details activity (as the torrent is now removed)
// Close the details activity (as the torrent is now removed) closeActivity(getString(withData ? R.string.result_removed_with_data : R.string.result_removed, torrent.getName()));
closeActivity(getString(withData ? R.string.result_removed_with_data : R.string.result_removed, torrent.getName())); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, false);
onCommunicationError((DaemonTaskFailureResult) result, false); }
} }
}
@UiThread
@UiThread protected void closeActivity(String closeText) {
protected void closeActivity(String closeText) { setResult(RESULT_OK, new Intent().putExtra("torrent_removed", true).putExtra("affected_torrent", torrent));
setResult(RESULT_OK, new Intent().putExtra("torrent_removed", true).putExtra("affected_torrent", torrent)); finish();
finish(); if (closeText != null) {
if (closeText != null) { SnackbarManager.show(Snackbar.with(this).text(closeText));
SnackbarManager.show(Snackbar.with(this).text(closeText)); }
} }
}
@Background
@Background @Override
@Override public void updateLabel(Torrent torrent, String newLabel) {
public void updateLabel(Torrent torrent, String newLabel) { torrent.mimicNewLabel(newLabel);
torrent.mimicNewLabel(newLabel); DaemonTaskResult result = SetLabelTask.create(currentConnection, torrent, newLabel == null ? "" : newLabel).execute(log);
DaemonTaskResult result = SetLabelTask.create(currentConnection, torrent, newLabel == null ? "" : newLabel).execute(log); if (result instanceof DaemonTaskSuccessResult) {
if (result instanceof DaemonTaskSuccessResult) { onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_labelset, newLabel));
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_labelset, newLabel)); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, false);
onCommunicationError((DaemonTaskFailureResult) result, false); }
} }
}
@Background
@Background @Override
@Override public void toggleSequentialDownload(Torrent torrent, boolean sequentialState) {
public void toggleSequentialDownload(Torrent torrent, boolean sequentialState) { torrent.mimicSequentialDownload(sequentialState);
torrent.mimicSequentialDownload(sequentialState); String onState = getString(R.string.result_togglesequential_onstate);
String onState = getString(R.string.result_togglesequential_onstate); String offState = getString(R.string.result_togglesequential_offstate);
String offState = getString(R.string.result_togglesequential_offstate); String stateString = sequentialState ? onState : offState;
String stateString = sequentialState ? onState : offState; DaemonTaskResult result = ToggleSequentialDownloadTask.create(currentConnection, torrent).execute(log);
DaemonTaskResult result = ToggleSequentialDownloadTask.create(currentConnection, torrent).execute(log); if (result instanceof DaemonTaskSuccessResult) {
if (result instanceof DaemonTaskSuccessResult) { onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglesequential, torrent.getName(), stateString));
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglesequential, torrent.getName(), stateString)); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, false);
onCommunicationError((DaemonTaskFailureResult) result, false); }
} }
}
@Background
@Background @Override
@Override public void toggleFirstLastPieceDownload(Torrent torrent, boolean firstLastPieceState) {
public void toggleFirstLastPieceDownload(Torrent torrent, boolean firstLastPieceState) { torrent.mimicFirstLastPieceDownload(firstLastPieceState);
torrent.mimicFirstLastPieceDownload(firstLastPieceState); String onState = getString(R.string.result_togglefirstlastpiece_onstate);
String onState = getString(R.string.result_togglefirstlastpiece_onstate); String offState = getString(R.string.result_togglefirstlastpiece_offstate);
String offState = getString(R.string.result_togglefirstlastpiece_offstate); String stateString = firstLastPieceState ? onState : offState;
String stateString = firstLastPieceState ? onState : offState; DaemonTaskResult result = ToggleFirstLastPieceDownloadTask.create(currentConnection, torrent).execute(log);
DaemonTaskResult result = ToggleFirstLastPieceDownloadTask.create(currentConnection, torrent).execute(log); if (result instanceof DaemonTaskSuccessResult) {
if (result instanceof DaemonTaskSuccessResult) { onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglefirstlastpiece, torrent.getName(), stateString));
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglefirstlastpiece, torrent.getName(), stateString)); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, false);
onCommunicationError((DaemonTaskFailureResult) result, false); }
} }
}
@Background
@Background @Override
@Override public void forceRecheckTorrent(Torrent torrent) {
public void forceRecheckTorrent(Torrent torrent) { torrent.mimicCheckingStatus();
torrent.mimicCheckingStatus(); DaemonTaskResult result = ForceRecheckTask.create(currentConnection, torrent).execute(log);
DaemonTaskResult result = ForceRecheckTask.create(currentConnection, torrent).execute(log); if (result instanceof DaemonTaskSuccessResult) {
if (result instanceof DaemonTaskSuccessResult) { onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_recheckedstarted, torrent.getName()));
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_recheckedstarted, torrent.getName())); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, false);
onCommunicationError((DaemonTaskFailureResult) result, false); }
} }
}
@Background
@Background @Override
@Override public void updateTrackers(Torrent torrent, List<String> newTrackers) {
public void updateTrackers(Torrent torrent, List<String> newTrackers) { DaemonTaskResult result = SetTrackersTask.create(currentConnection, torrent, newTrackers).execute(log);
DaemonTaskResult result = SetTrackersTask.create(currentConnection, torrent, newTrackers).execute(log); if (result instanceof DaemonTaskSuccessResult) {
if (result instanceof DaemonTaskSuccessResult) { onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_trackersupdated));
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_trackersupdated)); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, false);
onCommunicationError((DaemonTaskFailureResult) result, false); }
} }
}
@Background
@Background @Override
@Override public void updateLocation(Torrent torrent, String newLocation) {
public void updateLocation(Torrent torrent, String newLocation) { DaemonTaskResult result = SetDownloadLocationTask.create(currentConnection, torrent, newLocation).execute(log);
DaemonTaskResult result = SetDownloadLocationTask.create(currentConnection, torrent, newLocation).execute(log); if (result instanceof DaemonTaskSuccessResult) {
if (result instanceof DaemonTaskSuccessResult) { onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_locationset, newLocation));
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_locationset, newLocation)); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, false);
onCommunicationError((DaemonTaskFailureResult) result, false); }
} }
}
@Background
@Background @Override
@Override public void updatePriority(Torrent torrent, List<TorrentFile> files, Priority priority) {
public void updatePriority(Torrent torrent, List<TorrentFile> files, Priority priority) { DaemonTaskResult result = SetFilePriorityTask.create(currentConnection, torrent, priority, new ArrayList<>(files)).execute(log);
DaemonTaskResult result = SetFilePriorityTask.create(currentConnection, torrent, priority, new ArrayList<>(files)).execute(log); if (result instanceof DaemonTaskSuccessResult) {
if (result instanceof DaemonTaskSuccessResult) { onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_priotitiesset));
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_priotitiesset)); } else {
} else { onCommunicationError((DaemonTaskFailureResult) result, false);
onCommunicationError((DaemonTaskFailureResult) result, false); }
} }
}
@UiThread
@UiThread protected void onTaskSucceeded(DaemonTaskSuccessResult result, String successMessage) {
protected void onTaskSucceeded(DaemonTaskSuccessResult result, String successMessage) { // Set the activity result so the calling activity knows it needs to update its view
// Set the activity result so the calling activity knows it needs to update its view setResult(RESULT_OK, new Intent().putExtra("torrent_updated", true).putExtra("affected_torrent", torrent));
setResult(RESULT_OK, new Intent().putExtra("torrent_updated", true).putExtra("affected_torrent", torrent)); // Refresh the screen as well
// Refresh the screen as well refreshTorrent();
refreshTorrent(); refreshTorrentDetails(torrent);
refreshTorrentDetails(torrent); SnackbarManager.show(Snackbar.with(this).text(successMessage).duration(Snackbar.SnackbarDuration.LENGTH_SHORT));
SnackbarManager.show(Snackbar.with(this).text(successMessage).duration(Snackbar.SnackbarDuration.LENGTH_SHORT)); }
}
@UiThread
@UiThread protected void onTorrentDetailsRetrieved(Torrent torrent, TorrentDetails torrentDetails) {
protected void onTorrentDetailsRetrieved(Torrent torrent, TorrentDetails torrentDetails) { // Update the details fragment with the new fine details for the shown torrent
// Update the details fragment with the new fine details for the shown torrent if (fragmentDetails.isResumed())
if (fragmentDetails.isResumed()) fragmentDetails.updateTorrentDetails(torrent, torrentDetails);
fragmentDetails.updateTorrentDetails(torrent, torrentDetails); }
}
@UiThread
@UiThread protected void onTorrentFilesRetrieved(Torrent torrent, List<TorrentFile> torrentFiles) {
protected void onTorrentFilesRetrieved(Torrent torrent, List<TorrentFile> torrentFiles) { // Update the details fragment with the newly retrieved list of files
// Update the details fragment with the newly retrieved list of files if (fragmentDetails.isResumed())
if (fragmentDetails.isResumed()) fragmentDetails.updateTorrentFiles(torrent, new ArrayList<>(torrentFiles));
fragmentDetails.updateTorrentFiles(torrent, new ArrayList<>(torrentFiles)); }
}
@UiThread
@UiThread protected void onCommunicationError(DaemonTaskFailureResult result, boolean isCritical) {
protected void onCommunicationError(DaemonTaskFailureResult result, boolean isCritical) { log.i(this, result.getException().toString());
log.i(this, result.getException().toString()); String error = getString(LocalTorrent.getResourceForDaemonException(result.getException()));
String error = getString(LocalTorrent.getResourceForDaemonException(result.getException())); if (fragmentDetails.isResumed())
if (fragmentDetails.isResumed()) fragmentDetails.updateIsLoading(false, isCritical ? error : null);
fragmentDetails.updateIsLoading(false, isCritical ? error : null); SnackbarManager.show(Snackbar.with(this).text(getString(LocalTorrent.getResourceForDaemonException(result.getException())))
SnackbarManager.show(Snackbar.with(this).text(getString(LocalTorrent.getResourceForDaemonException(result.getException()))) .colorResource(R.color.red));
.colorResource(R.color.red)); }
}
@UiThread
@UiThread protected void onTorrentsRetrieved(List<Torrent> torrents, List<org.transdroid.daemon.Label> labels) {
protected void onTorrentsRetrieved(List<Torrent> torrents, List<org.transdroid.daemon.Label> labels) { // Update the details fragment accordingly
// Update the details fragment accordingly if (fragmentDetails.isResumed()) {
if (fragmentDetails.isResumed()) { fragmentDetails.updateIsLoading(false, null);
fragmentDetails.updateIsLoading(false, null); fragmentDetails.perhapsUpdateTorrent(torrents);
fragmentDetails.perhapsUpdateTorrent(torrents); fragmentDetails.updateLabels(Label.convertToNavigationLabels(labels, getResources().getString(R.string.labels_unlabeled)));
fragmentDetails.updateLabels(Label.convertToNavigationLabels(labels, getResources().getString(R.string.labels_unlabeled))); }
} }
}
} }

1217
app/src/main/java/org/transdroid/core/gui/DetailsFragment.java

File diff suppressed because it is too large Load Diff

69
app/src/main/java/org/transdroid/core/gui/ServerPickerDialog.java

@ -16,50 +16,47 @@
*/ */
package org.transdroid.core.gui; package org.transdroid.core.gui;
import java.util.List; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.app.DialogFragment;
import android.os.Bundle; import android.os.Bundle;
import androidx.fragment.app.DialogFragment;
import org.transdroid.R; import org.transdroid.R;
import org.transdroid.core.app.settings.ServerSetting; import org.transdroid.core.app.settings.ServerSetting;
import android.app.AlertDialog; import java.util.List;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
public class ServerPickerDialog extends DialogFragment { public class ServerPickerDialog extends DialogFragment {
@Override /**
public Dialog onCreateDialog(Bundle savedInstanceState) { * Opens a dialog that allows the selection of a configured server (manual or seedbox). The calling activity will
String[] serverNames = getArguments().getStringArray("serverNames"); * receive a callback on its switchServerAndAddFromIntent(int) method.
return new AlertDialog.Builder(getActivity()).setTitle(R.string.navigation_pickserver) *
.setItems(serverNames, new OnClickListener() { * @param activity The torrents activity from which the picker is started (and which received the callback)
@Override * @param serverSettings The list of all available servers, of which their names will be offered to the user to pick
public void onClick(DialogInterface dialog, int which) { * from (and its position in the list is returned to the activity)
if (getActivity() != null && getActivity() instanceof TorrentsActivity) */
((TorrentsActivity) getActivity()).switchServerAndAddFromIntent(which); public static void startServerPicker(final TorrentsActivity activity, List<ServerSetting> serverSettings) {
} final String[] serverNames = new String[serverSettings.size()];
}).create(); for (int i = 0; i < serverSettings.size(); i++) {
} serverNames[i] = serverSettings.get(i).getName();
}
ServerPickerDialog dialog = new ServerPickerDialog();
Bundle arguments = new Bundle();
arguments.putStringArray("serverNames", serverNames);
dialog.setArguments(arguments);
dialog.show(activity.getSupportFragmentManager(), "serverpicker");
}
/** @Override
* Opens a dialog that allows the selection of a configured server (manual or seedbox). The calling activity will public Dialog onCreateDialog(Bundle savedInstanceState) {
* receive a callback on its switchServerAndAddFromIntent(int) method. String[] serverNames = getArguments().getStringArray("serverNames");
* @param activity The torrents activity from which the picker is started (and which received the callback) return new AlertDialog.Builder(getActivity()).setTitle(R.string.navigation_pickserver)
* @param serverSettings The list of all available servers, of which their names will be offered to the user to pick .setItems(serverNames, (dialog, which) -> {
* from (and its position in the list is returned to the activity) if (getActivity() != null && getActivity() instanceof TorrentsActivity)
*/ ((TorrentsActivity) getActivity()).switchServerAndAddFromIntent(which);
public static void startServerPicker(final TorrentsActivity activity, List<ServerSetting> serverSettings) { }).create();
final String[] serverNames = new String[serverSettings.size()]; }
for (int i = 0; i < serverSettings.size(); i++) {
serverNames[i] = serverSettings.get(i).getName();
}
ServerPickerDialog dialog = new ServerPickerDialog();
Bundle arguments = new Bundle();
arguments.putStringArray("serverNames", serverNames);
dialog.setArguments(arguments);
dialog.show(activity.getFragmentManager(), "serverpicker");
}
} }

54
app/src/main/java/org/transdroid/core/gui/ServerSelectionView.java

@ -29,31 +29,33 @@ import org.transdroid.daemon.IDaemonAdapter;
@EViewGroup(R.layout.actionbar_serverselection) @EViewGroup(R.layout.actionbar_serverselection)
public class ServerSelectionView extends RelativeLayout { public class ServerSelectionView extends RelativeLayout {
@ViewById @ViewById
protected TextView filterText, serverText; protected TextView filterText, serverText;
public ServerSelectionView(Context context) { public ServerSelectionView(Context context) {
super(context); super(context);
} }
public ServerSelectionView(TorrentsActivity activity) { public ServerSelectionView(TorrentsActivity activity) {
super(activity.torrentsToolbar.getContext()); super(activity.torrentsToolbar.getContext());
} }
/** /**
* Updates the name of the current connected server. * Updates the name of the current connected server.
* @param currentServer The server currently connected to *
*/ * @param currentServer The server currently connected to
public void updateCurrentServer(IDaemonAdapter currentServer) { */
serverText.setText(currentServer.getSettings().getName()); public void updateCurrentServer(IDaemonAdapter currentServer) {
} serverText.setText(currentServer.getSettings().getName());
}
/**
* Updates the name of the selected filter. /**
* @param currentFilter The filter that is currently selected * Updates the name of the selected filter.
*/ *
public void updateCurrentFilter(NavigationFilter currentFilter) { * @param currentFilter The filter that is currently selected
filterText.setText(currentFilter.getName()); */
} public void updateCurrentFilter(NavigationFilter currentFilter) {
filterText.setText(currentFilter.getName());
}
} }

161
app/src/main/java/org/transdroid/core/gui/ServerStatusView.java

@ -37,87 +37,84 @@ import java.util.List;
@EViewGroup(R.layout.actionbar_serverstatus) @EViewGroup(R.layout.actionbar_serverstatus)
public class ServerStatusView extends RelativeLayout implements OnRatesPickedListener { public class ServerStatusView extends RelativeLayout implements OnRatesPickedListener {
@ViewById @ViewById
protected TextView downcountText, upcountText, downcountSign, upcountSign, downspeedText, upspeedText; protected TextView downcountText, upcountText, downcountSign, upcountSign, downspeedText, upspeedText;
@ViewById @ViewById
protected View speedswrapperLayout; protected View speedswrapperLayout;
private TorrentsActivity activity; private TorrentsActivity activity;
private OnClickListener onStartDownPickerClicked = v ->
public ServerStatusView(Context context) { SetTransferRatesDialog.show(getContext(), ServerStatusView.this);
super(context);
} public ServerStatusView(Context context) {
super(context);
public ServerStatusView(TorrentsActivity activity) { }
super(activity);
this.activity = activity; public ServerStatusView(TorrentsActivity activity) {
} super(activity);
this.activity = activity;
/** }
* Updates the statistics as shown in the action bar through this server status view.
* @param torrents The most recently received list of torrents /**
* @param dormantAsInactive Whether to treat dormant (0KB/s) torrent as inactive state torrents * Updates the statistics as shown in the action bar through this server status view.
* @param supportsSetTransferRates Whether the connected torrent client supports setting of max transfer speeds *
*/ * @param torrents The most recently received list of torrents
public void updateStatus(List<Torrent> torrents, boolean dormantAsInactive, boolean supportsSetTransferRates) { * @param dormantAsInactive Whether to treat dormant (0KB/s) torrent as inactive state torrents
* @param supportsSetTransferRates Whether the connected torrent client supports setting of max transfer speeds
if (torrents == null) { */
downcountText.setText(null); public void updateStatus(List<Torrent> torrents, boolean dormantAsInactive, boolean supportsSetTransferRates) {
upcountText.setText(null);
downspeedText.setText(null); if (torrents == null) {
upspeedText.setText(null); downcountText.setText(null);
downcountSign.setVisibility(View.INVISIBLE); upcountText.setText(null);
upcountSign.setVisibility(View.INVISIBLE); downspeedText.setText(null);
speedswrapperLayout.setOnClickListener(null); upspeedText.setText(null);
return; downcountSign.setVisibility(View.INVISIBLE);
} upcountSign.setVisibility(View.INVISIBLE);
speedswrapperLayout.setOnClickListener(null);
int downcount = 0, upcount = 0, downspeed = 0, upspeed = 0; return;
for (Torrent torrent : torrents) { }
// Downloading torrents count towards downloads and uploads, seeding torrents towards uploads int downcount = 0, upcount = 0, downspeed = 0, upspeed = 0;
if (torrent.isDownloading(dormantAsInactive)) { for (Torrent torrent : torrents) {
downcount++;
upcount++; // Downloading torrents count towards downloads and uploads, seeding torrents towards uploads
} else if (torrent.isSeeding(dormantAsInactive)) { if (torrent.isDownloading(dormantAsInactive)) {
upcount++; downcount++;
} upcount++;
downspeed += torrent.getRateDownload(); } else if (torrent.isSeeding(dormantAsInactive)) {
upspeed += torrent.getRateUpload(); upcount++;
}
} downspeed += torrent.getRateDownload();
upspeed += torrent.getRateUpload();
downcountText.setText(Integer.toString(downcount));
upcountText.setText(Integer.toString(upcount)); }
downspeedText.setText(FileSizeConverter.getSize(downspeed) + "/s");
upspeedText.setText(FileSizeConverter.getSize(upspeed) + "/s"); downcountText.setText(Integer.toString(downcount));
downcountSign.setVisibility(View.VISIBLE); upcountText.setText(Integer.toString(upcount));
upcountSign.setVisibility(View.VISIBLE); downspeedText.setText(FileSizeConverter.getSize(downspeed) + "/s");
if (supportsSetTransferRates) upspeedText.setText(FileSizeConverter.getSize(upspeed) + "/s");
speedswrapperLayout.setOnClickListener(onStartDownPickerClicked); downcountSign.setVisibility(View.VISIBLE);
else upcountSign.setVisibility(View.VISIBLE);
speedswrapperLayout.setBackgroundDrawable(null); if (supportsSetTransferRates)
speedswrapperLayout.setOnClickListener(onStartDownPickerClicked);
} else
speedswrapperLayout.setBackgroundDrawable(null);
private OnClickListener onStartDownPickerClicked = new OnClickListener() {
public void onClick(View v) { }
SetTransferRatesDialog.show(getContext(), ServerStatusView.this);
} @Override
}; public void onRatesPicked(int maxDownloadSpeed, int maxUploadSpeed) {
activity.updateMaxSpeeds(maxDownloadSpeed, maxUploadSpeed);
@Override }
public void onRatesPicked(int maxDownloadSpeed, int maxUploadSpeed) {
activity.updateMaxSpeeds(maxDownloadSpeed, maxUploadSpeed); @Override
} public void resetRates() {
activity.updateMaxSpeeds(null, null);
@Override }
public void resetRates() {
activity.updateMaxSpeeds(null, null); @Override
} public void onInvalidNumber() {
SnackbarManager.show(Snackbar.with(activity).text(R.string.error_notanumber).colorResource(R.color.red));
@Override }
public void onInvalidNumber() {
SnackbarManager.show(Snackbar.with(activity).text(R.string.error_notanumber).colorResource(R.color.red));
}
} }

32
app/src/main/java/org/transdroid/core/gui/TorrentTasksExecutor.java

@ -16,9 +16,6 @@
*/ */
package org.transdroid.core.gui; package org.transdroid.core.gui;
import androidx.appcompat.widget.ActionMenuView;
import androidx.appcompat.widget.Toolbar;
import org.transdroid.daemon.Priority; import org.transdroid.daemon.Priority;
import org.transdroid.daemon.Torrent; import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentFile; import org.transdroid.daemon.TorrentFile;
@ -27,34 +24,35 @@ import java.util.List;
/** /**
* Interface to be implemented by any activity that wants containing fragments to be able to load data and execute commands against a torrent server. * Interface to be implemented by any activity that wants containing fragments to be able to load data and execute commands against a torrent server.
*
* @author Eric Kok * @author Eric Kok
*/ */
public interface TorrentTasksExecutor { public interface TorrentTasksExecutor {
void resumeTorrent(Torrent torrent); void resumeTorrent(Torrent torrent);
void pauseTorrent(Torrent torrent); void pauseTorrent(Torrent torrent);
void startTorrent(Torrent torrent, boolean forced); void startTorrent(Torrent torrent, boolean forced);
void stopTorrent(Torrent torrent); void stopTorrent(Torrent torrent);
void removeTorrent(Torrent torrent, boolean withData); void removeTorrent(Torrent torrent, boolean withData);
void toggleSequentialDownload(Torrent torrent, boolean sequentialState); void toggleSequentialDownload(Torrent torrent, boolean sequentialState);
void toggleFirstLastPieceDownload(Torrent torrent, boolean firstLastPieceState); void toggleFirstLastPieceDownload(Torrent torrent, boolean firstLastPieceState);
void forceRecheckTorrent(Torrent torrent); void forceRecheckTorrent(Torrent torrent);
void updateLabel(Torrent torrent, String newLabel); void updateLabel(Torrent torrent, String newLabel);
void updateTrackers(Torrent torrent, List<String> newTrackers); void updateTrackers(Torrent torrent, List<String> newTrackers);
void updateLocation(Torrent torrent, String newLocation); void updateLocation(Torrent torrent, String newLocation);
void refreshTorrentDetails(Torrent torrent); void refreshTorrentDetails(Torrent torrent);
void refreshTorrentFiles(Torrent torrent); void refreshTorrentFiles(Torrent torrent);
void updatePriority(Torrent torrent, List<TorrentFile> files, Priority priority); void updatePriority(Torrent torrent, List<TorrentFile> files, Priority priority);
} }

2485
app/src/main/java/org/transdroid/core/gui/TorrentsActivity.java

File diff suppressed because it is too large Load Diff

864
app/src/main/java/org/transdroid/core/gui/TorrentsFragment.java

@ -16,12 +16,7 @@
*/ */
package org.transdroid.core.gui; package org.transdroid.core.gui;
import android.app.Fragment;
import android.content.Context; import android.content.Context;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.ActionMenuView;
import androidx.appcompat.widget.Toolbar;
import android.view.ActionMode; import android.view.ActionMode;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -31,6 +26,12 @@ import android.widget.ListView;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.ActionMenuView;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.getbase.floatingactionbutton.FloatingActionsMenu; import com.getbase.floatingactionbutton.FloatingActionsMenu;
import org.androidannotations.annotations.AfterViews; import org.androidannotations.annotations.AfterViews;
@ -64,436 +65,437 @@ import java.util.Locale;
/** /**
* Fragment that shows a list of torrents that are active on the server. It supports sorting and filtering and can show connection progress and * Fragment that shows a list of torrents that are active on the server. It supports sorting and filtering and can show connection progress and
* issues. However, actual task starting and execution and overall navigation elements are part of the containing activity, not this fragment. * issues. However, actual task starting and execution and overall navigation elements are part of the containing activity, not this fragment.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EFragment(R.layout.fragment_torrents) @EFragment(R.layout.fragment_torrents)
public class TorrentsFragment extends Fragment implements OnLabelPickedListener { public class TorrentsFragment extends Fragment implements OnLabelPickedListener {
// Local data // HACK Working around #391 while hopefully we rework the UI in the future to persist the list in db or something
@Bean protected static ArrayList<Torrent> torrents = null;
protected ApplicationSettings applicationSettings; // Local data
@Bean @Bean
protected SystemSettings systemSettings; protected ApplicationSettings applicationSettings;
// HACK Working around #391 while hopefully we rework the UI in the future to persist the list in db or something @Bean
protected static ArrayList<Torrent> torrents = null; protected SystemSettings systemSettings;
@InstanceState @InstanceState
protected ArrayList<Torrent> lastMultiSelectedTorrents; protected ArrayList<Torrent> lastMultiSelectedTorrents;
@InstanceState @InstanceState
protected ArrayList<Label> currentLabels; protected ArrayList<Label> currentLabels;
@InstanceState @InstanceState
protected NavigationFilter currentNavigationFilter = null; protected NavigationFilter currentNavigationFilter = null;
@InstanceState @InstanceState
protected TorrentsSortBy currentSortOrder = TorrentsSortBy.Alphanumeric; protected TorrentsSortBy currentSortOrder = TorrentsSortBy.Alphanumeric;
@InstanceState @InstanceState
protected boolean currentSortDescending = false; protected boolean currentSortDescending = false;
@InstanceState @InstanceState
protected String currentTextFilter = null; protected String currentTextFilter = null;
@InstanceState @InstanceState
protected boolean hasAConnection = false; protected boolean hasAConnection = false;
@InstanceState @InstanceState
protected boolean isLoading = true; protected boolean isLoading = true;
@InstanceState @InstanceState
protected String connectionErrorMessage = null; protected String connectionErrorMessage = null;
@InstanceState @InstanceState
protected Daemon daemonType; protected Daemon daemonType;
// Views // Views
@ViewById @ViewById
protected SwipeRefreshLayout swipeRefreshLayout; protected SwipeRefreshLayout swipeRefreshLayout;
@ViewById @ViewById
protected ListView torrentsList; protected ListView torrentsList;
@ViewById @ViewById
protected TextView emptyText; protected TextView emptyText;
@ViewById @ViewById
protected TextView nosettingsText; protected TextView nosettingsText;
@ViewById @ViewById
protected TextView errorText; protected TextView errorText;
@ViewById @ViewById
protected ProgressBar loadingProgress; protected ProgressBar loadingProgress;
private MultiChoiceModeListener onTorrentsSelected = new MultiChoiceModeListener() {
@AfterViews
protected void init() { private SelectionManagerMode selectionManagerMode;
private ActionMenuView actionsMenu;
// Load the requested sort order from the user settings private Toolbar actionsToolbar;
this.currentSortOrder = applicationSettings.getLastUsedSortOrder(); private FloatingActionsMenu addmenuButton;
this.currentSortDescending = applicationSettings.getLastUsedSortDescending();
@Override
// Set up the list adapter, which allows multi-select and fast scrolling public boolean onCreateActionMode(final ActionMode mode, Menu menu) {
torrentsList.setAdapter(TorrentsAdapter_.getInstance_(getActivity())); // Show contextual action bars to start/stop/remove/etc. torrents in batch mode
torrentsList.setMultiChoiceModeListener(onTorrentsSelected); if (actionsMenu == null) {
torrentsList.setFastScrollEnabled(true); actionsMenu = ((TorrentsActivity) getActivity()).contextualMenu;
if (torrents != null) { actionsToolbar = ((TorrentsActivity) getActivity()).actionsToolbar;
updateTorrents(torrents, currentLabels); addmenuButton = ((TorrentsActivity) getActivity()).addmenuButton;
} }
// Allow pulls on the list view to refresh the torrents actionsToolbar.setEnabled(false);
if (getActivity() != null && getActivity() instanceof RefreshableActivity) { actionsMenu.setVisibility(View.VISIBLE);
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { addmenuButton.setVisibility(View.GONE);
@Override actionsMenu.setOnMenuItemClickListener(menuItem -> onActionItemClicked(mode, menuItem));
public void onRefresh() { actionsMenu.getMenu().clear();
((RefreshableActivity) getActivity()).refreshScreen(); getActivity().getMenuInflater().inflate(R.menu.fragment_torrents_cab, actionsMenu.getMenu());
swipeRefreshLayout.setRefreshing(false); // Use our custom indicator Context themedContext = ((AppCompatActivity) getActivity()).getSupportActionBar().getThemedContext();
} selectionManagerMode = new SelectionManagerMode(themedContext, torrentsList, R.plurals.navigation_torrentsselected);
}); selectionManagerMode.onCreateActionMode(mode, menu);
} return true;
nosettingsText.setText(getString(R.string.navigation_nosettings, getString(R.string.app_name))); }
} @Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
/** selectionManagerMode.onPrepareActionMode(mode, menu);
* Updates the list adapter to show a new list of torrent objects, replacing the old torrents completely // Hide/show options depending on the type of server we are connected to
* @param newTorrents The new, updated list of torrents if (daemonType != null) {
*/ actionsMenu.getMenu().findItem(R.id.action_start).setVisible(Daemon.supportsStoppingStarting(daemonType));
public void updateTorrents(ArrayList<Torrent> newTorrents, ArrayList<Label> currentLabels) { actionsMenu.getMenu().findItem(R.id.action_stop).setVisible(Daemon.supportsStoppingStarting(daemonType));
if (this.isDetached()) { actionsMenu.getMenu().findItem(R.id.action_setlabel).setVisible(Daemon.supportsSetLabel(daemonType));
return; }
} // Pause autorefresh
if (getActivity() != null && getActivity() instanceof TorrentsActivity) {
torrents = newTorrents; ((TorrentsActivity) getActivity()).stopRefresh = true;
this.currentLabels = currentLabels; ((TorrentsActivity) getActivity()).stopAutoRefresh();
applyAllFilters(); }
} return true;
}
/**
* Just look for a specific torrent in the currently shown list (by its unique id) and update only this public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
* @param affected The affected torrent to update
* @param wasRemoved Whether the affected torrent was indeed removed; otherwise it was updated somehow // Get checked torrents
*/ ArrayList<Torrent> checked = new ArrayList<>();
public void quickUpdateTorrent(Torrent affected, boolean wasRemoved) { for (int i = 0; i < torrentsList.getCheckedItemPositions().size(); i++) {
if (this.isDetached()) { if (torrentsList.getCheckedItemPositions().valueAt(i) && i < torrentsList.getAdapter().getCount()) {
return; checked.add((Torrent) torrentsList.getAdapter().getItem(torrentsList.getCheckedItemPositions().keyAt(i)));
} }
}
// Remove the old torrent object first
Iterator<Torrent> iter = torrents.iterator(); int itemId = item.getItemId();
while (iter.hasNext()) { if (itemId == R.id.action_resume) {
Torrent torrent = iter.next(); for (Torrent torrent : checked) {
if (torrent.getUniqueID().equals(affected.getUniqueID())) { getTasksExecutor().resumeTorrent(torrent);
iter.remove(); }
break; mode.finish();
} return true;
} } else if (itemId == R.id.action_pause) {
// In case it was an update, add the updated torrent object for (Torrent torrent : checked) {
if (!wasRemoved) { getTasksExecutor().pauseTorrent(torrent);
torrents.add(affected); }
} mode.finish();
// Now refresh the screen return true;
applyAllFilters(); } else if (itemId == R.id.action_start) {
} for (Torrent torrent : checked) {
getTasksExecutor().startTorrent(torrent, false);
/** }
* Clears the currently visible list of torrents. mode.finish();
* @param clearError Also clear any error message return true;
* @param clearFilter Also clear any selected filter } else if (itemId == R.id.action_stop) {
*/ for (Torrent torrent : checked) {
public void clear(boolean clearError, boolean clearFilter) { getTasksExecutor().stopTorrent(torrent);
torrents = null; }
if (clearError) { mode.finish();
this.connectionErrorMessage = null; return true;
} } else if (itemId == R.id.action_remove_default) {
if (clearFilter) { for (Torrent torrent : checked) {
this.currentTextFilter = null; getTasksExecutor().removeTorrent(torrent, false);
this.currentNavigationFilter = null; }
} mode.finish();
applyAllFilters(); return true;
} } else if (itemId == R.id.action_remove_withdata) {
for (Torrent torrent : checked) {
/** getTasksExecutor().removeTorrent(torrent, true);
* Stores the new sort order (for future refreshes) and sorts the current visible list. If the given new sort property equals the existing }
* property, the list sort order is reversed instead. mode.finish();
* @param newSortOrder The sort order that the user selected. return true;
*/ } else if (itemId == R.id.action_setlabel) {
public void sortBy(TorrentsSortBy newSortOrder) { lastMultiSelectedTorrents = checked;
// Update the sort order property and direction and store this last used setting if (currentLabels != null) {
if (this.currentSortOrder == newSortOrder) { SetLabelDialog.show(getActivity(), TorrentsFragment.this, currentLabels);
this.currentSortDescending = !this.currentSortDescending; }
} else { mode.finish();
this.currentSortOrder = newSortOrder; return true;
this.currentSortDescending = false; } else {
} return false;
applicationSettings.setLastUsedSortOrder(this.currentSortOrder, this.currentSortDescending); }
applyAllFilters(); }
}
@Override
public void applyTextFilter(String newTextFilter) { public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
this.currentTextFilter = newTextFilter; selectionManagerMode.onItemCheckedStateChanged(mode, position, id, checked);
// Show the new filtered list }
applyAllFilters();
} @Override
public void onDestroyActionMode(ActionMode mode) {
/** // Resume autorefresh
* Apply a filter on the current list of all torrents, showing the appropriate sublist of torrents only if (getActivity() != null && getActivity() instanceof TorrentsActivity) {
* @param newFilter The new filter to apply to the local list of torrents ((TorrentsActivity) getActivity()).stopRefresh = false;
*/ ((TorrentsActivity) getActivity()).startAutoRefresh();
public void applyNavigationFilter(NavigationFilter newFilter) { }
this.currentNavigationFilter = newFilter; selectionManagerMode.onDestroyActionMode(mode);
applyAllFilters(); actionsMenu.setVisibility(View.GONE);
} actionsToolbar.setEnabled(true);
addmenuButton.setVisibility(View.VISIBLE);
private void applyAllFilters() { }
// No torrents? Directly update views accordingly };
if (torrents == null) {
updateViewVisibility(); @AfterViews
return; protected void init() {
}
// Load the requested sort order from the user settings
// Filter the list of torrents to show according to navigation and text filters this.currentSortOrder = applicationSettings.getLastUsedSortOrder();
ArrayList<Torrent> filteredTorrents = new ArrayList<>(torrents); this.currentSortDescending = applicationSettings.getLastUsedSortDescending();
if (currentNavigationFilter != null) {
// Remove torrents that do not match the selected navigation filter // Set up the list adapter, which allows multi-select and fast scrolling
for (Iterator<Torrent> torrentIter = filteredTorrents.iterator(); torrentIter.hasNext(); ) { torrentsList.setAdapter(TorrentsAdapter_.getInstance_(getActivity()));
if (!currentNavigationFilter.matches(torrentIter.next(), systemSettings.treatDormantAsInactive())) { torrentsList.setMultiChoiceModeListener(onTorrentsSelected);
torrentIter.remove(); torrentsList.setFastScrollEnabled(true);
} if (torrents != null) {
} updateTorrents(torrents, currentLabels);
} }
if (currentTextFilter != null) { // Allow pulls on the list view to refresh the torrents
// Remove torrents that do not contain the text filter string if (getActivity() != null && getActivity() instanceof RefreshableActivity) {
for (Iterator<Torrent> torrentIter = filteredTorrents.iterator(); torrentIter.hasNext(); ) { swipeRefreshLayout.setOnRefreshListener(() -> {
if (!torrentIter.next().getName().toLowerCase(Locale.getDefault()).contains(currentTextFilter.toLowerCase(Locale.getDefault()))) { ((RefreshableActivity) getActivity()).refreshScreen();
torrentIter.remove(); swipeRefreshLayout.setRefreshing(false); // Use our custom indicator
} });
} }
} nosettingsText.setText(getString(R.string.navigation_nosettings, getString(R.string.app_name)));
// Sort the list of filtered torrents }
Collections.sort(filteredTorrents, new TorrentsComparator(daemonType, this.currentSortOrder, this.currentSortDescending));
/**
if (torrentsList.getAdapter() != null) { * Updates the list adapter to show a new list of torrent objects, replacing the old torrents completely
((TorrentsAdapter) torrentsList.getAdapter()).update(filteredTorrents); *
} * @param newTorrents The new, updated list of torrents
updateViewVisibility(); */
} public void updateTorrents(ArrayList<Torrent> newTorrents, ArrayList<Label> currentLabels) {
if (this.isDetached()) {
private MultiChoiceModeListener onTorrentsSelected = new MultiChoiceModeListener() { return;
}
private SelectionManagerMode selectionManagerMode;
private ActionMenuView actionsMenu; torrents = newTorrents;
private Toolbar actionsToolbar; this.currentLabels = currentLabels;
private FloatingActionsMenu addmenuButton; applyAllFilters();
}
@Override
public boolean onCreateActionMode(final ActionMode mode, Menu menu) { /**
// Show contextual action bars to start/stop/remove/etc. torrents in batch mode * Just look for a specific torrent in the currently shown list (by its unique id) and update only this
if (actionsMenu == null) { *
actionsMenu = ((TorrentsActivity) getActivity()).contextualMenu; * @param affected The affected torrent to update
actionsToolbar = ((TorrentsActivity) getActivity()).actionsToolbar; * @param wasRemoved Whether the affected torrent was indeed removed; otherwise it was updated somehow
addmenuButton = ((TorrentsActivity) getActivity()).addmenuButton; */
} public void quickUpdateTorrent(Torrent affected, boolean wasRemoved) {
actionsToolbar.setEnabled(false); if (this.isDetached()) {
actionsMenu.setVisibility(View.VISIBLE); return;
addmenuButton.setVisibility(View.GONE); }
actionsMenu.setOnMenuItemClickListener(new ActionMenuView.OnMenuItemClickListener() {
@Override // Remove the old torrent object first
public boolean onMenuItemClick(MenuItem menuItem) { Iterator<Torrent> iter = torrents.iterator();
return onActionItemClicked(mode, menuItem); while (iter.hasNext()) {
} Torrent torrent = iter.next();
}); if (torrent.getUniqueID().equals(affected.getUniqueID())) {
actionsMenu.getMenu().clear(); iter.remove();
getActivity().getMenuInflater().inflate(R.menu.fragment_torrents_cab, actionsMenu.getMenu()); break;
Context themedContext = ((AppCompatActivity) getActivity()).getSupportActionBar().getThemedContext(); }
selectionManagerMode = new SelectionManagerMode(themedContext, torrentsList, R.plurals.navigation_torrentsselected); }
selectionManagerMode.onCreateActionMode(mode, menu); // In case it was an update, add the updated torrent object
return true; if (!wasRemoved) {
} torrents.add(affected);
}
@Override // Now refresh the screen
public boolean onPrepareActionMode(ActionMode mode, Menu menu) { applyAllFilters();
selectionManagerMode.onPrepareActionMode(mode, menu); }
// Hide/show options depending on the type of server we are connected to
if (daemonType != null) { /**
actionsMenu.getMenu().findItem(R.id.action_start).setVisible(Daemon.supportsStoppingStarting(daemonType)); * Clears the currently visible list of torrents.
actionsMenu.getMenu().findItem(R.id.action_stop).setVisible(Daemon.supportsStoppingStarting(daemonType)); *
actionsMenu.getMenu().findItem(R.id.action_setlabel).setVisible(Daemon.supportsSetLabel(daemonType)); * @param clearError Also clear any error message
} * @param clearFilter Also clear any selected filter
// Pause autorefresh */
if (getActivity() != null && getActivity() instanceof TorrentsActivity) { public void clear(boolean clearError, boolean clearFilter) {
((TorrentsActivity) getActivity()).stopRefresh = true; torrents = null;
((TorrentsActivity) getActivity()).stopAutoRefresh(); if (clearError) {
} this.connectionErrorMessage = null;
return true; }
} if (clearFilter) {
this.currentTextFilter = null;
public boolean onActionItemClicked(ActionMode mode, MenuItem item) { this.currentNavigationFilter = null;
}
// Get checked torrents applyAllFilters();
ArrayList<Torrent> checked = new ArrayList<>(); }
for (int i = 0; i < torrentsList.getCheckedItemPositions().size(); i++) {
if (torrentsList.getCheckedItemPositions().valueAt(i) && i < torrentsList.getAdapter().getCount()) { /**
checked.add((Torrent) torrentsList.getAdapter().getItem(torrentsList.getCheckedItemPositions().keyAt(i))); * Stores the new sort order (for future refreshes) and sorts the current visible list. If the given new sort property equals the existing
} * property, the list sort order is reversed instead.
} *
* @param newSortOrder The sort order that the user selected.
int itemId = item.getItemId(); */
if (itemId == R.id.action_resume) { public void sortBy(TorrentsSortBy newSortOrder) {
for (Torrent torrent : checked) { // Update the sort order property and direction and store this last used setting
getTasksExecutor().resumeTorrent(torrent); if (this.currentSortOrder == newSortOrder) {
} this.currentSortDescending = !this.currentSortDescending;
mode.finish(); } else {
return true; this.currentSortOrder = newSortOrder;
} else if (itemId == R.id.action_pause) { this.currentSortDescending = false;
for (Torrent torrent : checked) { }
getTasksExecutor().pauseTorrent(torrent); applicationSettings.setLastUsedSortOrder(this.currentSortOrder, this.currentSortDescending);
} applyAllFilters();
mode.finish(); }
return true;
} else if (itemId == R.id.action_start) { public void applyTextFilter(String newTextFilter) {
for (Torrent torrent : checked) { this.currentTextFilter = newTextFilter;
getTasksExecutor().startTorrent(torrent, false); // Show the new filtered list
} applyAllFilters();
mode.finish(); }
return true;
} else if (itemId == R.id.action_stop) { /**
for (Torrent torrent : checked) { * Apply a filter on the current list of all torrents, showing the appropriate sublist of torrents only
getTasksExecutor().stopTorrent(torrent); *
} * @param newFilter The new filter to apply to the local list of torrents
mode.finish(); */
return true; public void applyNavigationFilter(NavigationFilter newFilter) {
} else if (itemId == R.id.action_remove_default) { this.currentNavigationFilter = newFilter;
for (Torrent torrent : checked) { applyAllFilters();
getTasksExecutor().removeTorrent(torrent, false); }
}
mode.finish(); private void applyAllFilters() {
return true;
} else if (itemId == R.id.action_remove_withdata) { // No torrents? Directly update views accordingly
for (Torrent torrent : checked) { if (torrents == null) {
getTasksExecutor().removeTorrent(torrent, true); updateViewVisibility();
} return;
mode.finish(); }
return true;
} else if (itemId == R.id.action_setlabel) { // Filter the list of torrents to show according to navigation and text filters
lastMultiSelectedTorrents = checked; ArrayList<Torrent> filteredTorrents = new ArrayList<>(torrents);
if (currentLabels != null) { if (currentNavigationFilter != null) {
SetLabelDialog.show(getActivity(), TorrentsFragment.this, currentLabels); // Remove torrents that do not match the selected navigation filter
} for (Iterator<Torrent> torrentIter = filteredTorrents.iterator(); torrentIter.hasNext(); ) {
mode.finish(); if (!currentNavigationFilter.matches(torrentIter.next(), systemSettings.treatDormantAsInactive())) {
return true; torrentIter.remove();
} else { }
return false; }
} }
} if (currentTextFilter != null) {
// Remove torrents that do not contain the text filter string
@Override for (Iterator<Torrent> torrentIter = filteredTorrents.iterator(); torrentIter.hasNext(); ) {
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { if (!torrentIter.next().getName().toLowerCase(Locale.getDefault()).contains(currentTextFilter.toLowerCase(Locale.getDefault()))) {
selectionManagerMode.onItemCheckedStateChanged(mode, position, id, checked); torrentIter.remove();
} }
}
@Override }
public void onDestroyActionMode(ActionMode mode) {
// Resume autorefresh // Sort the list of filtered torrents
if (getActivity() != null && getActivity() instanceof TorrentsActivity) { Collections.sort(filteredTorrents, new TorrentsComparator(daemonType, this.currentSortOrder, this.currentSortDescending));
((TorrentsActivity) getActivity()).stopRefresh = false;
((TorrentsActivity) getActivity()).startAutoRefresh(); if (torrentsList.getAdapter() != null) {
} ((TorrentsAdapter) torrentsList.getAdapter()).update(filteredTorrents);
selectionManagerMode.onDestroyActionMode(mode); }
actionsMenu.setVisibility(View.GONE); updateViewVisibility();
actionsToolbar.setEnabled(true); }
addmenuButton.setVisibility(View.VISIBLE);
} @Click
protected void emptyTextClicked() {
}; // Refresh the activity (that contains this fragment) when the empty view gear is clicked
if (getActivity() != null && getActivity() instanceof RefreshableActivity) {
@Click ((RefreshableActivity) getActivity()).refreshScreen();
protected void emptyTextClicked() { }
// Refresh the activity (that contains this fragment) when the empty view gear is clicked }
if (getActivity() != null && getActivity() instanceof RefreshableActivity) {
((RefreshableActivity) getActivity()).refreshScreen(); @Click
} protected void errorTextClicked() {
} // Refresh the activity (that contains this fragment) when the error view gear is clicked
if (getActivity() != null && getActivity() instanceof RefreshableActivity) {
@Click ((RefreshableActivity) getActivity()).refreshScreen();
protected void errorTextClicked() { }
// Refresh the activity (that contains this fragment) when the error view gear is clicked }
if (getActivity() != null && getActivity() instanceof RefreshableActivity) {
((RefreshableActivity) getActivity()).refreshScreen(); @ItemClick(R.id.torrents_list)
} protected void torrentsListClicked(Torrent torrent) {
} // Show the torrent details fragment
((TorrentsActivity) getActivity()).openDetails(torrent);
@ItemClick(R.id.torrents_list) }
protected void torrentsListClicked(Torrent torrent) {
// Show the torrent details fragment @Override
((TorrentsActivity) getActivity()).openDetails(torrent); public void onLabelPicked(String newLabel) {
} for (Torrent torrent : lastMultiSelectedTorrents) {
getTasksExecutor().updateLabel(torrent, newLabel);
@Override }
public void onLabelPicked(String newLabel) { }
for (Torrent torrent : lastMultiSelectedTorrents) {
getTasksExecutor().updateLabel(torrent, newLabel); /**
} * Updates the shown screen depending on whether we have a connection (so torrents can be shown) or not (in case we need to show a message
} * suggesting help). This should only ever be called on the UI thread.
*
/** * @param hasAConnection True if the user has servers configured and therefore has a connection that can be used
* Updates the shown screen depending on whether we have a connection (so torrents can be shown) or not (in case we need to show a message */
* suggesting help). This should only ever be called on the UI thread. public void updateConnectionStatus(boolean hasAConnection, Daemon daemonType) {
* @param hasAConnection True if the user has servers configured and therefore has a connection that can be used if (!isResumed()) return;
*/ this.hasAConnection = hasAConnection;
public void updateConnectionStatus(boolean hasAConnection, Daemon daemonType) { this.daemonType = daemonType;
if (!isResumed()) return; if (!hasAConnection) {
this.hasAConnection = hasAConnection; torrentsList.setVisibility(View.GONE);
this.daemonType = daemonType; emptyText.setVisibility(View.GONE);
if (!hasAConnection) { loadingProgress.setVisibility(View.GONE);
torrentsList.setVisibility(View.GONE); errorText.setVisibility(View.GONE);
emptyText.setVisibility(View.GONE); nosettingsText.setVisibility(View.VISIBLE);
loadingProgress.setVisibility(View.GONE); swipeRefreshLayout.setEnabled(false);
errorText.setVisibility(View.GONE); clear(true, true); // Indirectly also calls updateViewVisibility()
nosettingsText.setVisibility(View.VISIBLE); } else {
swipeRefreshLayout.setEnabled(false); updateViewVisibility();
clear(true, true); // Indirectly also calls updateViewVisibility() }
} else { }
updateViewVisibility();
} /**
} * Updates the shown screen depending on whether the torrents are loading. This should only ever be called on the UI thread.
*
/** * @param isLoading True if the list of torrents is (re)loading, false otherwise
* Updates the shown screen depending on whether the torrents are loading. This should only ever be called on the UI thread. */
* @param isLoading True if the list of torrents is (re)loading, false otherwise public void updateIsLoading(boolean isLoading) {
*/ if (!isResumed()) return;
public void updateIsLoading(boolean isLoading) { this.isLoading = isLoading;
if (!isResumed()) return; if (isLoading) {
this.isLoading = isLoading; clear(true, false); // Indirectly also calls updateViewVisibility()
if (isLoading) { } else {
clear(true, false); // Indirectly also calls updateViewVisibility() updateViewVisibility();
} else { }
updateViewVisibility(); }
}
} /**
* Updates the shown screen depending on whether a connection error occurred. This should only ever be called on the UI thread.
/** *
* Updates the shown screen depending on whether a connection error occurred. This should only ever be called on the UI thread. * @param connectionErrorMessage The error message from the last failed connection attempt, or null to clear the visible error text
* @param connectionErrorMessage The error message from the last failed connection attempt, or null to clear the visible error text */
*/ public void updateError(String connectionErrorMessage) {
public void updateError(String connectionErrorMessage) { if (!isResumed()) return;
if (!isResumed()) return; this.connectionErrorMessage = connectionErrorMessage;
this.connectionErrorMessage = connectionErrorMessage; errorText.setText(connectionErrorMessage);
errorText.setText(connectionErrorMessage); if (connectionErrorMessage != null) {
if (connectionErrorMessage != null) { clear(false, false); // Indirectly also calls updateViewVisibility()
clear(false, false); // Indirectly also calls updateViewVisibility() } else {
} else { updateViewVisibility();
updateViewVisibility(); }
} }
}
private void updateViewVisibility() {
private void updateViewVisibility() { if (!hasAConnection) {
if (!hasAConnection) { return;
return; }
} boolean isEmpty = torrents == null || torrentsList.getAdapter() != null && torrentsList.getAdapter().isEmpty();
boolean isEmpty = torrents == null || torrentsList.getAdapter() != null && torrentsList.getAdapter().isEmpty(); boolean hasError = connectionErrorMessage != null;
boolean hasError = connectionErrorMessage != null; nosettingsText.setVisibility(View.GONE);
nosettingsText.setVisibility(View.GONE); errorText.setVisibility(hasError ? View.VISIBLE : View.GONE);
errorText.setVisibility(hasError ? View.VISIBLE : View.GONE); torrentsList.setVisibility(!hasError && !isLoading && !isEmpty ? View.VISIBLE : View.GONE);
torrentsList.setVisibility(!hasError && !isLoading && !isEmpty ? View.VISIBLE : View.GONE); loadingProgress.setVisibility(!hasError && isLoading ? View.VISIBLE : View.GONE);
loadingProgress.setVisibility(!hasError && isLoading ? View.VISIBLE : View.GONE); emptyText.setVisibility(!hasError && !isLoading && isEmpty ? View.VISIBLE : View.GONE);
emptyText.setVisibility(!hasError && !isLoading && isEmpty ? View.VISIBLE : View.GONE); swipeRefreshLayout.setEnabled(true);
swipeRefreshLayout.setEnabled(true); }
}
/**
/** * Returns the object responsible for executing torrent tasks against a connected server
* Returns the object responsible for executing torrent tasks against a connected server *
* @return The executor for tasks on some torrent * @return The executor for tasks on some torrent
*/ */
private TorrentTasksExecutor getTasksExecutor() { private TorrentTasksExecutor getTasksExecutor() {
// NOTE: Assumes the activity implements all the required torrent tasks // NOTE: Assumes the activity implements all the required torrent tasks
return (TorrentTasksExecutor) getActivity(); return (TorrentTasksExecutor) getActivity();
} }
} }

32
app/src/main/java/org/transdroid/core/gui/TransdroidApp.java

@ -17,11 +17,10 @@
package org.transdroid.core.gui; package org.transdroid.core.gui;
import android.app.Application; import android.app.Application;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.evernote.android.job.JobConfig; import com.evernote.android.job.JobConfig;
import com.evernote.android.job.JobManager; import com.evernote.android.job.JobManager;
import com.evernote.android.job.util.JobLogger;
import org.androidannotations.annotations.Bean; import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EApplication; import org.androidannotations.annotations.EApplication;
import org.transdroid.core.gui.log.Log; import org.transdroid.core.gui.log.Log;
@ -30,21 +29,16 @@ import org.transdroid.core.service.ScheduledJobCreator;
@EApplication @EApplication
public class TransdroidApp extends Application { public class TransdroidApp extends Application {
@Bean @Bean
protected Log log; protected Log log;
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
// Configure Android-Job // Configure Android-Job
JobConfig.addLogger(new JobLogger() { JobConfig.addLogger((priority, tag, message, t) -> log.d(tag, message));
@Override JobManager.create(this).addJobCreator(new ScheduledJobCreator());
public void log(int priority, @NonNull String tag, @NonNull String message, @Nullable Throwable t) { }
log.d(tag, message);
}
});
JobManager.create(this).addJobCreator(new ScheduledJobCreator());
}
} }

401
app/src/main/java/org/transdroid/core/gui/lists/DetailsAdapter.java

@ -16,215 +16,220 @@
*/ */
package org.transdroid.core.gui.lists; package org.transdroid.core.gui.lists;
import java.util.ArrayList;
import java.util.List;
import org.transdroid.R;
import org.transdroid.core.gui.navigation.*;
import org.transdroid.core.gui.lists.PiecesMapView;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentFile;
import android.content.Context; import android.content.Context;
import android.text.util.Linkify; import android.text.util.Linkify;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.BaseAdapter; import android.widget.BaseAdapter;
import org.transdroid.R;
import org.transdroid.core.gui.navigation.FilterSeparatorView_;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentFile;
import java.util.ArrayList;
import java.util.List;
/** /**
* List adapter that holds a header view showing torrent details and show the list list contained by the torrent. * List adapter that holds a header view showing torrent details and show the list list contained by the torrent.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class DetailsAdapter extends MergeAdapter { public class DetailsAdapter extends MergeAdapter {
private ViewHolderAdapter torrentDetailsViewAdapter = null; private ViewHolderAdapter torrentDetailsViewAdapter = null;
private TorrentDetailsView torrentDetailsView = null; private TorrentDetailsView torrentDetailsView = null;
private ViewHolderAdapter piecesSeparatorAdapter = null; private ViewHolderAdapter piecesSeparatorAdapter = null;
private ViewHolderAdapter piecesMapViewAdapter = null; private ViewHolderAdapter piecesMapViewAdapter = null;
private PiecesMapView piecesMapView = null; private PiecesMapView piecesMapView = null;
private ViewHolderAdapter trackersSeparatorAdapter = null; private ViewHolderAdapter trackersSeparatorAdapter = null;
private SimpleListItemAdapter trackersAdapter = null; private SimpleListItemAdapter trackersAdapter = null;
private ViewHolderAdapter errorsSeparatorAdapter = null; private ViewHolderAdapter errorsSeparatorAdapter = null;
private SimpleListItemAdapter errorsAdapter = null; private SimpleListItemAdapter errorsAdapter = null;
private ViewHolderAdapter torrentFilesSeparatorAdapter = null; private ViewHolderAdapter torrentFilesSeparatorAdapter = null;
private TorrentFilesAdapter torrentFilesAdapter = null; private TorrentFilesAdapter torrentFilesAdapter = null;
public DetailsAdapter(Context context) { public DetailsAdapter(Context context) {
// Immediately bind the adapters, or the MergeAdapter will not be able to determine the view types and instead // Immediately bind the adapters, or the MergeAdapter will not be able to determine the view types and instead
// display nothing at all // display nothing at all
// Torrent details header // Torrent details header
torrentDetailsView = TorrentDetailsView_.build(context); torrentDetailsView = TorrentDetailsView_.build(context);
torrentDetailsViewAdapter = new ViewHolderAdapter(torrentDetailsView); torrentDetailsViewAdapter = new ViewHolderAdapter(torrentDetailsView);
torrentDetailsViewAdapter.setViewEnabled(false); torrentDetailsViewAdapter.setViewEnabled(false);
torrentDetailsViewAdapter.setViewVisibility(View.GONE); torrentDetailsViewAdapter.setViewVisibility(View.GONE);
addAdapter(torrentDetailsViewAdapter); addAdapter(torrentDetailsViewAdapter);
// Pieces map // Pieces map
piecesSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText( piecesSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
context.getString(R.string.status_pieces))); context.getString(R.string.status_pieces)));
piecesSeparatorAdapter.setViewEnabled(false); piecesSeparatorAdapter.setViewEnabled(false);
piecesSeparatorAdapter.setViewVisibility(View.GONE); piecesSeparatorAdapter.setViewVisibility(View.GONE);
addAdapter(piecesSeparatorAdapter); addAdapter(piecesSeparatorAdapter);
piecesMapView = new PiecesMapView(context); piecesMapView = new PiecesMapView(context);
piecesMapViewAdapter = new ViewHolderAdapter(piecesMapView); piecesMapViewAdapter = new ViewHolderAdapter(piecesMapView);
piecesMapViewAdapter.setViewEnabled(false); piecesMapViewAdapter.setViewEnabled(false);
piecesMapViewAdapter.setViewVisibility(View.GONE); piecesMapViewAdapter.setViewVisibility(View.GONE);
addAdapter(piecesMapViewAdapter); addAdapter(piecesMapViewAdapter);
// Tracker errors // Tracker errors
errorsSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText( errorsSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
context.getString(R.string.status_errors))); context.getString(R.string.status_errors)));
errorsSeparatorAdapter.setViewEnabled(false); errorsSeparatorAdapter.setViewEnabled(false);
errorsSeparatorAdapter.setViewVisibility(View.GONE); errorsSeparatorAdapter.setViewVisibility(View.GONE);
addAdapter(errorsSeparatorAdapter); addAdapter(errorsSeparatorAdapter);
this.errorsAdapter = new SimpleListItemAdapter(context, new ArrayList<SimpleListItem>()); this.errorsAdapter = new SimpleListItemAdapter(context, new ArrayList<>());
this.errorsAdapter.setAutoLinkMask(Linkify.WEB_URLS); this.errorsAdapter.setAutoLinkMask(Linkify.WEB_URLS);
addAdapter(errorsAdapter); addAdapter(errorsAdapter);
// Trackers // Trackers
trackersSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText( trackersSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
context.getString(R.string.status_trackers))); context.getString(R.string.status_trackers)));
trackersSeparatorAdapter.setViewEnabled(false); trackersSeparatorAdapter.setViewEnabled(false);
trackersSeparatorAdapter.setViewVisibility(View.GONE); trackersSeparatorAdapter.setViewVisibility(View.GONE);
addAdapter(trackersSeparatorAdapter); addAdapter(trackersSeparatorAdapter);
this.trackersAdapter = new SimpleListItemAdapter(context, new ArrayList<SimpleListItem>()); this.trackersAdapter = new SimpleListItemAdapter(context, new ArrayList<>());
addAdapter(trackersAdapter); addAdapter(trackersAdapter);
// Torrent files // Torrent files
torrentFilesSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText( torrentFilesSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
context.getString(R.string.status_files))); context.getString(R.string.status_files)));
torrentFilesSeparatorAdapter.setViewEnabled(false); torrentFilesSeparatorAdapter.setViewEnabled(false);
torrentFilesSeparatorAdapter.setViewVisibility(View.GONE); torrentFilesSeparatorAdapter.setViewVisibility(View.GONE);
addAdapter(torrentFilesSeparatorAdapter); addAdapter(torrentFilesSeparatorAdapter);
this.torrentFilesAdapter = new TorrentFilesAdapter(context, new ArrayList<TorrentFile>()); this.torrentFilesAdapter = new TorrentFilesAdapter(context, new ArrayList<>());
addAdapter(torrentFilesAdapter); addAdapter(torrentFilesAdapter);
} }
/** /**
* Update the torrent data in the details header of this merge adapter * Update the torrent data in the details header of this merge adapter
* @param torrent The torrent for which detailed data is shown *
*/ * @param torrent The torrent for which detailed data is shown
public void updateTorrent(Torrent torrent) { */
torrentDetailsView.update(torrent); public void updateTorrent(Torrent torrent) {
torrentDetailsViewAdapter.setViewVisibility(torrent == null ? View.GONE : View.VISIBLE); torrentDetailsView.update(torrent);
} torrentDetailsViewAdapter.setViewVisibility(torrent == null ? View.GONE : View.VISIBLE);
}
/**
* Update the list of files contained in this torrent /**
* @param torrentFiles The new list of files, or null if the list and header should be hidden * Update the list of files contained in this torrent
*/ *
public void updateTorrentFiles(List<TorrentFile> torrentFiles) { * @param torrentFiles The new list of files, or null if the list and header should be hidden
if (torrentFiles == null) { */
torrentFilesAdapter.update(new ArrayList<TorrentFile>()); public void updateTorrentFiles(List<TorrentFile> torrentFiles) {
torrentFilesSeparatorAdapter.setViewVisibility(View.GONE); if (torrentFiles == null) {
} else { torrentFilesAdapter.update(new ArrayList<>());
torrentFilesAdapter.update(torrentFiles); torrentFilesSeparatorAdapter.setViewVisibility(View.GONE);
torrentFilesSeparatorAdapter.setViewVisibility(View.VISIBLE); } else {
} torrentFilesAdapter.update(torrentFiles);
} torrentFilesSeparatorAdapter.setViewVisibility(View.VISIBLE);
}
/** }
* Update the list of trackers
* @param trackers The new list of trackers known for this torrent, or null if the list and header should be hidden /**
*/ * Update the list of trackers
public void updateTrackers(List<? extends SimpleListItem> trackers) { *
if (trackers == null || trackers.isEmpty()) { * @param trackers The new list of trackers known for this torrent, or null if the list and header should be hidden
trackersAdapter.update(new ArrayList<SimpleListItemAdapter.SimpleStringItem>()); */
trackersSeparatorAdapter.setViewVisibility(View.GONE); public void updateTrackers(List<? extends SimpleListItem> trackers) {
} else { if (trackers == null || trackers.isEmpty()) {
trackersAdapter.update(trackers); trackersAdapter.update(new ArrayList<SimpleListItemAdapter.SimpleStringItem>());
trackersSeparatorAdapter.setViewVisibility(View.VISIBLE); trackersSeparatorAdapter.setViewVisibility(View.GONE);
} } else {
} trackersAdapter.update(trackers);
trackersSeparatorAdapter.setViewVisibility(View.VISIBLE);
/** }
* Update the list of errors }
* @param errors The new list of errors known for this torrent, or null if the list and header should be hidden
*/ /**
public void updateErrors(List<? extends SimpleListItem> errors) { * Update the list of errors
if (errors == null || errors.isEmpty()) { *
errorsAdapter.update(new ArrayList<SimpleListItemAdapter.SimpleStringItem>()); * @param errors The new list of errors known for this torrent, or null if the list and header should be hidden
errorsSeparatorAdapter.setViewVisibility(View.GONE); */
} else { public void updateErrors(List<? extends SimpleListItem> errors) {
errorsAdapter.update(errors); if (errors == null || errors.isEmpty()) {
errorsSeparatorAdapter.setViewVisibility(View.VISIBLE); errorsAdapter.update(new ArrayList<SimpleListItemAdapter.SimpleStringItem>());
} errorsSeparatorAdapter.setViewVisibility(View.GONE);
} } else {
errorsAdapter.update(errors);
public void updatePieces(List<Integer> pieces) { errorsSeparatorAdapter.setViewVisibility(View.VISIBLE);
if (pieces == null || pieces.isEmpty()) { }
piecesSeparatorAdapter.setViewEnabled(false); }
piecesSeparatorAdapter.setViewVisibility(View.GONE);
piecesMapViewAdapter.setViewEnabled(false); public void updatePieces(List<Integer> pieces) {
piecesMapViewAdapter.setViewVisibility(View.GONE); if (pieces == null || pieces.isEmpty()) {
} else { piecesSeparatorAdapter.setViewEnabled(false);
piecesMapView.setPieces(pieces); piecesSeparatorAdapter.setViewVisibility(View.GONE);
piecesMapViewAdapter.setViewEnabled(false);
piecesMapViewAdapter.setViewEnabled(true); piecesMapViewAdapter.setViewVisibility(View.GONE);
piecesMapViewAdapter.setViewVisibility(View.VISIBLE); } else {
piecesSeparatorAdapter.setViewEnabled(true); piecesMapView.setPieces(pieces);
piecesSeparatorAdapter.setViewVisibility(View.VISIBLE);
} piecesMapViewAdapter.setViewEnabled(true);
} piecesMapViewAdapter.setViewVisibility(View.VISIBLE);
piecesSeparatorAdapter.setViewEnabled(true);
/** piecesSeparatorAdapter.setViewVisibility(View.VISIBLE);
* Clear currently visible torrent, including header and shown lists }
*/ }
public void clear() {
updateTorrent(null); /**
updateTorrentFiles(null); * Clear currently visible torrent, including header and shown lists
updateErrors(null); */
updateTrackers(null); public void clear() {
} updateTorrent(null);
updateTorrentFiles(null);
protected static class TorrentFilesAdapter extends BaseAdapter { updateErrors(null);
updateTrackers(null);
private final Context context; }
private List<TorrentFile> items;
protected static class TorrentFilesAdapter extends BaseAdapter {
public TorrentFilesAdapter(Context context, List<TorrentFile> items) {
this.context = context; private final Context context;
this.items = items; private List<TorrentFile> items;
}
public TorrentFilesAdapter(Context context, List<TorrentFile> items) {
/** this.context = context;
* Allows updating of the full data list underlying this adapter, replacing all items this.items = items;
* @param newItems The new list of files to display }
*/
public void update(List<TorrentFile> newItems) { /**
this.items = newItems; * Allows updating of the full data list underlying this adapter, replacing all items
notifyDataSetChanged(); *
} * @param newItems The new list of files to display
*/
@Override public void update(List<TorrentFile> newItems) {
public int getCount() { this.items = newItems;
return items.size(); notifyDataSetChanged();
} }
@Override @Override
public TorrentFile getItem(int position) { public int getCount() {
return items.get(position); return items.size();
} }
@Override @Override
public long getItemId(int position) { public TorrentFile getItem(int position) {
return position; return items.get(position);
} }
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public long getItemId(int position) {
TorrentFileView torrentFileView; return position;
if (convertView == null) { }
torrentFileView = TorrentFileView_.build(context);
} else { @Override
torrentFileView = (TorrentFileView) convertView; public View getView(int position, View convertView, ViewGroup parent) {
} TorrentFileView torrentFileView;
torrentFileView.bind(getItem(position)); if (convertView == null) {
return torrentFileView; torrentFileView = TorrentFileView_.build(context);
} } else {
torrentFileView = (TorrentFileView) convertView;
} }
torrentFileView.bind(getItem(position));
return torrentFileView;
}
}
} }

436
app/src/main/java/org/transdroid/core/gui/lists/LocalTorrent.java

@ -16,7 +16,7 @@
*/ */
package org.transdroid.core.gui.lists; package org.transdroid.core.gui.lists;
import java.util.Locale; import android.content.res.Resources;
import org.transdroid.R; import org.transdroid.R;
import org.transdroid.daemon.DaemonException; import org.transdroid.daemon.DaemonException;
@ -25,227 +25,233 @@ import org.transdroid.daemon.TorrentStatus;
import org.transdroid.daemon.util.FileSizeConverter; import org.transdroid.daemon.util.FileSizeConverter;
import org.transdroid.daemon.util.TimespanConverter; import org.transdroid.daemon.util.TimespanConverter;
import android.content.res.Resources; import java.util.Locale;
/** /**
* Wrapper around Torrent to provide some addition getters that give translatable or otherwise formatted Strings of * Wrapper around Torrent to provide some addition getters that give translatable or otherwise formatted Strings of
* torrent statistics. * torrent statistics.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class LocalTorrent { public class LocalTorrent {
/** private static final String DECIMAL_FORMATTER = "%.1f";
* Creates the LocalTorrent object so that the translatable/formattable version of a Torrent can be used. private static final String DECIMAL_FORMATTER_2 = "%.2f";
* @param torrent The Torrent object private final Torrent t;
* @return The torrent wrapped as LocalTorrent object
*/ private LocalTorrent(Torrent torrent) {
public static LocalTorrent fromTorrent(Torrent torrent) { this.t = torrent;
return new LocalTorrent(torrent); }
}
/**
private final Torrent t; * Creates the LocalTorrent object so that the translatable/formattable version of a Torrent can be used.
*
private LocalTorrent(Torrent torrent) { * @param torrent The Torrent object
this.t = torrent; * @return The torrent wrapped as LocalTorrent object
} */
public static LocalTorrent fromTorrent(Torrent torrent) {
private static final String DECIMAL_FORMATTER = "%.1f"; return new LocalTorrent(torrent);
private static final String DECIMAL_FORMATTER_2 = "%.2f"; }
/** /**
* Builds a string showing the upload/download seed ratio. If not downloading, it will base the ratio on the total * Convert a DaemonException to a translatable human-readable error message
* size; so if you created the torrent yourself you will have downloaded 0 bytes, but the ratio will pretend you *
* have 100%. * @param e The exception that was thrown by the server
* @return A nicely formatted string containing the upload/download seed ratio * @return A string resource ID to show to the user
*/ */
public String getRatioString() { public static int getResourceForDaemonException(DaemonException e) {
long baseSize = t.getTotalSize(); switch (e.getType()) {
if (t.getStatusCode() == TorrentStatus.Downloading) { case MethodUnsupported:
baseSize = t.getDownloadedEver(); return R.string.error_unsupported;
} case UnexpectedResponse:
if (baseSize <= 0) { return R.string.error_jsonresponseerror;
return String.format(Locale.getDefault(), DECIMAL_FORMATTER_2, 0d); case ParsingFailed:
} else if (t.getRatio() == Double.POSITIVE_INFINITY) { return R.string.error_jsonrequesterror;
return "\u221E"; case NotConnected:
} else { return R.string.error_daemonnotconnected;
return String.format(Locale.getDefault(), DECIMAL_FORMATTER_2, t.getRatio()); case AuthenticationFailure:
} return R.string.error_401;
} case FileAccessError:
return R.string.error_torrentfile;
/** case ConnectionError:
* Returns a formatted string indicating the current progress in terms of transferred bytes default:
* @param r The context resources, to access translations return R.string.error_httperror;
* @param withAvailability Whether to show file availability in-line }
* @return A nicely formatted string indicating torrent status and, if applicable, progress in bytes }
*/
public String getProgressSizeText(Resources r, boolean withAvailability) { /**
* Builds a string showing the upload/download seed ratio. If not downloading, it will base the ratio on the total
switch (t.getStatusCode()) { * size; so if you created the torrent yourself you will have downloaded 0 bytes, but the ratio will pretend you
case Waiting: * have 100%.
case Error: *
// Not downloading yet * @return A nicely formatted string containing the upload/download seed ratio
return r.getString(R.string.status_waitingtodl, FileSizeConverter.getSize(t.getTotalSize())); */
case Checking: public String getRatioString() {
return r.getString(R.string.status_checking); long baseSize = t.getTotalSize();
case Downloading: if (t.getStatusCode() == TorrentStatus.Downloading) {
// Downloading baseSize = t.getDownloadedEver();
return r.getString( }
R.string.status_size1, if (baseSize <= 0) {
FileSizeConverter.getSize(t.getDownloadedEver()), return String.format(Locale.getDefault(), DECIMAL_FORMATTER_2, 0d);
FileSizeConverter.getSize(t.getTotalSize()), } else if (t.getRatio() == Double.POSITIVE_INFINITY) {
String.format(DECIMAL_FORMATTER, t.getDownloadedPercentage() * 100) return "\u221E";
+ "%" } else {
+ (!withAvailability ? "" : "/" return String.format(Locale.getDefault(), DECIMAL_FORMATTER_2, t.getRatio());
+ String.format(DECIMAL_FORMATTER, t.getAvailability() * 100) + "%")); }
case Seeding: }
case Paused:
case Queued: /**
// Seeding or paused * Returns a formatted string indicating the current progress in terms of transferred bytes
return r.getString(R.string.status_size2, FileSizeConverter.getSize(t.getTotalSize()), *
FileSizeConverter.getSize(t.getUploadedEver())); * @param r The context resources, to access translations
default: * @param withAvailability Whether to show file availability in-line
return ""; * @return A nicely formatted string indicating torrent status and, if applicable, progress in bytes
} */
public String getProgressSizeText(Resources r, boolean withAvailability) {
}
switch (t.getStatusCode()) {
/** case Waiting:
* Returns a formatted string indicating either the expected time to download (ETA) or, when seeding, the ratio case Error:
* @param r The context resources, to access translations // Not downloading yet
* @return A string like '~ 34 seconds', or 'RATIO 8.2' or an empty string return r.getString(R.string.status_waitingtodl, FileSizeConverter.getSize(t.getTotalSize()));
*/ case Checking:
public String getProgressEtaRatioText(Resources r) { return r.getString(R.string.status_checking);
switch (t.getStatusCode()) { case Downloading:
case Downloading: // Downloading
// Downloading return r.getString(
return getRemainingTimeString(r, true, false); R.string.status_size1,
case Seeding: FileSizeConverter.getSize(t.getDownloadedEver()),
case Paused: FileSizeConverter.getSize(t.getTotalSize()),
case Queued: String.format(DECIMAL_FORMATTER, t.getDownloadedPercentage() * 100)
// Seeding or paused + "%"
return r.getString(R.string.status_ratio, getRatioString()); + (!withAvailability ? "" : "/"
case Waiting: + String.format(DECIMAL_FORMATTER, t.getAvailability() * 100) + "%"));
case Checking: case Seeding:
case Error: case Paused:
default: case Queued:
return ""; // Seeding or paused
} return r.getString(R.string.status_size2, FileSizeConverter.getSize(t.getTotalSize()),
} FileSizeConverter.getSize(t.getUploadedEver()));
default:
/** return "";
* Returns a formatted string indicating the torrent status and connected peers }
* @param r The context resources, to access translations
* @return A string like 'Queued' or, when seeding or leeching, '2 OF 28 PEERS' }
*/
public String getProgressConnectionText(Resources r) { /**
* Returns a formatted string indicating either the expected time to download (ETA) or, when seeding, the ratio
switch (t.getStatusCode()) { *
case Waiting: * @param r The context resources, to access translations
return r.getString(R.string.status_waiting); * @return A string like '~ 34 seconds', or 'RATIO 8.2' or an empty string
case Checking: */
return r.getString(R.string.status_checking); public String getProgressEtaRatioText(Resources r) {
case Downloading: switch (t.getStatusCode()) {
return r.getString(R.string.status_seeders, t.getSeedersConnected(), t.getSeedersKnown()); case Downloading:
case Seeding: // Downloading
return r.getString(R.string.status_leechers, t.getLeechersConnected(), t.getLeechersKnown()); return getRemainingTimeString(r, true, false);
case Paused: case Seeding:
return r.getString(R.string.status_paused); case Paused:
case Queued: case Queued:
return r.getString(R.string.status_stopped); // Seeding or paused
case Error: return r.getString(R.string.status_ratio, getRatioString());
return r.getString(R.string.status_error); case Waiting:
default: case Checking:
return r.getString(R.string.status_unknown); case Error:
} default:
return "";
} }
}
/**
* Returns a formatted string indicating current transfer speeds for the torrent /**
* @param r The context resources, to access translations * Returns a formatted string indicating the torrent status and connected peers
* @return A string like ' 28KB/s 1.8MB/s', or an empty string when not transferrring *
*/ * @param r The context resources, to access translations
public String getProgressSpeedText(Resources r) { * @return A string like 'Queued' or, when seeding or leeching, '2 OF 28 PEERS'
*/
switch (t.getStatusCode()) { public String getProgressConnectionText(Resources r) {
case Waiting:
case Checking: switch (t.getStatusCode()) {
case Paused: case Waiting:
case Queued: return r.getString(R.string.status_waiting);
return ""; case Checking:
case Downloading: return r.getString(R.string.status_checking);
return r.getString(R.string.status_speed_down, FileSizeConverter.getSize(t.getRateDownload()) + "/s") + " " case Downloading:
+ r.getString(R.string.status_speed_up, FileSizeConverter.getSize(t.getRateUpload()) + "/s"); return r.getString(R.string.status_seeders, t.getSeedersConnected(), t.getSeedersKnown());
case Seeding: case Seeding:
return r.getString(R.string.status_speed_up, FileSizeConverter.getSize(t.getRateUpload()) + "/s"); return r.getString(R.string.status_leechers, t.getLeechersConnected(), t.getLeechersKnown());
default: case Paused:
return ""; return r.getString(R.string.status_paused);
} case Queued:
return r.getString(R.string.status_stopped);
} case Error:
return r.getString(R.string.status_error);
public String getProgressStatusEta(Resources r) { default:
switch (t.getStatusCode()) { return r.getString(R.string.status_unknown);
case Waiting: }
return r.getString(R.string.status_waiting).toUpperCase(Locale.getDefault());
case Checking: }
return r.getString(R.string.status_checking).toUpperCase(Locale.getDefault());
case Error: /**
return r.getString(R.string.status_error).toUpperCase(Locale.getDefault()); * Returns a formatted string indicating current transfer speeds for the torrent
case Downloading: *
// Downloading * @param r The context resources, to access translations
return r.getString(R.string.status_downloading).toUpperCase(Locale.getDefault()) + " (" * @return A string like ' 28KB/s 1.8MB/s', or an empty string when not transferrring
+ String.format(DECIMAL_FORMATTER, t.getDownloadedPercentage() * 100) + "%), " */
+ getRemainingTimeString(r, false, true); public String getProgressSpeedText(Resources r) {
case Seeding:
return r.getString(R.string.status_seeding).toUpperCase(Locale.getDefault()); switch (t.getStatusCode()) {
case Paused: case Downloading:
return r.getString(R.string.status_paused).toUpperCase(Locale.getDefault()); return r.getString(R.string.status_speed_down, FileSizeConverter.getSize(t.getRateDownload()) + "/s") + " "
case Queued: + r.getString(R.string.status_speed_up, FileSizeConverter.getSize(t.getRateUpload()) + "/s");
return r.getString(R.string.status_queued).toUpperCase(Locale.getDefault()); case Seeding:
default: return r.getString(R.string.status_speed_up, FileSizeConverter.getSize(t.getRateUpload()) + "/s");
return r.getString(R.string.status_unknown).toUpperCase(Locale.getDefault()); case Waiting:
} case Checking:
} case Paused:
case Queued:
/** default:
* Returns a formatted string indicating the remaining download time return "";
* @param r The context resources, to access translations }
* @param inDays Whether to show days or use hours for > 24 hours left instead
* @return A string like '4d 8h 34m 5s' or '2m 3s' }
*/
public String getRemainingTimeString(Resources r, boolean abbreviate, boolean inDays) { public String getProgressStatusEta(Resources r) {
if (t.getEta() == -1 || t.getEta() == -2) { switch (t.getStatusCode()) {
return r.getString(R.string.status_unknowneta); case Waiting:
} return r.getString(R.string.status_waiting).toUpperCase(Locale.getDefault());
return r.getString(abbreviate ? R.string.status_eta : R.string.status_etalong, case Checking:
TimespanConverter.getTime(t.getEta(), inDays)); return r.getString(R.string.status_checking).toUpperCase(Locale.getDefault());
} case Error:
return r.getString(R.string.status_error).toUpperCase(Locale.getDefault());
/** case Downloading:
* Convert a DaemonException to a translatable human-readable error message // Downloading
* @param e The exception that was thrown by the server return r.getString(R.string.status_downloading).toUpperCase(Locale.getDefault()) + " ("
* @return A string resource ID to show to the user + String.format(DECIMAL_FORMATTER, t.getDownloadedPercentage() * 100) + "%), "
*/ + getRemainingTimeString(r, false, true);
public static int getResourceForDaemonException(DaemonException e) { case Seeding:
switch (e.getType()) { return r.getString(R.string.status_seeding).toUpperCase(Locale.getDefault());
case MethodUnsupported: case Paused:
return R.string.error_unsupported; return r.getString(R.string.status_paused).toUpperCase(Locale.getDefault());
case ConnectionError: case Queued:
return R.string.error_httperror; return r.getString(R.string.status_queued).toUpperCase(Locale.getDefault());
case UnexpectedResponse: default:
return R.string.error_jsonresponseerror; return r.getString(R.string.status_unknown).toUpperCase(Locale.getDefault());
case ParsingFailed: }
return R.string.error_jsonrequesterror; }
case NotConnected:
return R.string.error_daemonnotconnected; /**
case AuthenticationFailure: * Returns a formatted string indicating the remaining download time
return R.string.error_401; *
case FileAccessError: * @param r The context resources, to access translations
return R.string.error_torrentfile; * @param inDays Whether to show days or use hours for > 24 hours left instead
default: * @return A string like '4d 8h 34m 5s' or '2m 3s'
return R.string.error_httperror; */
} public String getRemainingTimeString(Resources r, boolean abbreviate, boolean inDays) {
} if (t.getEta() == -1 || t.getEta() == -2) {
return r.getString(R.string.status_unknowneta);
}
return r.getString(abbreviate ? R.string.status_eta : R.string.status_etalong,
TimespanConverter.getTime(t.getEta(), inDays));
}
} }

557
app/src/main/java/org/transdroid/core/gui/lists/MergeAdapter.java

@ -16,8 +16,6 @@
*/ */
package org.transdroid.core.gui.lists; package org.transdroid.core.gui.lists;
import java.util.ArrayList;
import android.database.DataSetObserver; import android.database.DataSetObserver;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -27,288 +25,297 @@ import android.widget.ListAdapter;
import android.widget.SectionIndexer; import android.widget.SectionIndexer;
import android.widget.TextView; import android.widget.TextView;
import java.util.ArrayList;
import java.util.Arrays;
/** /**
* An adapter that can contain many other adapters and shows them in sequence. Taken from * An adapter that can contain many other adapters and shows them in sequence. Taken from
* http://stackoverflow.com/questions/7964259/android-attaching-multiple-adapters-to-one-adapter and based on the Apache * http://stackoverflow.com/questions/7964259/android-attaching-multiple-adapters-to-one-adapter and based on the Apache
* 2-licensed CommonsWare MergeAdapter. * 2-licensed CommonsWare MergeAdapter.
*
* @author Eric Kok * @author Eric Kok
* @author Alex Amiryan * @author Alex Amiryan
* @author Mark Murphy * @author Mark Murphy
*/ */
public class MergeAdapter extends BaseAdapter implements SectionIndexer { public class MergeAdapter extends BaseAdapter implements SectionIndexer {
protected ArrayList<ListAdapter> pieces = new ArrayList<ListAdapter>(); protected ArrayList<ListAdapter> pieces = new ArrayList<>();
protected String noItemsText; protected String noItemsText;
/** /**
* Stock constructor, simply chaining to the superclass. * Stock constructor, simply chaining to the superclass.
*/ */
public MergeAdapter() { public MergeAdapter() {
super(); super();
} }
/** /**
* Adds a new adapter to the roster of things to appear in the aggregate list. * Adds a new adapter to the roster of things to appear in the aggregate list.
* @param adapter Source for row views for this section *
*/ * @param adapter Source for row views for this section
public void addAdapter(ListAdapter adapter) { */
pieces.add(adapter); public void addAdapter(ListAdapter adapter) {
adapter.registerDataSetObserver(new CascadeDataSetObserver()); pieces.add(adapter);
} adapter.registerDataSetObserver(new CascadeDataSetObserver());
}
/**
* Get the data item associated with the specified position in the data set. /**
* @param position Position of the item whose data we want * Get the data item associated with the specified position in the data set.
*/ *
public Object getItem(int position) { * @param position Position of the item whose data we want
for (ListAdapter piece : pieces) { */
int size = piece.getCount(); public Object getItem(int position) {
for (ListAdapter piece : pieces) {
if (position < size) { int size = piece.getCount();
return (piece.getItem(position));
} if (position < size) {
return (piece.getItem(position));
position -= size; }
}
position -= size;
return (null); }
}
return (null);
public void setNoItemsText(String text) { }
noItemsText = text;
} public void setNoItemsText(String text) {
noItemsText = text;
/** }
* Get the adapter associated with the specified position in the data set.
* @param position Position of the item whose adapter we want /**
*/ * Get the adapter associated with the specified position in the data set.
public ListAdapter getAdapter(int position) { *
for (ListAdapter piece : pieces) { * @param position Position of the item whose adapter we want
int size = piece.getCount(); */
public ListAdapter getAdapter(int position) {
if (position < size) { for (ListAdapter piece : pieces) {
return (piece); int size = piece.getCount();
}
if (position < size) {
position -= size; return (piece);
} }
return (null); position -= size;
} }
/** return (null);
* How many items are in the data set represented by this {@link Adapter}. }
*/
public int getCount() { /**
int total = 0; * How many items are in the data set represented by this {@link Adapter}.
*/
for (ListAdapter piece : pieces) { public int getCount() {
total += piece.getCount(); int total = 0;
}
for (ListAdapter piece : pieces) {
if (total == 0 && noItemsText != null) { total += piece.getCount();
total = 1; }
}
if (total == 0 && noItemsText != null) {
return (total); total = 1;
} }
/** return (total);
* Returns the number of types of {@link View}s that will be created by {@link #getView(int, View, ViewGroup)}. }
*/
@Override /**
public int getViewTypeCount() { * Returns the number of types of {@link View}s that will be created by {@link #getView(int, View, ViewGroup)}.
int total = 0; */
@Override
for (ListAdapter piece : pieces) { public int getViewTypeCount() {
total += piece.getViewTypeCount(); int total = 0;
}
for (ListAdapter piece : pieces) {
return (Math.max(total, 1)); // needed for setListAdapter() before total += piece.getViewTypeCount();
// content add' }
}
return (Math.max(total, 1)); // needed for setListAdapter() before
/** // content add'
* Get the type of {@link View} that will be created by {@link #getView(int, View, ViewGroup)} for the specified item. }
* @param position Position of the item whose data we want
*/ /**
@Override * Get the type of {@link View} that will be created by {@link #getView(int, View, ViewGroup)} for the specified item.
public int getItemViewType(int position) { *
int typeOffset = 0; * @param position Position of the item whose data we want
int result = -1; */
@Override
for (ListAdapter piece : pieces) { public int getItemViewType(int position) {
int size = piece.getCount(); int typeOffset = 0;
int result = -1;
if (position < size) {
result = typeOffset + piece.getItemViewType(position); for (ListAdapter piece : pieces) {
break; int size = piece.getCount();
}
if (position < size) {
position -= size; result = typeOffset + piece.getItemViewType(position);
typeOffset += piece.getViewTypeCount(); break;
} }
return (result); position -= size;
} typeOffset += piece.getViewTypeCount();
}
/**
* Are all items in this {@link ListAdapter} enabled? If yes it means all items are selectable and clickable. return (result);
*/ }
@Override
public boolean areAllItemsEnabled() { /**
return (false); * Are all items in this {@link ListAdapter} enabled? If yes it means all items are selectable and clickable.
} */
@Override
/** public boolean areAllItemsEnabled() {
* Returns true if the item at the specified position is not a separator. return (false);
* @param position Position of the item whose data we want }
*/
@Override /**
public boolean isEnabled(int position) { * Returns true if the item at the specified position is not a separator.
for (ListAdapter piece : pieces) { *
int size = piece.getCount(); * @param position Position of the item whose data we want
*/
if (position < size) { @Override
return (piece.isEnabled(position)); public boolean isEnabled(int position) {
} for (ListAdapter piece : pieces) {
int size = piece.getCount();
position -= size;
} if (position < size) {
return (piece.isEnabled(position));
return (false); }
}
position -= size;
/** }
* Get a {@link View} that displays the data at the specified position in the data set.
* @param position Position of the item whose data we want return (false);
* @param convertView View to recycle, if not null }
* @param parent ViewGroup containing the returned View
*/ /**
public View getView(int position, View convertView, ViewGroup parent) { * Get a {@link View} that displays the data at the specified position in the data set.
for (ListAdapter piece : pieces) { *
int size = piece.getCount(); * @param position Position of the item whose data we want
* @param convertView View to recycle, if not null
if (position < size) { * @param parent ViewGroup containing the returned View
*/
return (piece.getView(position, convertView, parent)); public View getView(int position, View convertView, ViewGroup parent) {
} for (ListAdapter piece : pieces) {
int size = piece.getCount();
position -= size;
} if (position < size) {
if (noItemsText != null) { return (piece.getView(position, convertView, parent));
TextView text = new TextView(parent.getContext()); }
text.setText(noItemsText);
return text; position -= size;
} }
return (null); if (noItemsText != null) {
} TextView text = new TextView(parent.getContext());
text.setText(noItemsText);
/** return text;
* Get the row id associated with the specified position in the list. }
* @param position Position of the item whose data we want
*/ return (null);
public long getItemId(int position) { }
for (ListAdapter piece : pieces) {
int size = piece.getCount(); /**
* Get the row id associated with the specified position in the list.
if (position < size) { *
return (piece.getItemId(position)); * @param position Position of the item whose data we want
} */
public long getItemId(int position) {
position -= size; for (ListAdapter piece : pieces) {
} int size = piece.getCount();
return (-1); if (position < size) {
} return (piece.getItemId(position));
}
public final int getPositionForSection(int section) {
int position = 0; position -= size;
}
for (ListAdapter piece : pieces) {
if (piece instanceof SectionIndexer) { return (-1);
Object[] sections = ((SectionIndexer) piece).getSections(); }
int numSections = 0;
public final int getPositionForSection(int section) {
if (sections != null) { int position = 0;
numSections = sections.length;
} for (ListAdapter piece : pieces) {
if (piece instanceof SectionIndexer) {
if (section < numSections) { Object[] sections = ((SectionIndexer) piece).getSections();
return (position + ((SectionIndexer) piece).getPositionForSection(section)); int numSections = 0;
} else if (sections != null) {
section -= numSections; if (sections != null) {
} numSections = sections.length;
} }
position += piece.getCount(); if (section < numSections) {
} return (position + ((SectionIndexer) piece).getPositionForSection(section));
} else if (sections != null) {
return (0); section -= numSections;
} }
}
public final int getSectionForPosition(int position) {
int section = 0; position += piece.getCount();
}
for (ListAdapter piece : pieces) {
int size = piece.getCount(); return (0);
}
if (position < size) {
if (piece instanceof SectionIndexer) { public final int getSectionForPosition(int position) {
return (section + ((SectionIndexer) piece).getSectionForPosition(position)); int section = 0;
}
for (ListAdapter piece : pieces) {
return (0); int size = piece.getCount();
} else {
if (piece instanceof SectionIndexer) { if (position < size) {
Object[] sections = ((SectionIndexer) piece).getSections(); if (piece instanceof SectionIndexer) {
return (section + ((SectionIndexer) piece).getSectionForPosition(position));
if (sections != null) { }
section += sections.length;
} return (0);
} } else {
} if (piece instanceof SectionIndexer) {
Object[] sections = ((SectionIndexer) piece).getSections();
position -= size;
} if (sections != null) {
section += sections.length;
return (0); }
} }
}
public final Object[] getSections() {
ArrayList<Object> sections = new ArrayList<Object>(); position -= size;
}
for (ListAdapter piece : pieces) {
if (piece instanceof SectionIndexer) { return (0);
Object[] curSections = ((SectionIndexer) piece).getSections(); }
if (curSections != null) { public final Object[] getSections() {
for (Object section : curSections) { ArrayList<Object> sections = new ArrayList<>();
sections.add(section);
} for (ListAdapter piece : pieces) {
} if (piece instanceof SectionIndexer) {
} Object[] curSections = ((SectionIndexer) piece).getSections();
}
if (curSections != null) {
if (sections.size() == 0) { sections.addAll(Arrays.asList(curSections));
return (null); }
} }
}
return (sections.toArray(new Object[0]));
} if (sections.size() == 0) {
return (null);
private class CascadeDataSetObserver extends DataSetObserver { }
@Override
public void onChanged() { return (sections.toArray(new Object[0]));
notifyDataSetChanged(); }
}
private class CascadeDataSetObserver extends DataSetObserver {
@Override @Override
public void onInvalidated() { public void onChanged() {
notifyDataSetInvalidated(); notifyDataSetChanged();
} }
}
@Override
public void onInvalidated() {
notifyDataSetInvalidated();
}
}
} }

36
app/src/main/java/org/transdroid/core/gui/lists/PiecesMapView.java

@ -1,27 +1,24 @@
package org.transdroid.core.gui.lists; package org.transdroid.core.gui.lists;
import org.transdroid.R;
import android.content.Context; import android.content.Context;
import android.view.View;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
import android.view.View;
import org.transdroid.R;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.lang.Math;
class PiecesMapView extends View { class PiecesMapView extends View {
private final float scale = getContext().getResources().getDisplayMetrics().density; private final float scale = getContext().getResources().getDisplayMetrics().density;
private final int MINIMUM_HEIGHT = (int) (25 * scale); private final int MINIMUM_HEIGHT = (int) (25 * scale);
private final int MINIMUM_PIECE_WIDTH = (int) (2 * scale); private final int MINIMUM_PIECE_WIDTH = (int) (2 * scale);
private ArrayList<Integer> pieces = null;
private final Paint downloadingPaint = new Paint(); private final Paint downloadingPaint = new Paint();
private final Paint donePaint = new Paint(); private final Paint donePaint = new Paint();
private final Paint partialDonePaint = new Paint(); private final Paint partialDonePaint = new Paint();
private ArrayList<Integer> pieces = null;
public PiecesMapView(Context context) { public PiecesMapView(Context context) {
super(context); super(context);
@ -35,15 +32,15 @@ class PiecesMapView extends View {
} }
public void setPieces(List<Integer> pieces) { public void setPieces(List<Integer> pieces) {
this.pieces = new ArrayList<Integer>(pieces); this.pieces = new ArrayList<>(pieces);
invalidate(); invalidate();
} }
@Override @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int ws = MeasureSpec.getSize(widthMeasureSpec); int ws = MeasureSpec.getSize(widthMeasureSpec);
int hs = Math.max(getHeight(), MINIMUM_HEIGHT); int hs = Math.max(getHeight(), MINIMUM_HEIGHT);
setMeasuredDimension(ws, hs); setMeasuredDimension(ws, hs);
} }
@Override @Override
@ -62,10 +59,10 @@ class PiecesMapView extends View {
int pieceWidth; int pieceWidth;
pieceWidth = MINIMUM_PIECE_WIDTH; pieceWidth = MINIMUM_PIECE_WIDTH;
piecesScaled = new ArrayList<Integer>(); piecesScaled = new ArrayList<>();
int bucketCount = (int) Math.ceil((double) width / (double) pieceWidth); int bucketCount = (int) Math.ceil((double) width / (double) pieceWidth);
int bucketSize = (int) Math.floor((double)this.pieces.size() / (double) bucketCount); int bucketSize = (int) Math.floor((double) this.pieces.size() / (double) bucketCount);
// loop buckets // loop buckets
for (int i = 0; i < bucketCount; i++) { for (int i = 0; i < bucketCount; i++) {
@ -74,15 +71,15 @@ class PiecesMapView extends View {
int start = i * bucketSize; int start = i * bucketSize;
// If this is the last bucket, throw the remainder of the pieces array into it // If this is the last bucket, throw the remainder of the pieces array into it
int end = (i == bucketCount-1) ? this.pieces.size() : (i+1) * bucketSize; int end = (i == bucketCount - 1) ? this.pieces.size() : (i + 1) * bucketSize;
ArrayList<Integer> bucket = new ArrayList<Integer>(this.pieces.subList(start, end)); ArrayList<Integer> bucket = new ArrayList<>(this.pieces.subList(start, end));
int doneCount = 0; int doneCount = 0;
int downloadingCount = 0; int downloadingCount = 0;
// loop pieces in bucket // loop pieces in bucket
for(int j = 0; j < bucket.size(); j++) { for (int j = 0; j < bucket.size(); j++) {
// Count downloading pieces // Count downloading pieces
if (bucket.get(j) == 1) { if (bucket.get(j) == 1) {
downloadingCount++; downloadingCount++;
@ -114,10 +111,9 @@ class PiecesMapView extends View {
piecesScaled.add(state); piecesScaled.add(state);
} }
String scaledPiecesString = ""; StringBuilder scaledPiecesString = new StringBuilder();
for (int s : piecesScaled) for (int s : piecesScaled) {
{ scaledPiecesString.append(s);
scaledPiecesString += s;
} }
// Draw downscaled peices // Draw downscaled peices

3
app/src/main/java/org/transdroid/core/gui/lists/SimpleListItem.java

@ -19,10 +19,11 @@ package org.transdroid.core.gui.lists;
/** /**
* Represents a filter item as shown in the navigation list or spinner. * Represents a filter item as shown in the navigation list or spinner.
*
* @author Eric Kok * @author Eric Kok
*/ */
public interface SimpleListItem { public interface SimpleListItem {
public String getName(); String getName();
} }

177
app/src/main/java/org/transdroid/core/gui/lists/SimpleListItemAdapter.java

@ -16,99 +16,102 @@
*/ */
package org.transdroid.core.gui.lists; package org.transdroid.core.gui.lists;
import java.util.ArrayList;
import java.util.List;
import android.content.Context; import android.content.Context;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.BaseAdapter; import android.widget.BaseAdapter;
import java.util.ArrayList;
import java.util.List;
public class SimpleListItemAdapter extends BaseAdapter { public class SimpleListItemAdapter extends BaseAdapter {
private final Context context; private final Context context;
private List<? extends SimpleListItem> items; private List<? extends SimpleListItem> items;
private int autoLinkMask = 0; private int autoLinkMask = 0;
public SimpleListItemAdapter(Context context, List<? extends SimpleListItem> items) { public SimpleListItemAdapter(Context context, List<? extends SimpleListItem> items) {
this.context = context; this.context = context;
this.items = items; this.items = items;
} }
/** /**
* Allows updating of the full data list underlying this adapter, replacing all items * Allows updating of the full data list underlying this adapter, replacing all items
* @param newItems The new list of simple list items to display *
*/ * @param newItems The new list of simple list items to display
public void update(List<? extends SimpleListItem> newItems) { */
this.items = newItems; public void update(List<? extends SimpleListItem> newItems) {
notifyDataSetChanged(); this.items = newItems;
} notifyDataSetChanged();
}
public void setAutoLinkMask(int autoLinkMask) {
this.autoLinkMask = autoLinkMask; public void setAutoLinkMask(int autoLinkMask) {
} this.autoLinkMask = autoLinkMask;
}
@Override
public int getCount() { @Override
return items.size(); public int getCount() {
} return items.size();
}
@Override
public SimpleListItem getItem(int position) { @Override
return items.get(position); public SimpleListItem getItem(int position) {
} return items.get(position);
}
@Override
public long getItemId(int position) { @Override
return position; public long getItemId(int position) {
} return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) { @Override
SimpleListItemView filterItemView; public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null || !(convertView instanceof SimpleListItemView)) { SimpleListItemView filterItemView;
filterItemView = SimpleListItemView_.build(context); if (!(convertView instanceof SimpleListItemView)) {
} else { filterItemView = SimpleListItemView_.build(context);
filterItemView = (SimpleListItemView) convertView; } else {
} filterItemView = (SimpleListItemView) convertView;
filterItemView.bind(getItem(position), autoLinkMask); }
return filterItemView; filterItemView.bind(getItem(position), autoLinkMask);
} return filterItemView;
}
/**
* Represents a very simple list item that only contains a single string to show in the list. Use wrapStringsList to /**
* wrap an existing list of strings into a list of {@link SimpleListItem}s. * Represents a very simple list item that only contains a single string to show in the list. Use wrapStringsList to
* @author Eric Kok * wrap an existing list of strings into a list of {@link SimpleListItem}s.
*/ *
public static class SimpleStringItem implements SimpleListItem { * @author Eric Kok
*/
/** public static class SimpleStringItem implements SimpleListItem {
* Wraps a simple string of strings into a list of SimpleStringItem to add as data to a
* {@link SimpleListItemAdapter} private final String string;
* @param strings A list of string
* @return A list of SimpleStringItem objects representing the input strings public SimpleStringItem(String string) {
*/ this.string = string;
public static List<SimpleStringItem> wrapStringsList(List<String> strings) { }
ArrayList<SimpleStringItem> errors = new ArrayList<SimpleStringItem>();
if (strings != null) { /**
for (String string : strings) { * Wraps a simple string of strings into a list of SimpleStringItem to add as data to a
errors.add(new SimpleStringItem(string)); * {@link SimpleListItemAdapter}
} *
} * @param strings A list of string
return errors; * @return A list of SimpleStringItem objects representing the input strings
} */
public static List<SimpleStringItem> wrapStringsList(List<String> strings) {
private final String string; ArrayList<SimpleStringItem> errors = new ArrayList<>();
if (strings != null) {
public SimpleStringItem(String string) { for (String string : strings) {
this.string = string; errors.add(new SimpleStringItem(string));
} }
}
@Override return errors;
public String getName() { }
return this.string;
} @Override
public String getName() {
} return this.string;
}
}
} }

64
app/src/main/java/org/transdroid/core/gui/lists/SimpleListItemSpinnerAdapter.java

@ -16,51 +16,53 @@
*/ */
package org.transdroid.core.gui.lists; package org.transdroid.core.gui.lists;
import java.util.List;
import android.content.Context; import android.content.Context;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.TextView; import android.widget.TextView;
import java.util.List;
/** /**
* A wrapper around {@link ArrayAdapter} that contains {@link SimpleListItem}s which simply show their name in the * A wrapper around {@link ArrayAdapter} that contains {@link SimpleListItem}s which simply show their name in the
* Spinner. The standard Android spinner resources are used for the layout. * Spinner. The standard Android spinner resources are used for the layout.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class SimpleListItemSpinnerAdapter<T extends SimpleListItem> extends ArrayAdapter<T> { public class SimpleListItemSpinnerAdapter<T extends SimpleListItem> extends ArrayAdapter<T> {
/** /**
* Constructs the adapter, supplying the {@link SimpleListItem}s to show in the spinner. The given resource will be * Constructs the adapter, supplying the {@link SimpleListItem}s to show in the spinner. The given resource will be
* ignored as the standard Android Spinner layout is used instead. * ignored as the standard Android Spinner layout is used instead.
* @param context The UI context to inflate the layout in *
* @param resource This is ignored; android.R.layout.simple_spinner_item is always used instead * @param context The UI context to inflate the layout in
* @param objects The items to show in the spinner, which can simply display some name * @param resource This is ignored; android.R.layout.simple_spinner_item is always used instead
*/ * @param objects The items to show in the spinner, which can simply display some name
public SimpleListItemSpinnerAdapter(Context context, int resource, List<T> objects) { */
super(context, android.R.layout.simple_spinner_item, objects); public SimpleListItemSpinnerAdapter(Context context, int resource, List<T> objects) {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); super(context, android.R.layout.simple_spinner_item, objects);
} setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
}
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
// This relies on the ArrayAdapter implementation and the used standard xml layouts that simply return a // This relies on the ArrayAdapter implementation and the used standard xml layouts that simply return a
// TextView; this can then be filled with the SimpleListItem's name instead of the standard toString() // TextView; this can then be filled with the SimpleListItem's name instead of the standard toString()
// implementation // implementation
TextView text = (TextView) super.getView(position, convertView, parent); TextView text = (TextView) super.getView(position, convertView, parent);
text.setText(getItem(position).getName()); text.setText(getItem(position).getName());
return text; return text;
} }
@Override @Override
public View getDropDownView(int position, View convertView, ViewGroup parent) { public View getDropDownView(int position, View convertView, ViewGroup parent) {
// This relies on the ArrayAdapter implementation and the used standard xml layouts that simply return a // This relies on the ArrayAdapter implementation and the used standard xml layouts that simply return a
// TextView; this can then be filled with the SimpleListItem's name instead of the standard toString() // TextView; this can then be filled with the SimpleListItem's name instead of the standard toString()
// implementation // implementation
TextView text = (TextView) super.getDropDownView(position, convertView, parent); TextView text = (TextView) super.getDropDownView(position, convertView, parent);
text.setText(getItem(position).getName()); text.setText(getItem(position).getName());
return text; return text;
} }
} }

23
app/src/main/java/org/transdroid/core/gui/lists/SimpleListItemView.java

@ -26,23 +26,24 @@ import org.transdroid.R;
/** /**
* View that represents some {@link SimpleListItem} object and simple prints out the text (in proper style) * View that represents some {@link SimpleListItem} object and simple prints out the text (in proper style)
*
* @author Eric Kok * @author Eric Kok
*/ */
@EViewGroup(R.layout.list_item_simple) @EViewGroup(R.layout.list_item_simple)
public class SimpleListItemView extends FrameLayout { public class SimpleListItemView extends FrameLayout {
@ViewById @ViewById
protected TextView itemText; protected TextView itemText;
public SimpleListItemView(Context context) { public SimpleListItemView(Context context) {
super(context); super(context);
} }
public void bind(SimpleListItem filterItem, int autoLinkMask) { public void bind(SimpleListItem filterItem, int autoLinkMask) {
itemText.setText(filterItem.getName()); itemText.setText(filterItem.getName());
if (autoLinkMask > 0) { if (autoLinkMask > 0) {
itemText.setAutoLinkMask(autoLinkMask); itemText.setAutoLinkMask(autoLinkMask);
} }
} }
} }

93
app/src/main/java/org/transdroid/core/gui/lists/SortByListItem.java

@ -17,62 +17,65 @@
package org.transdroid.core.gui.lists; package org.transdroid.core.gui.lists;
import android.content.Context; import android.content.Context;
import org.transdroid.R; import org.transdroid.R;
import org.transdroid.daemon.TorrentsSortBy; import org.transdroid.daemon.TorrentsSortBy;
/** /**
* Represents a way in which a torrents list can be sorted. * Represents a way in which a torrents list can be sorted.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class SortByListItem implements SimpleListItem { public class SortByListItem implements SimpleListItem {
private final TorrentsSortBy sortBy; private final TorrentsSortBy sortBy;
private final String name; private final String name;
public SortByListItem(Context context, TorrentsSortBy sortBy) { public SortByListItem(Context context, TorrentsSortBy sortBy) {
this.sortBy = sortBy; this.sortBy = sortBy;
switch (sortBy) { switch (sortBy) {
case DateAdded: case DateAdded:
this.name = context.getString(R.string.action_sort_added); this.name = context.getString(R.string.action_sort_added);
break; break;
case DateDone: case DateDone:
this.name = context.getString(R.string.action_sort_done); this.name = context.getString(R.string.action_sort_done);
break; break;
case Ratio: case Ratio:
this.name = context.getString(R.string.action_sort_ratio); this.name = context.getString(R.string.action_sort_ratio);
break; break;
case Status: case Status:
this.name = context.getString(R.string.action_sort_status); this.name = context.getString(R.string.action_sort_status);
break; break;
case UploadSpeed: case UploadSpeed:
this.name = context.getString(R.string.action_sort_upspeed); this.name = context.getString(R.string.action_sort_upspeed);
break; break;
case DownloadSpeed: case DownloadSpeed:
this.name = context.getString(R.string.action_sort_downspeed); this.name = context.getString(R.string.action_sort_downspeed);
break; break;
case Percent: case Percent:
this.name = context.getString(R.string.action_sort_percent); this.name = context.getString(R.string.action_sort_percent);
break; break;
case Size: case Size:
this.name = context.getString(R.string.action_sort_size); this.name = context.getString(R.string.action_sort_size);
break; break;
default: default:
this.name = context.getString(R.string.action_sort_alpha); this.name = context.getString(R.string.action_sort_alpha);
break; break;
} }
} }
/** /**
* Returns the contained represented sort order. * Returns the contained represented sort order.
* @return The sort by order as its enumeration value *
*/ * @return The sort by order as its enumeration value
public TorrentsSortBy getSortBy() { */
return sortBy; public TorrentsSortBy getSortBy() {
} return sortBy;
}
@Override @Override
public String getName() { public String getName() {
return name; return name;
} }
} }

110
app/src/main/java/org/transdroid/core/gui/lists/TorrentDetailsView.java

@ -32,73 +32,75 @@ import org.transdroid.daemon.util.FileSizeConverter;
/** /**
* Represents a group of views that show torrent status, sizes, speeds and other details. * Represents a group of views that show torrent status, sizes, speeds and other details.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EViewGroup(R.layout.fragment_details_header) @EViewGroup(R.layout.fragment_details_header)
public class TorrentDetailsView extends RelativeLayout { public class TorrentDetailsView extends RelativeLayout {
@ViewById @ViewById
protected TextView labelText, dateaddedText, uploadedText, uploadedunitText, ratioText, upspeedText, seedersText, downloadedunitText, protected TextView labelText, dateaddedText, uploadedText, uploadedunitText, ratioText, upspeedText, seedersText, downloadedunitText,
downloadedText, totalsizeText, downspeedText, leechersText, statusText; downloadedText, totalsizeText, downspeedText, leechersText, statusText;
@ViewById @ViewById
protected TorrentStatusLayout statusLayout; protected TorrentStatusLayout statusLayout;
public TorrentDetailsView(Context context) { public TorrentDetailsView(Context context) {
super(context); super(context);
} }
/** /**
* Update the text fields with new/updated torrent details * Update the text fields with new/updated torrent details
* @param torrent The torrent for which to show details *
*/ * @param torrent The torrent for which to show details
public void update(Torrent torrent) { */
public void update(Torrent torrent) {
if (torrent == null) { if (torrent == null) {
return; return;
} }
LocalTorrent local = LocalTorrent.fromTorrent(torrent); LocalTorrent local = LocalTorrent.fromTorrent(torrent);
// Set label text // Set label text
if (Daemon.supportsLabels(torrent.getDaemon())) { if (Daemon.supportsLabels(torrent.getDaemon())) {
if (TextUtils.isEmpty(torrent.getLabelName())) { if (TextUtils.isEmpty(torrent.getLabelName())) {
labelText.setText(getResources().getString(R.string.labels_unlabeled)); labelText.setText(getResources().getString(R.string.labels_unlabeled));
} else { } else {
labelText.setText(torrent.getLabelName()); labelText.setText(torrent.getLabelName());
} }
labelText.setVisibility(View.VISIBLE); labelText.setVisibility(View.VISIBLE);
} else { } else {
labelText.setVisibility(View.INVISIBLE); labelText.setVisibility(View.INVISIBLE);
} }
// Set status texts // Set status texts
if (torrent.getDateAdded() != null) { if (torrent.getDateAdded() != null) {
dateaddedText.setText(getResources().getString(R.string.status_sincedate, DateUtils dateaddedText.setText(getResources().getString(R.string.status_sincedate, DateUtils
.getRelativeDateTimeString(getContext(), torrent.getDateAdded().getTime(), DateUtils.SECOND_IN_MILLIS, .getRelativeDateTimeString(getContext(), torrent.getDateAdded().getTime(), DateUtils.SECOND_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_ABBREV_MONTH))); DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_ABBREV_MONTH)));
dateaddedText.setVisibility(View.VISIBLE); dateaddedText.setVisibility(View.VISIBLE);
} else { } else {
dateaddedText.setVisibility(View.INVISIBLE); dateaddedText.setVisibility(View.INVISIBLE);
} }
statusLayout.setStatus(torrent.getStatusCode()); statusLayout.setStatus(torrent.getStatusCode());
statusText.setText(getResources().getString(R.string.status_status, local.getProgressStatusEta(getResources()))); statusText.setText(getResources().getString(R.string.status_status, local.getProgressStatusEta(getResources())));
ratioText.setText(getResources().getString(R.string.status_ratio, local.getRatioString())); ratioText.setText(getResources().getString(R.string.status_ratio, local.getRatioString()));
seedersText.setText(getResources().getString(R.string.status_seeders, torrent.getSeedersConnected(), torrent.getSeedersKnown())); seedersText.setText(getResources().getString(R.string.status_seeders, torrent.getSeedersConnected(), torrent.getSeedersKnown()));
leechersText.setText(getResources().getString(R.string.status_leechers, torrent.getLeechersConnected(), torrent.getLeechersKnown())); leechersText.setText(getResources().getString(R.string.status_leechers, torrent.getLeechersConnected(), torrent.getLeechersKnown()));
// TODO: Add field that displays torrent errors (as opposed to tracker errors) // TODO: Add field that displays torrent errors (as opposed to tracker errors)
// TODO: Add field that displays availability // TODO: Add field that displays availability
// Sizes and speeds texts // Sizes and speeds texts
totalsizeText.setText(getResources().getString(R.string.status_ofsize, FileSizeConverter.getSize(torrent.getTotalSize()))); totalsizeText.setText(getResources().getString(R.string.status_ofsize, FileSizeConverter.getSize(torrent.getTotalSize())));
downloadedText.setText(FileSizeConverter.getSize(torrent.getDownloadedEver(), false)); downloadedText.setText(FileSizeConverter.getSize(torrent.getDownloadedEver(), false));
downloadedunitText.setText(FileSizeConverter.getSizeUnit(torrent.getDownloadedEver()).toString()); downloadedunitText.setText(FileSizeConverter.getSizeUnit(torrent.getDownloadedEver()).toString());
uploadedText.setText(FileSizeConverter.getSize(torrent.getUploadedEver(), false)); uploadedText.setText(FileSizeConverter.getSize(torrent.getUploadedEver(), false));
uploadedunitText.setText(FileSizeConverter.getSizeUnit(torrent.getUploadedEver()).toString()); uploadedunitText.setText(FileSizeConverter.getSizeUnit(torrent.getUploadedEver()).toString());
downspeedText downspeedText
.setText(getResources().getString(R.string.status_speed_down_details, FileSizeConverter.getSize(torrent.getRateDownload()) + "/s")); .setText(getResources().getString(R.string.status_speed_down_details, FileSizeConverter.getSize(torrent.getRateDownload()) + "/s"));
upspeedText.setText(getResources().getString(R.string.status_speed_up, FileSizeConverter.getSize(torrent.getRateUpload()) + "/s")); upspeedText.setText(getResources().getString(R.string.status_speed_up, FileSizeConverter.getSize(torrent.getRateUpload()) + "/s"));
} }
} }

110
app/src/main/java/org/transdroid/core/gui/lists/TorrentFilePriorityLayout.java

@ -16,9 +16,6 @@
*/ */
package org.transdroid.core.gui.lists; package org.transdroid.core.gui.lists;
import org.transdroid.R;
import org.transdroid.daemon.Priority;
import android.content.Context; import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
@ -26,75 +23,76 @@ import android.graphics.RectF;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import org.transdroid.R;
import org.transdroid.daemon.Priority;
/** /**
* A relative layout that that is checkable (to be used in a contextual action bar) and shows a coloured bar in the far * A relative layout that that is checkable (to be used in a contextual action bar) and shows a coloured bar in the far
* left indicating the priority of the represented file. The darker the green, the higher the priority, while grey means * left indicating the priority of the represented file. The darker the green, the higher the priority, while grey means
* the file isn't downloaded at all. * the file isn't downloaded at all.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class TorrentFilePriorityLayout extends RelativeLayout { public class TorrentFilePriorityLayout extends RelativeLayout {
private final float scale = getContext().getResources().getDisplayMetrics().density; private final float scale = getContext().getResources().getDisplayMetrics().density;
private final int WIDTH = (int) (6 * scale + 0.5f); private final int WIDTH = (int) (6 * scale + 0.5f);
private final Paint offPaint = new Paint();
private Priority priority = null; private final Paint lowPaint = new Paint();
private final Paint offPaint = new Paint(); private final Paint highPaint = new Paint();
private final Paint lowPaint = new Paint(); private final Paint normalPaint = new Paint();
private final Paint highPaint = new Paint(); private final RectF fullRect = new RectF();
private final Paint normalPaint = new Paint(); private Priority priority = null;
private final RectF fullRect = new RectF();
public TorrentFilePriorityLayout(Context context) { public TorrentFilePriorityLayout(Context context) {
super(context); super(context);
initPaints(); initPaints();
setWillNotDraw(false); setWillNotDraw(false);
} }
public TorrentFilePriorityLayout(Context context, AttributeSet attrs) { public TorrentFilePriorityLayout(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
initPaints(); initPaints();
setWillNotDraw(false); setWillNotDraw(false);
} }
private void initPaints() { private void initPaints() {
offPaint.setColor(getResources().getColor(R.color.file_off)); offPaint.setColor(getResources().getColor(R.color.file_off));
lowPaint.setColor(getResources().getColor(R.color.file_low)); lowPaint.setColor(getResources().getColor(R.color.file_low));
normalPaint.setColor(getResources().getColor(R.color.file_normal)); normalPaint.setColor(getResources().getColor(R.color.file_normal));
highPaint.setColor(getResources().getColor(R.color.file_high)); highPaint.setColor(getResources().getColor(R.color.file_high));
} }
public void setPriority(Priority priority) { public void setPriority(Priority priority) {
this.priority = priority; this.priority = priority;
this.invalidate(); this.invalidate();
} }
@Override @Override
protected void onDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
super.onDraw(canvas); super.onDraw(canvas);
int height = getHeight(); fullRect.set(0, 0, WIDTH, getHeight());
int width = WIDTH;
fullRect.set(0, 0, width, height);
if (priority == null) { if (priority == null) {
return; return;
} }
switch (priority) { switch (priority) {
case Low: case Low:
canvas.drawRect(fullRect, lowPaint); canvas.drawRect(fullRect, lowPaint);
break; break;
case Normal: case Normal:
canvas.drawRect(fullRect, normalPaint); canvas.drawRect(fullRect, normalPaint);
break; break;
case High: case High:
canvas.drawRect(fullRect, highPaint); canvas.drawRect(fullRect, highPaint);
break; break;
default: // Off default: // Off
canvas.drawRect(fullRect, offPaint); canvas.drawRect(fullRect, offPaint);
break; break;
} }
} }
} }

23
app/src/main/java/org/transdroid/core/gui/lists/TorrentFileView.java

@ -26,23 +26,24 @@ import org.transdroid.daemon.TorrentFile;
/** /**
* View that represents some {@link TorrentFile} object and show the file's name, status and priority * View that represents some {@link TorrentFile} object and show the file's name, status and priority
*
* @author Eric Kok * @author Eric Kok
*/ */
@EViewGroup(R.layout.list_item_torrentfile) @EViewGroup(R.layout.list_item_torrentfile)
public class TorrentFileView extends TorrentFilePriorityLayout { public class TorrentFileView extends TorrentFilePriorityLayout {
@ViewById @ViewById
protected TextView nameText, progressText, sizesText; protected TextView nameText, progressText, sizesText;
public TorrentFileView(Context context) { public TorrentFileView(Context context) {
super(context, null); super(context, null);
} }
public void bind(TorrentFile torrentFile) { public void bind(TorrentFile torrentFile) {
nameText.setText(torrentFile.getName()); nameText.setText(torrentFile.getName());
sizesText.setText(torrentFile.getDownloadedAndTotalSizeText()); sizesText.setText(torrentFile.getDownloadedAndTotalSizeText());
progressText.setText(torrentFile.getProgressText()); progressText.setText(torrentFile.getProgressText());
setPriority(torrentFile.getPriority()); setPriority(torrentFile.getPriority());
} }
} }

179
app/src/main/java/org/transdroid/core/gui/lists/TorrentProgressBar.java

@ -16,8 +16,6 @@
*/ */
package org.transdroid.core.gui.lists; package org.transdroid.core.gui.lists;
import org.transdroid.R;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Canvas; import android.graphics.Canvas;
@ -26,6 +24,8 @@ import android.graphics.RectF;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import org.transdroid.R;
/** /**
* Draws a progress bar indicating the download progress as well as the torrent status. * Draws a progress bar indicating the download progress as well as the torrent status.
* *
@ -33,93 +33,92 @@ import android.view.View;
*/ */
public class TorrentProgressBar extends View { public class TorrentProgressBar extends View {
private final float scale = getContext().getResources().getDisplayMetrics().density; private final float scale = getContext().getResources().getDisplayMetrics().density;
private final int MINIMUM_HEIGHT = (int) (3 * scale + 0.5f); private final int MINIMUM_HEIGHT = (int) (3 * scale + 0.5f);
private final Paint notdonePaint = new Paint();
private int progress; private final Paint inactiveDonePaint = new Paint();
private boolean isActive; private final Paint inactivePaint = new Paint();
private boolean isError; private final Paint progressPaint = new Paint();
private final Paint notdonePaint = new Paint(); private final Paint donePaint = new Paint();
private final Paint inactiveDonePaint = new Paint(); private final Paint errorPaint = new Paint();
private final Paint inactivePaint = new Paint(); private final RectF fullRect = new RectF();
private final Paint progressPaint = new Paint(); private final RectF progressRect = new RectF();
private final Paint donePaint = new Paint(); private int progress;
private final Paint errorPaint = new Paint(); private boolean isActive;
private final RectF fullRect = new RectF(); private boolean isError;
private final RectF progressRect = new RectF();
public TorrentProgressBar(Context context) {
public void setProgress(int progress) { super(context);
this.progress = progress; initPaints();
this.invalidate(); }
}
public TorrentProgressBar(Context context, AttributeSet attrs) {
public void setActive(boolean isActive) { super(context, attrs);
this.isActive = isActive; initPaints();
this.invalidate();
} // Parse any set attributes from XML
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TorrentProgressBar);
public void setError(boolean isError) { if (a.hasValue(R.styleable.TorrentProgressBar_progress)) {
this.isError = isError; this.progress = a.getIndex(R.styleable.TorrentProgressBar_progress);
this.invalidate(); this.isActive = a.getBoolean(R.styleable.TorrentProgressBar_isActive, false);
} }
a.recycle();
public TorrentProgressBar(Context context) { }
super(context);
initPaints(); public void setProgress(int progress) {
} this.progress = progress;
this.invalidate();
public TorrentProgressBar(Context context, AttributeSet attrs) { }
super(context, attrs);
initPaints(); public void setActive(boolean isActive) {
this.isActive = isActive;
// Parse any set attributes from XML this.invalidate();
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TorrentProgressBar); }
if (a.hasValue(R.styleable.TorrentProgressBar_progress)) {
this.progress = a.getIndex(R.styleable.TorrentProgressBar_progress); public void setError(boolean isError) {
this.isActive = a.getBoolean(R.styleable.TorrentProgressBar_isActive, false); this.isError = isError;
} this.invalidate();
a.recycle(); }
}
private void initPaints() {
private void initPaints() { notdonePaint.setColor(getResources().getColor(R.color.torrent_background));
notdonePaint.setColor(getResources().getColor(R.color.torrent_background)); inactiveDonePaint.setColor(getResources().getColor(R.color.torrent_paused));
inactiveDonePaint.setColor(getResources().getColor(R.color.torrent_paused)); inactivePaint.setColor(getResources().getColor(R.color.torrent_other));
inactivePaint.setColor(getResources().getColor(R.color.torrent_other)); progressPaint.setColor(getResources().getColor(R.color.torrent_downloading));
progressPaint.setColor(getResources().getColor(R.color.torrent_downloading)); donePaint.setColor(getResources().getColor(R.color.torrent_seeding));
donePaint.setColor(getResources().getColor(R.color.torrent_seeding)); errorPaint.setColor(getResources().getColor(R.color.torrent_error));
errorPaint.setColor(getResources().getColor(R.color.torrent_error)); }
}
@Override
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int ws = MeasureSpec.getSize(widthMeasureSpec);
int ws = MeasureSpec.getSize(widthMeasureSpec); int hs = Math.max(getHeight(), MINIMUM_HEIGHT);
int hs = Math.max(getHeight(), MINIMUM_HEIGHT); setMeasuredDimension(ws, hs);
setMeasuredDimension(ws, hs); }
}
@Override
@Override protected void onDraw(Canvas canvas) {
protected void onDraw(Canvas canvas) { super.onDraw(canvas);
super.onDraw(canvas);
int height = getHeight();
int height = getHeight(); int width = getWidth();
int width = getWidth(); fullRect.set(0, 0, width, height);
fullRect.set(0, 0, width, height);
// Error?
// Error? if (isError) {
if (isError) { canvas.drawRect(fullRect, errorPaint);
canvas.drawRect(fullRect, errorPaint); } else {
} else { // Background rounded rectangle
// Background rounded rectangle canvas.drawRect(fullRect, notdonePaint);
canvas.drawRect(fullRect, notdonePaint);
// Foreground progress indicator
// Foreground progress indicator if (progress > 0) {
if (progress > 0) { progressRect.set(0, 0, width * ((float) progress / 100), height);
progressRect.set(0, 0, width * ((float) progress / 100), height); canvas.drawRect(progressRect, (isActive ? (progress == 100 ? donePaint : progressPaint)
canvas.drawRect(progressRect, (isActive ? (progress == 100 ? donePaint : progressPaint) : (progress == 100 ? inactiveDonePaint : inactivePaint)));
: (progress == 100 ? inactiveDonePaint : inactivePaint))); }
} }
}
}
}
} }

131
app/src/main/java/org/transdroid/core/gui/lists/TorrentStatusLayout.java

@ -16,9 +16,6 @@
*/ */
package org.transdroid.core.gui.lists; package org.transdroid.core.gui.lists;
import org.transdroid.R;
import org.transdroid.daemon.TorrentStatus;
import android.content.Context; import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
@ -26,85 +23,87 @@ import android.graphics.RectF;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import org.transdroid.R;
import org.transdroid.daemon.TorrentStatus;
/** /**
* A relative layout that is checkable (to be used in a contextual action bar) and shows a coloured bar in the far left * A relative layout that is checkable (to be used in a contextual action bar) and shows a coloured bar in the far left
* indicating the status of the represented torrent. Active downloads are blue, seeding torrents are green, errors are * indicating the status of the represented torrent. Active downloads are blue, seeding torrents are green, errors are
* red, etc. * red, etc.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class TorrentStatusLayout extends RelativeLayout { public class TorrentStatusLayout extends RelativeLayout {
private final float scale = getContext().getResources().getDisplayMetrics().density; private final float scale = getContext().getResources().getDisplayMetrics().density;
private final int WIDTH = (int) (6 * scale + 0.5f); private final int WIDTH = (int) (6 * scale + 0.5f);
private final Paint pausedPaint = new Paint();
private TorrentStatus status = null; private final Paint otherPaint = new Paint();
private final Paint pausedPaint = new Paint(); private final Paint downloadingPaint = new Paint();
private final Paint otherPaint = new Paint(); private final Paint seedingPaint = new Paint();
private final Paint downloadingPaint = new Paint(); private final Paint errorPaint = new Paint();
private final Paint seedingPaint = new Paint(); private final RectF fullRect = new RectF();
private final Paint errorPaint = new Paint(); private TorrentStatus status = null;
private final RectF fullRect = new RectF();
public TorrentStatusLayout(Context context) { public TorrentStatusLayout(Context context) {
super(context); super(context);
initPaints(); initPaints();
setWillNotDraw(false); setWillNotDraw(false);
} }
public TorrentStatusLayout(Context context, AttributeSet attrs) { public TorrentStatusLayout(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
initPaints(); initPaints();
setWillNotDraw(false); setWillNotDraw(false);
} }
private void initPaints() { private void initPaints() {
pausedPaint.setColor(getResources().getColor(R.color.torrent_paused)); pausedPaint.setColor(getResources().getColor(R.color.torrent_paused));
otherPaint.setColor(getResources().getColor(R.color.torrent_other)); otherPaint.setColor(getResources().getColor(R.color.torrent_other));
downloadingPaint.setColor(getResources().getColor(R.color.torrent_downloading)); downloadingPaint.setColor(getResources().getColor(R.color.torrent_downloading));
seedingPaint.setColor(getResources().getColor(R.color.torrent_seeding)); seedingPaint.setColor(getResources().getColor(R.color.torrent_seeding));
errorPaint.setColor(getResources().getColor(R.color.torrent_error)); errorPaint.setColor(getResources().getColor(R.color.torrent_error));
} }
/** /**
* Registers the status of the represented torrent and invalidates the view so the status colour will be updated * Registers the status of the represented torrent and invalidates the view so the status colour will be updated
* accordingly. * accordingly.
* @param status The updated torrent status to show *
*/ * @param status The updated torrent status to show
public void setStatus(TorrentStatus status) { */
this.status = status; public void setStatus(TorrentStatus status) {
this.invalidate(); this.status = status;
} this.invalidate();
}
@Override @Override
protected void onDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
super.onDraw(canvas); super.onDraw(canvas);
int height = getHeight(); fullRect.set(0, 0, WIDTH, getHeight());
int width = WIDTH;
fullRect.set(0, 0, width, height);
if (status == null) { if (status == null) {
return; return;
} }
switch (status) { switch (status) {
case Downloading: case Downloading:
canvas.drawRect(fullRect, downloadingPaint); canvas.drawRect(fullRect, downloadingPaint);
break; break;
case Paused: case Paused:
canvas.drawRect(fullRect, pausedPaint); canvas.drawRect(fullRect, pausedPaint);
break; break;
case Seeding: case Seeding:
canvas.drawRect(fullRect, seedingPaint); canvas.drawRect(fullRect, seedingPaint);
break; break;
case Error: case Error:
canvas.drawRect(fullRect, errorPaint); canvas.drawRect(fullRect, errorPaint);
break; break;
default: // Checking, Waiting, Queued, Unknown default: // Checking, Waiting, Queued, Unknown
canvas.drawRect(fullRect, otherPaint); canvas.drawRect(fullRect, otherPaint);
break; break;
} }
} }
} }

85
app/src/main/java/org/transdroid/core/gui/lists/TorrentView.java

@ -29,55 +29,56 @@ import org.transdroid.daemon.TorrentStatus;
/** /**
* View that represents some {@link Torrent} object and displays progress, status, speeds, etc. * View that represents some {@link Torrent} object and displays progress, status, speeds, etc.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EViewGroup(R.layout.list_item_torrent) @EViewGroup(R.layout.list_item_torrent)
public class TorrentView extends TorrentStatusLayout { public class TorrentView extends TorrentStatusLayout {
@ViewById @ViewById
protected ImageView priorityImage; protected ImageView priorityImage;
@ViewById @ViewById
protected TextView nameText, ratioText, progressText, speedText, peersText; protected TextView nameText, ratioText, progressText, speedText, peersText;
@ViewById @ViewById
protected TorrentProgressBar torrentProgressbar; protected TorrentProgressBar torrentProgressbar;
public TorrentView(Context context) { public TorrentView(Context context) {
super(context); super(context);
} }
public void bind(Torrent torrent) { public void bind(Torrent torrent) {
LocalTorrent local = LocalTorrent.fromTorrent(torrent); LocalTorrent local = LocalTorrent.fromTorrent(torrent);
setStatus(torrent.getStatusCode()); setStatus(torrent.getStatusCode());
nameText.setText(torrent.getName()); nameText.setText(torrent.getName());
progressText.setText(local.getProgressSizeText(getResources(), false)); progressText.setText(local.getProgressSizeText(getResources(), false));
ratioText.setText(local.getProgressEtaRatioText(getResources())); ratioText.setText(local.getProgressEtaRatioText(getResources()));
// TODO: Implement per-torrent priority and set priorityImage // TODO: Implement per-torrent priority and set priorityImage
priorityImage.setVisibility(View.INVISIBLE); priorityImage.setVisibility(View.INVISIBLE);
// Only show status bar, peers and speed fields if relevant, i.e. when downloading or actively seeding // Only show status bar, peers and speed fields if relevant, i.e. when downloading or actively seeding
if (torrent.getStatusCode() == TorrentStatus.Downloading || if (torrent.getStatusCode() == TorrentStatus.Downloading ||
(torrent.getStatusCode() == TorrentStatus.Seeding && torrent.getRateUpload() > 0)) { (torrent.getStatusCode() == TorrentStatus.Seeding && torrent.getRateUpload() > 0)) {
torrentProgressbar.setVisibility(View.VISIBLE); torrentProgressbar.setVisibility(View.VISIBLE);
torrentProgressbar.setProgress((int) (torrent.getDownloadedPercentage() * 100)); torrentProgressbar.setProgress((int) (torrent.getDownloadedPercentage() * 100));
torrentProgressbar.setActive(torrent.canPause()); torrentProgressbar.setActive(torrent.canPause());
torrentProgressbar.setError(torrent.getStatusCode() == TorrentStatus.Error); torrentProgressbar.setError(torrent.getStatusCode() == TorrentStatus.Error);
peersText.setVisibility(View.VISIBLE); peersText.setVisibility(View.VISIBLE);
peersText.setText(local.getProgressConnectionText(getResources())); peersText.setText(local.getProgressConnectionText(getResources()));
speedText.setVisibility(View.VISIBLE); speedText.setVisibility(View.VISIBLE);
speedText.setText(local.getProgressSpeedText(getResources())); speedText.setText(local.getProgressSpeedText(getResources()));
} else if (torrent.getPartDone() < 1) { } else if (torrent.getPartDone() < 1) {
// Not active, but also not complete, so show the status bar // Not active, but also not complete, so show the status bar
torrentProgressbar.setVisibility(View.VISIBLE); torrentProgressbar.setVisibility(View.VISIBLE);
torrentProgressbar.setProgress((int) (torrent.getDownloadedPercentage() * 100)); torrentProgressbar.setProgress((int) (torrent.getDownloadedPercentage() * 100));
torrentProgressbar.setActive(torrent.canPause()); torrentProgressbar.setActive(torrent.canPause());
torrentProgressbar.setError(torrent.getStatusCode() == TorrentStatus.Error); torrentProgressbar.setError(torrent.getStatusCode() == TorrentStatus.Error);
peersText.setVisibility(View.GONE); peersText.setVisibility(View.GONE);
speedText.setVisibility(View.GONE); speedText.setVisibility(View.GONE);
} else { } else {
torrentProgressbar.setVisibility(View.GONE); torrentProgressbar.setVisibility(View.GONE);
peersText.setVisibility(View.GONE); peersText.setVisibility(View.GONE);
speedText.setVisibility(View.GONE); speedText.setVisibility(View.GONE);
} }
} }
} }

91
app/src/main/java/org/transdroid/core/gui/lists/TorrentsAdapter.java

@ -29,61 +29,62 @@ import java.util.ArrayList;
/** /**
* Adapter that contains a list of torrent objects to show. * Adapter that contains a list of torrent objects to show.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EBean @EBean
public class TorrentsAdapter extends BaseAdapter { public class TorrentsAdapter extends BaseAdapter {
private ArrayList<Torrent> torrents = null; @RootContext
protected Context context;
@RootContext private ArrayList<Torrent> torrents = null;
protected Context context;
/** /**
* Allows updating the full internal list of torrents at once, replacing the old list * Allows updating the full internal list of torrents at once, replacing the old list
* @param newTorrents The new list of torrent objects *
*/ * @param newTorrents The new list of torrent objects
public void update(ArrayList<Torrent> newTorrents) { */
this.torrents = newTorrents; public void update(ArrayList<Torrent> newTorrents) {
notifyDataSetChanged(); this.torrents = newTorrents;
} notifyDataSetChanged();
}
@Override @Override
public boolean hasStableIds() { public boolean hasStableIds() {
return true; return true;
} }
@Override @Override
public int getCount() { public int getCount() {
if (torrents == null) { if (torrents == null) {
return 0; return 0;
} }
return torrents.size(); return torrents.size();
} }
@Override @Override
public Torrent getItem(int position) { public Torrent getItem(int position) {
if (torrents == null) { if (torrents == null) {
return null; return null;
} }
return torrents.get(position); return torrents.get(position);
} }
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
return position; return position;
} }
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
TorrentView torrentView; TorrentView torrentView;
if (convertView == null) { if (convertView == null) {
torrentView = TorrentView_.build(context); torrentView = TorrentView_.build(context);
} else { } else {
torrentView = (TorrentView) convertView; torrentView = (TorrentView) convertView;
} }
torrentView.bind(getItem(position)); torrentView.bind(getItem(position));
return torrentView; return torrentView;
} }
} }

158
app/src/main/java/org/transdroid/core/gui/lists/ViewHolderAdapter.java

@ -28,96 +28,100 @@ import android.widget.ListView;
* the view object. This is required since otherwise the adapter's consumer (i.e. a {@link ListView}) does not update * the view object. This is required since otherwise the adapter's consumer (i.e. a {@link ListView}) does not update
* the list row accordingly. Use {@link #setViewEnabled(boolean)} to enable or disable this contained view for user * the list row accordingly. Use {@link #setViewEnabled(boolean)} to enable or disable this contained view for user
* interaction. * interaction.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class ViewHolderAdapter extends BaseAdapter { public class ViewHolderAdapter extends BaseAdapter {
private final View view; private final View view;
/** /**
* Instantiates this wrapper adapter with the one and only view to show. It can not be updated and view visibility * Instantiates this wrapper adapter with the one and only view to show. It can not be updated and view visibility
* should be set directly on this adapter using {@link #setViewVisibility(int)}. Use * should be set directly on this adapter using {@link #setViewVisibility(int)}. Use
* {@link #setViewEnabled(boolean)} to enable or disable this contained view for user interaction. * {@link #setViewEnabled(boolean)} to enable or disable this contained view for user interaction.
* @param view The view that will be wrapper in an adapter to show in a list view *
*/ * @param view The view that will be wrapper in an adapter to show in a list view
public ViewHolderAdapter(View view) { */
this.view = view; public ViewHolderAdapter(View view) {
} this.view = view;
}
/** /**
* Sets the visibility on the contained view and notifies consumers of this adapter (i.e. a {@link ListView}) * Sets the visibility on the contained view and notifies consumers of this adapter (i.e. a {@link ListView})
* accordingly. Use {@link View#GONE} to hide this adapter's view altogether. * accordingly. Use {@link View#GONE} to hide this adapter's view altogether.
* @param visibility The visibility to set on the contained view *
*/ * @param visibility The visibility to set on the contained view
public void setViewVisibility(int visibility) { */
view.setVisibility(visibility); public void setViewVisibility(int visibility) {
notifyDataSetChanged(); view.setVisibility(visibility);
} notifyDataSetChanged();
}
/** /**
* Sets whether the contained view should be enabled and notifies consumers of this adapter (i.e. a {@link ListView} * Sets whether the contained view should be enabled and notifies consumers of this adapter (i.e. a {@link ListView}
* ) accordingly. A contained enabled view allows user interaction (clicks, focus), while a disabled view does not. * ) accordingly. A contained enabled view allows user interaction (clicks, focus), while a disabled view does not.
* @param enabled Whether the contained view should be enabled *
*/ * @param enabled Whether the contained view should be enabled
public void setViewEnabled(boolean enabled) { */
view.setEnabled(enabled); public void setViewEnabled(boolean enabled) {
notifyDataSetChanged(); view.setEnabled(enabled);
} notifyDataSetChanged();
}
/** /**
* Returns 1 if the contained view is {@link View#VISIBLE} or {@link View#INVISIBLE}, return 0 if {@link View#GONE}. * Returns 1 if the contained view is {@link View#VISIBLE} or {@link View#INVISIBLE}, return 0 if {@link View#GONE}.
*/ */
@Override @Override
public int getCount() { public int getCount() {
return view.getVisibility() == View.VISIBLE ? 1 : 0; return view.getVisibility() == View.VISIBLE ? 1 : 0;
} }
/** /**
* Always directly returns the single contained view instance. * Always directly returns the single contained view instance.
*/ */
@Override @Override
public Object getItem(int position) { public Object getItem(int position) {
return view; return view;
} }
/** /**
* Always returns the position directly as item id. * Always returns the position directly as item id.
*/ */
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
return position; return position;
} }
/** /**
* Always directly returns the single contained view instance. * Always directly returns the single contained view instance.
*/ */
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
return view; return view;
} }
/** /**
* Always returns true, as there is only one contained item and it is never changed. * Always returns true, as there is only one contained item and it is never changed.
*/ */
@Override @Override
public boolean hasStableIds() { public boolean hasStableIds() {
return true; return true;
} }
/** /**
* Returns false, as the contained view can still be enabled and disabled. * Returns false, as the contained view can still be enabled and disabled.
*/ */
@Override @Override
public boolean areAllItemsEnabled() { public boolean areAllItemsEnabled() {
return false; return false;
} }
/** /**
* Returns true if the contained view is enabled, returns false otherwise. * Returns true if the contained view is enabled, returns false otherwise.
*/ */
@Override @Override
public boolean isEnabled(int position) { public boolean isEnabled(int position) {
return view.isEnabled(); return view.isEnabled();
} }
} }

62
app/src/main/java/org/transdroid/core/gui/log/DatabaseHelper.java

@ -16,54 +16,56 @@
*/ */
package org.transdroid.core.gui.log; package org.transdroid.core.gui.log;
import java.sql.SQLException;
import android.content.Context; import android.content.Context;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import androidx.annotation.Keep;
import android.util.Log; import android.util.Log;
import androidx.annotation.Keep;
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper; import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper;
import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils; import com.j256.ormlite.table.TableUtils;
import java.sql.SQLException;
/** /**
* Helper to access the database to access persisting objects. * Helper to access the database to access persisting objects.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
private static final String DATABASE_NAME = "transdroid.db"; private static final String DATABASE_NAME = "transdroid.db";
private static final int DATABASE_VERSION = 1; private static final int DATABASE_VERSION = 1;
@Keep @Keep
public DatabaseHelper(Context context) { public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION); super(context, DATABASE_NAME, null, DATABASE_VERSION);
} }
@Override @Override
public void onCreate(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource) { public void onCreate(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource) {
try { try {
TableUtils.createTable(connectionSource, ErrorLogEntry.class); TableUtils.createTable(connectionSource, ErrorLogEntry.class);
} catch (SQLException e) { } catch (SQLException e) {
Log.e(org.transdroid.core.gui.log.Log.LOG_NAME, "Could not create new table for ErrorLogEntry", e); Log.e(org.transdroid.core.gui.log.Log.LOG_NAME, "Could not create new table for ErrorLogEntry", e);
} }
} }
@Override @Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource, int oldVersion, public void onUpgrade(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource, int oldVersion,
int newVersion) { int newVersion) {
try { try {
switch (oldVersion) { switch (oldVersion) {
case 1: case 1:
TableUtils.createTable(connectionSource, ErrorLogEntry.class); TableUtils.createTable(connectionSource, ErrorLogEntry.class);
/*case 1: /*case 2:
etc...*/ etc...*/
} }
} catch (SQLException e) { } catch (SQLException e) {
Log.e(org.transdroid.core.gui.log.Log.LOG_NAME, "Could not upgrade the table for ErrorLogEntry", e); Log.e(org.transdroid.core.gui.log.Log.LOG_NAME, "Could not upgrade the table for ErrorLogEntry", e);
} }
} }
} }

149
app/src/main/java/org/transdroid/core/gui/log/ErrorLogEntry.java

@ -16,93 +16,92 @@
*/ */
package org.transdroid.core.gui.log; package org.transdroid.core.gui.log;
import java.util.Date;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable; import com.j256.ormlite.table.DatabaseTable;
import java.util.Date;
/** /**
* Represents an error log entry to be registered in the database. * Represents an error log entry to be registered in the database.
*
* @author Eric Kok * @author Eric Kok
*/ */
@DatabaseTable(tableName = "ErrorLogEntry") @DatabaseTable(tableName = "ErrorLogEntry")
public class ErrorLogEntry implements Parcelable { public class ErrorLogEntry implements Parcelable {
public static final String ID = "logId"; public static final String ID = "logId";
public static final String DATEANDTIME = "dateAndTime"; public static final String DATEANDTIME = "dateAndTime";
public static final Parcelable.Creator<ErrorLogEntry> CREATOR = new Parcelable.Creator<ErrorLogEntry>() {
@DatabaseField(id = true, columnName = ID) public ErrorLogEntry createFromParcel(Parcel in) {
private Integer logId; return new ErrorLogEntry(in);
@DatabaseField(columnName = DATEANDTIME) }
private Date dateAndTime;
@DatabaseField public ErrorLogEntry[] newArray(int size) {
private Integer priority; return new ErrorLogEntry[size];
@DatabaseField }
private String tag; };
@DatabaseField @DatabaseField(id = true, columnName = ID)
private String message; private Integer logId;
@DatabaseField(columnName = DATEANDTIME)
public ErrorLogEntry() { private Date dateAndTime;
} @DatabaseField
private Integer priority;
public ErrorLogEntry(Integer priority, String tag, String message) { @DatabaseField
this.dateAndTime = new Date(); private String tag;
this.priority = priority; @DatabaseField
this.tag = tag; private String message;
this.message = message;
} public ErrorLogEntry() {
}
public Integer getLogId() {
return logId; public ErrorLogEntry(Integer priority, String tag, String message) {
} this.dateAndTime = new Date();
this.priority = priority;
public Date getDateAndTime() { this.tag = tag;
return dateAndTime; this.message = message;
} }
public Integer getPriority() { private ErrorLogEntry(Parcel in) {
return priority; logId = in.readInt();
} dateAndTime = new Date(in.readLong());
priority = in.readInt();
public String getTag() { tag = in.readString();
return tag; message = in.readString();
} }
public String getMessage() { public Integer getLogId() {
return message; return logId;
} }
public int describeContents() { public Date getDateAndTime() {
return 0; return dateAndTime;
} }
public void writeToParcel(Parcel out, int flags) { public Integer getPriority() {
out.writeInt(logId); return priority;
out.writeLong(dateAndTime.getTime()); }
out.writeInt(priority);
out.writeString(tag); public String getTag() {
out.writeString(message); return tag;
} }
public static final Parcelable.Creator<ErrorLogEntry> CREATOR = new Parcelable.Creator<ErrorLogEntry>() { public String getMessage() {
public ErrorLogEntry createFromParcel(Parcel in) { return message;
return new ErrorLogEntry(in); }
}
public int describeContents() {
public ErrorLogEntry[] newArray(int size) { return 0;
return new ErrorLogEntry[size]; }
}
}; public void writeToParcel(Parcel out, int flags) {
out.writeInt(logId);
private ErrorLogEntry(Parcel in) { out.writeLong(dateAndTime.getTime());
logId = in.readInt(); out.writeInt(priority);
dateAndTime = new Date(in.readLong()); out.writeString(tag);
priority = in.readInt(); out.writeString(message);
tag = in.readString(); }
message = in.readString();
}
} }

112
app/src/main/java/org/transdroid/core/gui/log/ErrorLogSender.java

@ -16,8 +16,11 @@
*/ */
package org.transdroid.core.gui.log; package org.transdroid.core.gui.log;
import java.sql.SQLException; import android.app.Activity;
import java.util.List; import android.content.ActivityNotFoundException;
import android.content.Intent;
import com.j256.ormlite.dao.Dao;
import org.androidannotations.annotations.Bean; import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean; import org.androidannotations.annotations.EBean;
@ -26,70 +29,67 @@ import org.transdroid.R;
import org.transdroid.core.app.settings.ServerSetting; import org.transdroid.core.app.settings.ServerSetting;
import org.transdroid.core.gui.navigation.NavigationHelper; import org.transdroid.core.gui.navigation.NavigationHelper;
import android.app.Activity; import java.sql.SQLException;
import android.content.ActivityNotFoundException; import java.util.List;
import android.content.Intent;
import com.j256.ormlite.dao.Dao;
@EBean @EBean
public class ErrorLogSender { public class ErrorLogSender {
@Bean @Bean
protected Log log; protected Log log;
@Bean @Bean
protected NavigationHelper navigationHelper; protected NavigationHelper navigationHelper;
@OrmLiteDao(helper = DatabaseHelper.class) @OrmLiteDao(helper = DatabaseHelper.class)
protected Dao<ErrorLogEntry, Integer> errorLogDao; protected Dao<ErrorLogEntry, Integer> errorLogDao;
public void collectAndSendLog(final Activity callingActivity, final ServerSetting serverSetting) { public void collectAndSendLog(final Activity callingActivity, final ServerSetting serverSetting) {
try { try {
// Prepare an email with error logging information // Prepare an email with error logging information
StringBuilder body = new StringBuilder(); StringBuilder body = new StringBuilder();
body.append("Please describe your problem:\n\n\n"); body.append("Please describe your problem:\n\n\n");
body.append("\n"); body.append("\n");
body.append(navigationHelper.getAppNameAndVersion()); body.append(navigationHelper.getAppNameAndVersion());
body.append("\n"); body.append("\n");
if (serverSetting == null) { if (serverSetting == null) {
body.append("(No server settings)"); body.append("(No server settings)");
} else { } else {
body.append(serverSetting.getType().toString()); body.append(serverSetting.getType().toString());
body.append(" settings: "); body.append(" settings: ");
body.append(serverSetting.getHumanReadableIdentifier()); body.append(serverSetting.getHumanReadableIdentifier());
} }
body.append("\n\nConnection and error log:"); body.append("\n\nConnection and error log:");
// Print the individual error log messages as stored in the database // Print the individual error log messages as stored in the database
List<ErrorLogEntry> all = errorLogDao.queryBuilder().orderBy(ErrorLogEntry.ID, true).query(); List<ErrorLogEntry> all = errorLogDao.queryBuilder().orderBy(ErrorLogEntry.ID, true).query();
for (ErrorLogEntry errorLogEntry : all) { for (ErrorLogEntry errorLogEntry : all) {
body.append("\n"); body.append("\n");
body.append(errorLogEntry.getLogId()); body.append(errorLogEntry.getLogId());
body.append(" -- "); body.append(" -- ");
body.append(errorLogEntry.getDateAndTime()); body.append(errorLogEntry.getDateAndTime());
body.append(" -- "); body.append(" -- ");
body.append(errorLogEntry.getPriority()); body.append(errorLogEntry.getPriority());
body.append(" -- "); body.append(" -- ");
body.append(errorLogEntry.getMessage()); body.append(errorLogEntry.getMessage());
} }
Intent target = new Intent(Intent.ACTION_SEND); Intent target = new Intent(Intent.ACTION_SEND);
target.setType("message/rfc822"); target.setType("message/rfc822");
target.putExtra(Intent.EXTRA_EMAIL, new String[] { "transdroid@2312.nl" }); target.putExtra(Intent.EXTRA_EMAIL, new String[]{"transdroid@2312.nl"});
target.putExtra(Intent.EXTRA_SUBJECT, "Transdroid error report"); target.putExtra(Intent.EXTRA_SUBJECT, "Transdroid error report");
target.putExtra(Intent.EXTRA_TEXT, body.toString()); target.putExtra(Intent.EXTRA_TEXT, body.toString());
try { try {
callingActivity.startActivity(Intent.createChooser(target, callingActivity.startActivity(Intent.createChooser(target,
callingActivity.getString(R.string.pref_sendlog)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); callingActivity.getString(R.string.pref_sendlog)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} catch (ActivityNotFoundException e) { } catch (ActivityNotFoundException e) {
log.i(callingActivity, "Tried to send error log, but there is no email app installed."); log.i(callingActivity, "Tried to send error log, but there is no email app installed.");
} }
} catch (SQLException e) { } catch (SQLException e) {
log.e(callingActivity, "Cannot read the error log to build an error report to send: " + e.toString()); log.e(callingActivity, "Cannot read the error log to build an error report to send: " + e.toString());
} }
} }
} }

63
app/src/main/java/org/transdroid/core/gui/log/Log.java

@ -28,46 +28,47 @@ import java.util.Date;
/** /**
* Application-wide logging class that registers entries in the database (for a certain time). * Application-wide logging class that registers entries in the database (for a certain time).
*
* @author Eric Kok * @author Eric Kok
*/ */
@EBean(scope = Scope.Singleton) @EBean(scope = Scope.Singleton)
public class Log { public class Log {
public static final String LOG_NAME = "Transdroid"; public static final String LOG_NAME = "Transdroid";
private static final long MAX_LOG_AGE = 15 * 60 * 1000; // 15 minutes private static final long MAX_LOG_AGE = 15 * 60 * 1000; // 15 minutes
@OrmLiteDao(helper = DatabaseHelper.class) @OrmLiteDao(helper = DatabaseHelper.class)
Dao<ErrorLogEntry, Integer> errorLogDao; Dao<ErrorLogEntry, Integer> errorLogDao;
protected void log(Object object, int priority, String message) { protected void log(Object object, int priority, String message) {
log(object instanceof String ? (String) object : object.getClass().getSimpleName(), priority, message); log(object instanceof String ? (String) object : object.getClass().getSimpleName(), priority, message);
} }
protected void log(String logName, int priority, String message) { protected void log(String logName, int priority, String message) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
android.util.Log.println(priority, LOG_NAME, message); android.util.Log.println(priority, LOG_NAME, message);
} }
try { try {
// Store this log message to the database // Store this log message to the database
errorLogDao.create(new ErrorLogEntry(priority, logName, message)); errorLogDao.create(new ErrorLogEntry(priority, logName, message));
// Truncate the error log // Truncate the error log
DeleteBuilder<ErrorLogEntry, Integer> db = errorLogDao.deleteBuilder(); DeleteBuilder<ErrorLogEntry, Integer> db = errorLogDao.deleteBuilder();
db.setWhere(db.where().le(ErrorLogEntry.DATEANDTIME, new Date(new Date().getTime() - MAX_LOG_AGE))); db.setWhere(db.where().le(ErrorLogEntry.DATEANDTIME, new Date(new Date().getTime() - MAX_LOG_AGE)));
errorLogDao.delete(db.prepare()); errorLogDao.delete(db.prepare());
} catch (Exception e) { } catch (Exception e) {
android.util.Log.e(LOG_NAME, "Cannot write log message to database: " + e.toString()); android.util.Log.e(LOG_NAME, "Cannot write log message to database: " + e.toString());
} }
} }
public void d(Object object, String msg) { public void d(Object object, String msg) {
log(object, android.util.Log.DEBUG, msg); log(object, android.util.Log.DEBUG, msg);
} }
public void i(Object object, String msg) { public void i(Object object, String msg) {
log(object, android.util.Log.DEBUG, msg); log(object, android.util.Log.DEBUG, msg);
} }
public void e(Object object, String msg) { public void e(Object object, String msg) {
log(object, android.util.Log.ERROR, msg); log(object, android.util.Log.ERROR, msg);
} }
} }

54
app/src/main/java/org/transdroid/core/gui/log/LogUncaughtExceptionHandler.java

@ -20,32 +20,32 @@ import android.content.Context;
public class LogUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { public class LogUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private final Context context; private final Context context;
private final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler; private final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;
public LogUncaughtExceptionHandler(Context context, Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler) { public LogUncaughtExceptionHandler(Context context, Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler) {
this.context = context; this.context = context;
this.defaultUncaughtExceptionHandler = defaultUncaughtExceptionHandler; this.defaultUncaughtExceptionHandler = defaultUncaughtExceptionHandler;
} }
@Override @Override
public void uncaughtException(Thread thread, Throwable ex) { public void uncaughtException(Thread thread, Throwable ex) {
// Write exception stack trace to the log // Write exception stack trace to the log
String prefix = "E: "; String prefix = "E: ";
Log_ log = Log_.getInstance_(context); Log_ log = Log_.getInstance_(context);
log.e(this, prefix + ex.toString()); log.e(this, prefix + ex.toString());
if (ex.getCause() != null) { if (ex.getCause() != null) {
for (StackTraceElement e : ex.getCause().getStackTrace()) { for (StackTraceElement e : ex.getCause().getStackTrace()) {
log.e(this, prefix + e.toString()); log.e(this, prefix + e.toString());
} }
} }
for (StackTraceElement e : ex.getStackTrace()) { for (StackTraceElement e : ex.getStackTrace()) {
log.e(this, prefix + e.toString()); log.e(this, prefix + e.toString());
} }
// Rely on default Android exception handling // Rely on default Android exception handling
defaultUncaughtExceptionHandler.uncaughtException(thread, ex); defaultUncaughtExceptionHandler.uncaughtException(thread, ex);
} }
} }

142
app/src/main/java/org/transdroid/core/gui/navigation/DialogHelper.java

@ -16,12 +16,6 @@
*/ */
package org.transdroid.core.gui.navigation; package org.transdroid.core.gui.navigation;
import java.io.Serializable;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.Extra;
import org.transdroid.core.gui.*;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
@ -32,82 +26,92 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.Window; import android.view.Window;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.Extra;
import org.transdroid.core.gui.TorrentsActivity_;
import java.io.Serializable;
/** /**
* Helper class that show a dialog either as pop-up or as full screen activity. Should be used by calling * Helper class that show a dialog either as pop-up or as full screen activity. Should be used by calling
* {@link #showDialog(Context, DialogSpecification)} with in instance of the dialog specification that should be shown, * {@link #showDialog(Context, DialogSpecification)} with in instance of the dialog specification that should be shown,
* from the calling activity's {@link Activity#onCreateDialog(int)}. * from the calling activity's {@link Activity#onCreateDialog(int)}.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EActivity @EActivity
public class DialogHelper extends Activity { public class DialogHelper extends Activity {
@Extra @Extra
protected DialogSpecification dialog; protected DialogSpecification dialog;
/**
* Call this from {@link Activity#onCreateDialog(int)}, supplying an instance of the {@link DialogSpecification}
* that should be shown to the user.
*
* @param context The activity that calls this method and which will own the constructed dialog
* @param dialog An instance of the specification for the dialog that needs to be shown
* @return Either an instance of a {@link Dialog} that the activity should further control or null if the dialog
* will instead be opened as a full screen activity
*/
public static Dialog showDialog(Context context, DialogSpecification dialog) {
// If the device is large (i.e. a tablet) then return a dialog to show
if (!NavigationHelper_.getInstance_(context).isSmallScreen())
return new PopupDialog(context, dialog);
// This is a small device; create a full screen dialog (which is just an activity)
DialogHelper_.intent(context).dialog(dialog).start();
return null;
}
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(dialog.getDialogLayoutId()); setContentView(dialog.getDialogLayoutId());
// TODO getActionBar().setDisplayHomeAsUpEnabled(true); // TODO getActionBar().setDisplayHomeAsUpEnabled(true);
} }
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater menuInflater = getMenuInflater(); MenuInflater menuInflater = getMenuInflater();
menuInflater.inflate(dialog.getDialogMenuId(), menu); menuInflater.inflate(dialog.getDialogMenuId(), menu);
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) { if (item.getItemId() == android.R.id.home) {
// Action bar up button clicked; navigate up all the way back to the torrents activity // Action bar up button clicked; navigate up all the way back to the torrents activity
TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start(); TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
return true; return true;
} }
return dialog.onMenuItemSelected(this, item.getItemId()); return dialog.onMenuItemSelected(this, item.getItemId());
} }
/** /**
* Call this from {@link Activity#onCreateDialog(int)}, supplying an instance of the {@link DialogSpecification} * Specification for some dialog that can be show to the user, consisting of a custom layout and possibly an action
* that should be shown to the user. * bar menu. Warning: the action bar, and thus the menu options, is only shown when the dialog is presented as full
* @param context The activity that calls this method and which will own the constructed dialog * screen activity. Use only for unimportant actions.
* @param dialog An instance of the specification for the dialog that needs to be shown */
* @return Either an instance of a {@link Dialog} that the activity should further control or null if the dialog public interface DialogSpecification extends Serializable {
* will instead be opened as a full screen activity int getDialogLayoutId();
*/
public static Dialog showDialog(Context context, DialogSpecification dialog) { int getDialogMenuId();
// If the device is large (i.e. a tablet) then return a dialog to show boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId);
if (!NavigationHelper_.getInstance_(context).isSmallScreen()) }
return new PopupDialog(context, dialog);
/**
// This is a small device; create a full screen dialog (which is just an activity) * A specific dialog that shows some layout (resource) as contents. It has no buttons or other chrome.
DialogHelper_.intent(context).dialog(dialog).start(); */
return null; protected static class PopupDialog extends Dialog {
public PopupDialog(Context context, DialogSpecification dialog) {
} super(context);
requestWindowFeature(Window.FEATURE_NO_TITLE);
/** setContentView(dialog.getDialogLayoutId());
* A specific dialog that shows some layout (resource) as contents. It has no buttons or other chrome. }
*/ }
protected static class PopupDialog extends Dialog {
public PopupDialog(Context context, DialogSpecification dialog) {
super(context);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(dialog.getDialogLayoutId());
}
}
/**
* Specification for some dialog that can be show to the user, consisting of a custom layout and possibly an action
* bar menu. Warning: the action bar, and thus the menu options, is only shown when the dialog is presented as full
* screen activity. Use only for unimportant actions.
*/
public interface DialogSpecification extends Serializable {
int getDialogLayoutId();
int getDialogMenuId();
boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId);
}
} }

141
app/src/main/java/org/transdroid/core/gui/navigation/FilterListAdapter.java

@ -24,7 +24,6 @@ import org.androidannotations.annotations.RootContext;
import org.transdroid.R; import org.transdroid.R;
import org.transdroid.core.app.settings.ServerSetting; import org.transdroid.core.app.settings.ServerSetting;
import org.transdroid.core.gui.lists.MergeAdapter; import org.transdroid.core.gui.lists.MergeAdapter;
import org.transdroid.core.gui.lists.SimpleListItem;
import org.transdroid.core.gui.lists.ViewHolderAdapter; import org.transdroid.core.gui.lists.ViewHolderAdapter;
import org.transdroid.core.gui.navigation.StatusType.StatusTypeFilter; import org.transdroid.core.gui.navigation.StatusType.StatusTypeFilter;
@ -33,81 +32,85 @@ import java.util.List;
/** /**
* List adapter that holds filter items, that is, servers, view types and labels. A header item is inserted where appropriate. * List adapter that holds filter items, that is, servers, view types and labels. A header item is inserted where appropriate.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EBean @EBean
public class FilterListAdapter extends MergeAdapter { public class FilterListAdapter extends MergeAdapter {
@RootContext @RootContext
protected Context context; protected Context context;
private FilterListItemAdapter serverItems = null; protected ViewHolderAdapter statusTypeSeparator;
private FilterListItemAdapter statusTypeItems = null; protected ViewHolderAdapter labelSeperator;
private FilterListItemAdapter labelItems = null; protected ViewHolderAdapter serverSeparator;
protected ViewHolderAdapter statusTypeSeparator; private FilterListItemAdapter serverItems = null;
protected ViewHolderAdapter labelSeperator; private FilterListItemAdapter statusTypeItems = null;
protected ViewHolderAdapter serverSeparator; private FilterListItemAdapter labelItems = null;
/** /**
* Update the list of available servers * Update the list of available servers
* @param servers The new list of available servers *
*/ * @param servers The new list of available servers
public void updateServers(List<ServerSetting> servers) { */
if (this.serverItems == null && servers != null) { public void updateServers(List<ServerSetting> servers) {
serverSeparator = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(context.getString(R.string.navigation_servers))); if (this.serverItems == null && servers != null) {
serverSeparator.setViewVisibility(servers.isEmpty() ? View.GONE : View.VISIBLE); serverSeparator = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(context.getString(R.string.navigation_servers)));
addAdapter(serverSeparator); serverSeparator.setViewVisibility(servers.isEmpty() ? View.GONE : View.VISIBLE);
this.serverItems = new FilterListItemAdapter(context, servers); addAdapter(serverSeparator);
addAdapter(serverItems); this.serverItems = new FilterListItemAdapter(context, servers);
} else if (this.serverItems != null && servers != null) { addAdapter(serverItems);
serverSeparator.setViewVisibility(servers.isEmpty() ? View.GONE : View.VISIBLE); } else if (this.serverItems != null && servers != null) {
this.serverItems.update(servers); serverSeparator.setViewVisibility(servers.isEmpty() ? View.GONE : View.VISIBLE);
} else { this.serverItems.update(servers);
serverSeparator.setViewVisibility(View.GONE); } else {
this.serverItems.update(new ArrayList<SimpleListItem>()); serverSeparator.setViewVisibility(View.GONE);
} this.serverItems.update(new ArrayList<>());
notifyDataSetChanged(); }
} notifyDataSetChanged();
}
/** /**
* Update the list of available status types * Update the list of available status types
* @param statusTypes The new list of available status types *
*/ * @param statusTypes The new list of available status types
public void updateStatusTypes(List<StatusTypeFilter> statusTypes) { */
if (this.statusTypeItems == null && statusTypes != null) { public void updateStatusTypes(List<StatusTypeFilter> statusTypes) {
statusTypeSeparator = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(context.getString(R.string.navigation_status))); if (this.statusTypeItems == null && statusTypes != null) {
statusTypeSeparator.setViewVisibility(statusTypes.isEmpty() ? View.GONE : View.VISIBLE); statusTypeSeparator = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(context.getString(R.string.navigation_status)));
addAdapter(statusTypeSeparator); statusTypeSeparator.setViewVisibility(statusTypes.isEmpty() ? View.GONE : View.VISIBLE);
this.statusTypeItems = new FilterListItemAdapter(context, statusTypes); addAdapter(statusTypeSeparator);
addAdapter(statusTypeItems); this.statusTypeItems = new FilterListItemAdapter(context, statusTypes);
} else if (this.statusTypeItems != null && statusTypes != null) { addAdapter(statusTypeItems);
statusTypeSeparator.setViewVisibility(statusTypes.isEmpty() ? View.GONE : View.VISIBLE); } else if (this.statusTypeItems != null && statusTypes != null) {
this.statusTypeItems.update(statusTypes); statusTypeSeparator.setViewVisibility(statusTypes.isEmpty() ? View.GONE : View.VISIBLE);
} else { this.statusTypeItems.update(statusTypes);
statusTypeSeparator.setViewVisibility(View.GONE); } else {
this.statusTypeItems.update(new ArrayList<SimpleListItem>()); statusTypeSeparator.setViewVisibility(View.GONE);
} this.statusTypeItems.update(new ArrayList<>());
notifyDataSetChanged(); }
} notifyDataSetChanged();
}
/** /**
* Update the list of available labels * Update the list of available labels
* @param labels The new list of available labels *
*/ * @param labels The new list of available labels
public void updateLabels(List<Label> labels) { */
if (this.labelItems == null && labels != null) { public void updateLabels(List<Label> labels) {
labelSeperator = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(context.getString(R.string.navigation_labels))); if (this.labelItems == null && labels != null) {
labelSeperator.setViewVisibility(labels.isEmpty() ? View.GONE : View.VISIBLE); labelSeperator = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(context.getString(R.string.navigation_labels)));
addAdapter(labelSeperator); labelSeperator.setViewVisibility(labels.isEmpty() ? View.GONE : View.VISIBLE);
this.labelItems = new FilterListItemAdapter(context, labels); addAdapter(labelSeperator);
addAdapter(labelItems); this.labelItems = new FilterListItemAdapter(context, labels);
} else if (this.labelItems != null && labels != null) { addAdapter(labelItems);
labelSeperator.setViewVisibility(labels.isEmpty() ? View.GONE : View.VISIBLE); } else if (this.labelItems != null && labels != null) {
this.labelItems.update(labels); labelSeperator.setViewVisibility(labels.isEmpty() ? View.GONE : View.VISIBLE);
} else { this.labelItems.update(labels);
labelSeperator.setViewVisibility(View.GONE); } else {
this.labelItems.update(new ArrayList<SimpleListItem>()); labelSeperator.setViewVisibility(View.GONE);
} this.labelItems.update(new ArrayList<>());
notifyDataSetChanged(); }
} notifyDataSetChanged();
}
} }

75
app/src/main/java/org/transdroid/core/gui/navigation/FilterListItemAdapter.java

@ -28,48 +28,49 @@ import java.util.List;
public class FilterListItemAdapter extends BaseAdapter { public class FilterListItemAdapter extends BaseAdapter {
private final Context context; private final Context context;
private List<? extends SimpleListItem> items; private List<? extends SimpleListItem> items;
public FilterListItemAdapter(Context context, List<? extends SimpleListItem> items) { public FilterListItemAdapter(Context context, List<? extends SimpleListItem> items) {
this.context = context; this.context = context;
this.items = items; this.items = items;
} }
/** /**
* Allows updating of the full data list underlying this adapter, replacing all items * Allows updating of the full data list underlying this adapter, replacing all items
* @param newItems The new list of filter items to display *
*/ * @param newItems The new list of filter items to display
public void update(List<? extends SimpleListItem> newItems) { */
this.items = newItems; public void update(List<? extends SimpleListItem> newItems) {
notifyDataSetChanged(); this.items = newItems;
} notifyDataSetChanged();
}
@Override @Override
public int getCount() { public int getCount() {
return items.size(); return items.size();
} }
@Override @Override
public SimpleListItem getItem(int position) { public SimpleListItem getItem(int position) {
return items.get(position); return items.get(position);
} }
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
return position; return position;
} }
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
FilterListItemView filterItemView; FilterListItemView filterItemView;
if (convertView == null || !(convertView instanceof SimpleListItemView)) { if (!(convertView instanceof SimpleListItemView)) {
filterItemView = FilterListItemView_.build(context); filterItemView = FilterListItemView_.build(context);
} else { } else {
filterItemView = (FilterListItemView) convertView; filterItemView = (FilterListItemView) convertView;
} }
filterItemView.bind(getItem(position)); filterItemView.bind(getItem(position));
return filterItemView; return filterItemView;
} }
} }

17
app/src/main/java/org/transdroid/core/gui/navigation/FilterListItemView.java

@ -27,20 +27,21 @@ import org.transdroid.core.gui.lists.SimpleListItem;
/** /**
* View that represents some {@link SimpleListItem} object specifically used to represent a navigation filter item. * View that represents some {@link SimpleListItem} object specifically used to represent a navigation filter item.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EViewGroup(R.layout.list_item_filter) @EViewGroup(R.layout.list_item_filter)
public class FilterListItemView extends FrameLayout { public class FilterListItemView extends FrameLayout {
@ViewById @ViewById
protected TextView itemText; protected TextView itemText;
public FilterListItemView(Context context) { public FilterListItemView(Context context) {
super(context); super(context);
} }
public void bind(SimpleListItem filterItem) { public void bind(SimpleListItem filterItem) {
itemText.setText(filterItem.getName()); itemText.setText(filterItem.getName());
} }
} }

40
app/src/main/java/org/transdroid/core/gui/navigation/FilterSeparatorView.java

@ -27,29 +27,31 @@ import org.transdroid.R;
/** /**
* A list item that shows a sub header or separator (in underlined Holo style). * A list item that shows a sub header or separator (in underlined Holo style).
*
* @author Eric Kok * @author Eric Kok
*/ */
@EViewGroup(R.layout.list_item_separator) @EViewGroup(R.layout.list_item_separator)
public class FilterSeparatorView extends FrameLayout { public class FilterSeparatorView extends FrameLayout {
protected String text; protected String text;
@ViewById @ViewById
protected TextView separatorText; protected TextView separatorText;
public FilterSeparatorView(Context context) { public FilterSeparatorView(Context context) {
super(context); super(context);
} }
/** /**
* Sets the text that will be shown in this separator (sub header) * Sets the text that will be shown in this separator (sub header)
* @param text The new text to show *
* @return Itself, for convenience of method chaining * @param text The new text to show
*/ * @return Itself, for convenience of method chaining
public FilterSeparatorView setText(String text) { */
separatorText.setText(text); public FilterSeparatorView setText(String text) {
setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.WRAP_CONTENT, AbsListView.LayoutParams.WRAP_CONTENT)); separatorText.setText(text);
return this; setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.WRAP_CONTENT, AbsListView.LayoutParams.WRAP_CONTENT));
} return this;
}
} }

219
app/src/main/java/org/transdroid/core/gui/navigation/Label.java

@ -29,118 +29,119 @@ import java.util.List;
/** /**
* Represents some label that is active or available on the server. * Represents some label that is active or available on the server.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class Label implements SimpleListItem, NavigationFilter, Comparable<Label> { public class Label implements SimpleListItem, NavigationFilter, Comparable<Label> {
private static String unnamedLabelText = null; public static final Parcelable.Creator<Label> CREATOR = new Parcelable.Creator<Label>() {
public Label createFromParcel(Parcel in) {
private final boolean isEmptyLabel; return new Label(in);
private final String name; }
private final int count;
public Label[] newArray(int size) {
private Label(String name, int count, boolean isEmptyLabel) { return new Label[size];
this.name = name; }
this.count = count; };
this.isEmptyLabel = isEmptyLabel; private static String unnamedLabelText = null;
} private final boolean isEmptyLabel;
private final String name;
public Label(org.transdroid.daemon.Label daemonLabel) { private final int count;
this(daemonLabel.getName(), daemonLabel.getCount(), false);
} private Label(String name, int count, boolean isEmptyLabel) {
this.name = name;
@Override this.count = count;
public String getName() { this.isEmptyLabel = isEmptyLabel;
if (TextUtils.isEmpty(this.name)) { }
return unnamedLabelText;
} public Label(org.transdroid.daemon.Label daemonLabel) {
return this.name; this(daemonLabel.getName(), daemonLabel.getCount(), false);
} }
@Override private Label(Parcel in) {
public String getCode() { this.name = in.readString();
// Use the class name and label name to provide a unique navigation filter code this.count = in.readInt();
return Label.class.getSimpleName() + "_" + name; this.isEmptyLabel = in.readInt() == 1;
} }
public int getCount() { /**
return count; * Converts a list of labels as retrieved from a server daemon into a list of labels that can be used in the UI as navigation filters.
} *
* @param daemonLabels The raw list of labels as received from the server daemon adapter
public boolean isEmptyLabel() { * @param unnamedLabel The text to show for the empty label (i.e. the unnamed label)
return isEmptyLabel; * @return A label items that can be used in a filter list such as the action bar spinner
} */
public static ArrayList<Label> convertToNavigationLabels(List<org.transdroid.daemon.Label> daemonLabels, String unnamedLabel) {
/** if (daemonLabels == null) {
* Returns true if the torrent label's name matches this (selected) label's name, false otherwise return null;
* @param torrent The torrent to match against this label }
* @param dormantAsInactive This property is ignored for label comparisons ArrayList<Label> localLabels = new ArrayList<>();
*/ unnamedLabelText = unnamedLabel;
@Override
public boolean matches(Torrent torrent, boolean dormantAsInactive) { for (org.transdroid.daemon.Label label : daemonLabels) {
if (isEmptyLabel) { if (label != null && !TextUtils.isEmpty(label.getName())) {
return TextUtils.isEmpty(torrent.getLabelName()); localLabels.add(new Label(label));
} }
return torrent.getLabelName() != null && torrent.getLabelName().equals(name); }
} Collections.sort(localLabels);
@Override // force unlabelled to be at the top
public int compareTo(Label another) { localLabels.add(0, new Label(unnamedLabel, -1, true));
return this.name.compareTo(another.getName());
} return localLabels;
}
/**
* Converts a list of labels as retrieved from a server daemon into a list of labels that can be used in the UI as navigation filters. @Override
* @param daemonLabels The raw list of labels as received from the server daemon adapter public String getName() {
* @param unnamedLabel The text to show for the empty label (i.e. the unnamed label) if (TextUtils.isEmpty(this.name)) {
* @return A label items that can be used in a filter list such as the action bar spinner return unnamedLabelText;
*/ }
public static ArrayList<Label> convertToNavigationLabels(List<org.transdroid.daemon.Label> daemonLabels, String unnamedLabel) { return this.name;
if (daemonLabels == null) { }
return null;
} @Override
ArrayList<Label> localLabels = new ArrayList<>(); public String getCode() {
unnamedLabelText = unnamedLabel; // Use the class name and label name to provide a unique navigation filter code
return Label.class.getSimpleName() + "_" + name;
for (org.transdroid.daemon.Label label : daemonLabels) { }
if (label != null && !TextUtils.isEmpty(label.getName())) {
localLabels.add(new Label(label)); public int getCount() {
} return count;
} }
Collections.sort(localLabels);
public boolean isEmptyLabel() {
// force unlabelled to be at the top return isEmptyLabel;
localLabels.add(0, new Label(unnamedLabel, -1, true)); }
return localLabels; /**
} * Returns true if the torrent label's name matches this (selected) label's name, false otherwise
*
private Label(Parcel in) { * @param torrent The torrent to match against this label
this.name = in.readString(); * @param dormantAsInactive This property is ignored for label comparisons
this.count = in.readInt(); */
this.isEmptyLabel = in.readInt() == 1; @Override
} public boolean matches(Torrent torrent, boolean dormantAsInactive) {
if (isEmptyLabel) {
public static final Parcelable.Creator<Label> CREATOR = new Parcelable.Creator<Label>() { return TextUtils.isEmpty(torrent.getLabelName());
public Label createFromParcel(Parcel in) { }
return new Label(in); return torrent.getLabelName() != null && torrent.getLabelName().equals(name);
} }
public Label[] newArray(int size) { @Override
return new Label[size]; public int compareTo(Label another) {
} return this.name.compareTo(another.getName());
}; }
@Override @Override
public int describeContents() { public int describeContents() {
return 0; return 0;
} }
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name); dest.writeString(name);
dest.writeInt(count); dest.writeInt(count);
dest.writeInt(isEmptyLabel ? 1 : 0); dest.writeInt(isEmptyLabel ? 1 : 0);
} }
} }

40
app/src/main/java/org/transdroid/core/gui/navigation/NavigationFilter.java

@ -22,29 +22,33 @@ import org.transdroid.daemon.Torrent;
/** /**
* Represents a filter, used in the app navigation, that can check if some torrent matches the user-set filter * Represents a filter, used in the app navigation, that can check if some torrent matches the user-set filter
*
* @author Eric Kok * @author Eric Kok
*/ */
public interface NavigationFilter extends Parcelable { public interface NavigationFilter extends Parcelable {
/** /**
* Implementations should check if the supplied torrent matches the filter; for example a label filter should return true if the torrent's label * Implementations should check if the supplied torrent matches the filter; for example a label filter should return true if the torrent's label
* equals this items label name. * equals this items label name.
* @param torrent The torrent to check for matches *
* @param dormantAsInactive If true, dormant (0KB/s, so no data transfer) torrents are never actively downloading or seeding * @param torrent The torrent to check for matches
* @return True if the torrent matches the filter and should be shown in the current screen, false otherwise * @param dormantAsInactive If true, dormant (0KB/s, so no data transfer) torrents are never actively downloading or seeding
*/ * @return True if the torrent matches the filter and should be shown in the current screen, false otherwise
boolean matches(Torrent torrent, boolean dormantAsInactive); */
boolean matches(Torrent torrent, boolean dormantAsInactive);
/** /**
* Implementations should return a name that can be shown to indicate the active filter * Implementations should return a name that can be shown to indicate the active filter
* @return The name of the filter item as string *
*/ * @return The name of the filter item as string
String getName(); */
String getName();
/** /**
* Implementations should return a code that (within reasonable expectations) uniquely identifies it in the list of navigation filters * Implementations should return a code that (within reasonable expectations) uniquely identifies it in the list of navigation filters
* @return The code to uniquely identify this specific navigation filter, such as the name with a class name prefix *
*/ * @return The code to uniquely identify this specific navigation filter, such as the name with a class name prefix
String getCode(); */
String getCode();
} }

490
app/src/main/java/org/transdroid/core/gui/navigation/NavigationHelper.java

@ -26,14 +26,13 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.style.TypefaceSpan; import android.text.style.TypefaceSpan;
import com.afollestad.materialdialogs.DialogAction; import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.MaterialDialog;
import com.nostra13.universalimageloader.cache.disc.impl.ext.LruDiskCache; import com.nostra13.universalimageloader.cache.disc.impl.ext.LruDiskCache;
import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator; import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator;
@ -54,250 +53,255 @@ import java.util.List;
/** /**
* Helper for activities to make navigation-related decisions, such as when a device can display a larger, tablet style layout or how to display * Helper for activities to make navigation-related decisions, such as when a device can display a larger, tablet style layout or how to display
* errors. * errors.
*
* @author Eric Kok * @author Eric Kok
*/ */
@SuppressLint("ResourceAsColor") @SuppressLint("ResourceAsColor")
@EBean @EBean
public class NavigationHelper { public class NavigationHelper {
private static final int REQUEST_TORRENT_READ_PERMISSION = 0; private static final int REQUEST_TORRENT_READ_PERMISSION = 0;
private static final int REQUEST_SETTINGS_READ_PERMISSION = 1; private static final int REQUEST_SETTINGS_READ_PERMISSION = 1;
private static final int REQUEST_SETTINGS_WRITE_PERMISSION = 2; private static final int REQUEST_SETTINGS_WRITE_PERMISSION = 2;
private static ImageLoader imageCache; private static ImageLoader imageCache;
@RootContext @RootContext
protected Context context; protected Context context;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN) /**
public boolean checkTorrentReadPermission(final Activity activity) { * Converts a string into a {@link Spannable} that displays the string in the Roboto Condensed font
return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT || *
checkPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_TORRENT_READ_PERMISSION); * @param string A plain text {@link String}
} * @return A {@link Spannable} that can be applied to supporting views (such as the action bar title) so that the input string will be displayed
* using the Roboto Condensed font (if the OS has this)
@TargetApi(Build.VERSION_CODES.JELLY_BEAN) */
public boolean checkSettingsReadPermission(final Activity activity) { public static SpannableString buildCondensedFontString(String string) {
return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT || if (string == null) {
checkPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_SETTINGS_READ_PERMISSION); return null;
} }
SpannableString s = new SpannableString(string);
@TargetApi(Build.VERSION_CODES.JELLY_BEAN) s.setSpan(new TypefaceSpan("sans-serif-condensed"), 0, s.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
public boolean checkSettingsWritePermission(final Activity activity) { return s;
return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT || }
checkPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE, REQUEST_SETTINGS_WRITE_PERMISSION);
} /**
* Analyses a torrent http or magnet URI and tries to come up with a reasonable human-readable name.
private boolean checkPermission(final Activity activity, final String permission, final int requestCode) { *
if (hasPermission(permission)) * @param rawTorrentUri The raw http:// or magnet: link to the torrent
// Permission already granted * @return A best-guess, reasonably long name for the linked torrent
return true; */
if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) { public static String extractNameFromUri(Uri rawTorrentUri) {
// Never asked again: show a dialog with an explanation
activity.runOnUiThread(new Runnable() { if (rawTorrentUri.getScheme() == null) {
public void run() { // Probably an incorrect URI; just return the whole thing
new MaterialDialog.Builder(context).content(R.string.permission_readtorrent).positiveText(android.R.string.ok) return rawTorrentUri.toString();
.onPositive(new MaterialDialog.SingleButtonCallback() { }
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { if (rawTorrentUri.getScheme().equals("magnet")) {
ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode); // Magnet links might have a dn (display name) parameter
} String dn = getQueryParameter(rawTorrentUri, "dn");
}).show(); if (dn != null && !dn.equals("")) {
} return dn;
}); }
return false; // If not, try to return the hash that is specified as xt (exact topci)
} String xt = getQueryParameter(rawTorrentUri, "xt");
// Permission not granted (and we asked for it already before) if (xt != null && !xt.equals("")) {
ActivityCompat.requestPermissions(activity, new String[]{permission}, REQUEST_TORRENT_READ_PERMISSION); return xt;
return false; }
} }
private boolean hasPermission(String requiredPermission) { if (rawTorrentUri.isHierarchical()) {
return ContextCompat.checkSelfPermission(context, requiredPermission) == PackageManager.PERMISSION_GRANTED; String path = rawTorrentUri.getPath();
} if (path != null) {
if (path.contains("/")) {
public Boolean handleTorrentReadPermissionResult(int requestCode, int[] grantResults) { path = path.substring(path.lastIndexOf("/"));
if (requestCode == REQUEST_TORRENT_READ_PERMISSION) { }
// Return permission granting result return path;
return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; }
} }
return null;
} // No idea what to do with this; return as is
return rawTorrentUri.toString();
public Boolean handleSettingsReadPermissionResult(int requestCode, int[] grantResults) { }
if (requestCode == REQUEST_SETTINGS_READ_PERMISSION) {
// Return permission granting result private static String getQueryParameter(Uri uri, String parameter) {
return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; int start = uri.toString().indexOf(parameter + "=");
} if (start >= 0) {
return null; int begin = start + (parameter + "=").length();
} int end = uri.toString().indexOf("&", begin);
return uri.toString().substring(begin, end >= 0 ? end : uri.toString().length());
public Boolean handleSettingsWritePermissionResult(int requestCode, int[] grantResults) { }
if (requestCode == REQUEST_SETTINGS_WRITE_PERMISSION) { return null;
// Return permission granting result }
return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
} @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
return null; public boolean checkTorrentReadPermission(final Activity activity) {
} return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT ||
checkPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_TORRENT_READ_PERMISSION);
/** }
* Converts a string into a {@link Spannable} that displays the string in the Roboto Condensed font
* @param string A plain text {@link String} @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
* @return A {@link Spannable} that can be applied to supporting views (such as the action bar title) so that the input string will be displayed public boolean checkSettingsReadPermission(final Activity activity) {
* using the Roboto Condensed font (if the OS has this) return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT ||
*/ checkPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_SETTINGS_READ_PERMISSION);
public static SpannableString buildCondensedFontString(String string) { }
if (string == null) {
return null; @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
} public boolean checkSettingsWritePermission(final Activity activity) {
SpannableString s = new SpannableString(string); return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT ||
s.setSpan(new TypefaceSpan("sans-serif-condensed"), 0, s.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); checkPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE, REQUEST_SETTINGS_WRITE_PERMISSION);
return s; }
}
private boolean checkPermission(final Activity activity, final String permission, final int requestCode) {
/** if (hasPermission(permission))
* Analyses a torrent http or magnet URI and tries to come up with a reasonable human-readable name. // Permission already granted
* @param rawTorrentUri The raw http:// or magnet: link to the torrent return true;
* @return A best-guess, reasonably long name for the linked torrent if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {
*/ // Never asked again: show a dialog with an explanation
public static String extractNameFromUri(Uri rawTorrentUri) { activity.runOnUiThread(() ->
new MaterialDialog.Builder(context)
if (rawTorrentUri.getScheme() == null) { .content(R.string.permission_readtorrent)
// Probably an incorrect URI; just return the whole thing .positiveText(android.R.string.ok)
return rawTorrentUri.toString(); .onPositive((dialog, which) ->
} ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode)).show());
return false;
if (rawTorrentUri.getScheme().equals("magnet")) { }
// Magnet links might have a dn (display name) parameter // Permission not granted (and we asked for it already before)
String dn = getQueryParameter(rawTorrentUri, "dn"); ActivityCompat.requestPermissions(activity, new String[]{permission}, REQUEST_TORRENT_READ_PERMISSION);
if (dn != null && !dn.equals("")) { return false;
return dn; }
}
// If not, try to return the hash that is specified as xt (exact topci) private boolean hasPermission(String requiredPermission) {
String xt = getQueryParameter(rawTorrentUri, "xt"); return ContextCompat.checkSelfPermission(context, requiredPermission) == PackageManager.PERMISSION_GRANTED;
if (xt != null && !xt.equals("")) { }
return xt;
} public Boolean handleTorrentReadPermissionResult(int requestCode, int[] grantResults) {
} if (requestCode == REQUEST_TORRENT_READ_PERMISSION) {
// Return permission granting result
if (rawTorrentUri.isHierarchical()) { return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
String path = rawTorrentUri.getPath(); }
if (path != null) { return null;
if (path.contains("/")) { }
path = path.substring(path.lastIndexOf("/"));
} public Boolean handleSettingsReadPermissionResult(int requestCode, int[] grantResults) {
return path; if (requestCode == REQUEST_SETTINGS_READ_PERMISSION) {
} // Return permission granting result
} return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
}
// No idea what to do with this; return as is return null;
return rawTorrentUri.toString(); }
}
public Boolean handleSettingsWritePermissionResult(int requestCode, int[] grantResults) {
private static String getQueryParameter(Uri uri, String parameter) { if (requestCode == REQUEST_SETTINGS_WRITE_PERMISSION) {
int start = uri.toString().indexOf(parameter + "="); // Return permission granting result
if (start >= 0) { return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
int begin = start + (parameter + "=").length(); }
int end = uri.toString().indexOf("&", begin); return null;
return uri.toString().substring(begin, end >= 0 ? end : uri.toString().length()); }
}
return null; /**
} * Returns (and initialises, if needed) an image cache that uses memory and (1MB) local storage.
*
/** * @return An image cache that loads web images synchronously and transparently
* Returns (and initialises, if needed) an image cache that uses memory and (1MB) local storage. */
* @return An image cache that loads web images synchronously and transparently public ImageLoader getImageCache() {
*/ if (imageCache == null) {
public ImageLoader getImageCache() { imageCache = ImageLoader.getInstance();
if (imageCache == null) { try {
imageCache = ImageLoader.getInstance(); LruDiskCache diskCache = new LruDiskCache(context.getCacheDir(), null, new Md5FileNameGenerator(), 640000, 25);
try { // @formatter:off
LruDiskCache diskCache = new LruDiskCache(context.getCacheDir(), null, new Md5FileNameGenerator(), 640000, 25); Builder imageCacheBuilder = new Builder(context)
// @formatter:off .defaultDisplayImageOptions(
Builder imageCacheBuilder = new Builder(context) new DisplayImageOptions.Builder()
.defaultDisplayImageOptions( .cacheInMemory(true)
new DisplayImageOptions.Builder() .cacheOnDisk(true)
.cacheInMemory(true) .imageScaleType(ImageScaleType.IN_SAMPLE_INT)
.cacheOnDisk(true) .showImageForEmptyUri(R.drawable.ic_launcher).build())
.imageScaleType(ImageScaleType.IN_SAMPLE_INT) .memoryCache(new UsingFreqLimitedMemoryCache(1024 * 1024))
.showImageForEmptyUri(R.drawable.ic_launcher).build()) .diskCache(diskCache);
.memoryCache(new UsingFreqLimitedMemoryCache(1024 * 1024)) imageCache.init(imageCacheBuilder.build());
.diskCache(diskCache); // @formatter:on
imageCache.init(imageCacheBuilder.build()); } catch (IOException e) {
// @formatter:on // The cache directory is always available on Android; ignore this exception
} catch (IOException e) { }
// The cache directory is always available on Android; ignore this exception }
} return imageCache;
} }
return imageCache;
} public void forceOpenInBrowser(Uri link) {
Intent intent = new Intent(Intent.ACTION_VIEW).setData(link);
public void forceOpenInBrowser(Uri link) { List<ResolveInfo> activities = context.getPackageManager().queryIntentActivities(intent, 0);
Intent intent = new Intent(Intent.ACTION_VIEW).setData(link); for (ResolveInfo resolveInfo : activities) {
List<ResolveInfo> activities = context.getPackageManager().queryIntentActivities(intent, 0); if (activities.size() == 1 || (resolveInfo.isDefault && resolveInfo.activityInfo.packageName.equals(context.getPackageName()))) {
for (ResolveInfo resolveInfo : activities) { // There is a default browser; use this
if (activities.size() == 1 || (resolveInfo.isDefault && resolveInfo.activityInfo.packageName.equals(context.getPackageName()))) { intent.setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);
// There is a default browser; use this return;
intent.setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name); }
return; }
} // No default browser found: open chooser
} try {
// No default browser found: open chooser context.startActivity(Intent.createChooser(intent, "Open..."));
try { } catch (Exception e) {
context.startActivity(Intent.createChooser(intent, "Open...")); // No browser installed; consume and fail silently
} catch (Exception e) { }
// No browser installed; consume and fail silently }
}
} /**
* Returns the application name (like Transdroid) and version name (like 1.5.0), appended by the version code (like 180).
/** *
* Returns the application name (like Transdroid) and version name (like 1.5.0), appended by the version code (like 180). * @return The app name and version, such as 'Transdroid 1.5.0 (180)'
* @return The app name and version, such as 'Transdroid 1.5.0 (180)' */
*/ public String getAppNameAndVersion() {
public String getAppNameAndVersion() { return context.getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")";
return context.getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME + " (" + Integer.toString(BuildConfig.VERSION_CODE) + ")"; }
}
/**
/** * Returns whether the device is considered small (i.e. a phone) rather than large (i.e. a tablet). Can, for example, be used to determine if a
* Returns whether the device is considered small (i.e. a phone) rather than large (i.e. a tablet). Can, for example, be used to determine if a * dialog should be shown full screen. Currently is true if the device's smallest dimension is 500 dip.
* dialog should be shown full screen. Currently is true if the device's smallest dimension is 500 dip. *
* @return True if the app runs on a small device, false otherwise * @return True if the app runs on a small device, false otherwise
*/ */
public boolean isSmallScreen() { public boolean isSmallScreen() {
return context.getResources().getBoolean(R.bool.show_dialog_fullscreen); return context.getResources().getBoolean(R.bool.show_dialog_fullscreen);
} }
/** /**
* Whether any search-related UI components should be shown in the interface. At the moment returns false only if we run as Transdroid Lite * Whether any search-related UI components should be shown in the interface. At the moment returns false only if we run as Transdroid Lite
* version. * version.
* @return True if search is enabled, false otherwise *
*/ * @return True if search is enabled, false otherwise
public boolean enableSearchUi() { */
return context.getResources().getBoolean(R.bool.search_available); public boolean enableSearchUi() {
} return context.getResources().getBoolean(R.bool.search_available);
}
/**
* Whether any RSS-related UI components should be shown in the interface. At the moment returns false only if we run as Transdroid Lite version. /**
* @return True if search is enabled, false otherwise * Whether any RSS-related UI components should be shown in the interface. At the moment returns false only if we run as Transdroid Lite version.
*/ *
public boolean enableRssUi() { * @return True if search is enabled, false otherwise
return context.getResources().getBoolean(R.bool.rss_available); */
} public boolean enableRssUi() {
return context.getResources().getBoolean(R.bool.rss_available);
/** }
* Returns whether any seedbox-related components should be shown in the interface; specifically the option to add server settings via easy
* seedbox-specific screens. /**
* @return True if seedbox settings should be shown, false otherwise * Returns whether any seedbox-related components should be shown in the interface; specifically the option to add server settings via easy
*/ * seedbox-specific screens.
public boolean enableSeedboxes() { *
return context.getResources().getBoolean(R.bool.seedboxes_available); * @return True if seedbox settings should be shown, false otherwise
} */
public boolean enableSeedboxes() {
/** return context.getResources().getBoolean(R.bool.seedboxes_available);
* Whether the custom app update checker should be used to check for new app and search module versions. }
* @return True if it should be checked against transdroid.org if there are app updates (as opposed to using the Play Store for updates, for
* example), false otherwise /**
*/ * Whether the custom app update checker should be used to check for new app and search module versions.
public boolean enableUpdateChecker() { *
return context.getResources().getBoolean(R.bool.updatecheck_available); * @return True if it should be checked against transdroid.org if there are app updates (as opposed to using the Play Store for updates, for
} * example), false otherwise
*/
public boolean enableUpdateChecker() {
return context.getResources().getBoolean(R.bool.updatecheck_available);
}
} }

3
app/src/main/java/org/transdroid/core/gui/navigation/RefreshableActivity.java

@ -18,10 +18,11 @@ package org.transdroid.core.gui.navigation;
/** /**
* Interface to be implemented by any activity that allows its content to be refreshed; fragments can ask for user-initiated refreshes. * Interface to be implemented by any activity that allows its content to be refreshed; fragments can ask for user-initiated refreshes.
*
* @author Eric Kok * @author Eric Kok
*/ */
public interface RefreshableActivity { public interface RefreshableActivity {
void refreshScreen(); void refreshScreen();
} }

201
app/src/main/java/org/transdroid/core/gui/navigation/SelectionManagerMode.java

@ -16,9 +16,6 @@
*/ */
package org.transdroid.core.gui.navigation; package org.transdroid.core.gui.navigation;
import org.transdroid.core.gui.navigation.SelectionModificationSpinner.OnModificationActionSelectedListener;
import org.transdroid.daemon.Finishable;
import android.content.Context; import android.content.Context;
import android.util.SparseBooleanArray; import android.util.SparseBooleanArray;
import android.view.ActionMode; import android.view.ActionMode;
@ -28,119 +25,125 @@ import android.view.ViewGroup;
import android.widget.AbsListView.MultiChoiceModeListener; import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.ListView; import android.widget.ListView;
import org.transdroid.core.gui.navigation.SelectionModificationSpinner.OnModificationActionSelectedListener;
import org.transdroid.daemon.Finishable;
/** /**
* A helper to implement {@link ListView} selection modification behaviour with the {@link SelectionModificationSpinner} * A helper to implement {@link ListView} selection modification behaviour with the {@link SelectionModificationSpinner}
* by implementing the specific actions and providing a title based on the number of currently selected items. It is * by implementing the specific actions and providing a title based on the number of currently selected items. It is
* important that the provided list was instantiated already. * important that the provided list was instantiated already.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class SelectionManagerMode implements MultiChoiceModeListener, OnModificationActionSelectedListener { public class SelectionManagerMode implements MultiChoiceModeListener, OnModificationActionSelectedListener {
private final Context themedContext; private final Context themedContext;
private final ListView managedList; private final ListView managedList;
private final int titleTemplateResource; private final int titleTemplateResource;
private Class<?> onlyCheckClass = null; private Class<?> onlyCheckClass = null;
/** /**
* Instantiates the helper by binding it to a specific {@link ListView} and providing the text resource to display * Instantiates the helper by binding it to a specific {@link ListView} and providing the text resource to display
* as title in the spinner. * as title in the spinner.
* @param themedContext The context which is associated with the correct theme to apply when inflating views, i.e. the toolbar context *
* @param managedList The list to manage the selection for and execute selection action to * @param themedContext The context which is associated with the correct theme to apply when inflating views, i.e. the toolbar context
* @param titleTemplateResource The string resource id to show as the spinners title; the number of selected items * @param managedList The list to manage the selection for and execute selection action to
* will be supplied as numeric formatting argument * @param titleTemplateResource The string resource id to show as the spinners title; the number of selected items
*/ * will be supplied as numeric formatting argument
public SelectionManagerMode(Context themedContext, ListView managedList, int titleTemplateResource) { */
this.themedContext = themedContext; public SelectionManagerMode(Context themedContext, ListView managedList, int titleTemplateResource) {
this.managedList = managedList; this.themedContext = themedContext;
this.titleTemplateResource = titleTemplateResource; this.managedList = managedList;
} this.titleTemplateResource = titleTemplateResource;
}
/** /**
* Set the class type of items that are allowed to be checked in the {@link ListView}. Defaults to null, which means * Set the class type of items that are allowed to be checked in the {@link ListView}. Defaults to null, which means
* every list view row can be checked. * every list view row can be checked.
* @param onlyCheckClass The {@link Class} instance to use to check list item types against *
*/ * @param onlyCheckClass The {@link Class} instance to use to check list item types against
public void setOnlyCheckClass(Class<?> onlyCheckClass) { */
this.onlyCheckClass = onlyCheckClass; public void setOnlyCheckClass(Class<?> onlyCheckClass) {
} this.onlyCheckClass = onlyCheckClass;
}
@Override @Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) { public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Allow modification of selection through a spinner // Allow modification of selection through a spinner
SelectionModificationSpinner selectionSpinner = new SelectionModificationSpinner(themedContext); SelectionModificationSpinner selectionSpinner = new SelectionModificationSpinner(themedContext);
selectionSpinner.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, selectionSpinner.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT)); ViewGroup.LayoutParams.MATCH_PARENT));
selectionSpinner.setOnModificationActionSelectedListener(this); selectionSpinner.setOnModificationActionSelectedListener(this);
mode.setCustomView(selectionSpinner); mode.setCustomView(selectionSpinner);
return true; return true;
} }
@Override @Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) { public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false; return false;
} }
@Override @Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
int checkedCount = 0; int checkedCount = 0;
for (int i = 0; i < managedList.getCheckedItemPositions().size(); i++) { for (int i = 0; i < managedList.getCheckedItemPositions().size(); i++) {
if (managedList.getCheckedItemPositions().valueAt(i) if (managedList.getCheckedItemPositions().valueAt(i)
&& (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(managedList && (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(managedList
.getCheckedItemPositions().keyAt(i))))) .getCheckedItemPositions().keyAt(i)))))
checkedCount++; checkedCount++;
} }
((SelectionModificationSpinner) mode.getCustomView()).updateTitle(themedContext.getResources() ((SelectionModificationSpinner) mode.getCustomView()).updateTitle(themedContext.getResources()
.getQuantityString(titleTemplateResource, checkedCount, checkedCount)); .getQuantityString(titleTemplateResource, checkedCount, checkedCount));
} }
@Override @Override
public void onDestroyActionMode(ActionMode mode) { public void onDestroyActionMode(ActionMode mode) {
} }
@Override @Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) { public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return false; return false;
} }
/** /**
* Implements the {@link SelectionModificationSpinner}'s invert selection command by flipping the checked status for * Implements the {@link SelectionModificationSpinner}'s invert selection command by flipping the checked status for
* each (enabled) items in the {@link ListView}. * each (enabled) items in the {@link ListView}.
*/ */
@Override @Override
public void invertSelection() { public void invertSelection() {
SparseBooleanArray checked = managedList.getCheckedItemPositions(); SparseBooleanArray checked = managedList.getCheckedItemPositions();
for (int i = 0; i < managedList.getAdapter().getCount(); i++) { for (int i = 0; i < managedList.getAdapter().getCount(); i++) {
if (managedList.getAdapter().isEnabled(i) if (managedList.getAdapter().isEnabled(i)
&& (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(i)))) && (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(i))))
managedList.setItemChecked(i, !checked.get(i, false)); managedList.setItemChecked(i, !checked.get(i, false));
} }
} }
/** /**
* Implements the {@link SelectionModificationSpinner}'s select all command by checking each (enabled) item in the * Implements the {@link SelectionModificationSpinner}'s select all command by checking each (enabled) item in the
* {@link ListView}. * {@link ListView}.
*/ */
@Override @Override
public void selectAll() { public void selectAll() {
for (int i = 0; i < managedList.getAdapter().getCount(); i++) { for (int i = 0; i < managedList.getAdapter().getCount(); i++) {
if (managedList.getAdapter().isEnabled(i) if (managedList.getAdapter().isEnabled(i)
&& (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(i)))) && (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(i))))
managedList.setItemChecked(i, true); managedList.setItemChecked(i, true);
} }
} }
/** /**
* Implements the {@link SelectionModificationSpinner}'s select finished command by checking each (enabled) item * Implements the {@link SelectionModificationSpinner}'s select finished command by checking each (enabled) item
* that represents something that is {@link Finishable} and indeed is finished; * that represents something that is {@link Finishable} and indeed is finished;
*/ */
@Override @Override
public void selectFinished() { public void selectFinished() {
for (int i = 0; i < managedList.getAdapter().getCount(); i++) { for (int i = 0; i < managedList.getAdapter().getCount(); i++) {
if (managedList.getAdapter().isEnabled(i) if (managedList.getAdapter().isEnabled(i)
&& (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(i))) && (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(i)))
&& managedList.getItemAtPosition(i) instanceof Finishable) && managedList.getItemAtPosition(i) instanceof Finishable)
managedList.setItemChecked(i, ((Finishable) managedList.getItemAtPosition(i)).isFinished()); managedList.setItemChecked(i, ((Finishable) managedList.getItemAtPosition(i)).isFinished());
} }
} }
} }

179
app/src/main/java/org/transdroid/core/gui/navigation/SelectionModificationSpinner.java

@ -16,102 +16,109 @@
*/ */
package org.transdroid.core.gui.navigation; package org.transdroid.core.gui.navigation;
import org.transdroid.R;
import android.content.Context; import android.content.Context;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import androidx.appcompat.widget.AppCompatSpinner;
import org.transdroid.R;
/** /**
* Spinner that holds actions that can be performed on list selections. The spinner itself has some title, which can for * Spinner that holds actions that can be performed on list selections. The spinner itself has some title, which can for
* example be used to show the number of selected items. * example be used to show the number of selected items.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class SelectionModificationSpinner extends Spinner { public class SelectionModificationSpinner extends AppCompatSpinner {
private SelectionDropDownAdapter selectionAdapter; private SelectionDropDownAdapter selectionAdapter;
private OnModificationActionSelectedListener onModificationActionSelected = null; private OnModificationActionSelectedListener onModificationActionSelected = null;
/** /**
* Instantiates a spinner that contains some fixed actions for a user to modify selections. * Instantiates a spinner that contains some fixed actions for a user to modify selections.
* @param context The interface context where the spinner will be shown in *
*/ * @param context The interface context where the spinner will be shown in
public SelectionModificationSpinner(Context context) { */
super(context); public SelectionModificationSpinner(Context context) {
selectionAdapter = new SelectionDropDownAdapter(context); super(context);
setAdapter(selectionAdapter); selectionAdapter = new SelectionDropDownAdapter(context);
} setAdapter(selectionAdapter);
}
/**
* Updates the fixed title text shown in the spinner, regardless of spinner item action selection. /**
* @param title The new static string to show, such as the number of selected items * Updates the fixed title text shown in the spinner, regardless of spinner item action selection.
*/ *
public void updateTitle(String title) { * @param title The new static string to show, such as the number of selected items
selectionAdapter.titleView.setText(title); */
invalidate(); public void updateTitle(String title) {
} selectionAdapter.titleView.setText(title);
invalidate();
/** }
* Sets the listener for action selection events.
* @param onModificationActionSelected The listener that handles performing of the actions as selected in this /**
* spinner by the user * Sets the listener for action selection events.
*/ *
public void setOnModificationActionSelectedListener(OnModificationActionSelectedListener onModificationActionSelected) { * @param onModificationActionSelected The listener that handles performing of the actions as selected in this
this.onModificationActionSelected = onModificationActionSelected; * spinner by the user
} */
public void setOnModificationActionSelectedListener(OnModificationActionSelectedListener onModificationActionSelected) {
@Override this.onModificationActionSelected = onModificationActionSelected;
public void setSelection(int position) { }
if (position == 0) {
onModificationActionSelected.selectAll(); @Override
} else if (position == 1) { public void setSelection(int position) {
onModificationActionSelected.selectFinished(); if (position == 0) {
} else if (position == 2) { onModificationActionSelected.selectAll();
onModificationActionSelected.invertSelection(); } else if (position == 1) {
} onModificationActionSelected.selectFinished();
super.setSelection(position); } else if (position == 2) {
} onModificationActionSelected.invertSelection();
}
/** super.setSelection(position);
* Local adapter that holds the actions which can be performed and a title text view that always shows instead of a }
* list item as in a normal spinner.
*/ /**
private class SelectionDropDownAdapter extends ArrayAdapter<String> { * Interface to implement if an interface want to respond to selection modification actions.
*/
protected TextView titleView = null; public interface OnModificationActionSelectedListener {
void selectAll();
public SelectionDropDownAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1, new String[] { void selectFinished();
context.getString(R.string.navigation_selectall),
context.getString(R.string.navigation_selectfinished), void invertSelection();
context.getString(R.string.navigation_invertselection) }); }
titleView = new TextView(getContext());
} /**
* Local adapter that holds the actions which can be performed and a title text view that always shows instead of a
@Override * list item as in a normal spinner.
public View getView(int position, View convertView, ViewGroup parent) { */
// This returns the singleton text view showing the title with the number of selected items private static class SelectionDropDownAdapter extends ArrayAdapter<String> {
return titleView;
} protected TextView titleView = null;
@Override public SelectionDropDownAdapter(Context context) {
public View getDropDownView(int position, View convertView, ViewGroup parent) { super(context, android.R.layout.simple_list_item_1, new String[]{
// This returns the actions to show in the spinner list context.getString(R.string.navigation_selectall),
return super.getView(position, convertView, parent); context.getString(R.string.navigation_selectfinished),
} context.getString(R.string.navigation_invertselection)});
titleView = new TextView(getContext());
} }
/** @Override
* Interface to implement if an interface want to respond to selection modification actions. public View getView(int position, View convertView, ViewGroup parent) {
*/ // This returns the singleton text view showing the title with the number of selected items
public interface OnModificationActionSelectedListener { return titleView;
public void selectAll(); }
public void selectFinished();
public void invertSelection(); @Override
} public View getDropDownView(int position, View convertView, ViewGroup parent) {
// This returns the actions to show in the spinner list
return super.getView(position, convertView, parent);
}
}
} }

111
app/src/main/java/org/transdroid/core/gui/navigation/SetLabelDialog.java

@ -20,8 +20,6 @@ import android.content.Context;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ListView; import android.widget.ListView;
@ -37,71 +35,64 @@ import java.util.List;
public class SetLabelDialog { public class SetLabelDialog {
/** /**
* A dialog fragment that allows picking a label or entering a new label to set this new label to the torrent. * A dialog fragment that allows picking a label or entering a new label to set this new label to the torrent.
* @param context The activity context that opens (and owns) this dialog *
* @param onLabelPickedListener The callback when a new label has been entered or picked by the user * @param context The activity context that opens (and owns) this dialog
* @param currentLabels The list of labels as currently exist on the server, to present as list for easy selection * @param onLabelPickedListener The callback when a new label has been entered or picked by the user
*/ * @param currentLabels The list of labels as currently exist on the server, to present as list for easy selection
public static void show(final Context context, final OnLabelPickedListener onLabelPickedListener, List<Label> currentLabels) { */
public static void show(final Context context, final OnLabelPickedListener onLabelPickedListener, List<Label> currentLabels) {
// Discard the empty label in this list before storing it locally // Discard the empty label in this list before storing it locally
for (Iterator<Label> iter = currentLabels.iterator(); iter.hasNext(); ) { for (Iterator<Label> iter = currentLabels.iterator(); iter.hasNext(); ) {
if (iter.next().isEmptyLabel()) { if (iter.next().isEmptyLabel()) {
iter.remove(); iter.remove();
} }
} }
final View setLabelLayout = LayoutInflater.from(context).inflate(R.layout.dialog_setlabel, null); final View setLabelLayout = LayoutInflater.from(context).inflate(R.layout.dialog_setlabel, null);
final ListView labelsList = (ListView) setLabelLayout.findViewById(R.id.labels_list); final ListView labelsList = (ListView) setLabelLayout.findViewById(R.id.labels_list);
final EditText newLabelEdit = (EditText) setLabelLayout.findViewById(R.id.newlabel_edit); final EditText newLabelEdit = (EditText) setLabelLayout.findViewById(R.id.newlabel_edit);
MaterialDialog.Builder builder = new MaterialDialog.Builder(context) MaterialDialog.Builder builder = new MaterialDialog.Builder(context)
.customView(setLabelLayout, false) .customView(setLabelLayout, false)
.positiveText(R.string.status_update) .positiveText(R.string.status_update)
.neutralText(R.string.status_label_remove) .neutralText(R.string.status_label_remove)
.negativeText(android.R.string.cancel) .negativeText(android.R.string.cancel)
.callback(new MaterialDialog.ButtonCallback() { .onPositive((dialog, which) -> {
@Override // User should have provided a new label
public void onPositive(MaterialDialog dialog) { if (TextUtils.isEmpty(newLabelEdit.getText())) {
// User should have provided a new label SnackbarManager.show(Snackbar.with(context).text(R.string.error_notalabel).colorResource(R.color.red));
if (TextUtils.isEmpty(newLabelEdit.getText())) { return;
SnackbarManager.show(Snackbar.with(context).text(R.string.error_notalabel).colorResource(R.color.red)); }
return; onLabelPickedListener.onLabelPicked(newLabelEdit.getText().toString());
} })
onLabelPickedListener.onLabelPicked(newLabelEdit.getText().toString()); .onNeutral((dialog, which) ->
} onLabelPickedListener.onLabelPicked(null));
@Override final MaterialDialog dialog = SettingsUtils
public void onNeutral(MaterialDialog dialog) { .applyDialogTheme(builder)
onLabelPickedListener.onLabelPicked(null); .build();
}
});
final MaterialDialog dialog = SettingsUtils
.applyDialogTheme(builder)
.build();
if (currentLabels.size() == 0) { if (currentLabels.size() == 0) {
// Hide the list (and its label) if there are no labels yet // Hide the list (and its label) if there are no labels yet
setLabelLayout.findViewById(R.id.pick_label).setVisibility(View.GONE); setLabelLayout.findViewById(R.id.pick_label).setVisibility(View.GONE);
labelsList.setVisibility(View.GONE); labelsList.setVisibility(View.GONE);
} else { } else {
labelsList.setAdapter(new FilterListItemAdapter(context, currentLabels)); labelsList.setAdapter(new FilterListItemAdapter(context, currentLabels));
labelsList.setOnItemClickListener(new OnItemClickListener() { labelsList.setOnItemClickListener((parent, view, position, id) -> {
@Override onLabelPickedListener.onLabelPicked(((Label) labelsList.getItemAtPosition(position)).getName());
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { dialog.dismiss();
onLabelPickedListener.onLabelPicked(((Label) labelsList.getItemAtPosition(position)).getName()); });
dialog.dismiss(); }
}
});
}
dialog.show(); dialog.show();
} }
public interface OnLabelPickedListener { public interface OnLabelPickedListener {
void onLabelPicked(String newLabel); void onLabelPicked(String newLabel);
} }
} }

58
app/src/main/java/org/transdroid/core/gui/navigation/SetStorageLocationDialog.java

@ -28,35 +28,33 @@ import org.transdroid.core.app.settings.SettingsUtils;
public class SetStorageLocationDialog { public class SetStorageLocationDialog {
/** /**
* A dialog fragment that allows changing of the storage location by editing the path text directly. * A dialog fragment that allows changing of the storage location by editing the path text directly.
* @param context The activity context that opens (and owns) this dialog *
* @param onStorageLocationUpdatedListener The callback for when the user is done updating the storage location * @param context The activity context that opens (and owns) this dialog
* @param currentLocation The current storage location that will be available to the user to edit * @param onStorageLocationUpdatedListener The callback for when the user is done updating the storage location
*/ * @param currentLocation The current storage location that will be available to the user to edit
public static void show(final Context context, final OnStorageLocationUpdatedListener onStorageLocationUpdatedListener, String currentLocation) { */
View locationLayout = LayoutInflater.from(context).inflate(R.layout.dialog_storagelocation, null); public static void show(final Context context, final OnStorageLocationUpdatedListener onStorageLocationUpdatedListener, String currentLocation) {
final EditText locationText = (EditText) locationLayout.findViewById(R.id.location_edit); View locationLayout = LayoutInflater.from(context).inflate(R.layout.dialog_storagelocation, null);
locationText.setText(currentLocation); final EditText locationText = (EditText) locationLayout.findViewById(R.id.location_edit);
MaterialDialog.Builder builder = new MaterialDialog.Builder(context) locationText.setText(currentLocation);
.customView(locationLayout, false) MaterialDialog.Builder builder = new MaterialDialog.Builder(context)
.positiveText(R.string.status_update) .customView(locationLayout, false)
.negativeText(android.R.string.cancel) .positiveText(R.string.status_update)
.callback(new MaterialDialog.ButtonCallback() { .negativeText(android.R.string.cancel)
@Override .onPositive((dialog, which) -> {
public void onPositive(MaterialDialog dialog) { // User is done editing and requested to update given the text input
// User is done editing and requested to update given the text input onStorageLocationUpdatedListener.onStorageLocationUpdated(locationText.getText().toString());
onStorageLocationUpdatedListener.onStorageLocationUpdated(locationText.getText().toString()); });
}
}); SettingsUtils
.applyDialogTheme(builder)
SettingsUtils .show();
.applyDialogTheme(builder) }
.show();
} public interface OnStorageLocationUpdatedListener {
void onStorageLocationUpdated(String newLocation);
public interface OnStorageLocationUpdatedListener { }
void onStorageLocationUpdated(String newLocation);
}
} }

55
app/src/main/java/org/transdroid/core/gui/navigation/SetTrackersDialog.java

@ -16,12 +16,13 @@
*/ */
package org.transdroid.core.gui.navigation; package org.transdroid.core.gui.navigation;
import android.app.DialogFragment;
import android.content.Context; import android.content.Context;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.EditText; import android.widget.EditText;
import androidx.fragment.app.DialogFragment;
import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.MaterialDialog;
import org.transdroid.R; import org.transdroid.R;
@ -32,32 +33,30 @@ import java.util.List;
public class SetTrackersDialog extends DialogFragment { public class SetTrackersDialog extends DialogFragment {
/** /**
* A dialog fragment that allows changing the trackers of a torrent by editing the text directly. * A dialog fragment that allows changing the trackers of a torrent by editing the text directly.
* @param context The activity context that opens (and owns) this dialog *
* @param onTrackersUpdatedListener The callback for when the user is done updating the trackers list * @param context The activity context that opens (and owns) this dialog
* @param currentTrackers The current trackers text/list that will be available to the user to edit * @param onTrackersUpdatedListener The callback for when the user is done updating the trackers list
*/ * @param currentTrackers The current trackers text/list that will be available to the user to edit
public static void show(final Context context, final OnTrackersUpdatedListener onTrackersUpdatedListener, String currentTrackers) { */
View trackersLayout = LayoutInflater.from(context).inflate(R.layout.dialog_trackers, null); public static void show(final Context context, final OnTrackersUpdatedListener onTrackersUpdatedListener, String currentTrackers) {
final EditText trackersText = (EditText) trackersLayout.findViewById(R.id.trackers_edit); View trackersLayout = LayoutInflater.from(context).inflate(R.layout.dialog_trackers, null);
trackersText.setText(currentTrackers); final EditText trackersText = (EditText) trackersLayout.findViewById(R.id.trackers_edit);
MaterialDialog.Builder builder = new MaterialDialog.Builder(context) trackersText.setText(currentTrackers);
.customView(trackersLayout, false) MaterialDialog.Builder builder = new MaterialDialog.Builder(context)
.positiveText(R.string.status_update) .customView(trackersLayout, false)
.negativeText(android.R.string.cancel) .positiveText(R.string.status_update)
.callback(new MaterialDialog.ButtonCallback() { .negativeText(android.R.string.cancel)
@Override .onPositive((dialog, which) -> {
public void onPositive(MaterialDialog dialog) { // User is done editing and requested to update given the text input
// User is done editing and requested to update given the text input onTrackersUpdatedListener.onTrackersUpdated(Arrays.asList(trackersText.getText().toString().split("\n")));
onTrackersUpdatedListener.onTrackersUpdated(Arrays.asList(trackersText.getText().toString().split("\n"))); });
} SettingsUtils.applyDialogTheme(builder).show();
}); }
SettingsUtils.applyDialogTheme(builder).show();
} public interface OnTrackersUpdatedListener {
void onTrackersUpdated(List<String> updatedTrackers);
public interface OnTrackersUpdatedListener { }
void onTrackersUpdated(List<String> updatedTrackers);
}
} }

153
app/src/main/java/org/transdroid/core/gui/navigation/SetTransferRatesDialog.java

@ -30,85 +30,78 @@ import org.transdroid.core.app.settings.SettingsUtils;
public class SetTransferRatesDialog { public class SetTransferRatesDialog {
/** private static OnClickListener onNumberClicked = v -> {
* A dialog fragment that allow picking of maximum download and upload transfer rates as well as the resetting of these values. // Append the text contents of the button itself as text to the current number (as reference in the view's
* @param context The activity context that opens (and owns) this dialog // tag)
* @param onRatesPickedListener The callback for results in this dialog (with newly selected values or a reset) TextView numberView = (TextView) v.getTag();
*/ if (numberView.getText().toString().equals(v.getContext().getString(R.string.status_maxspeed_novalue))) {
public static void show(final Context context, final OnRatesPickedListener onRatesPickedListener) { numberView.setText("");
}
View transferRatesLayout = LayoutInflater.from(context).inflate(R.layout.dialog_transferrates, null); numberView.setText(numberView.getText().toString() + ((Button) v).getText().toString());
final TextView maxSpeedDown = (TextView) transferRatesLayout.findViewById(R.id.maxspeeddown_text); };
final TextView maxSpeedUp = (TextView) transferRatesLayout.findViewById(R.id.maxspeedup_text);
/**
MaterialDialog.Builder builder = new MaterialDialog.Builder(context) * A dialog fragment that allow picking of maximum download and upload transfer rates as well as the resetting of these values.
.customView(transferRatesLayout, false) *
.positiveText(R.string.status_update) * @param context The activity context that opens (and owns) this dialog
.neutralText(R.string.status_maxspeed_reset) * @param onRatesPickedListener The callback for results in this dialog (with newly selected values or a reset)
.negativeText(android.R.string.cancel) */
.callback(new MaterialDialog.ButtonCallback() { public static void show(final Context context, final OnRatesPickedListener onRatesPickedListener) {
@Override
public void onPositive(MaterialDialog dialog) { View transferRatesLayout = LayoutInflater.from(context).inflate(R.layout.dialog_transferrates, null);
int maxDown = -1, maxUp = -1; final TextView maxSpeedDown = (TextView) transferRatesLayout.findViewById(R.id.maxspeeddown_text);
try { final TextView maxSpeedUp = (TextView) transferRatesLayout.findViewById(R.id.maxspeedup_text);
maxDown = Integer.parseInt(maxSpeedDown.getText().toString());
maxUp = Integer.parseInt(maxSpeedUp.getText().toString()); MaterialDialog.Builder builder = new MaterialDialog.Builder(context)
} catch (NumberFormatException e) { .customView(transferRatesLayout, false)
// Impossible as we only input via the number buttons .positiveText(R.string.status_update)
} .neutralText(R.string.status_maxspeed_reset)
if (maxDown <= 0 || maxUp <= 0) { .negativeText(android.R.string.cancel)
onRatesPickedListener.onInvalidNumber(); .onPositive((dialog, which) -> {
return; int maxDown = -1, maxUp = -1;
} try {
onRatesPickedListener.onRatesPicked(maxDown, maxUp); maxDown = Integer.parseInt(maxSpeedDown.getText().toString());
} maxUp = Integer.parseInt(maxSpeedUp.getText().toString());
} catch (NumberFormatException e) {
@Override // Impossible as we only input via the number buttons
public void onNeutral(MaterialDialog dialog) { }
onRatesPickedListener.resetRates(); if (maxDown <= 0 || maxUp <= 0) {
} onRatesPickedListener.onInvalidNumber();
}); return;
MaterialDialog dialog = SettingsUtils.applyDialogTheme(builder).build(); }
onRatesPickedListener.onRatesPicked(maxDown, maxUp);
bindButtons(dialog.getCustomView(), maxSpeedDown, R.id.down1Button, R.id.down2Button, R.id.down3Button, R.id.down4Button, R.id.down5Button, })
R.id.down6Button, R.id.down7Button, R.id.down8Button, R.id.down9Button, R.id.down0Button); .onNeutral((dialog, which) ->
bindButtons(dialog.getCustomView(), maxSpeedUp, R.id.up1Button, R.id.up2Button, R.id.up3Button, R.id.up4Button, R.id.up5Button, onRatesPickedListener.resetRates());
R.id.up6Button, R.id.up7Button, R.id.up8Button, R.id.up9Button, R.id.up0Button);
MaterialDialog dialog = SettingsUtils.applyDialogTheme(builder).build();
dialog.show();
bindButtons(dialog.getCustomView(), maxSpeedDown, R.id.down1Button, R.id.down2Button, R.id.down3Button, R.id.down4Button, R.id.down5Button,
} R.id.down6Button, R.id.down7Button, R.id.down8Button, R.id.down9Button, R.id.down0Button);
bindButtons(dialog.getCustomView(), maxSpeedUp, R.id.up1Button, R.id.up2Button, R.id.up3Button, R.id.up4Button, R.id.up5Button,
private static void bindButtons(View transferRatesContent, View numberView, int... buttonResource) { R.id.up6Button, R.id.up7Button, R.id.up8Button, R.id.up9Button, R.id.up0Button);
for (int i : buttonResource) {
// Keep the relevant number as reference in the view tag and bind the click listerner dialog.show();
transferRatesContent.findViewById(i).setTag(numberView);
transferRatesContent.findViewById(i).setOnClickListener(onNumberClicked); }
}
} private static void bindButtons(View transferRatesContent, View numberView, int... buttonResource) {
for (int i : buttonResource) {
private static OnClickListener onNumberClicked = new OnClickListener() { // Keep the relevant number as reference in the view tag and bind the click listerner
@Override transferRatesContent.findViewById(i).setTag(numberView);
public void onClick(View v) { transferRatesContent.findViewById(i).setOnClickListener(onNumberClicked);
// Append the text contents of the button itself as text to the current number (as reference in the view's }
// tag) }
TextView numberView = (TextView) v.getTag();
if (numberView.getText().toString().equals(v.getContext().getString(R.string.status_maxspeed_novalue))) { /**
numberView.setText(""); * Listener interface to the user having picked or wanting to resets the current maximum transfer speeds;
} */
numberView.setText(numberView.getText().toString() + ((Button) v).getText().toString()); public interface OnRatesPickedListener {
} void onRatesPicked(int maxDownloadSpeed, int maxUploadSpeed);
};
void resetRates();
/**
* Listener interface to the user having picked or wanting to resets the current maximum transfer speeds; void onInvalidNumber();
*/ }
public interface OnRatesPickedListener {
void onRatesPicked(int maxDownloadSpeed, int maxUploadSpeed);
void resetRates();
void onInvalidNumber();
}
} }

270
app/src/main/java/org/transdroid/core/gui/navigation/StatusType.java

@ -16,150 +16,154 @@
*/ */
package org.transdroid.core.gui.navigation; package org.transdroid.core.gui.navigation;
import java.util.Arrays; import android.content.Context;
import java.util.List; import android.os.Parcel;
import android.os.Parcelable;
import org.transdroid.R; import org.transdroid.R;
import org.transdroid.core.gui.lists.SimpleListItem; import org.transdroid.core.gui.lists.SimpleListItem;
import org.transdroid.daemon.Torrent; import org.transdroid.daemon.Torrent;
import android.content.Context; import java.util.Arrays;
import android.os.Parcel; import java.util.List;
import android.os.Parcelable;
/** /**
* Enumeration of all status types, which filter the list of shown torrents based on transfer activity. * Enumeration of all status types, which filter the list of shown torrents based on transfer activity.
*
* @author Eric Kok * @author Eric Kok
*/ */
public enum StatusType { public enum StatusType {
ShowAll { ShowAll {
public StatusTypeFilter getFilterItem(Context context) { public StatusTypeFilter getFilterItem(Context context) {
return new StatusTypeFilter(StatusType.ShowAll, context.getString(R.string.navigation_status_showall)); return new StatusTypeFilter(StatusType.ShowAll, context.getString(R.string.navigation_status_showall));
} }
}, },
OnlyDownloading { OnlyDownloading {
public StatusTypeFilter getFilterItem(Context context) { public StatusTypeFilter getFilterItem(Context context) {
return new StatusTypeFilter(StatusType.OnlyDownloading, context.getString(R.string.navigation_status_onlydown)); return new StatusTypeFilter(StatusType.OnlyDownloading, context.getString(R.string.navigation_status_onlydown));
} }
}, },
OnlyUploading { OnlyUploading {
public StatusTypeFilter getFilterItem(Context context) { public StatusTypeFilter getFilterItem(Context context) {
return new StatusTypeFilter(StatusType.OnlyUploading, context.getString(R.string.navigation_status_onlyup)); return new StatusTypeFilter(StatusType.OnlyUploading, context.getString(R.string.navigation_status_onlyup));
} }
}, },
OnlyActive { OnlyActive {
public StatusTypeFilter getFilterItem(Context context) { public StatusTypeFilter getFilterItem(Context context) {
return new StatusTypeFilter(StatusType.OnlyActive, context.getString(R.string.navigation_status_onlyactive)); return new StatusTypeFilter(StatusType.OnlyActive, context.getString(R.string.navigation_status_onlyactive));
} }
}, },
OnlyInactive { OnlyInactive {
public StatusTypeFilter getFilterItem(Context context) { public StatusTypeFilter getFilterItem(Context context) {
return new StatusTypeFilter(StatusType.OnlyInactive, context.getString(R.string.navigation_status_onlyinactive)); return new StatusTypeFilter(StatusType.OnlyInactive, context.getString(R.string.navigation_status_onlyinactive));
} }
}; };
/** /**
* Returns the status type to show all torrents, represented as filter item to show in the navigation list. * Returns the status type to show all torrents, represented as filter item to show in the navigation list.
* @param context The Android UI context, to access translations *
* @return The show ShowAll status type filter item * @param context The Android UI context, to access translations
*/ * @return The show ShowAll status type filter item
public static StatusTypeFilter getShowAllType(Context context) { */
return ShowAll.getFilterItem(context); public static StatusTypeFilter getShowAllType(Context context) {
} return ShowAll.getFilterItem(context);
}
/**
* Returns a list with all status types, represented as filter item that can be shown in the GUI. /**
* @param context The Android UI context, to access translations * Returns a list with all status types, represented as filter item that can be shown in the GUI.
* @return A list of filter items for all available status types *
*/ * @param context The Android UI context, to access translations
public static List<StatusTypeFilter> getAllStatusTypes(Context context) { * @return A list of filter items for all available status types
return Arrays.asList(ShowAll.getFilterItem(context), OnlyDownloading.getFilterItem(context), */
OnlyUploading.getFilterItem(context), OnlyActive.getFilterItem(context), public static List<StatusTypeFilter> getAllStatusTypes(Context context) {
OnlyInactive.getFilterItem(context)); return Arrays.asList(ShowAll.getFilterItem(context), OnlyDownloading.getFilterItem(context),
} OnlyUploading.getFilterItem(context), OnlyActive.getFilterItem(context),
OnlyInactive.getFilterItem(context));
/** }
* Every status type can return a filter item that represents it in the navigation
* @param context The Android UI context, to access translations /**
* @return A filter item object to show in the GUI * Every status type can return a filter item that represents it in the navigation
*/ *
public abstract StatusTypeFilter getFilterItem(Context context); * @param context The Android UI context, to access translations
* @return A filter item object to show in the GUI
public static class StatusTypeFilter implements SimpleListItem, NavigationFilter { */
public abstract StatusTypeFilter getFilterItem(Context context);
private final StatusType statusType;
private final String name; public static class StatusTypeFilter implements SimpleListItem, NavigationFilter {
StatusTypeFilter(StatusType statusType, String name) { public static final Parcelable.Creator<StatusTypeFilter> CREATOR = new Parcelable.Creator<StatusTypeFilter>() {
this.statusType = statusType; public StatusTypeFilter createFromParcel(Parcel in) {
this.name = name; return new StatusTypeFilter(in);
} }
public StatusType getStatusType() { public StatusTypeFilter[] newArray(int size) {
return statusType; return new StatusTypeFilter[size];
} }
};
@Override private final StatusType statusType;
public String getName() { private final String name;
return name;
} StatusTypeFilter(StatusType statusType, String name) {
this.statusType = statusType;
@Override this.name = name;
public String getCode() { }
// Uses the class name and status type enum to provide a unique navigation filter code
return StatusTypeFilter.class.getSimpleName() + "_" + statusType.name(); private StatusTypeFilter(Parcel in) {
} this.statusType = StatusType.valueOf(in.readString());
this.name = in.readString();
/** }
* Returns true if the torrent status matches this (selected) status type, false otherwise
* @param torrent The torrent to match against this status type public StatusType getStatusType() {
* @param dormantAsInactive If true, dormant (0KB/s, so no data transfer) torrents are never actively return statusType;
* downloading or seeding }
*/
@Override @Override
public boolean matches(Torrent torrent, boolean dormantAsInactive) { public String getName() {
switch (statusType) { return name;
case OnlyDownloading: }
return torrent.isDownloading(dormantAsInactive);
case OnlyUploading: @Override
return torrent.isSeeding(dormantAsInactive); public String getCode() {
case OnlyActive: // Uses the class name and status type enum to provide a unique navigation filter code
return torrent.isDownloading(dormantAsInactive) return StatusTypeFilter.class.getSimpleName() + "_" + statusType.name();
|| torrent.isSeeding(dormantAsInactive); }
case OnlyInactive:
return !torrent.isDownloading(dormantAsInactive) && !torrent.isSeeding(dormantAsInactive); /**
default: * Returns true if the torrent status matches this (selected) status type, false otherwise
return true; *
} * @param torrent The torrent to match against this status type
} * @param dormantAsInactive If true, dormant (0KB/s, so no data transfer) torrents are never actively
* downloading or seeding
private StatusTypeFilter(Parcel in) { */
this.statusType = StatusType.valueOf(in.readString()); @Override
this.name = in.readString(); public boolean matches(Torrent torrent, boolean dormantAsInactive) {
} switch (statusType) {
case OnlyDownloading:
public static final Parcelable.Creator<StatusTypeFilter> CREATOR = new Parcelable.Creator<StatusTypeFilter>() { return torrent.isDownloading(dormantAsInactive);
public StatusTypeFilter createFromParcel(Parcel in) { case OnlyUploading:
return new StatusTypeFilter(in); return torrent.isSeeding(dormantAsInactive);
} case OnlyActive:
return torrent.isDownloading(dormantAsInactive)
public StatusTypeFilter[] newArray(int size) { || torrent.isSeeding(dormantAsInactive);
return new StatusTypeFilter[size]; case OnlyInactive:
} return !torrent.isDownloading(dormantAsInactive) && !torrent.isSeeding(dormantAsInactive);
}; default:
return true;
@Override }
public int describeContents() { }
return 0;
} @Override
public int describeContents() {
@Override return 0;
public void writeToParcel(Parcel dest, int flags) { }
dest.writeString(statusType.name());
dest.writeString(name); @Override
} public void writeToParcel(Parcel dest, int flags) {
dest.writeString(statusType.name());
} dest.writeString(name);
}
}
} }

191
app/src/main/java/org/transdroid/core/gui/remoterss/RemoteRssFragment.java

@ -17,13 +17,14 @@
package org.transdroid.core.gui.remoterss; package org.transdroid.core.gui.remoterss;
import androidx.fragment.app.Fragment;
import android.view.View; import android.view.View;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.ListView; import android.widget.ListView;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import androidx.fragment.app.Fragment;
import org.androidannotations.annotations.AfterViews; import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Bean; import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EFragment; import org.androidannotations.annotations.EFragment;
@ -44,102 +45,102 @@ import java.util.List;
/** /**
* Fragment that shows a list of RSS items from the server and allows the user * Fragment that shows a list of RSS items from the server and allows the user
* to download remotely, without having to set up RSS feeds on the Android device. * to download remotely, without having to set up RSS feeds on the Android device.
*
* @author Twig * @author Twig
*/ */
@EFragment(R.layout.fragment_remoterss) @EFragment(R.layout.fragment_remoterss)
public class RemoteRssFragment extends Fragment { public class RemoteRssFragment extends Fragment {
@Bean @Bean
protected Log log; protected Log log;
// Local data // Local data
protected ArrayList<RemoteRssItem> remoteRssItems; protected ArrayList<RemoteRssItem> remoteRssItems;
// Views // Views
@ViewById @ViewById
protected View detailsContainer; protected View detailsContainer;
@ViewById(R.id.remoterss_filter) @ViewById(R.id.remoterss_filter)
protected Spinner remoteRssFilter; protected Spinner remoteRssFilter;
@ViewById @ViewById
protected ListView torrentsList; protected ListView torrentsList;
@ViewById(R.id.remoterss_status_message) @ViewById(R.id.remoterss_status_message)
protected TextView remoteRssStatusMessage; protected TextView remoteRssStatusMessage;
@AfterViews @AfterViews
protected void init() { protected void init() {
// Inject menu options in the actions toolbar // Inject menu options in the actions toolbar
setHasOptionsMenu(true); setHasOptionsMenu(true);
// Set up details adapter // Set up details adapter
RemoteRssItemsAdapter adapter = new RemoteRssItemsAdapter(getActivity()); RemoteRssItemsAdapter adapter = new RemoteRssItemsAdapter(getActivity());
torrentsList.setAdapter(adapter); torrentsList.setAdapter(adapter);
torrentsList.setFastScrollEnabled(true); torrentsList.setFastScrollEnabled(true);
} }
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
this.refreshScreen(); this.refreshScreen();
} }
@OptionsItem(R.id.action_refresh) @OptionsItem(R.id.action_refresh)
protected void refreshScreen() { protected void refreshScreen() {
RssFeedsActivity rssActivity = (RssFeedsActivity) getActivity(); RssFeedsActivity rssActivity = (RssFeedsActivity) getActivity();
rssActivity.refreshRemoteFeeds(); rssActivity.refreshRemoteFeeds();
} }
@OptionsItem(R.id.action_settings) @OptionsItem(R.id.action_settings)
protected void openSettings() { protected void openSettings() {
MainSettingsActivity_.intent(getActivity()).start(); MainSettingsActivity_.intent(getActivity()).start();
} }
/** /**
* Updates the UI with a new list of RSS items. * Updates the UI with a new list of RSS items.
*/ */
public void updateRemoteItems(List<RemoteRssItem> remoteItems, boolean scrollToTop) { public void updateRemoteItems(List<RemoteRssItem> remoteItems, boolean scrollToTop) {
RemoteRssItemsAdapter adapter = (RemoteRssItemsAdapter) torrentsList.getAdapter(); RemoteRssItemsAdapter adapter = (RemoteRssItemsAdapter) torrentsList.getAdapter();
remoteRssItems = new ArrayList<>(remoteItems); remoteRssItems = new ArrayList<>(remoteItems);
adapter.updateItems(remoteRssItems); adapter.updateItems(remoteRssItems);
if (scrollToTop) { if (scrollToTop) {
torrentsList.smoothScrollToPosition(0); torrentsList.smoothScrollToPosition(0);
} }
// Show/hide a nice message if there are no items to show // Show/hide a nice message if there are no items to show
if (remoteRssItems.size() > 0) { if (remoteRssItems.size() > 0) {
remoteRssStatusMessage.setVisibility(View.GONE); remoteRssStatusMessage.setVisibility(View.GONE);
} } else {
else { remoteRssStatusMessage.setVisibility(View.VISIBLE);
remoteRssStatusMessage.setVisibility(View.VISIBLE); remoteRssStatusMessage.setText(R.string.remoterss_no_files);
remoteRssStatusMessage.setText(R.string.remoterss_no_files); }
} }
}
public void updateChannelFilters(List<RemoteRssChannel> feedLabels) {
public void updateChannelFilters(List<RemoteRssChannel> feedLabels) { List<String> labels = new ArrayList<>();
List<String> labels = new ArrayList<>();
for (RemoteRssChannel feedLabel : feedLabels) {
for (RemoteRssChannel feedLabel : feedLabels) { labels.add(feedLabel.getName());
labels.add(feedLabel.getName()); }
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(this.getContext(), android.R.layout.simple_spinner_dropdown_item, labels);
ArrayAdapter<String> adapter = new ArrayAdapter<>(this.getContext(), android.R.layout.simple_spinner_dropdown_item, labels); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); remoteRssFilter.setAdapter(adapter);
remoteRssFilter.setAdapter(adapter); }
}
/**
/** * When the user clicks on an item, prepare to download it.
* When the user clicks on an item, prepare to download it. */
*/ @ItemClick(resName = "torrents_list")
@ItemClick(resName = "torrents_list") protected void detailsListClicked(int position) {
protected void detailsListClicked(int position) { RemoteRssItemsAdapter adapter = (RemoteRssItemsAdapter) torrentsList.getAdapter();
RemoteRssItemsAdapter adapter = (RemoteRssItemsAdapter) torrentsList.getAdapter(); RemoteRssItem item = (RemoteRssItem) adapter.getItem(position);
RemoteRssItem item = (RemoteRssItem) adapter.getItem(position);
((RssFeedsActivity) getActivity()).downloadRemoteRssItem(item);
((RssFeedsActivity) getActivity()).downloadRemoteRssItem(item); }
}
@ItemSelect(R.id.remoterss_filter)
@ItemSelect(R.id.remoterss_filter) protected void onFeedSelected(boolean selected, int position) {
protected void onFeedSelected(boolean selected, int position) { ((RssFeedsActivity) getActivity()).onFeedSelected(position);
((RssFeedsActivity) getActivity()).onFeedSelected(position); }
}
} }

31
app/src/main/java/org/transdroid/core/gui/remoterss/RemoteRssItemView.java

@ -28,26 +28,27 @@ import org.transdroid.core.gui.remoterss.data.RemoteRssItem;
/** /**
* View that represents some {@link RemoteRssItem} object. * View that represents some {@link RemoteRssItem} object.
*
* @author Twig * @author Twig
*/ */
@EViewGroup(R.layout.list_item_remoterssitem) @EViewGroup(R.layout.list_item_remoterssitem)
public class RemoteRssItemView extends LinearLayout { public class RemoteRssItemView extends LinearLayout {
// Views // Views
@ViewById @ViewById
protected TextView nameText, dateText, labelText; protected TextView nameText, dateText, labelText;
public RemoteRssItemView(Context context) { public RemoteRssItemView(Context context) {
super(context); super(context);
} }
public void bind(RemoteRssItem item) { public void bind(RemoteRssItem item) {
labelText.setText(item.getSourceName()); labelText.setText(item.getSourceName());
nameText.setText(item.getName()); nameText.setText(item.getName());
dateText.setText( dateText.setText(
DateFormat.getDateFormat(getContext()).format(item.getTimestamp()) + DateFormat.getDateFormat(getContext()).format(item.getTimestamp()) +
" " + " " +
DateFormat.getTimeFormat(getContext()).format(item.getTimestamp()) DateFormat.getTimeFormat(getContext()).format(item.getTimestamp())
); );
} }
} }

85
app/src/main/java/org/transdroid/core/gui/remoterss/RemoteRssItemsAdapter.java

@ -11,47 +11,46 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
public class RemoteRssItemsAdapter extends BaseAdapter { public class RemoteRssItemsAdapter extends BaseAdapter {
protected Context context; protected Context context;
protected List<RemoteRssItem> items; protected List<RemoteRssItem> items;
public RemoteRssItemsAdapter(Context context) { public RemoteRssItemsAdapter(Context context) {
this.context = context; this.context = context;
items = new ArrayList<>(); items = new ArrayList<>();
} }
@Override @Override
public int getCount() { public int getCount() {
return items.size(); return items.size();
} }
@Override @Override
public Object getItem(int position) { public Object getItem(int position) {
return items.get(position); return items.get(position);
} }
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
return position; return position;
} }
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
RemoteRssItemView itemView; RemoteRssItemView itemView;
if (convertView == null) { if (convertView == null) {
itemView = RemoteRssItemView_.build(context); itemView = RemoteRssItemView_.build(context);
} } else {
else { itemView = (RemoteRssItemView) convertView;
itemView = (RemoteRssItemView) convertView; }
}
itemView.bind((RemoteRssItem) getItem(position));
itemView.bind((RemoteRssItem) getItem(position));
return itemView;
return itemView; }
}
public void updateItems(List<RemoteRssItem> remoteItems) {
public void updateItems(List<RemoteRssItem> remoteItems) { items = remoteItems;
items = remoteItems; notifyDataSetChanged();
notifyDataSetChanged(); }
}
} }

725
app/src/main/java/org/transdroid/core/gui/rss/RssFeedsActivity.java

@ -22,17 +22,18 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcel; import android.os.Parcel;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.material.tabs.TabLayout;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import android.text.TextUtils; import androidx.viewpager.widget.PagerAdapter;
import android.view.View; import androidx.viewpager.widget.ViewPager;
import android.view.ViewGroup;
import com.google.android.material.tabs.TabLayout;
import com.nispok.snackbar.Snackbar; import com.nispok.snackbar.Snackbar;
import com.nispok.snackbar.SnackbarManager; import com.nispok.snackbar.SnackbarManager;
import com.nispok.snackbar.enums.SnackbarType; import com.nispok.snackbar.enums.SnackbarType;
@ -71,372 +72,364 @@ import org.transdroid.daemon.task.DaemonTaskSuccessResult;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@EActivity(R.layout.activity_rssfeeds) @EActivity(R.layout.activity_rssfeeds)
public class RssFeedsActivity extends AppCompatActivity { public class RssFeedsActivity extends AppCompatActivity {
// Settings and local data protected static final int RSS_FEEDS_LOCAL = 0;
@Bean protected static final int RSS_FEEDS_REMOTE = 1;
protected Log log; // Settings and local data
@Bean @Bean
protected ApplicationSettings applicationSettings; protected Log log;
@Bean
protected static final int RSS_FEEDS_LOCAL = 0; protected ApplicationSettings applicationSettings;
protected static final int RSS_FEEDS_REMOTE = 1; @FragmentById(R.id.rssfeeds_fragment)
protected RssFeedsFragment fragmentLocalFeeds;
@FragmentById(R.id.rssfeeds_fragment) @FragmentById(R.id.rssitems_fragment)
protected RssFeedsFragment fragmentLocalFeeds; protected RssItemsFragment fragmentItems;
@FragmentById(R.id.rssitems_fragment) @FragmentById(R.id.remoterss_fragment)
protected RssItemsFragment fragmentItems; protected RemoteRssFragment fragmentRemoteFeeds;
@FragmentById(R.id.remoterss_fragment)
protected RemoteRssFragment fragmentRemoteFeeds; @ViewById(R.id.rssfeeds_toolbar)
protected Toolbar rssFeedsToolbar;
@ViewById(R.id.rssfeeds_toolbar) @ViewById(R.id.rssfeeds_tabs)
protected Toolbar rssFeedsToolbar; protected TabLayout tabLayout;
@ViewById(R.id.rssfeeds_tabs) @ViewById(R.id.rssfeeds_pager)
protected TabLayout tabLayout; protected ViewPager viewPager;
@ViewById(R.id.rssfeeds_pager)
protected ViewPager viewPager; // remote RSS stuff
@NonConfigurationInstance
// remote RSS stuff protected ArrayList<RemoteRssChannel> feeds;
@NonConfigurationInstance @InstanceState
protected ArrayList<RemoteRssChannel> feeds; protected int selectedFilter;
@InstanceState @NonConfigurationInstance
protected int selectedFilter; protected ArrayList<RemoteRssItem> recentItems;
@NonConfigurationInstance @Bean
protected ArrayList<RemoteRssItem> recentItems; protected ConnectivityHelper connectivityHelper;
@Bean
protected ConnectivityHelper connectivityHelper; @Override
public void onCreate(Bundle savedInstanceState) {
SettingsUtils.applyDayNightTheme(this);
protected class LayoutPagerAdapter extends PagerAdapter { super.onCreate(savedInstanceState);
boolean hasRemoteRss; }
String serverName;
@AfterViews
public LayoutPagerAdapter(boolean hasRemoteRss, String name) { protected void init() {
super(); setSupportActionBar(rssFeedsToolbar);
getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(getString(R.string.rss_feeds)));
this.hasRemoteRss = hasRemoteRss; getSupportActionBar().setDisplayHomeAsUpEnabled(true);
this.serverName = (name.length() > 0 ? name : getString(R.string.navigation_rss_tabs_remote));
} IDaemonAdapter currentConnection = this.getCurrentConnection();
boolean hasRemoteRss = Daemon.supportsRemoteRssManagement(currentConnection.getType());
@NonNull
@Override PagerAdapter pagerAdapter = new LayoutPagerAdapter(hasRemoteRss, currentConnection.getSettings().getName());
public Object instantiateItem(@NonNull ViewGroup container, int position) { viewPager.setAdapter(pagerAdapter);
int resId = 0; tabLayout.setupWithViewPager(viewPager);
if (position == RSS_FEEDS_LOCAL) { // if local feeds dont have any entries but remote does, show it instead
resId = R.id.layout_rssfeeds_local; int defaultTab = RSS_FEEDS_LOCAL;
}
else if (position == RSS_FEEDS_REMOTE) { if (hasRemoteRss && applicationSettings.getRssfeedSettings().size() == 0) {
resId = R.id.layout_rss_feeds_remote; if (currentConnection instanceof RemoteRssSupplier) {
} RemoteRssSupplier remoteConnection = ((RemoteRssSupplier) (currentConnection));
boolean hasRemoteFeeds = false;
return findViewById(resId);
} try {
hasRemoteFeeds = remoteConnection.getRemoteRssChannels(log).size() > 0;
@Override } catch (DaemonException ignored) {
public int getCount() { }
return (this.hasRemoteRss ? 2 : 1);
} if (hasRemoteFeeds) {
defaultTab = RSS_FEEDS_REMOTE;
@Override }
public boolean isViewFromObject(@NonNull View view, @NonNull Object o) { }
return (view == o); }
} viewPager.setCurrentItem(defaultTab);
@Override if (!hasRemoteRss) {
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { tabLayout.setVisibility(View.GONE);
container.removeView((View) object); }
} }
@Nullable @TargetApi(Build.VERSION_CODES.HONEYCOMB)
@Override @OptionsItem(android.R.id.home)
public CharSequence getPageTitle(int position) { protected void navigateUp() {
switch (position) { TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
case RSS_FEEDS_LOCAL: }
return getString(R.string.navigation_rss_tabs_local);
case RSS_FEEDS_REMOTE: /**
return this.serverName; * Reload the RSS feed settings and start loading all the feeds. To be called from contained fragments.
} */
public void refreshFeeds() {
return super.getPageTitle(position); List<RssfeedLoader> loaders = new ArrayList<>();
} // For each RSS feed setting the user created, start a loader that retrieved the RSS feed (via a background
} // thread) and, on success, determines the new items in the feed
for (RssfeedSetting setting : applicationSettings.getRssfeedSettings()) {
@Override RssfeedLoader loader = new RssfeedLoader(setting);
public void onCreate(Bundle savedInstanceState) { loaders.add(loader);
SettingsUtils.applyDayNightTheme(this); loadRssfeed(loader);
super.onCreate(savedInstanceState); }
}
fragmentLocalFeeds.update(loaders);
@AfterViews }
protected void init() {
setSupportActionBar(rssFeedsToolbar); /**
getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(getString(R.string.rss_feeds))); * Performs the loading of the RSS feed content and parsing of items, in a background thread.
getSupportActionBar().setDisplayHomeAsUpEnabled(true); *
* @param loader The RSS feed loader for which to retrieve the contents
IDaemonAdapter currentConnection = this.getCurrentConnection(); */
boolean hasRemoteRss = Daemon.supportsRemoteRssManagement(currentConnection.getType()); @Background
protected void loadRssfeed(RssfeedLoader loader) {
PagerAdapter pagerAdapter = new LayoutPagerAdapter(hasRemoteRss, currentConnection.getSettings().getName()); try {
viewPager.setAdapter(pagerAdapter); // Load and parse the feed
tabLayout.setupWithViewPager(viewPager); RssParser parser =
new RssParser(loader.getSetting().getUrl(), loader.getSetting().getExcludeFilter(), loader.getSetting().getIncludeFilter());
// if local feeds dont have any entries but remote does, show it instead parser.parse();
int defaultTab = RSS_FEEDS_LOCAL; handleRssfeedResult(loader, parser.getChannel(), false);
} catch (Exception e) {
if (hasRemoteRss && applicationSettings.getRssfeedSettings().size() == 0) { // Catch any error that may occurred and register this failure
if (currentConnection instanceof RemoteRssSupplier) { handleRssfeedResult(loader, null, true);
RemoteRssSupplier remoteConnection = ((RemoteRssSupplier) (currentConnection)); log.i(this, "RSS feed " + loader.getSetting().getUrl() + " error: " + e.toString());
boolean hasRemoteFeeds = false; }
}
try {
hasRemoteFeeds = remoteConnection.getRemoteRssChannels(log).size() > 0; /**
} catch (DaemonException e) {} * Stores the retrieved RSS feed content channel into the loader and updates the RSS feed in the feeds list fragment.
*
if (hasRemoteFeeds) { * @param loader The RSS feed loader that was executed
defaultTab = RSS_FEEDS_REMOTE; * @param channel The data that was retrieved, or null if it could not be parsed
} * @param hasError True if a connection error occurred in the loading of the feed; false otherwise
} */
} @UiThread
viewPager.setCurrentItem(defaultTab); protected void handleRssfeedResult(RssfeedLoader loader, Channel channel, boolean hasError) {
loader.update(channel, hasError);
if (!hasRemoteRss) {
tabLayout.setVisibility(View.GONE); fragmentLocalFeeds.notifyDataSetChanged();
} }
}
/**
@TargetApi(Build.VERSION_CODES.HONEYCOMB) * Opens an RSS feed in the dedicated fragment (if there was space in the UI) or a new {@link RssItemsActivity}. Optionally this also registers in
@OptionsItem(android.R.id.home) * the user preferences that the feed was now viewed, so that in the future the new items can be properly marked.
protected void navigateUp() { *
TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start(); * @param loader The RSS feed loader (with settings and the loaded content channel) to show
} * @param markAsViewedNow True if the user settings should be updated to reflect this feed's last viewed date; false otherwise
*/
/** public void openRssfeed(RssfeedLoader loader, boolean markAsViewedNow) {
* Reload the RSS feed settings and start loading all the feeds. To be called from contained fragments.
*/ // The RSS feed content was loaded and can now be shown in the dedicated fragment or a new activity
public void refreshFeeds() { if (fragmentItems != null && fragmentItems.isAdded()) {
List<RssfeedLoader> loaders = new ArrayList<>();
// For each RSS feed setting the user created, start a loader that retrieved the RSS feed (via a background // If desired, update the lastViewedDate and lastViewedItemUrl of this feed in the user setting; this won't
// thread) and, on success, determines the new items in the feed // be loaded until the RSS feeds screen in opened again.
for (RssfeedSetting setting : applicationSettings.getRssfeedSettings()) { if (!loader.hasError() && loader.getChannel() != null && markAsViewedNow) {
RssfeedLoader loader = new RssfeedLoader(setting); String lastViewedItemUrl = null;
loaders.add(loader); if (loader.getChannel().getItems() != null && loader.getChannel().getItems().size() > 0) {
loadRssfeed(loader); lastViewedItemUrl = loader.getChannel().getItems().get(0).getTheLink();
} }
applicationSettings.setRssfeedLastViewer(loader.getSetting().getOrder(), new Date(), lastViewedItemUrl);
fragmentLocalFeeds.update(loaders); }
} fragmentItems.update(loader.getChannel(), loader.hasError(), loader.getSetting().requiresExternalAuthentication());
/** } else {
* Performs the loading of the RSS feed content and parsing of items, in a background thread.
* @param loader The RSS feed loader for which to retrieve the contents // Error message or not yet loaded? Show a toast message instead of opening the items activity
*/ if (loader.hasError()) {
@Background SnackbarManager.show(Snackbar.with(this).text(R.string.rss_error).colorResource(R.color.red));
protected void loadRssfeed(RssfeedLoader loader) { return;
try { }
// Load and parse the feed if (loader.getChannel() == null || loader.getChannel().getItems().size() == 0) {
RssParser parser = SnackbarManager.show(Snackbar.with(this).text(R.string.rss_notloaded).colorResource(R.color.red));
new RssParser(loader.getSetting().getUrl(), loader.getSetting().getExcludeFilter(), loader.getSetting().getIncludeFilter()); return;
parser.parse(); }
handleRssfeedResult(loader, parser.getChannel(), false);
} catch (Exception e) { // If desired, update the lastViewedDate and lastViewedItemUrl of this feed in the user setting; this won't
// Catch any error that may occurred and register this failure // be loaded until the RSS feeds screen in opened again
handleRssfeedResult(loader, null, true); if (markAsViewedNow) {
log.i(this, "RSS feed " + loader.getSetting().getUrl() + " error: " + e.toString()); String lastViewedItemUrl = null;
} if (loader.getChannel().getItems() != null && loader.getChannel().getItems().size() > 0) {
} lastViewedItemUrl = loader.getChannel().getItems().get(0).getTheLink();
}
/** applicationSettings.setRssfeedLastViewer(loader.getSetting().getOrder(), new Date(), lastViewedItemUrl);
* Stores the retrieved RSS feed content channel into the loader and updates the RSS feed in the feeds list fragment. }
* @param loader The RSS feed loader that was executed
* @param channel The data that was retrieved, or null if it could not be parsed String name = loader.getChannel().getTitle();
* @param hasError True if a connection error occurred in the loading of the feed; false otherwise if (TextUtils.isEmpty(name)) {
*/ name = loader.getSetting().getName();
@UiThread }
protected void handleRssfeedResult(RssfeedLoader loader, Channel channel, boolean hasError) { if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(loader.getSetting().getUrl())) {
loader.update(channel, hasError); name = Uri.parse(loader.getSetting().getUrl()).getHost();
}
fragmentLocalFeeds.notifyDataSetChanged(); RssItemsActivity_.intent(this).rssfeed(loader.getChannel()).rssfeedName(name)
} .requiresExternalAuthentication(loader.getSetting().requiresExternalAuthentication()).start();
/** }
* Opens an RSS feed in the dedicated fragment (if there was space in the UI) or a new {@link RssItemsActivity}. Optionally this also registers in }
* the user preferences that the feed was now viewed, so that in the future the new items can be properly marked.
* @param loader The RSS feed loader (with settings and the loaded content channel) to show protected IDaemonAdapter getCurrentConnection() {
* @param markAsViewedNow True if the user settings should be updated to reflect this feed's last viewed date; false otherwise ServerSetting lastUsed = applicationSettings.getLastUsedServer();
*/ return lastUsed.createServerAdapter(connectivityHelper.getConnectedNetworkName(), this);
public void openRssfeed(RssfeedLoader loader, boolean markAsViewedNow) { }
// The RSS feed content was loaded and can now be shown in the dedicated fragment or a new activity
if (fragmentItems != null && fragmentItems.isAdded()) {
// If desired, update the lastViewedDate and lastViewedItemUrl of this feed in the user setting; this won't
// be loaded until the RSS feeds screen in opened again.
if (!loader.hasError() && loader.getChannel() != null && markAsViewedNow) {
String lastViewedItemUrl = null;
if (loader.getChannel().getItems() != null && loader.getChannel().getItems().size() > 0) {
lastViewedItemUrl = loader.getChannel().getItems().get(0).getTheLink();
}
applicationSettings.setRssfeedLastViewer(loader.getSetting().getOrder(), new Date(), lastViewedItemUrl);
}
fragmentItems.update(loader.getChannel(), loader.hasError(), loader.getSetting().requiresExternalAuthentication());
} else {
// Error message or not yet loaded? Show a toast message instead of opening the items activity
if (loader.hasError()) {
SnackbarManager.show(Snackbar.with(this).text(R.string.rss_error).colorResource(R.color.red));
return;
}
if (loader.getChannel() == null || loader.getChannel().getItems().size() == 0) {
SnackbarManager.show(Snackbar.with(this).text(R.string.rss_notloaded).colorResource(R.color.red));
return;
}
// If desired, update the lastViewedDate and lastViewedItemUrl of this feed in the user setting; this won't
// be loaded until the RSS feeds screen in opened again
if (markAsViewedNow) {
String lastViewedItemUrl = null;
if (loader.getChannel().getItems() != null && loader.getChannel().getItems().size() > 0) {
lastViewedItemUrl = loader.getChannel().getItems().get(0).getTheLink();
}
applicationSettings.setRssfeedLastViewer(loader.getSetting().getOrder(), new Date(), lastViewedItemUrl);
}
String name = loader.getChannel().getTitle();
if (TextUtils.isEmpty(name)) {
name = loader.getSetting().getName();
}
if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(loader.getSetting().getUrl())) {
name = Uri.parse(loader.getSetting().getUrl()).getHost();
}
RssItemsActivity_.intent(this).rssfeed(loader.getChannel()).rssfeedName(name)
.requiresExternalAuthentication(loader.getSetting().requiresExternalAuthentication()).start();
}
}
protected IDaemonAdapter getCurrentConnection() {
ServerSetting lastUsed = applicationSettings.getLastUsedServer();
return lastUsed.createServerAdapter(connectivityHelper.getConnectedNetworkName(), this);
}
// @Background // @Background
public void refreshRemoteFeeds() { public void refreshRemoteFeeds() {
// Connect to the last used server // Connect to the last used server
IDaemonAdapter currentConnection = this.getCurrentConnection(); IDaemonAdapter currentConnection = this.getCurrentConnection();
// remote rss not supported for this connection type // remote rss not supported for this connection type
if (currentConnection instanceof RemoteRssSupplier == false) { if (!(currentConnection instanceof RemoteRssSupplier)) {
return; return;
} }
try { try {
feeds = ((RemoteRssSupplier) (currentConnection)).getRemoteRssChannels(log); feeds = ((RemoteRssSupplier) (currentConnection)).getRemoteRssChannels(log);
// By default it displays the latest items within the last month. // By default it displays the latest items within the last month.
recentItems = new ArrayList<>(); recentItems = new ArrayList<>();
Calendar calendar = Calendar.getInstance(); Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MONTH, -1); calendar.add(Calendar.MONTH, -1);
Date oneMonthAgo = calendar.getTime(); Date oneMonthAgo = calendar.getTime();
for (RemoteRssChannel feed : feeds) { for (RemoteRssChannel feed : feeds) {
for (RemoteRssItem item : feed.getItems()) { for (RemoteRssItem item : feed.getItems()) {
if (item.getTimestamp().after(oneMonthAgo)) { if (item.getTimestamp().after(oneMonthAgo)) {
recentItems.add(item); recentItems.add(item);
} }
} }
} }
// Sort by -newest // Sort by -newest
Collections.sort(recentItems, new Comparator<RemoteRssItem>() { Collections.sort(recentItems, (lhs, rhs) ->
@Override rhs.getTimestamp().compareTo(lhs.getTimestamp()));
public int compare(RemoteRssItem lhs, RemoteRssItem rhs) { } catch (DaemonException e) {
return rhs.getTimestamp().compareTo(lhs.getTimestamp()); onCommunicationError(e);
} return;
}); }
} catch (DaemonException e) {
onCommunicationError(e); // @UIThread
return; fragmentRemoteFeeds.updateRemoteItems(
} selectedFilter == 0 ? recentItems : feeds.get(selectedFilter - 1).getItems(),
false /* allow android to restore scroll position */);
// @UIThread showRemoteChannelFilters();
fragmentRemoteFeeds.updateRemoteItems( }
selectedFilter == 0 ? recentItems : feeds.get(selectedFilter -1).getItems(),
false /* allow android to restore scroll position */ ); @UiThread
showRemoteChannelFilters(); protected void onCommunicationError(DaemonException daemonException) {
} log.i(this, daemonException.toString());
String error = getString(LocalTorrent.getResourceForDaemonException(daemonException));
@UiThread SnackbarManager.show(Snackbar.with(this).text(error).colorResource(R.color.red).type(SnackbarType.MULTI_LINE));
protected void onCommunicationError(DaemonException daemonException) { }
//noinspection ThrowableResultOfMethodCallIgnored
log.i(this, daemonException.toString()); public void onFeedSelected(int position) {
String error = getString(LocalTorrent.getResourceForDaemonException(daemonException)); selectedFilter = position;
SnackbarManager.show(Snackbar.with(this).text(error).colorResource(R.color.red).type(SnackbarType.MULTI_LINE));
} if (position == 0) {
fragmentRemoteFeeds.updateRemoteItems(recentItems, true);
} else {
public void onFeedSelected(int position) { RemoteRssChannel channel = feeds.get(selectedFilter - 1);
selectedFilter = position; fragmentRemoteFeeds.updateRemoteItems(channel.getItems(), true);
}
if (position == 0) { }
fragmentRemoteFeeds.updateRemoteItems(recentItems, true);
} /**
else { * Download the item in a background thread and display success/fail accordingly.
RemoteRssChannel channel = feeds.get(selectedFilter -1); */
fragmentRemoteFeeds.updateRemoteItems(channel.getItems(), true); @Background
} public void downloadRemoteRssItem(RemoteRssItem item) {
} final RemoteRssSupplier supplier = (RemoteRssSupplier) this.getCurrentConnection();
/** try {
* Download the item in a background thread and display success/fail accordingly. RemoteRssChannel channel = feeds.get(selectedFilter);
*/ supplier.downloadRemoteRssItem(log, item, channel);
@Background onTaskSucceeded(null, getString(R.string.result_added, item.getTitle()));
public void downloadRemoteRssItem(RemoteRssItem item) { } catch (DaemonException e) {
final RemoteRssSupplier supplier = (RemoteRssSupplier) this.getCurrentConnection(); onTaskFailed(getString(LocalTorrent.getResourceForDaemonException(e)));
}
try { }
RemoteRssChannel channel = feeds.get(selectedFilter);
supplier.downloadRemoteRssItem(log, item, channel); @UiThread
onTaskSucceeded(null, getString(R.string.result_added, item.getTitle())); protected void onTaskSucceeded(DaemonTaskSuccessResult result, String successMessage) {
} catch (DaemonException e) { SnackbarManager.show(Snackbar.with(this).text(successMessage));
onTaskFailed(getString(LocalTorrent.getResourceForDaemonException(e))); }
}
} @UiThread
protected void onTaskFailed(String message) {
@UiThread SnackbarManager.show(Snackbar.with(this)
protected void onTaskSucceeded(DaemonTaskSuccessResult result, String successMessage) { .text(message)
SnackbarManager.show(Snackbar.with(this).text(successMessage)); .colorResource(R.color.red)
} .type(SnackbarType.MULTI_LINE)
);
@UiThread }
protected void onTaskFailed(String message) {
SnackbarManager.show(Snackbar.with(this) private void showRemoteChannelFilters() {
.text(message) List<RemoteRssChannel> feedLabels = new ArrayList<>(feeds.size() + 1);
.colorResource(R.color.red) feedLabels.add(new RemoteRssChannel() {
.type(SnackbarType.MULTI_LINE) @Override
); public String getName() {
} return getString(R.string.remoterss_filter_allrecent);
}
private void showRemoteChannelFilters() {
List<RemoteRssChannel> feedLabels = new ArrayList<>(feeds.size() +1); @Override
feedLabels.add(new RemoteRssChannel() { public void writeToParcel(Parcel dest, int flags) {
@Override }
public String getName() { });
return getString(R.string.remoterss_filter_allrecent); feedLabels.addAll(feeds);
}
fragmentRemoteFeeds.updateChannelFilters(feedLabels);
@Override }
public void writeToParcel(Parcel dest, int flags) {
} protected class LayoutPagerAdapter extends PagerAdapter {
}); boolean hasRemoteRss;
feedLabels.addAll(feeds); String serverName;
fragmentRemoteFeeds.updateChannelFilters(feedLabels); public LayoutPagerAdapter(boolean hasRemoteRss, String name) {
} super();
this.hasRemoteRss = hasRemoteRss;
this.serverName = (name.length() > 0 ? name : getString(R.string.navigation_rss_tabs_remote));
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
int resId = 0;
if (position == RSS_FEEDS_LOCAL) {
resId = R.id.layout_rssfeeds_local;
} else if (position == RSS_FEEDS_REMOTE) {
resId = R.id.layout_rss_feeds_remote;
}
return findViewById(resId);
}
@Override
public int getCount() {
return (this.hasRemoteRss ? 2 : 1);
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
return (view == o);
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView((View) object);
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case RSS_FEEDS_LOCAL:
return getString(R.string.navigation_rss_tabs_local);
case RSS_FEEDS_REMOTE:
return this.serverName;
}
return super.getPageTitle(position);
}
}
} }

124
app/src/main/java/org/transdroid/core/gui/rss/RssFeedsFragment.java

@ -16,13 +16,14 @@
*/ */
package org.transdroid.core.gui.rss; package org.transdroid.core.gui.rss;
import androidx.fragment.app.Fragment;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.ListView; import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import androidx.fragment.app.Fragment;
import org.androidannotations.annotations.AfterViews; import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Bean; import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EFragment; import org.androidannotations.annotations.EFragment;
@ -37,71 +38,72 @@ import java.util.List;
/** /**
* Fragment lists the RSS feeds the user wants to monitor and, if room, the list of items in a feed in a right pane. * Fragment lists the RSS feeds the user wants to monitor and, if room, the list of items in a feed in a right pane.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EFragment(R.layout.fragment_rssfeeds) @EFragment(R.layout.fragment_rssfeeds)
@OptionsMenu(R.menu.fragment_rssfeeds) @OptionsMenu(R.menu.fragment_rssfeeds)
public class RssFeedsFragment extends Fragment { public class RssFeedsFragment extends Fragment {
// Views // Views
@ViewById(R.id.rssfeeds_list) @ViewById(R.id.rssfeeds_list)
protected ListView feedsList; protected ListView feedsList;
@Bean @Bean
protected RssfeedsAdapter rssfeedsAdapter; protected RssfeedsAdapter rssfeedsAdapter;
@ViewById @ViewById
protected TextView nosettingsText; protected TextView nosettingsText;
@AfterViews @AfterViews
protected void init() { protected void init() {
feedsList.setAdapter(rssfeedsAdapter); feedsList.setAdapter(rssfeedsAdapter);
} }
public void update(List<RssfeedLoader> loaders) { public void update(List<RssfeedLoader> loaders) {
rssfeedsAdapter.update(loaders); rssfeedsAdapter.update(loaders);
boolean hasSettings = !(loaders == null || loaders.size() == 0); boolean hasSettings = !(loaders == null || loaders.size() == 0);
feedsList.setVisibility(hasSettings ? View.VISIBLE : View.GONE); feedsList.setVisibility(hasSettings ? View.VISIBLE : View.GONE);
nosettingsText.setVisibility(hasSettings ? View.GONE : View.VISIBLE); nosettingsText.setVisibility(hasSettings ? View.GONE : View.VISIBLE);
getActivity().invalidateOptionsMenu(); getActivity().invalidateOptionsMenu();
} }
@Override @Override
public void onPrepareOptionsMenu(Menu menu) { public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu); super.onPrepareOptionsMenu(menu);
boolean hasFeeds = rssfeedsAdapter != null && rssfeedsAdapter.getCount() > 0; boolean hasFeeds = rssfeedsAdapter != null && rssfeedsAdapter.getCount() > 0;
menu.findItem(R.id.action_refresh).setVisible(hasFeeds); menu.findItem(R.id.action_refresh).setVisible(hasFeeds);
menu.findItem(R.id.action_settings).setShowAsAction(!hasFeeds ? MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_NEVER); menu.findItem(R.id.action_settings).setShowAsAction(!hasFeeds ? MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_NEVER);
} }
@OptionsItem(R.id.action_settings) @OptionsItem(R.id.action_settings)
protected void openSettings() { protected void openSettings() {
MainSettingsActivity_.intent(getActivity()).start(); MainSettingsActivity_.intent(getActivity()).start();
} }
protected RssFeedsActivity getRssActivity() { protected RssFeedsActivity getRssActivity() {
return (RssFeedsActivity) getActivity(); return (RssFeedsActivity) getActivity();
} }
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
this.refreshScreen(); this.refreshScreen();
} }
@OptionsItem(R.id.action_refresh) @OptionsItem(R.id.action_refresh)
protected void refreshScreen() { protected void refreshScreen() {
getRssActivity().refreshFeeds(); getRssActivity().refreshFeeds();
} }
@ItemClick(R.id.rssfeeds_list) @ItemClick(R.id.rssfeeds_list)
protected void onFeedClicked(RssfeedLoader loader) { protected void onFeedClicked(RssfeedLoader loader) {
getRssActivity().openRssfeed(loader, true); getRssActivity().openRssfeed(loader, true);
} }
/** /**
* Notifies the contained list of RSS feeds that the underlying data has been changed. * Notifies the contained list of RSS feeds that the underlying data has been changed.
*/ */
public void notifyDataSetChanged() { public void notifyDataSetChanged() {
rssfeedsAdapter.notifyDataSetChanged(); rssfeedsAdapter.notifyDataSetChanged();
} }
} }

67
app/src/main/java/org/transdroid/core/gui/rss/RssItemsActivity.java

@ -20,6 +20,7 @@ import android.annotation.TargetApi;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
@ -38,45 +39,45 @@ import org.transdroid.core.rssparser.Channel;
@EActivity(R.layout.activity_rssitems) @EActivity(R.layout.activity_rssitems)
public class RssItemsActivity extends AppCompatActivity { public class RssItemsActivity extends AppCompatActivity {
@Extra @Extra
protected Channel rssfeed = null; protected Channel rssfeed = null;
@Extra @Extra
protected String rssfeedName; protected String rssfeedName;
@Extra @Extra
protected boolean requiresExternalAuthentication; protected boolean requiresExternalAuthentication;
@FragmentById(R.id.rssitems_fragment) @FragmentById(R.id.rssitems_fragment)
protected RssItemsFragment fragmentItems; protected RssItemsFragment fragmentItems;
@ViewById @ViewById
protected Toolbar rssfeedsToolbar; protected Toolbar rssfeedsToolbar;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
SettingsUtils.applyDayNightTheme(this); SettingsUtils.applyDayNightTheme(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
} }
@AfterViews @AfterViews
protected void init() { protected void init() {
// We require an RSS feed to be specified; otherwise close the activity // We require an RSS feed to be specified; otherwise close the activity
if (rssfeed == null) { if (rssfeed == null) {
finish(); finish();
return; return;
} }
setSupportActionBar(rssfeedsToolbar); setSupportActionBar(rssfeedsToolbar);
getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(rssfeedName)); getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(rssfeedName));
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Get the intent extras and show them to the already loaded fragment // Get the intent extras and show them to the already loaded fragment
fragmentItems.update(rssfeed, false, requiresExternalAuthentication); fragmentItems.update(rssfeed, false, requiresExternalAuthentication);
} }
@TargetApi(Build.VERSION_CODES.HONEYCOMB) @TargetApi(Build.VERSION_CODES.HONEYCOMB)
@OptionsItem(android.R.id.home) @OptionsItem(android.R.id.home)
protected void navigateUp() { protected void navigateUp() {
TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start(); TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
} }
} }

355
app/src/main/java/org/transdroid/core/gui/rss/RssItemsFragment.java

@ -23,8 +23,6 @@ import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import androidx.fragment.app.Fragment;
import androidx.appcompat.app.AppCompatActivity;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.ActionMode; import android.view.ActionMode;
import android.view.Menu; import android.view.Menu;
@ -35,6 +33,9 @@ import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import com.nispok.snackbar.Snackbar; import com.nispok.snackbar.Snackbar;
import com.nispok.snackbar.SnackbarManager; import com.nispok.snackbar.SnackbarManager;
@ -57,184 +58,186 @@ import java.util.List;
/** /**
* Fragment that lists the items in a specific RSS feed * Fragment that lists the items in a specific RSS feed
*
* @author Eric Kok * @author Eric Kok
*/ */
@EFragment(R.layout.fragment_rssitems) @EFragment(R.layout.fragment_rssitems)
public class RssItemsFragment extends Fragment { public class RssItemsFragment extends Fragment {
@InstanceState @InstanceState
protected Channel rssFeed = null; protected Channel rssFeed = null;
@InstanceState @InstanceState
protected boolean hasError = false; protected boolean hasError = false;
@InstanceState @InstanceState
protected boolean requiresExternalAuthentication = false; protected boolean requiresExternalAuthentication = false;
@Bean @Bean
protected NavigationHelper navigationHelper; protected NavigationHelper navigationHelper;
// Views // Views
@ViewById(R.id.rssitems_list) @ViewById(R.id.rssitems_list)
protected ListView rssItemsList; protected ListView rssItemsList;
private MultiChoiceModeListener onItemsSelected = new MultiChoiceModeListener() { @Bean
protected RssitemsAdapter rssitemsAdapter;
SelectionManagerMode selectionManagerMode; @ViewById
protected TextView emptyText;
@Override private MultiChoiceModeListener onItemsSelected = new MultiChoiceModeListener() {
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Show contextual action bar to add items in batch mode SelectionManagerMode selectionManagerMode;
mode.getMenuInflater().inflate(R.menu.fragment_rssitems_cab, menu);
Context themedContext = ((AppCompatActivity) getActivity()).getSupportActionBar().getThemedContext(); @Override
selectionManagerMode = new SelectionManagerMode(themedContext, rssItemsList, R.plurals.rss_itemsselected); public boolean onCreateActionMode(ActionMode mode, Menu menu) {
selectionManagerMode.onCreateActionMode(mode, menu); // Show contextual action bar to add items in batch mode
return true; mode.getMenuInflater().inflate(R.menu.fragment_rssitems_cab, menu);
} Context themedContext = ((AppCompatActivity) getActivity()).getSupportActionBar().getThemedContext();
selectionManagerMode = new SelectionManagerMode(themedContext, rssItemsList, R.plurals.rss_itemsselected);
@Override selectionManagerMode.onCreateActionMode(mode, menu);
public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return true;
return selectionManagerMode.onPrepareActionMode(mode, menu); }
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) { public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return selectionManagerMode.onPrepareActionMode(mode, menu);
// Get checked torrents }
List<Item> checked = new ArrayList<>();
for (int i = 0; i < rssItemsList.getCheckedItemPositions().size(); i++) { public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (rssItemsList.getCheckedItemPositions().valueAt(i)) {
checked.add(rssitemsAdapter.getItem(rssItemsList.getCheckedItemPositions().keyAt(i))); // Get checked torrents
} List<Item> checked = new ArrayList<>();
} for (int i = 0; i < rssItemsList.getCheckedItemPositions().size(); i++) {
if (rssItemsList.getCheckedItemPositions().valueAt(i)) {
int itemId = item.getItemId(); checked.add(rssitemsAdapter.getItem(rssItemsList.getCheckedItemPositions().keyAt(i)));
if (itemId == R.id.action_addall) { }
}
// Start an Intent that adds multiple items at once, by supplying the urls and titles as string array
// extras and setting the Intent action to ADD_MULTIPLE int itemId = item.getItemId();
Intent intent = new Intent("org.transdroid.ADD_MULTIPLE"); if (itemId == R.id.action_addall) {
String[] urls = new String[checked.size()];
String[] titles = new String[checked.size()]; // Start an Intent that adds multiple items at once, by supplying the urls and titles as string array
for (int i = 0; i < checked.size(); i++) { // extras and setting the Intent action to ADD_MULTIPLE
urls[i] = checked.get(i).getTheLink(); Intent intent = new Intent("org.transdroid.ADD_MULTIPLE");
titles[i] = checked.get(i).getTitle(); String[] urls = new String[checked.size()];
} String[] titles = new String[checked.size()];
intent.putExtra("TORRENT_URLS", urls); for (int i = 0; i < checked.size(); i++) {
intent.putExtra("TORRENT_TITLES", titles); urls[i] = checked.get(i).getTheLink();
startActivity(intent); titles[i] = checked.get(i).getTitle();
mode.finish(); }
return true; intent.putExtra("TORRENT_URLS", urls);
intent.putExtra("TORRENT_TITLES", titles);
} else if (itemId == R.id.action_copytoclipboard) { startActivity(intent);
mode.finish();
StringBuilder names = new StringBuilder(); return true;
for (int f = 0; f < checked.size(); f++) {
if (f != 0) { } else if (itemId == R.id.action_copytoclipboard) {
names.append("\n");
} StringBuilder names = new StringBuilder();
names.append(checked.get(f).getTitle()); for (int f = 0; f < checked.size(); f++) {
} if (f != 0) {
ClipboardManager clipboardManager = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); names.append("\n");
clipboardManager.setPrimaryClip(ClipData.newPlainText("Transdroid", names.toString())); }
mode.finish(); names.append(checked.get(f).getTitle());
return true; }
ClipboardManager clipboardManager = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
} else { clipboardManager.setPrimaryClip(ClipData.newPlainText("Transdroid", names.toString()));
mode.finish();
// The other items only operate on one (the first) selected item return true;
if (checked.size() < 1) {
return false; } else {
}
final Item first = checked.get(0); // The other items only operate on one (the first) selected item
if (itemId == R.id.action_showdetails) { if (checked.size() < 1) {
// Show a dialog box with the RSS item description text return false;
new AlertDialog.Builder(getActivity()).setMessage(first.getDescription()) }
.setPositiveButton(R.string.action_close, null).show(); final Item first = checked.get(0);
} else if (itemId == R.id.action_openwebsite) { if (itemId == R.id.action_showdetails) {
// Open the browser to show the website contained in the item's link tag // Show a dialog box with the RSS item description text
Toast.makeText(getActivity(), getString(R.string.search_openingdetails, first.getTitle()), Toast.LENGTH_LONG).show(); new AlertDialog.Builder(getActivity()).setMessage(first.getDescription())
if (!TextUtils.isEmpty(first.getLink())) { .setPositiveButton(R.string.action_close, null).show();
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(first.getLink()))); } else if (itemId == R.id.action_openwebsite) {
} else { // Open the browser to show the website contained in the item's link tag
// No URL was specified in the RSS feed item link tag (or no link tag was present) Toast.makeText(getActivity(), getString(R.string.search_openingdetails, first.getTitle()), Toast.LENGTH_LONG).show();
SnackbarManager.show(Snackbar.with(getActivity()).text(R.string.error_no_link).colorResource(R.color.red)); if (!TextUtils.isEmpty(first.getLink())) {
} startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(first.getLink())));
} else if (itemId == R.id.action_useassearch) { } else {
// Use the RSS item title to start a new search (mimicking the search manager style) // No URL was specified in the RSS feed item link tag (or no link tag was present)
Intent search = SearchActivity_.intent(getActivity()).get(); SnackbarManager.show(Snackbar.with(getActivity()).text(R.string.error_no_link).colorResource(R.color.red));
search.setAction(Intent.ACTION_SEARCH); }
search.putExtra(SearchManager.QUERY, first.getTitle()); } else if (itemId == R.id.action_useassearch) {
startActivity(search); // Use the RSS item title to start a new search (mimicking the search manager style)
} Intent search = SearchActivity_.intent(getActivity()).get();
mode.finish(); search.setAction(Intent.ACTION_SEARCH);
return true; search.putExtra(SearchManager.QUERY, first.getTitle());
startActivity(search);
} }
} mode.finish();
return true;
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { }
selectionManagerMode.onItemCheckedStateChanged(mode, position, id, checked); }
}
@Override
@Override public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
public void onDestroyActionMode(ActionMode mode) { selectionManagerMode.onItemCheckedStateChanged(mode, position, id, checked);
selectionManagerMode.onDestroyActionMode(mode); }
}
@Override
}; public void onDestroyActionMode(ActionMode mode) {
@Bean selectionManagerMode.onDestroyActionMode(mode);
protected RssitemsAdapter rssitemsAdapter; }
@ViewById
protected TextView emptyText; };
@AfterViews @AfterViews
protected void init() { protected void init() {
// Set up the list adapter, which allows multi-select // Set up the list adapter, which allows multi-select
rssItemsList.setAdapter(rssitemsAdapter); rssItemsList.setAdapter(rssitemsAdapter);
rssItemsList.setMultiChoiceModeListener(onItemsSelected); rssItemsList.setMultiChoiceModeListener(onItemsSelected);
update(rssFeed, hasError, requiresExternalAuthentication); update(rssFeed, hasError, requiresExternalAuthentication);
} }
/** /**
* Update the shown RSS items in the list. * Update the shown RSS items in the list.
* @param channel The loaded RSS content channel object *
* @param hasError True if there were errors in loading the channel, in which case an error text is shown; false otherwise * @param channel The loaded RSS content channel object
* @param requiresExternalAuthentication Whether this RSS feed requires external authentication and should thus be redirected to a browser * @param hasError True if there were errors in loading the channel, in which case an error text is shown; false otherwise
*/ * @param requiresExternalAuthentication Whether this RSS feed requires external authentication and should thus be redirected to a browser
public void update(Channel channel, boolean hasError, boolean requiresExternalAuthentication) { */
this.requiresExternalAuthentication = requiresExternalAuthentication; public void update(Channel channel, boolean hasError, boolean requiresExternalAuthentication) {
rssitemsAdapter.update(channel); this.requiresExternalAuthentication = requiresExternalAuthentication;
rssItemsList.setVisibility(View.GONE); rssitemsAdapter.update(channel);
emptyText.setVisibility(View.VISIBLE); rssItemsList.setVisibility(View.GONE);
if (hasError) { emptyText.setVisibility(View.VISIBLE);
emptyText.setText(R.string.rss_error); if (hasError) {
return; emptyText.setText(R.string.rss_error);
} return;
if (channel == null) { }
emptyText.setText(R.string.rss_noselection); if (channel == null) {
return; emptyText.setText(R.string.rss_noselection);
} return;
if (channel.getItems().size() == 0) { }
emptyText.setText(R.string.rss_empty); if (channel.getItems().size() == 0) {
return; emptyText.setText(R.string.rss_empty);
} return;
rssItemsList.setVisibility(View.VISIBLE); }
emptyText.setVisibility(View.INVISIBLE); rssItemsList.setVisibility(View.VISIBLE);
} emptyText.setVisibility(View.INVISIBLE);
}
@ItemClick(resName = "rssitems_list")
protected void onItemClicked(Item item) { @ItemClick(resName = "rssitems_list")
if (requiresExternalAuthentication) { protected void onItemClicked(Item item) {
// Redirect to the browser, as this feed requires cookie authentication which we piggy-back on using the browser cookies if (requiresExternalAuthentication) {
navigationHelper.forceOpenInBrowser(item.getTheLinkUri()); // Redirect to the browser, as this feed requires cookie authentication which we piggy-back on using the browser cookies
return; navigationHelper.forceOpenInBrowser(item.getTheLinkUri());
} return;
}
// Don't broadcast this intent; we can safely assume this is intended for Transdroid only
Intent i = TorrentsActivity_.intent(getActivity()).get(); // Don't broadcast this intent; we can safely assume this is intended for Transdroid only
i.setData(item.getTheLinkUri()); Intent i = TorrentsActivity_.intent(getActivity()).get();
i.putExtra("TORRENT_TITLE", item.getTitle()); i.setData(item.getTheLinkUri());
startActivity(i); i.putExtra("TORRENT_TITLE", item.getTitle());
} startActivity(i);
}
} }

131
app/src/main/java/org/transdroid/core/gui/rss/RssfeedLoader.java

@ -21,89 +21,84 @@ import org.transdroid.core.rssparser.Channel;
import org.transdroid.core.rssparser.Item; import org.transdroid.core.rssparser.Item;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
/** /**
* A container class that holds RSS feed settings and, after they have been retrieved, the contents as {@link Channel}, the number of new items and an * A container class that holds RSS feed settings and, after they have been retrieved, the contents as {@link Channel}, the number of new items and an
* indication of a connection error. * indication of a connection error.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class RssfeedLoader { public class RssfeedLoader {
private final RssfeedSetting setting; private final RssfeedSetting setting;
private Channel channel = null; private Channel channel = null;
private int newCount = -1; private int newCount = -1;
private boolean hasError = false; private boolean hasError = false;
public RssfeedLoader(RssfeedSetting setting) { public RssfeedLoader(RssfeedSetting setting) {
this.setting = setting; this.setting = setting;
} }
public void update(Channel channel, boolean hasError) { public void update(Channel channel, boolean hasError) {
this.channel = channel; this.channel = channel;
this.hasError = hasError; this.hasError = hasError;
if (channel == null || channel.getItems() == null || hasError) { if (channel == null || channel.getItems() == null || hasError) {
this.hasError = true; this.hasError = true;
newCount = -1; newCount = -1;
return; return;
} }
// Peek if this feed properly supports publish dates // Peek if this feed properly supports publish dates
boolean usePublishDate = false; boolean usePublishDate = false;
if (channel.getItems().size() > 0) { if (channel.getItems().size() > 0) {
Date pubDate = channel.getItems().get(0).getPubdate(); Date pubDate = channel.getItems().get(0).getPubdate();
usePublishDate = pubDate != null && pubDate.getTime() > 0; usePublishDate = pubDate != null && pubDate.getTime() > 0;
} }
if (usePublishDate) { newCount = 0;
// Count the number of new items, based on the date that this RSS feed was last viewed by the user if (usePublishDate) {
newCount = 0; // Count the number of new items, based on the date that this RSS feed was last viewed by the user
List<Item> items = channel.getItems(); List<Item> items = channel.getItems();
// Reverse-order sort the items on their published date // Reverse-order sort the items on their published date
Collections.sort(items, new Comparator<Item>() { Collections.sort(items, (lhs, rhs) ->
@Override -lhs.getPubdate().compareTo(rhs.getPubdate()));
public int compare(Item lhs, Item rhs) { for (Item item : items) {
return 0 - lhs.getPubdate().compareTo(rhs.getPubdate()); if (item.getPubdate() == null || setting.getLastViewed() == null || item.getPubdate().after(setting.getLastViewed())) {
} newCount++;
}); item.setIsNew(true);
for (Item item : items) { } else {
if (item.getPubdate() == null || setting.getLastViewed() == null || item.getPubdate().after(setting.getLastViewed())) { item.setIsNew(false);
newCount++; }
item.setIsNew(true); }
} else { } else {
item.setIsNew(false); // Use the url of the last RSS item the last time the feed was viewed by the user to count new items
} boolean isNew = true;
} for (Item item : channel.getItems()) {
} else { if (item.getTheLink() != null && setting.getLastViewedItemUrl() != null && item.getTheLink().equals(setting.getLastViewedItemUrl())) {
// Use the url of the last RSS item the last time the feed was viewed by the user to count new items isNew = false;
newCount = 0; }
boolean isNew = true; if (isNew) {
for (Item item : channel.getItems()) { newCount++;
if (item.getTheLink() != null && setting.getLastViewedItemUrl() != null && item.getTheLink().equals(setting.getLastViewedItemUrl())) { }
isNew = false; item.setIsNew(isNew);
} }
if (isNew) { }
newCount++; }
}
item.setIsNew(isNew);
}
}
}
public Channel getChannel() { public Channel getChannel() {
return channel; return channel;
} }
public RssfeedSetting getSetting() { public RssfeedSetting getSetting() {
return setting; return setting;
} }
public int getNewCount() { public int getNewCount() {
return newCount; return newCount;
} }
public boolean hasError() { public boolean hasError() {
return hasError; return hasError;
} }
} }

59
app/src/main/java/org/transdroid/core/gui/rss/RssfeedView.java

@ -33,46 +33,47 @@ import org.transdroid.core.gui.navigation.NavigationHelper;
/** /**
* View that represents some {@link RssfeedSetting} object and displays name as well as loads a favicon for the feed's site and can load how many new * View that represents some {@link RssfeedSetting} object and displays name as well as loads a favicon for the feed's site and can load how many new
* items are available. * items are available.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EViewGroup(R.layout.list_item_rssfeed) @EViewGroup(R.layout.list_item_rssfeed)
public class RssfeedView extends LinearLayout { public class RssfeedView extends LinearLayout {
private static final String GRABICON_URL = "https://besticon-demo.herokuapp.com/icon?url=%1$s&size=72"; private static final String GRABICON_URL = "https://besticon-demo.herokuapp.com/icon?url=%1$s&size=72";
@Bean @Bean
protected NavigationHelper navigationHelper; protected NavigationHelper navigationHelper;
// Views // Views
@ViewById @ViewById
protected ImageView faviconImage; protected ImageView faviconImage;
@ViewById @ViewById
protected TextView nameText, newcountText; protected TextView nameText, newcountText;
@ViewById @ViewById
protected ProgressBar loadingProgress; protected ProgressBar loadingProgress;
public RssfeedView(Context context) { public RssfeedView(Context context) {
super(context); super(context);
} }
public void bind(RssfeedLoader rssfeedLoader) { public void bind(RssfeedLoader rssfeedLoader) {
// Show the RSS feed name and either a loading indicator or the number of new items // Show the RSS feed name and either a loading indicator or the number of new items
nameText.setText(rssfeedLoader.getSetting().getName()); nameText.setText(rssfeedLoader.getSetting().getName());
if (rssfeedLoader.hasError() || rssfeedLoader.getChannel() != null) { if (rssfeedLoader.hasError() || rssfeedLoader.getChannel() != null) {
loadingProgress.setVisibility(View.GONE); loadingProgress.setVisibility(View.GONE);
newcountText.setVisibility(View.VISIBLE); newcountText.setVisibility(View.VISIBLE);
newcountText.setText(rssfeedLoader.hasError() ? "?" : Integer.toString(rssfeedLoader.getNewCount())); newcountText.setText(rssfeedLoader.hasError() ? "?" : Integer.toString(rssfeedLoader.getNewCount()));
} else { } else {
loadingProgress.setVisibility(View.VISIBLE); loadingProgress.setVisibility(View.VISIBLE);
newcountText.setVisibility(View.GONE); newcountText.setVisibility(View.GONE);
} }
// Clear and then asynchronously load the RSS feed site' favicon // Clear and then asynchronously load the RSS feed site' favicon
// Uses the g.etfv.co service to resolve the favicon of any feed URL // Uses the g.etfv.co service to resolve the favicon of any feed URL
faviconImage.setImageDrawable(null); faviconImage.setImageDrawable(null);
navigationHelper.getImageCache().displayImage(String.format(GRABICON_URL, rssfeedLoader.getSetting().getUrl()), faviconImage); navigationHelper.getImageCache().displayImage(String.format(GRABICON_URL, rssfeedLoader.getSetting().getUrl()), faviconImage);
} }
} }

91
app/src/main/java/org/transdroid/core/gui/rss/RssfeedsAdapter.java

@ -29,61 +29,62 @@ import java.util.List;
/** /**
* Adapter that contains a list of {@link RssfeedSetting}s, each with associated loaded RSS feed {@link org.transdroid.core.rssparser.Channel}. * Adapter that contains a list of {@link RssfeedSetting}s, each with associated loaded RSS feed {@link org.transdroid.core.rssparser.Channel}.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EBean @EBean
public class RssfeedsAdapter extends BaseAdapter { public class RssfeedsAdapter extends BaseAdapter {
private List<RssfeedLoader> loaders = null; @RootContext
protected Context context;
@RootContext private List<RssfeedLoader> loaders = null;
protected Context context;
/** /**
* Allows updating the full internal list of feed loaders at once, replacing the old list * Allows updating the full internal list of feed loaders at once, replacing the old list
* @param loaders The new list of RSS feed loader objects, which pair settings and a loaded channel *
*/ * @param loaders The new list of RSS feed loader objects, which pair settings and a loaded channel
public void update(List<RssfeedLoader> loaders) { */
this.loaders = loaders; public void update(List<RssfeedLoader> loaders) {
notifyDataSetChanged(); this.loaders = loaders;
} notifyDataSetChanged();
}
@Override @Override
public boolean hasStableIds() { public boolean hasStableIds() {
return true; return true;
} }
@Override @Override
public int getCount() { public int getCount() {
if (loaders == null) { if (loaders == null) {
return 0; return 0;
} }
return loaders.size(); return loaders.size();
} }
@Override @Override
public RssfeedLoader getItem(int position) { public RssfeedLoader getItem(int position) {
if (loaders == null) { if (loaders == null) {
return null; return null;
} }
return loaders.get(position); return loaders.get(position);
} }
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
return position; return position;
} }
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
RssfeedView rssfeedView; RssfeedView rssfeedView;
if (convertView == null) { if (convertView == null) {
rssfeedView = RssfeedView_.build(context); rssfeedView = RssfeedView_.build(context);
} else { } else {
rssfeedView = (RssfeedView) convertView; rssfeedView = (RssfeedView) convertView;
} }
rssfeedView.bind(getItem(position)); rssfeedView.bind(getItem(position));
return rssfeedView; return rssfeedView;
} }
} }

69
app/src/main/java/org/transdroid/core/gui/rss/RssitemStatusLayout.java

@ -28,53 +28,52 @@ import org.transdroid.R;
/** /**
* A relative layout that that is checkable (to be used in a contextual action bar) and shows a coloured bar in the far left indicating the view * A relative layout that that is checkable (to be used in a contextual action bar) and shows a coloured bar in the far left indicating the view
* status, that is, if the item is new to the user or was viewed earlier. * status, that is, if the item is new to the user or was viewed earlier.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class RssitemStatusLayout extends RelativeLayout { public class RssitemStatusLayout extends RelativeLayout {
private final float scale = getContext().getResources().getDisplayMetrics().density; private final float scale = getContext().getResources().getDisplayMetrics().density;
private final int WIDTH = (int) (6 * scale + 0.5f); private final int WIDTH = (int) (6 * scale + 0.5f);
private final Paint oldPaint = new Paint(); private final Paint oldPaint = new Paint();
private final Paint newPaint = new Paint(); private final Paint newPaint = new Paint();
private final RectF fullRect = new RectF(); private final RectF fullRect = new RectF();
private Boolean isNew = null; private Boolean isNew = null;
public RssitemStatusLayout(Context context) { public RssitemStatusLayout(Context context) {
super(context); super(context);
initPaints(); initPaints();
setWillNotDraw(false); setWillNotDraw(false);
} }
public RssitemStatusLayout(Context context, AttributeSet attrs) { public RssitemStatusLayout(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
initPaints(); initPaints();
setWillNotDraw(false); setWillNotDraw(false);
} }
private void initPaints() { private void initPaints() {
oldPaint.setColor(getResources().getColor(R.color.file_off)); // Grey oldPaint.setColor(getResources().getColor(R.color.file_off)); // Grey
newPaint.setColor(getResources().getColor(R.color.file_normal)); // Normal green newPaint.setColor(getResources().getColor(R.color.file_normal)); // Normal green
} }
public void setIsNew(Boolean isNew) { public void setIsNew(Boolean isNew) {
this.isNew = isNew; this.isNew = isNew;
this.invalidate(); this.invalidate();
} }
@Override @Override
protected void onDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
super.onDraw(canvas); super.onDraw(canvas);
int height = getHeight(); fullRect.set(0, 0, WIDTH, getHeight());
int width = WIDTH;
fullRect.set(0, 0, width, height);
if (isNew == null) { if (isNew == null) {
return; return;
} }
canvas.drawRect(fullRect, isNew ? newPaint : oldPaint); canvas.drawRect(fullRect, isNew ? newPaint : oldPaint);
} }
} }

27
app/src/main/java/org/transdroid/core/gui/rss/RssitemView.java

@ -27,27 +27,28 @@ import org.transdroid.core.rssparser.Item;
/** /**
* View that represents some {@link Item} object, which is a single item in some RSS feed. * View that represents some {@link Item} object, which is a single item in some RSS feed.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EViewGroup(R.layout.list_item_rssitem) @EViewGroup(R.layout.list_item_rssitem)
public class RssitemView extends RssitemStatusLayout { public class RssitemView extends RssitemStatusLayout {
// Views // Views
@ViewById @ViewById
protected TextView nameText, dateText; protected TextView nameText, dateText;
public RssitemView(Context context) { public RssitemView(Context context) {
super(context); super(context);
} }
public void bind(Item rssitem) { public void bind(Item rssitem) {
nameText.setText(rssitem.getTitle()); nameText.setText(rssitem.getTitle());
dateText.setText(rssitem.getPubdate() == null ? "" : DateUtils dateText.setText(rssitem.getPubdate() == null ? "" : DateUtils
.getRelativeDateTimeString(getContext(), rssitem.getPubdate().getTime(), DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, .getRelativeDateTimeString(getContext(), rssitem.getPubdate().getTime(), DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS,
DateUtils.FORMAT_ABBREV_MONTH)); DateUtils.FORMAT_ABBREV_MONTH));
setIsNew(rssitem.isNew()); setIsNew(rssitem.isNew());
} }
} }

91
app/src/main/java/org/transdroid/core/gui/rss/RssitemsAdapter.java

@ -28,61 +28,62 @@ import org.transdroid.core.rssparser.Item;
/** /**
* Adapter that contains a list of {@link Item}s in an RSS feed. * Adapter that contains a list of {@link Item}s in an RSS feed.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EBean @EBean
public class RssitemsAdapter extends BaseAdapter { public class RssitemsAdapter extends BaseAdapter {
private Channel rssfeed = null; @RootContext
protected Context context;
@RootContext private Channel rssfeed = null;
protected Context context;
/** /**
* Allows updating the full RSS feed (channel and contained items), replacing the old data * Allows updating the full RSS feed (channel and contained items), replacing the old data
* @param rssfeed The new RSS feed contents *
*/ * @param rssfeed The new RSS feed contents
public void update(Channel rssfeed) { */
this.rssfeed = rssfeed; public void update(Channel rssfeed) {
notifyDataSetChanged(); this.rssfeed = rssfeed;
} notifyDataSetChanged();
}
@Override @Override
public boolean hasStableIds() { public boolean hasStableIds() {
return true; return true;
} }
@Override @Override
public int getCount() { public int getCount() {
if (rssfeed == null) { if (rssfeed == null) {
return 0; return 0;
} }
return rssfeed.getItems().size(); return rssfeed.getItems().size();
} }
@Override @Override
public Item getItem(int position) { public Item getItem(int position) {
if (rssfeed == null) { if (rssfeed == null) {
return null; return null;
} }
return rssfeed.getItems().get(position); return rssfeed.getItems().get(position);
} }
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
return position; return position;
} }
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
RssitemView rssitemView; RssitemView rssitemView;
if (convertView == null) { if (convertView == null) {
rssitemView = RssitemView_.build(context); rssitemView = RssitemView_.build(context);
} else { } else {
rssitemView = (RssitemView) convertView; rssitemView = (RssitemView) convertView;
} }
rssitemView.bind(getItem(position)); rssitemView.bind(getItem(position));
return rssitemView; return rssitemView;
} }
} }

99
app/src/main/java/org/transdroid/core/gui/search/BarcodeHelper.java

@ -20,8 +20,6 @@ import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
@ -31,57 +29,56 @@ import java.lang.ref.WeakReference;
public class BarcodeHelper { public class BarcodeHelper {
// A 'random' ID to identify QR-encoded settings scan intents // A 'random' ID to identify QR-encoded settings scan intents
public static final int ACTIVITY_BARCODE_QRSETTINGS = 0x0000c0df; public static final int ACTIVITY_BARCODE_QRSETTINGS = 0x0000c0df;
private static final Uri SCANNER_MARKET_URI = Uri.parse("market://search?q=pname:com.google.zxing.client.android"); private static final Uri SCANNER_MARKET_URI = Uri.parse("market://search?q=pname:com.google.zxing.client.android");
/** /**
* Call this to start a bar code scanner intent. The calling activity will receive an Intent result with the given * Call this to start a bar code scanner intent. The calling activity will receive an Intent result with the given
* request code. * request code.
* @param activity The calling activity, to which the result is returned or a dialog is bound that asks to install *
* the bar code scanner * @param activity The calling activity, to which the result is returned or a dialog is bound that asks to install
* @param requestCode {@link #ACTIVITY_BARCODE_QRSETTINGS} * the bar code scanner
*/ * @param requestCode {@link #ACTIVITY_BARCODE_QRSETTINGS}
public static void startBarcodeScanner(final Activity activity, int requestCode) { */
// Start a bar code scanner that can handle the SCAN intent (specifically ZXing) public static void startBarcodeScanner(final Activity activity, int requestCode) {
startBarcodeIntent(activity, new Intent("com.google.zxing.client.android.SCAN"), requestCode); // Start a bar code scanner that can handle the SCAN intent (specifically ZXing)
} startBarcodeIntent(activity, new Intent("com.google.zxing.client.android.SCAN"), requestCode);
}
/** /**
* Call this to share content encoded in a QR code, specially used to share settings. The calling activity will * Call this to share content encoded in a QR code, specially used to share settings. The calling activity will
* receive an Intent result with ID {@link #ACTIVITY_BARCODE_QRSETTINGS}. From there the returned intent will * receive an Intent result with ID {@link #ACTIVITY_BARCODE_QRSETTINGS}. From there the returned intent will
* contain the data as SCAN_RESULT String extra. * contain the data as SCAN_RESULT String extra.
* @param activity The calling activity, to which the result is returned or a dialog is bound that asks to install *
* the bar code scanner * @param activity The calling activity, to which the result is returned or a dialog is bound that asks to install
* @param content The content to share, that is, the raw data (Transdroid settings encoded as JSON data structure) * the bar code scanner
* to share as QR code * @param content The content to share, that is, the raw data (Transdroid settings encoded as JSON data structure)
*/ * to share as QR code
public static void shareContentBarcode(final Activity activity, final String content) { */
// Start a bar code encoded that can handle the ENCODE intent (specifically ZXing) public static void shareContentBarcode(final Activity activity, final String content) {
Intent encodeIntent = new Intent("com.google.zxing.client.android.ENCODE"); // Start a bar code encoded that can handle the ENCODE intent (specifically ZXing)
encodeIntent.putExtra("ENCODE_TYPE", "TEXT_TYPE"); Intent encodeIntent = new Intent("com.google.zxing.client.android.ENCODE");
encodeIntent.putExtra("ENCODE_DATA", content); encodeIntent.putExtra("ENCODE_TYPE", "TEXT_TYPE");
encodeIntent.putExtra("ENCODE_SHOW_CONTENTS", false); encodeIntent.putExtra("ENCODE_DATA", content);
startBarcodeIntent(activity, encodeIntent, -1); encodeIntent.putExtra("ENCODE_SHOW_CONTENTS", false);
} startBarcodeIntent(activity, encodeIntent, -1);
}
@SuppressLint("ValidFragment") @SuppressLint("ValidFragment")
private static void startBarcodeIntent(final Activity activity, final Intent intent, int requestCode) { private static void startBarcodeIntent(final Activity activity, final Intent intent, int requestCode) {
try { try {
activity.startActivityForResult(intent, requestCode); activity.startActivityForResult(intent, requestCode);
} catch (Exception e) { } catch (Exception e) {
// Can't start the bar code scanner, for example with a SecurityException or when ZXing is not present // Can't start the bar code scanner, for example with a SecurityException or when ZXing is not present
final WeakReference<Context> intentStartContext = new WeakReference<Context>(activity); final WeakReference<Context> intentStartContext = new WeakReference<>(activity);
new AlertDialog.Builder(activity).setIcon(android.R.drawable.ic_dialog_alert) new AlertDialog.Builder(activity).setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(activity.getString(R.string.search_barcodescannernotfound)) .setMessage(activity.getString(R.string.search_barcodescannernotfound))
.setPositiveButton(android.R.string.yes, new OnClickListener() { .setPositiveButton(android.R.string.yes, (dialog, which) -> {
@Override if (intentStartContext.get() != null)
public void onClick(DialogInterface dialog, int which) { intentStartContext.get().startActivity(new Intent(Intent.ACTION_VIEW, SCANNER_MARKET_URI));
if (intentStartContext.get() != null) }).setNegativeButton(android.R.string.no, null).show();
intentStartContext.get().startActivity(new Intent(Intent.ACTION_VIEW, SCANNER_MARKET_URI)); }
} }
}).setNegativeButton(android.R.string.no, null).show();
}
}
} }

70
app/src/main/java/org/transdroid/core/gui/search/FilePickerHelper.java

@ -16,54 +16,50 @@
*/ */
package org.transdroid.core.gui.search; package org.transdroid.core.gui.search;
import org.transdroid.R;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import org.transdroid.R;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
public class FilePickerHelper { public class FilePickerHelper {
public static final int ACTIVITY_FILEPICKER = 0x0000c0df; // A 'random' ID to identify file picker intents public static final int ACTIVITY_FILEPICKER = 0x0000c0df; // A 'random' ID to identify file picker intents
public static final Uri FILEMANAGER_MARKET_URI = Uri.parse("market://search?q=pname:org.openintents.filemanager"); public static final Uri FILEMANAGER_MARKET_URI = Uri.parse("market://search?q=pname:org.openintents.filemanager");
/** /**
* Call this to start a file picker intent. The calling activity will receive an Intent result with ID * Call this to start a file picker intent. The calling activity will receive an Intent result with ID
* {@link #ACTIVITY_FILEPICKER} with an Intent that contains the selected local file as data Intent. * {@link #ACTIVITY_FILEPICKER} with an Intent that contains the selected local file as data Intent.
* @param activity The calling activity, to which the result is returned or a dialog is bound that asks to install *
* the file picker * @param activity The calling activity, to which the result is returned or a dialog is bound that asks to install
*/ * the file picker
@SuppressLint("ValidFragment") */
public static void startFilePicker(final Activity activity) { @SuppressLint("ValidFragment")
try { public static void startFilePicker(final Activity activity) {
// Start a file manager that can handle the file/* file/* intents try {
activity.startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT).setType("application/x-bittorrent"), // Start a file manager that can handle the file/* file/* intents
ACTIVITY_FILEPICKER); activity.startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT).setType("application/x-bittorrent"),
} catch (Exception e1) { ACTIVITY_FILEPICKER);
try { } catch (Exception e1) {
// Start a file manager that can handle the PICK_FILE intent (specifically IO File Manager) try {
activity.startActivityForResult(new Intent("org.openintents.action.PICK_FILE"), ACTIVITY_FILEPICKER); // Start a file manager that can handle the PICK_FILE intent (specifically IO File Manager)
} catch (Exception e2) { activity.startActivityForResult(new Intent("org.openintents.action.PICK_FILE"), ACTIVITY_FILEPICKER);
// Can't start the file manager, for example with a SecurityException or when IO File Manager is not present } catch (Exception e2) {
final WeakReference<Context> intentStartContext = new WeakReference<Context>(activity); // Can't start the file manager, for example with a SecurityException or when IO File Manager is not present
new AlertDialog.Builder(activity).setIcon(android.R.drawable.ic_dialog_alert) final WeakReference<Context> intentStartContext = new WeakReference<>(activity);
.setMessage(activity.getString(R.string.search_filemanagernotfound)) new AlertDialog.Builder(activity).setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.yes, new OnClickListener() { .setMessage(activity.getString(R.string.search_filemanagernotfound))
@Override .setPositiveButton(android.R.string.yes, (dialog, which) -> {
public void onClick(DialogInterface dialog, int which) { if (intentStartContext.get() != null)
if (intentStartContext.get() != null) intentStartContext.get().startActivity(new Intent(Intent.ACTION_VIEW, FILEMANAGER_MARKET_URI));
intentStartContext.get().startActivity(new Intent(Intent.ACTION_VIEW, FILEMANAGER_MARKET_URI)); }).setNegativeButton(android.R.string.no, null).show();
} }
}).setNegativeButton(android.R.string.no, null).show(); }
} }
}
}
} }

554
app/src/main/java/org/transdroid/core/gui/search/SearchActivity.java

@ -23,9 +23,6 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.SearchRecentSuggestions; import android.provider.SearchRecentSuggestions;
import androidx.core.view.MenuItemCompat;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
@ -36,6 +33,9 @@ import android.widget.SearchView;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import org.androidannotations.annotations.AfterViews; import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Bean; import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity; import org.androidannotations.annotations.EActivity;
@ -58,284 +58,282 @@ import java.util.List;
/** /**
* An activity that shows search results to the user (after a query was supplied by the standard Android search manager) and either shows the list of * An activity that shows search results to the user (after a query was supplied by the standard Android search manager) and either shows the list of
* search sites on the left (e.g. on tablets) or allows switching between search sites via the action bar spinner. * search sites on the left (e.g. on tablets) or allows switching between search sites via the action bar spinner.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EActivity(R.layout.activity_search) @EActivity(R.layout.activity_search)
public class SearchActivity extends AppCompatActivity { public class SearchActivity extends AppCompatActivity {
@ViewById @ViewById
protected Toolbar searchToolbar; protected Toolbar searchToolbar;
@ViewById @ViewById
protected Spinner sitesSpinner; protected Spinner sitesSpinner;
@FragmentById(R.id.searchresults_fragment) @FragmentById(R.id.searchresults_fragment)
protected SearchResultsFragment fragmentResults; protected SearchResultsFragment fragmentResults;
@ViewById @ViewById
protected ListView searchsitesList; protected ListView searchsitesList;
@ViewById @ViewById
protected TextView installmoduleText; protected TextView installmoduleText;
@Bean @Bean
protected ApplicationSettings applicationSettings; protected ApplicationSettings applicationSettings;
@Bean @Bean
protected SearchHelper searchHelper; protected SearchHelper searchHelper;
@SystemService @SystemService
protected SearchManager searchManager; protected SearchManager searchManager;
private MenuItem searchMenu = null; private MenuItem searchMenu = null;
private SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SearchHistoryProvider.AUTHORITY, SearchHistoryProvider.MODE); private SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SearchHistoryProvider.AUTHORITY, SearchHistoryProvider.MODE);
private List<SearchSetting> searchSites; private List<SearchSetting> searchSites;
private SearchSetting lastUsedSite; private SearchSetting lastUsedSite;
private String lastUsedQuery; private String lastUsedQuery;
private OnItemClickListener onSearchSiteClicked = new OnItemClickListener() {
@Override
public void onCreate(Bundle savedInstanceState) { @Override
SettingsUtils.applyDayNightTheme(this); public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
super.onCreate(savedInstanceState); lastUsedSite = searchSites.get(position);
} refreshSearch();
}
@AfterViews };
protected void init() { private AdapterView.OnItemSelectedListener onSearchSiteSelected = new AdapterView.OnItemSelectedListener() {
@Override
searchToolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material); public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
searchToolbar.setNavigationOnClickListener(new View.OnClickListener() { lastUsedSite = searchSites.get(position);
@Override refreshSearch();
public void onClick(View v) { }
TorrentsActivity_.intent(SearchActivity.this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
} @Override
}); public void onNothingSelected(AdapterView<?> parent) {
setSupportActionBar(searchToolbar); }
};
// Get the user query, as coming from the standard SearchManager
handleIntent(getIntent()); @Override
public void onCreate(Bundle savedInstanceState) {
if (!searchHelper.isTorrentSearchInstalled()) { SettingsUtils.applyDayNightTheme(this);
// The module install text will be shown instead (in onPrepareOptionsMenu) super.onCreate(savedInstanceState);
return; }
}
@AfterViews
// Load sites and find the last used (or set as default) search site protected void init() {
searchSites = applicationSettings.getSearchSettings();
lastUsedSite = applicationSettings.getLastUsedSearchSite(); searchToolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material);
int lastUsedPosition = -1; searchToolbar.setNavigationOnClickListener(v ->
if (lastUsedSite != null) { TorrentsActivity_.intent(SearchActivity.this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start());
for (int i = 0; i < searchSites.size(); i++) { setSupportActionBar(searchToolbar);
if (searchSites.get(i).getKey().equals(lastUsedSite.getKey())) {
lastUsedPosition = i; // Get the user query, as coming from the standard SearchManager
break; handleIntent(getIntent());
}
} if (!searchHelper.isTorrentSearchInstalled()) {
} // The module install text will be shown instead (in onPrepareOptionsMenu)
return;
// Allow site selection via list (on large screens) or action bar spinner }
if (searchsitesList != null) {
// The current layout has a dedicated list view to select the search site // Load sites and find the last used (or set as default) search site
SearchSitesAdapter searchSitesAdapter = SearchSitesAdapter_.getInstance_(this); searchSites = applicationSettings.getSearchSettings();
searchSitesAdapter.update(searchSites); lastUsedSite = applicationSettings.getLastUsedSearchSite();
searchsitesList.setAdapter(searchSitesAdapter); int lastUsedPosition = -1;
searchsitesList.setOnItemClickListener(onSearchSiteClicked); if (lastUsedSite != null) {
// Select the last used site and start the search for (int i = 0; i < searchSites.size(); i++) {
if (lastUsedPosition >= 0) { if (searchSites.get(i).getKey().equals(lastUsedSite.getKey())) {
searchsitesList.setItemChecked(lastUsedPosition, true); lastUsedPosition = i;
lastUsedSite = searchSites.get(lastUsedPosition); break;
refreshSearch(); }
} else { }
fragmentResults.clearResults(); }
}
} else { // Allow site selection via list (on large screens) or action bar spinner
// Use the action bar spinner to select sites if (searchsitesList != null) {
if (getSupportActionBar() != null) // The current layout has a dedicated list view to select the search site
getSupportActionBar().setTitle(""); SearchSitesAdapter searchSitesAdapter = SearchSitesAdapter_.getInstance_(this);
sitesSpinner.setVisibility(View.VISIBLE); searchSitesAdapter.update(searchSites);
sitesSpinner.setAdapter(new SearchSettingsDropDownAdapter(searchToolbar.getContext(), searchSites)); searchsitesList.setAdapter(searchSitesAdapter);
sitesSpinner.setOnItemSelectedListener(onSearchSiteSelected); searchsitesList.setOnItemClickListener(onSearchSiteClicked);
// Select the last used site; this also starts the search! // Select the last used site and start the search
if (lastUsedPosition >= 0) { if (lastUsedPosition >= 0) {
sitesSpinner.setSelection(lastUsedPosition); searchsitesList.setItemChecked(lastUsedPosition, true);
lastUsedSite = searchSites.get(lastUsedPosition); lastUsedSite = searchSites.get(lastUsedPosition);
refreshSearch(); refreshSearch();
} else { } else {
fragmentResults.clearResults(); fragmentResults.clearResults();
} }
} } else {
invalidateOptionsMenu(); // Use the action bar spinner to select sites
if (getSupportActionBar() != null)
} getSupportActionBar().setTitle("");
sitesSpinner.setVisibility(View.VISIBLE);
@Override sitesSpinner.setAdapter(new SearchSettingsDropDownAdapter(searchToolbar.getContext(), searchSites));
public boolean onCreateOptionsMenu(Menu menu) { sitesSpinner.setOnItemSelectedListener(onSearchSiteSelected);
super.onCreateOptionsMenu(menu); // Select the last used site; this also starts the search!
// Manually insert the actions into the main torrent and secondary actions toolbars if (lastUsedPosition >= 0) {
searchToolbar.inflateMenu(R.menu.activity_search); sitesSpinner.setSelection(lastUsedPosition);
// Add an expandable SearchView to the action bar lastUsedSite = searchSites.get(lastUsedPosition);
MenuItem item = menu.findItem(R.id.action_search); refreshSearch();
final SearchView searchView = new SearchView(searchToolbar.getContext()); } else {
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); fragmentResults.clearResults();
searchView.setQueryRefinementEnabled(true); }
searchView.setIconified(false); }
searchView.setIconifiedByDefault(false); invalidateOptionsMenu();
MenuItemCompat.setActionView(item, searchView);
searchMenu = item; }
final MenuItem sortBySeeders = menu.findItem(R.id.action_sort_seeders);
final MenuItem sortByAdded = menu.findItem(R.id.action_sort_added); @Override
final SearchSortOrder sortOrder = applicationSettings.getLastUsedSearchSortOrder(); public boolean onCreateOptionsMenu(Menu menu) {
if (sortOrder == SearchSortOrder.BySeeders) { super.onCreateOptionsMenu(menu);
sortBySeeders.setChecked(true); // Manually insert the actions into the main torrent and secondary actions toolbars
} else { searchToolbar.inflateMenu(R.menu.activity_search);
sortByAdded.setChecked(true); // Add an expandable SearchView to the action bar
} MenuItem item = menu.findItem(R.id.action_search);
return true; final SearchView searchView = new SearchView(searchToolbar.getContext());
} searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
searchView.setQueryRefinementEnabled(true);
@Override searchView.setIconified(false);
public boolean onPrepareOptionsMenu(Menu menu) { searchView.setIconifiedByDefault(false);
super.onPrepareOptionsMenu(menu); item.setActionView(searchView);
searchMenu = item;
boolean searchInstalled = searchHelper.isTorrentSearchInstalled(); final MenuItem sortBySeeders = menu.findItem(R.id.action_sort_seeders);
searchToolbar.getMenu().findItem(R.id.action_search).setVisible(searchInstalled); final MenuItem sortByAdded = menu.findItem(R.id.action_sort_added);
searchToolbar.getMenu().findItem(R.id.action_refresh).setVisible(searchInstalled); final SearchSortOrder sortOrder = applicationSettings.getLastUsedSearchSortOrder();
searchToolbar.getMenu().findItem(R.id.action_downloadsearch).setVisible(!searchInstalled); if (sortOrder == SearchSortOrder.BySeeders) {
if (searchsitesList != null) { sortBySeeders.setChecked(true);
searchsitesList.setVisibility(searchInstalled ? View.VISIBLE : View.GONE); } else {
} sortByAdded.setChecked(true);
if (searchInstalled) { }
getFragmentManager().beginTransaction().show(fragmentResults).commit(); return true;
} else { }
getFragmentManager().beginTransaction().hide(fragmentResults).commit();
} @Override
installmoduleText.setVisibility(searchInstalled ? View.GONE : View.VISIBLE); public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
return true;
} boolean searchInstalled = searchHelper.isTorrentSearchInstalled();
searchToolbar.getMenu().findItem(R.id.action_search).setVisible(searchInstalled);
@Override searchToolbar.getMenu().findItem(R.id.action_refresh).setVisible(searchInstalled);
protected void onNewIntent(Intent intent) { searchToolbar.getMenu().findItem(R.id.action_downloadsearch).setVisible(!searchInstalled);
handleIntent(intent); if (searchsitesList != null) {
refreshSearch(); searchsitesList.setVisibility(searchInstalled ? View.VISIBLE : View.GONE);
} }
if (searchInstalled) {
private void handleIntent(Intent intent) { getSupportFragmentManager().beginTransaction().show(fragmentResults).commit();
} else {
lastUsedQuery = parseQuery(intent); getSupportFragmentManager().beginTransaction().hide(fragmentResults).commit();
}
// Is this actually a full HTTP URL? Then redirect this request to add the URL directly installmoduleText.setVisibility(searchInstalled ? View.GONE : View.VISIBLE);
if (lastUsedQuery != null && (lastUsedQuery.startsWith("http") || lastUsedQuery.startsWith("https") ||
lastUsedQuery.startsWith("magnet") || lastUsedQuery.startsWith("file"))) { return true;
// Don't broadcast this intent; we can safely assume this is intended for Transdroid only }
Intent i = TorrentsActivity_.intent(this).get();
i.setData(Uri.parse(lastUsedQuery)); @Override
startActivity(i); protected void onNewIntent(Intent intent) {
finish(); super.onNewIntent(intent);
} handleIntent(intent);
refreshSearch();
} }
@Override private void handleIntent(Intent intent) {
public boolean onSearchRequested() {
if (searchMenu != null) { lastUsedQuery = parseQuery(intent);
searchMenu.expandActionView();
} // Is this actually a full HTTP URL? Then redirect this request to add the URL directly
return true; if (lastUsedQuery != null && (lastUsedQuery.startsWith("http") || lastUsedQuery.startsWith("https") ||
} lastUsedQuery.startsWith("magnet") || lastUsedQuery.startsWith("file"))) {
// Don't broadcast this intent; we can safely assume this is intended for Transdroid only
private OnItemClickListener onSearchSiteClicked = new OnItemClickListener() { Intent i = TorrentsActivity_.intent(this).get();
i.setData(Uri.parse(lastUsedQuery));
@Override startActivity(i);
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { finish();
lastUsedSite = searchSites.get(position); }
refreshSearch();
} }
};
private AdapterView.OnItemSelectedListener onSearchSiteSelected = new AdapterView.OnItemSelectedListener() { @Override
@Override public boolean onSearchRequested() {
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { if (searchMenu != null) {
lastUsedSite = searchSites.get(position); searchMenu.expandActionView();
refreshSearch(); }
} return true;
}
@Override
public void onNothingSelected(AdapterView<?> parent) { /**
} * Extracts the query string from the search {@link Intent}
}; *
* @return The query string that was entered by the user
/** */
* Extracts the query string from the search {@link Intent} private String parseQuery(Intent intent) {
* @return The query string that was entered by the user
*/ String query = null;
private String parseQuery(Intent intent) { if (intent.getAction().equals(Intent.ACTION_SEARCH)) {
query = intent.getStringExtra(SearchManager.QUERY).trim();
String query = null; } else if (intent.getAction().equals(Intent.ACTION_SEND)) {
if (intent.getAction().equals(Intent.ACTION_SEARCH)) { query = SendIntentHelper.cleanUpText(intent).trim();
query = intent.getStringExtra(SearchManager.QUERY).trim(); }
} else if (intent.getAction().equals(Intent.ACTION_SEND)) { if (query != null && query.length() > 0) {
query = SendIntentHelper.cleanUpText(intent).trim();
} // Remember this search query to later show as a suggestion
if (query != null && query.length() > 0) { suggestions.saveRecentQuery(query, null);
return query;
// Remember this search query to later show as a suggestion
suggestions.saveRecentQuery(query, null); }
return query; return null;
} }
return null;
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
} @OptionsItem(android.R.id.home)
protected void navigateUp() {
@TargetApi(Build.VERSION_CODES.HONEYCOMB) TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
@OptionsItem(android.R.id.home) }
protected void navigateUp() {
TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start(); @OptionsItem(R.id.action_refresh)
} protected void refreshSearch() {
@OptionsItem(R.id.action_refresh) if (searchMenu != null) {
protected void refreshSearch() { // Close the search view in the action bar
searchMenu.collapseActionView();
if (searchMenu != null) { }
// Close the search view in the action bar
searchMenu.collapseActionView(); if (lastUsedSite instanceof WebsearchSetting) {
}
// Start a browser page directly to the requested search results
if (lastUsedSite instanceof WebsearchSetting) { WebsearchSetting websearch = (WebsearchSetting) lastUsedSite;
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(websearch.getBaseUrl().replace("%s", lastUsedQuery))));
// Start a browser page directly to the requested search results finish();
WebsearchSetting websearch = (WebsearchSetting) lastUsedSite;
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(websearch.getBaseUrl().replace("%s", lastUsedQuery)))); } else if (lastUsedSite instanceof SearchSite) {
finish();
// Save the search site currently used to search for future usage
} else if (lastUsedSite instanceof SearchSite) { applicationSettings.setLastUsedSearchSite(lastUsedSite);
// Update the activity title (only shown on large devices)
// Save the search site currently used to search for future usage if (sitesSpinner == null && getSupportActionBar() != null)
applicationSettings.setLastUsedSearchSite(lastUsedSite); getSupportActionBar()
// Update the activity title (only shown on large devices) .setTitle(NavigationHelper.buildCondensedFontString(getString(R.string.search_queryonsite, lastUsedQuery, lastUsedSite.getName())));
if (sitesSpinner == null && getSupportActionBar() != null) // Ask the results fragment to start a search for the specified query
getSupportActionBar() fragmentResults.startSearch(lastUsedQuery, (SearchSite) lastUsedSite, applicationSettings.getLastUsedSearchSortOrder());
.setTitle(NavigationHelper.buildCondensedFontString(getString(R.string.search_queryonsite, lastUsedQuery, lastUsedSite.getName())));
// Ask the results fragment to start a search for the specified query }
fragmentResults.startSearch(lastUsedQuery, (SearchSite) lastUsedSite, applicationSettings.getLastUsedSearchSortOrder()); }
} @OptionsItem(R.id.action_downloadsearch)
} protected void downloadSearchModule() {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.transdroid.org/latest-search")));
@OptionsItem(R.id.action_downloadsearch) }
protected void downloadSearchModule() {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.transdroid.org/latest-search"))); @OptionsItem(R.id.action_sort_added)
} protected void sortByDateAdded() {
if (applicationSettings.getLastUsedSearchSortOrder() == SearchSortOrder.Combined) {
@OptionsItem(R.id.action_sort_added) return;
protected void sortByDateAdded() { }
if (applicationSettings.getLastUsedSearchSortOrder() == SearchSortOrder.Combined) { invalidateOptionsMenu();
return; applicationSettings.setLastUsedSearchSortOrder(SearchSortOrder.Combined);
} refreshSearch();
invalidateOptionsMenu(); }
applicationSettings.setLastUsedSearchSortOrder(SearchSortOrder.Combined);
refreshSearch(); @OptionsItem(R.id.action_sort_seeders)
} protected void sortBySeeders() {
if (applicationSettings.getLastUsedSearchSortOrder() == SearchSortOrder.BySeeders) {
@OptionsItem(R.id.action_sort_seeders) return;
protected void sortBySeeders() { }
if (applicationSettings.getLastUsedSearchSortOrder() == SearchSortOrder.BySeeders) { invalidateOptionsMenu();
return; applicationSettings.setLastUsedSearchSortOrder(SearchSortOrder.BySeeders);
} refreshSearch();
invalidateOptionsMenu(); }
applicationSettings.setLastUsedSearchSortOrder(SearchSortOrder.BySeeders);
refreshSearch();
}
} }

17
app/src/main/java/org/transdroid/core/gui/search/SearchHistoryProvider.java

@ -24,19 +24,20 @@ import org.transdroid.BuildConfig;
/** /**
* Provides search suggestions by simply returning previous user entries. * Provides search suggestions by simply returning previous user entries.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class SearchHistoryProvider extends SearchRecentSuggestionsProvider { public class SearchHistoryProvider extends SearchRecentSuggestionsProvider {
public final static String AUTHORITY = BuildConfig.APPLICATION_ID + ".search.SearchHistoryProvider"; public final static String AUTHORITY = BuildConfig.APPLICATION_ID + ".search.SearchHistoryProvider";
public final static int MODE = DATABASE_MODE_QUERIES; public final static int MODE = DATABASE_MODE_QUERIES;
public SearchHistoryProvider() { public SearchHistoryProvider() {
setupSuggestions(AUTHORITY, MODE); setupSuggestions(AUTHORITY, MODE);
} }
public static void clearHistory(Context context) { public static void clearHistory(Context context) {
new SearchRecentSuggestions(context, AUTHORITY, MODE).clearHistory(); new SearchRecentSuggestions(context, AUTHORITY, MODE).clearHistory();
} }
} }

41
app/src/main/java/org/transdroid/core/gui/search/SearchResultView.java

@ -16,41 +16,42 @@
*/ */
package org.transdroid.core.gui.search; package org.transdroid.core.gui.search;
import org.androidannotations.annotations.EViewGroup;
import org.androidannotations.annotations.ViewById;
import org.transdroid.R;
import org.transdroid.core.app.search.SearchResult;
import android.content.Context; import android.content.Context;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import org.androidannotations.annotations.EViewGroup;
import org.androidannotations.annotations.ViewById;
import org.transdroid.R;
import org.transdroid.core.app.search.SearchResult;
/** /**
* View that represents a {@link SearchResult} object from an in-app search * View that represents a {@link SearchResult} object from an in-app search
*
* @author Eric Kok * @author Eric Kok
*/ */
@EViewGroup(resName = "list_item_searchresult") @EViewGroup(resName = "list_item_searchresult")
public class SearchResultView extends RelativeLayout { public class SearchResultView extends RelativeLayout {
// Views // Views
@ViewById @ViewById
protected TextView nameText, seedersText, leechersText, sizeText, dateText; protected TextView nameText, seedersText, leechersText, sizeText, dateText;
public SearchResultView(Context context) { public SearchResultView(Context context) {
super(context); super(context);
} }
public void bind(SearchResult result) { public void bind(SearchResult result) {
nameText.setText(result.getName()); nameText.setText(result.getName());
sizeText.setText(result.getSize()); sizeText.setText(result.getSize());
dateText.setText(result.getAddedOn() == null ? "" : DateUtils.getRelativeDateTimeString(getContext(), result dateText.setText(result.getAddedOn() == null ? "" : DateUtils.getRelativeDateTimeString(getContext(), result
.getAddedOn().getTime(), DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, .getAddedOn().getTime(), DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS,
DateUtils.FORMAT_ABBREV_MONTH)); DateUtils.FORMAT_ABBREV_MONTH));
seedersText.setText(getContext().getString(R.string.search_seeders, result.getSeeders())); seedersText.setText(getContext().getString(R.string.search_seeders, result.getSeeders()));
leechersText.setText(getContext().getString(R.string.search_leechers, result.getLeechers())); leechersText.setText(getContext().getString(R.string.search_leechers, result.getLeechers()));
} }
} }

91
app/src/main/java/org/transdroid/core/gui/search/SearchResultsAdapter.java

@ -29,61 +29,62 @@ import java.util.List;
/** /**
* Adapter that contains a list of {@link SearchResult}s. * Adapter that contains a list of {@link SearchResult}s.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EBean @EBean
public class SearchResultsAdapter extends BaseAdapter { public class SearchResultsAdapter extends BaseAdapter {
private List<SearchResult> results = null; @RootContext
protected Context context;
@RootContext private List<SearchResult> results = null;
protected Context context;
/** /**
* Allows updating the search results, replacing the old data * Allows updating the search results, replacing the old data
* @param results The new list of search results *
*/ * @param results The new list of search results
public void update(List<SearchResult> results) { */
this.results = results; public void update(List<SearchResult> results) {
notifyDataSetChanged(); this.results = results;
} notifyDataSetChanged();
}
@Override @Override
public boolean hasStableIds() { public boolean hasStableIds() {
return true; return true;
} }
@Override @Override
public int getCount() { public int getCount() {
if (results == null) { if (results == null) {
return 0; return 0;
} }
return results.size(); return results.size();
} }
@Override @Override
public SearchResult getItem(int position) { public SearchResult getItem(int position) {
if (results == null) { if (results == null) {
return null; return null;
} }
return results.get(position); return results.get(position);
} }
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
return position; return position;
} }
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
SearchResultView rssitemView; SearchResultView rssitemView;
if (convertView == null) { if (convertView == null) {
rssitemView = SearchResultView_.build(context); rssitemView = SearchResultView_.build(context);
} else { } else {
rssitemView = (SearchResultView) convertView; rssitemView = (SearchResultView) convertView;
} }
rssitemView.bind(getItem(position)); rssitemView.bind(getItem(position));
return rssitemView; return rssitemView;
} }
} }

326
app/src/main/java/org/transdroid/core/gui/search/SearchResultsFragment.java

@ -16,11 +16,9 @@
*/ */
package org.transdroid.core.gui.search; package org.transdroid.core.gui.search;
import android.app.Fragment;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import androidx.appcompat.app.AppCompatActivity;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.ActionMode; import android.view.ActionMode;
import android.view.Menu; import android.view.Menu;
@ -32,6 +30,9 @@ import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import com.nispok.snackbar.Snackbar; import com.nispok.snackbar.Snackbar;
import com.nispok.snackbar.SnackbarManager; import com.nispok.snackbar.SnackbarManager;
@ -48,7 +49,6 @@ import org.transdroid.core.app.search.SearchHelper;
import org.transdroid.core.app.search.SearchHelper.SearchSortOrder; import org.transdroid.core.app.search.SearchHelper.SearchSortOrder;
import org.transdroid.core.app.search.SearchResult; import org.transdroid.core.app.search.SearchResult;
import org.transdroid.core.app.search.SearchSite; import org.transdroid.core.app.search.SearchSite;
import org.transdroid.core.app.settings.SystemSettings_;
import org.transdroid.core.gui.TorrentsActivity_; import org.transdroid.core.gui.TorrentsActivity_;
import org.transdroid.core.gui.navigation.NavigationHelper_; import org.transdroid.core.gui.navigation.NavigationHelper_;
import org.transdroid.core.gui.navigation.SelectionManagerMode; import org.transdroid.core.gui.navigation.SelectionManagerMode;
@ -58,170 +58,170 @@ import java.util.List;
/** /**
* Fragment that lists the items in a specific RSS feed * Fragment that lists the items in a specific RSS feed
*
* @author Eric Kok * @author Eric Kok
*/ */
@EFragment(R.layout.fragment_searchresults) @EFragment(R.layout.fragment_searchresults)
public class SearchResultsFragment extends Fragment { public class SearchResultsFragment extends Fragment {
@InstanceState @InstanceState
protected ArrayList<SearchResult> results = null; protected ArrayList<SearchResult> results = null;
@InstanceState @InstanceState
protected String resultsSource; protected String resultsSource;
@Bean @Bean
protected SearchHelper searchHelper; protected SearchHelper searchHelper;
// Views // Views
@ViewById(R.id.searchresults_list) @ViewById(R.id.searchresults_list)
protected ListView resultsList; protected ListView resultsList;
@Bean @Bean
protected SearchResultsAdapter resultsAdapter; protected SearchResultsAdapter resultsAdapter;
@ViewById @ViewById
protected TextView emptyText; protected TextView emptyText;
@ViewById @ViewById
protected ProgressBar loadingProgress; protected ProgressBar loadingProgress;
private MultiChoiceModeListener onItemsSelected = new MultiChoiceModeListener() {
@AfterViews
protected void init() { SelectionManagerMode selectionManagerMode;
// On large screens where this fragment is shown next to the sites list; we show a continues grey vertical line @Override
// to separate the lists visually public boolean onCreateActionMode(ActionMode mode, Menu menu) {
if (!NavigationHelper_.getInstance_(getActivity()).isSmallScreen()) { // Show contextual action bar to add items in batch mode
resultsList.setBackgroundResource(R.drawable.details_list_background); mode.getMenuInflater().inflate(R.menu.fragment_searchresults_cab, menu);
} Context themedContext = ((AppCompatActivity) getActivity()).getSupportActionBar().getThemedContext();
selectionManagerMode = new SelectionManagerMode(themedContext, resultsList, R.plurals.search_resutlsselected);
// Set up the list adapter, which allows multi-select selectionManagerMode.onCreateActionMode(mode, menu);
resultsList.setAdapter(resultsAdapter); return true;
resultsList.setMultiChoiceModeListener(onItemsSelected); }
if (results != null) {
showResults(); @Override
} public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return selectionManagerMode.onPrepareActionMode(mode, menu);
} }
public void startSearch(String query, SearchSite site, SearchSortOrder sortBy) { public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
loadingProgress.setVisibility(View.VISIBLE);
resultsList.setVisibility(View.GONE); // Get checked torrents
emptyText.setVisibility(View.GONE); List<SearchResult> checked = new ArrayList<>();
performSearch(query, site, sortBy); for (int i = 0; i < resultsList.getCheckedItemPositions().size(); i++) {
} if (resultsList.getCheckedItemPositions().valueAt(i)) {
checked.add(resultsAdapter.getItem(resultsList.getCheckedItemPositions().keyAt(i)));
@Background }
protected void performSearch(String query, SearchSite site, SearchSortOrder sortBy) { }
results = searchHelper.search(query, site, sortBy);
resultsSource = site.isPrivate() ? site.getKey() : null; int itemId = item.getItemId();
showResults(); if (itemId == R.id.action_addall) {
} // Start an Intent that adds multiple items at once, by supplying the urls and titles as string array
// extras and setting the Intent action to ADD_MULTIPLE
@UiThread Intent intent = new Intent("org.transdroid.ADD_MULTIPLE");
protected void showResults() { String[] urls = new String[checked.size()];
loadingProgress.setVisibility(View.GONE); String[] titles = new String[checked.size()];
if (results == null || results.size() == 0) { for (int i = 0; i < checked.size(); i++) {
resultsList.setVisibility(View.GONE); urls[i] = checked.get(i).getTorrentUrl();
emptyText.setVisibility(View.VISIBLE); titles[i] = checked.get(i).getName();
return; }
} intent.putExtra("TORRENT_URLS", urls);
resultsAdapter.update(results); intent.putExtra("TORRENT_TITLES", titles);
resultsList.setVisibility(View.VISIBLE); if (resultsSource != null) {
emptyText.setVisibility(View.GONE); intent.putExtra("PRIVATE_SOURCE", resultsSource);
} }
startActivity(intent);
public void clearResults() { mode.finish();
loadingProgress.setVisibility(View.GONE); return true;
resultsList.setVisibility(View.GONE); } else if (itemId == R.id.action_showdetails) {
emptyText.setVisibility(View.VISIBLE); SearchResult first = checked.get(0);
} // Open the torrent's web page in the browser
if (checked.size() > 1) {
@ItemClick(R.id.searchresults_list) Toast.makeText(getActivity(), getString(R.string.search_openingdetails, first.getName()), Toast.LENGTH_LONG).show();
protected void onItemClicked(SearchResult item) { }
if (item.getTorrentUrl() == null) { if (TextUtils.isEmpty(first.getDetailsUrl())) {
SnackbarManager.show(Snackbar.with(getActivity()).text(R.string.error_notorrentfile).colorResource(R.color.red)); Toast.makeText(getActivity(), getString(R.string.error_invalid_url_form, first.getName()), Toast.LENGTH_LONG).show();
return; return false;
} }
// Don't broadcast this intent; we can safely assume this is intended for Transdroid only startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(first.getDetailsUrl())));
Intent i = TorrentsActivity_.intent(getActivity()).get(); return true;
i.setData(Uri.parse(item.getTorrentUrl())); } else {
i.putExtra("TORRENT_TITLE", item.getName()); return false;
if (resultsSource != null) { }
i.putExtra("PRIVATE_SOURCE", resultsSource); }
}
startActivity(i); @Override
} public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
selectionManagerMode.onItemCheckedStateChanged(mode, position, id, checked);
private MultiChoiceModeListener onItemsSelected = new MultiChoiceModeListener() { }
SelectionManagerMode selectionManagerMode; @Override
public void onDestroyActionMode(ActionMode mode) {
@Override selectionManagerMode.onDestroyActionMode(mode);
public boolean onCreateActionMode(ActionMode mode, Menu menu) { }
// Show contextual action bar to add items in batch mode
mode.getMenuInflater().inflate(R.menu.fragment_searchresults_cab, menu); };
Context themedContext = ((AppCompatActivity) getActivity()).getSupportActionBar().getThemedContext();
selectionManagerMode = new SelectionManagerMode(themedContext, resultsList, R.plurals.search_resutlsselected); @AfterViews
selectionManagerMode.onCreateActionMode(mode, menu); protected void init() {
return true;
} // On large screens where this fragment is shown next to the sites list; we show a continues grey vertical line
// to separate the lists visually
@Override if (!NavigationHelper_.getInstance_(getActivity()).isSmallScreen()) {
public boolean onPrepareActionMode(ActionMode mode, Menu menu) { resultsList.setBackgroundResource(R.drawable.details_list_background);
return selectionManagerMode.onPrepareActionMode(mode, menu); }
}
// Set up the list adapter, which allows multi-select
public boolean onActionItemClicked(ActionMode mode, MenuItem item) { resultsList.setAdapter(resultsAdapter);
resultsList.setMultiChoiceModeListener(onItemsSelected);
// Get checked torrents if (results != null) {
List<SearchResult> checked = new ArrayList<SearchResult>(); showResults();
for (int i = 0; i < resultsList.getCheckedItemPositions().size(); i++) { }
if (resultsList.getCheckedItemPositions().valueAt(i)) {
checked.add(resultsAdapter.getItem(resultsList.getCheckedItemPositions().keyAt(i))); }
}
} public void startSearch(String query, SearchSite site, SearchSortOrder sortBy) {
loadingProgress.setVisibility(View.VISIBLE);
int itemId = item.getItemId(); resultsList.setVisibility(View.GONE);
if (itemId == R.id.action_addall) { emptyText.setVisibility(View.GONE);
// Start an Intent that adds multiple items at once, by supplying the urls and titles as string array performSearch(query, site, sortBy);
// extras and setting the Intent action to ADD_MULTIPLE }
Intent intent = new Intent("org.transdroid.ADD_MULTIPLE");
String[] urls = new String[checked.size()]; @Background
String[] titles = new String[checked.size()]; protected void performSearch(String query, SearchSite site, SearchSortOrder sortBy) {
for (int i = 0; i < checked.size(); i++) { results = searchHelper.search(query, site, sortBy);
urls[i] = checked.get(i).getTorrentUrl(); resultsSource = site.isPrivate() ? site.getKey() : null;
titles[i] = checked.get(i).getName(); showResults();
} }
intent.putExtra("TORRENT_URLS", urls);
intent.putExtra("TORRENT_TITLES", titles); @UiThread
if (resultsSource != null) { protected void showResults() {
intent.putExtra("PRIVATE_SOURCE", resultsSource); loadingProgress.setVisibility(View.GONE);
} if (results == null || results.size() == 0) {
startActivity(intent); resultsList.setVisibility(View.GONE);
mode.finish(); emptyText.setVisibility(View.VISIBLE);
return true; return;
} else if (itemId == R.id.action_showdetails) { }
SearchResult first = checked.get(0); resultsAdapter.update(results);
// Open the torrent's web page in the browser resultsList.setVisibility(View.VISIBLE);
if (checked.size() > 1) { emptyText.setVisibility(View.GONE);
Toast.makeText(getActivity(), getString(R.string.search_openingdetails, first.getName()), Toast.LENGTH_LONG).show(); }
}
if (TextUtils.isEmpty(first.getDetailsUrl())) { public void clearResults() {
Toast.makeText(getActivity(), getString(R.string.error_invalid_url_form, first.getName()), Toast.LENGTH_LONG).show(); loadingProgress.setVisibility(View.GONE);
return false; resultsList.setVisibility(View.GONE);
} emptyText.setVisibility(View.VISIBLE);
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(first.getDetailsUrl()))); }
return true;
} else { @ItemClick(R.id.searchresults_list)
return false; protected void onItemClicked(SearchResult item) {
} if (item.getTorrentUrl() == null) {
} SnackbarManager.show(Snackbar.with(getActivity()).text(R.string.error_notorrentfile).colorResource(R.color.red));
return;
@Override }
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { // Don't broadcast this intent; we can safely assume this is intended for Transdroid only
selectionManagerMode.onItemCheckedStateChanged(mode, position, id, checked); Intent i = TorrentsActivity_.intent(getActivity()).get();
} i.setData(Uri.parse(item.getTorrentUrl()));
i.putExtra("TORRENT_TITLE", item.getName());
@Override if (resultsSource != null) {
public void onDestroyActionMode(ActionMode mode) { i.putExtra("PRIVATE_SOURCE", resultsSource);
selectionManagerMode.onDestroyActionMode(mode); }
} startActivity(i);
}
};
} }

22
app/src/main/java/org/transdroid/core/gui/search/SearchSetting.java

@ -20,16 +20,18 @@ import org.transdroid.core.gui.lists.SimpleListItem;
public interface SearchSetting extends SimpleListItem { public interface SearchSetting extends SimpleListItem {
/** /**
* Should return a unique key for this search setting, so that it can be compared (using equals()) to other settings. * Should return a unique key for this search setting, so that it can be compared (using equals()) to other settings.
* @return A unique string identifying this search setting *
*/ * @return A unique string identifying this search setting
public String getKey(); */
String getKey();
/** /**
* Should return an URL (which may still be abstract and not the actual search URL) specific to the search site * Should return an URL (which may still be abstract and not the actual search URL) specific to the search site
* @return A clean URL directing to the search site, to, for example, get the favicon of the site *
*/ * @return A clean URL directing to the search site, to, for example, get the favicon of the site
public String getBaseUrl(); */
String getBaseUrl();
} }

17
app/src/main/java/org/transdroid/core/gui/search/SearchSettingSelectionView.java

@ -26,20 +26,21 @@ import org.transdroid.R;
/** /**
* View that shows, as part of the action bar spinner, which {@link SearchSetting} is currently chosen. * View that shows, as part of the action bar spinner, which {@link SearchSetting} is currently chosen.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EViewGroup(R.layout.actionbar_searchsite) @EViewGroup(R.layout.actionbar_searchsite)
public class SearchSettingSelectionView extends FrameLayout { public class SearchSettingSelectionView extends FrameLayout {
@ViewById @ViewById
protected TextView searchsiteText; protected TextView searchsiteText;
public SearchSettingSelectionView(Context context) { public SearchSettingSelectionView(Context context) {
super(context); super(context);
} }
public void bind(SearchSetting searchSettingItem) { public void bind(SearchSetting searchSettingItem) {
searchsiteText.setText(searchSettingItem.getName()); searchsiteText.setText(searchSettingItem.getName());
} }
} }

51
app/src/main/java/org/transdroid/core/gui/search/SearchSettingsDropDownAdapter.java

@ -27,34 +27,35 @@ import java.util.List;
/** /**
* List adapter that holds search settings, that is, web searches and in-app search sites, displayed as content to a Spinner instead of a ListView. * List adapter that holds search settings, that is, web searches and in-app search sites, displayed as content to a Spinner instead of a ListView.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class SearchSettingsDropDownAdapter extends FilterListItemAdapter { public class SearchSettingsDropDownAdapter extends FilterListItemAdapter {
private final Context context; private final Context context;
public SearchSettingsDropDownAdapter(Context context, List<? extends SimpleListItem> items) { public SearchSettingsDropDownAdapter(Context context, List<? extends SimpleListItem> items) {
super(context, items); super(context, items);
this.context = context; this.context = context;
} }
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
// This returns the item to show in the action bar spinner // This returns the item to show in the action bar spinner
SearchSettingSelectionView filterItemView; SearchSettingSelectionView filterItemView;
if (convertView == null || !(convertView instanceof SearchSettingSelectionView)) { if (!(convertView instanceof SearchSettingSelectionView)) {
filterItemView = SearchSettingSelectionView_.build(context); filterItemView = SearchSettingSelectionView_.build(context);
} else { } else {
filterItemView = (SearchSettingSelectionView) convertView; filterItemView = (SearchSettingSelectionView) convertView;
} }
filterItemView.bind((SearchSetting) getItem(position)); filterItemView.bind((SearchSetting) getItem(position));
return filterItemView; return filterItemView;
} }
@Override @Override
public View getDropDownView(int position, View convertView, ViewGroup parent) { public View getDropDownView(int position, View convertView, ViewGroup parent) {
// This returns the item to show in the drop down list // This returns the item to show in the drop down list
return super.getView(position, convertView, parent); return super.getView(position, convertView, parent);
} }
} }

39
app/src/main/java/org/transdroid/core/gui/search/SearchSiteView.java

@ -31,35 +31,36 @@ import org.transdroid.core.gui.navigation.NavigationHelper;
/** /**
* View that represents some {@link RssfeedSetting} object and displays name as well as loads a favicon for the feed's site and can load how many new * View that represents some {@link RssfeedSetting} object and displays name as well as loads a favicon for the feed's site and can load how many new
* items are available. * items are available.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EViewGroup(R.layout.list_item_searchsite) @EViewGroup(R.layout.list_item_searchsite)
public class SearchSiteView extends LinearLayout { public class SearchSiteView extends LinearLayout {
private static final String GETFVO_URL = "http://g.etfv.co/%1$s"; private static final String GETFVO_URL = "http://g.etfv.co/%1$s";
@Bean @Bean
protected NavigationHelper navigationHelper; protected NavigationHelper navigationHelper;
// Views // Views
@ViewById @ViewById
protected ImageView faviconImage; protected ImageView faviconImage;
@ViewById @ViewById
protected TextView nameText; protected TextView nameText;
public SearchSiteView(Context context) { public SearchSiteView(Context context) {
super(context); super(context);
} }
public void bind(SearchSetting rssfeedLoader) { public void bind(SearchSetting rssfeedLoader) {
// Show the RSS feed name and either a loading indicator or the number of new items // Show the RSS feed name and either a loading indicator or the number of new items
nameText.setText(rssfeedLoader.getName()); nameText.setText(rssfeedLoader.getName());
// Clear and then asynchronously load the site's favicon // Clear and then asynchronously load the site's favicon
// Uses the g.etfv.co service to resolve the favicon of any URL // Uses the g.etfv.co service to resolve the favicon of any URL
faviconImage.setImageDrawable(null); faviconImage.setImageDrawable(null);
navigationHelper.getImageCache().displayImage(String.format(GETFVO_URL, rssfeedLoader.getBaseUrl()), faviconImage); navigationHelper.getImageCache().displayImage(String.format(GETFVO_URL, rssfeedLoader.getBaseUrl()), faviconImage);
} }
} }

91
app/src/main/java/org/transdroid/core/gui/search/SearchSitesAdapter.java

@ -30,61 +30,62 @@ import java.util.List;
/** /**
* Adapter that contains a list of {@link SearchSetting}s, either {@link SearchSite} or {@link WebsearchSetting}. * Adapter that contains a list of {@link SearchSetting}s, either {@link SearchSite} or {@link WebsearchSetting}.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EBean @EBean
public class SearchSitesAdapter extends BaseAdapter { public class SearchSitesAdapter extends BaseAdapter {
private List<SearchSetting> sites = null; @RootContext
protected Context context;
@RootContext private List<SearchSetting> sites = null;
protected Context context;
/** /**
* Allows updating the full internal list of sites at once, replacing the old list * Allows updating the full internal list of sites at once, replacing the old list
* @param sites The new list of search sites, either in-app or web search settings *
*/ * @param sites The new list of search sites, either in-app or web search settings
public void update(List<SearchSetting> sites) { */
this.sites = sites; public void update(List<SearchSetting> sites) {
notifyDataSetChanged(); this.sites = sites;
} notifyDataSetChanged();
}
@Override @Override
public boolean hasStableIds() { public boolean hasStableIds() {
return true; return true;
} }
@Override @Override
public int getCount() { public int getCount() {
if (sites == null) { if (sites == null) {
return 0; return 0;
} }
return sites.size(); return sites.size();
} }
@Override @Override
public SearchSetting getItem(int position) { public SearchSetting getItem(int position) {
if (sites == null) { if (sites == null) {
return null; return null;
} }
return sites.get(position); return sites.get(position);
} }
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
return position; return position;
} }
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
SearchSiteView rssfeedView; SearchSiteView rssfeedView;
if (convertView == null) { if (convertView == null) {
rssfeedView = SearchSiteView_.build(context); rssfeedView = SearchSiteView_.build(context);
} else { } else {
rssfeedView = (SearchSiteView) convertView; rssfeedView = (SearchSiteView) convertView;
} }
rssfeedView.bind(getItem(position)); rssfeedView.bind(getItem(position));
return rssfeedView; return rssfeedView;
} }
} }

88
app/src/main/java/org/transdroid/core/gui/search/SendIntentHelper.java

@ -21,57 +21,59 @@ import android.content.Intent;
/** /**
* Used to clean up text as received from a generic ACTION_SEND intent. This class is highly custom-based for known * Used to clean up text as received from a generic ACTION_SEND intent. This class is highly custom-based for known
* applications, i.e. the EXTRA_TEXT send by some known applications. * applications, i.e. the EXTRA_TEXT send by some known applications.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class SendIntentHelper { public class SendIntentHelper {
private static final String SOUNDHOUND1 = "Just used #SoundHound to find "; private static final String SOUNDHOUND1 = "Just used #SoundHound to find ";
private static final String SOUNDHOUND1_END = " http://"; private static final String SOUNDHOUND1_END = " http://";
private static final String SHAZAM = "I just used Shazam to discover "; private static final String SHAZAM = "I just used Shazam to discover ";
private static final String SHAZAM_END = ". http://"; private static final String SHAZAM_END = ". http://";
private static final String YOUTUBE_ID = "Watch \""; private static final String YOUTUBE_ID = "Watch \"";
private static final String YOUTUBE_START = "\""; private static final String YOUTUBE_START = "\"";
private static final String YOUTUBE_END = "\""; private static final String YOUTUBE_END = "\"";
/** /**
* Cleans a SEND intent text string by removing irrelevant parts, so that the remaining text can be used as search * Cleans a SEND intent text string by removing irrelevant parts, so that the remaining text can be used as search
* string. Typically deals with specific known applications such as Shazam and YouTube's SEND intents. * string. Typically deals with specific known applications such as Shazam and YouTube's SEND intents.
* @param intent The original SEND intent that was received *
* @return A cleaned string to be used as search query * @param intent The original SEND intent that was received
*/ * @return A cleaned string to be used as search query
public static String cleanUpText(Intent intent) { */
public static String cleanUpText(Intent intent) {
if (intent == null || !intent.hasExtra(Intent.EXTRA_TEXT)) { if (intent == null || !intent.hasExtra(Intent.EXTRA_TEXT)) {
return null; return null;
} }
String text = intent.getStringExtra(Intent.EXTRA_TEXT); String text = intent.getStringExtra(Intent.EXTRA_TEXT);
try { try {
// Soundhound song/artist share // Soundhound song/artist share
if (text.startsWith(SOUNDHOUND1)) { if (text.startsWith(SOUNDHOUND1)) {
return cutOut(text, SOUNDHOUND1, SOUNDHOUND1_END).replace(" by ", " "); return cutOut(text, SOUNDHOUND1, SOUNDHOUND1_END).replace(" by ", " ");
} }
// Shazam song share // Shazam song share
if (text.startsWith(SHAZAM)) { if (text.startsWith(SHAZAM)) {
return cutOut(text, SHAZAM, SHAZAM_END).replace(" by ", " "); return cutOut(text, SHAZAM, SHAZAM_END).replace(" by ", " ");
} }
// YouTube app share (stores title in EXTRA_SUBJECT) // YouTube app share (stores title in EXTRA_SUBJECT)
if (intent.hasExtra(Intent.EXTRA_SUBJECT)) { if (intent.hasExtra(Intent.EXTRA_SUBJECT)) {
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT); String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
if (subject.startsWith(YOUTUBE_ID)) { if (subject.startsWith(YOUTUBE_ID)) {
return cutOut(subject, YOUTUBE_START, YOUTUBE_END); return cutOut(subject, YOUTUBE_START, YOUTUBE_END);
} }
} }
} catch (Exception e) { } catch (Exception e) {
// Ignore any errors in parsing; just return the raw text // Ignore any errors in parsing; just return the raw text
} }
return text; return text;
} }
private static String cutOut(String text, String start, String end) { private static String cutOut(String text, String start, String end) {
int startAt = text.indexOf(start) + start.length(); int startAt = text.indexOf(start) + start.length();
return text.substring(startAt, text.indexOf(end, startAt)); return text.substring(startAt, text.indexOf(end, startAt));
} }
} }

62
app/src/main/java/org/transdroid/core/gui/search/UrlEntryDialog.java

@ -19,7 +19,6 @@ package org.transdroid.core.gui.search;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.text.InputType;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -33,35 +32,36 @@ import org.transdroid.core.gui.navigation.NavigationHelper;
public class UrlEntryDialog { public class UrlEntryDialog {
/** /**
* Opens a dialog that allows entry of a single URL string, which (on confirmation) will be supplied to the calling activity's {@link * Opens a dialog that allows entry of a single URL string, which (on confirmation) will be supplied to the calling activity's {@link
* TorrentsActivity#addTorrentByUrl(String, String) method}. * TorrentsActivity#addTorrentByUrl(String, String) method}.
* @param activity The activity that opens (and owns) this dialog *
*/ * @param activity The activity that opens (and owns) this dialog
public static void show(final TorrentsActivity activity) { */
View inputLayout = LayoutInflater.from(activity).inflate(R.layout.dialog_url, null); public static void show(final TorrentsActivity activity) {
final EditText urlEdit = (EditText) inputLayout.findViewById(R.id.url_edit); View inputLayout = LayoutInflater.from(activity).inflate(R.layout.dialog_url, null);
ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); final EditText urlEdit = (EditText) inputLayout.findViewById(R.id.url_edit);
if (clipboard.hasPrimaryClip() && clipboard.getPrimaryClip().getItemCount() > 0) { ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
CharSequence content = clipboard.getPrimaryClip().getItemAt(0).coerceToText(activity); if (clipboard.hasPrimaryClip() && clipboard.getPrimaryClip().getItemCount() > 0) {
urlEdit.setText(content); CharSequence content = clipboard.getPrimaryClip().getItemAt(0).coerceToText(activity);
} urlEdit.setText(content);
new MaterialDialog.Builder(activity).customView(inputLayout, false).positiveText(android.R.string.ok).negativeText(android.R.string.cancel) }
.callback(new MaterialDialog.ButtonCallback() { new MaterialDialog.Builder(activity)
@Override .customView(inputLayout, false)
public void onPositive(MaterialDialog dialog) { .positiveText(android.R.string.ok)
String url = urlEdit.getText().toString(); .negativeText(android.R.string.cancel)
Uri uri = Uri.parse(url); .onPositive((dialog, which) -> {
if (!TextUtils.isEmpty(url)) { String url = urlEdit.getText().toString();
String title = NavigationHelper.extractNameFromUri(uri); Uri uri = Uri.parse(url);
if (uri.getScheme() != null && uri.getScheme().equals("magnet")) { if (!TextUtils.isEmpty(url)) {
activity.addTorrentByMagnetUrl(url, title); String title = NavigationHelper.extractNameFromUri(uri);
} else { if (uri.getScheme() != null && uri.getScheme().equals("magnet")) {
activity.addTorrentByUrl(url, title); activity.addTorrentByMagnetUrl(url, title);
} } else {
} activity.addTorrentByUrl(url, title);
} }
}).show(); }
} }).show();
}
} }

47
app/src/main/java/org/transdroid/core/gui/settings/AboutDialog.java

@ -16,38 +16,39 @@
*/ */
package org.transdroid.core.gui.settings; package org.transdroid.core.gui.settings;
import org.transdroid.R;
import org.transdroid.core.gui.navigation.DialogHelper;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import org.transdroid.R;
import org.transdroid.core.gui.navigation.DialogHelper;
/** /**
* Fragment that shows info about the application developer and used open source libraries. * Fragment that shows info about the application developer and used open source libraries.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class AboutDialog implements DialogHelper.DialogSpecification { public class AboutDialog implements DialogHelper.DialogSpecification {
private static final long serialVersionUID = -4711432869714292985L; private static final long serialVersionUID = -4711432869714292985L;
@Override @Override
public int getDialogLayoutId() { public int getDialogLayoutId() {
return R.layout.dialog_about; return R.layout.dialog_about;
} }
@Override @Override
public int getDialogMenuId() { public int getDialogMenuId() {
return R.menu.dialog_about; return R.menu.dialog_about;
} }
@Override @Override
public boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId) { public boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId) {
if (selectedItemId == R.id.action_visitwebsite) { if (selectedItemId == R.id.action_visitwebsite) {
ownerActivity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://transdroid.org"))); ownerActivity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://transdroid.org")));
return true; return true;
} }
return false; return false;
} }
} }

47
app/src/main/java/org/transdroid/core/gui/settings/ChangelogDialog.java

@ -16,38 +16,39 @@
*/ */
package org.transdroid.core.gui.settings; package org.transdroid.core.gui.settings;
import org.transdroid.R;
import org.transdroid.core.gui.navigation.DialogHelper;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import org.transdroid.R;
import org.transdroid.core.gui.navigation.DialogHelper;
/** /**
* Fragment that shows recent app changes. * Fragment that shows recent app changes.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class ChangelogDialog implements DialogHelper.DialogSpecification { public class ChangelogDialog implements DialogHelper.DialogSpecification {
private static final long serialVersionUID = -4563410777022941124L; private static final long serialVersionUID = -4563410777022941124L;
@Override @Override
public int getDialogLayoutId() { public int getDialogLayoutId() {
return R.layout.dialog_changelog; return R.layout.dialog_changelog;
} }
@Override @Override
public int getDialogMenuId() { public int getDialogMenuId() {
return R.menu.dialog_about; return R.menu.dialog_about;
} }
@Override @Override
public boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId) { public boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId) {
if (selectedItemId == R.id.action_visitwebsite) { if (selectedItemId == R.id.action_visitwebsite) {
ownerActivity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://transdroid.org/about/changelog/"))); ownerActivity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://transdroid.org/about/changelog/")));
return true; return true;
} }
return false; return false;
} }
} }

143
app/src/main/java/org/transdroid/core/gui/settings/HelpSettingsActivity.java

@ -39,84 +39,69 @@ import org.transdroid.core.gui.navigation.NavigationHelper;
@EActivity @EActivity
public class HelpSettingsActivity extends PreferenceCompatActivity { public class HelpSettingsActivity extends PreferenceCompatActivity {
protected static final int DIALOG_CHANGELOG = 0; protected static final int DIALOG_CHANGELOG = 0;
protected static final int DIALOG_ABOUT = 1; protected static final int DIALOG_ABOUT = 1;
protected static final String INSTALLHELP_URI = "http://www.transdroid.org/download/"; protected static final String INSTALLHELP_URI = "http://www.transdroid.org/download/";
@Bean @Bean
protected NavigationHelper navigationHelper; protected NavigationHelper navigationHelper;
@Bean @Bean
protected ApplicationSettings applicationSettings; protected ApplicationSettings applicationSettings;
@Bean @Bean
protected ErrorLogSender errorLogSender; protected ErrorLogSender errorLogSender;
@Bean @Bean
protected SettingsPersistence settingsPersistence; protected SettingsPersistence settingsPersistence;
private OnPreferenceClickListener onSendLogClick = new OnPreferenceClickListener() {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { public boolean onPreferenceClick(Preference preference) {
super.onCreate(savedInstanceState); errorLogSender.collectAndSendLog(HelpSettingsActivity.this, applicationSettings.getLastUsedServer());
return true;
getSupportActionBar().setDisplayHomeAsUpEnabled(true); }
};
// Just load the system-related preferences from XML private OnPreferenceClickListener onInstallHelpClick = preference -> {
addPreferencesFromResource(R.xml.pref_help); startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(INSTALLHELP_URI)));
return true;
// Handle outgoing links and preference changes };
findPreference("system_sendlog").setOnPreferenceClickListener(onSendLogClick); private OnPreferenceClickListener onChangeLogClick = preference -> {
findPreference("system_installhelp").setOnPreferenceClickListener(onInstallHelpClick); showDialog(DIALOG_CHANGELOG);
findPreference("system_changelog").setOnPreferenceClickListener(onChangeLogClick); return true;
findPreference("system_about").setTitle(getString(R.string.pref_about, getString(R.string.app_name))); };
findPreference("system_about").setOnPreferenceClickListener(onAboutClick); private OnPreferenceClickListener onAboutClick = preference -> {
} showDialog(DIALOG_ABOUT);
return true;
@TargetApi(Build.VERSION_CODES.HONEYCOMB) };
@OptionsItem(android.R.id.home)
protected void navigateUp() { @Override
MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start(); protected void onCreate(Bundle savedInstanceState) {
} super.onCreate(savedInstanceState);
private OnPreferenceClickListener onSendLogClick = new OnPreferenceClickListener() { getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@Override
public boolean onPreferenceClick(Preference preference) { // Just load the system-related preferences from XML
errorLogSender.collectAndSendLog(HelpSettingsActivity.this, applicationSettings.getLastUsedServer()); addPreferencesFromResource(R.xml.pref_help);
return true;
} // Handle outgoing links and preference changes
}; findPreference("system_sendlog").setOnPreferenceClickListener(onSendLogClick);
findPreference("system_installhelp").setOnPreferenceClickListener(onInstallHelpClick);
private OnPreferenceClickListener onInstallHelpClick = new OnPreferenceClickListener() { findPreference("system_changelog").setOnPreferenceClickListener(onChangeLogClick);
@Override findPreference("system_about").setTitle(getString(R.string.pref_about, getString(R.string.app_name)));
public boolean onPreferenceClick(Preference preference) { findPreference("system_about").setOnPreferenceClickListener(onAboutClick);
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(INSTALLHELP_URI))); }
return true;
} @TargetApi(Build.VERSION_CODES.HONEYCOMB)
}; @OptionsItem(android.R.id.home)
protected void navigateUp() {
private OnPreferenceClickListener onChangeLogClick = new OnPreferenceClickListener() { MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
@SuppressWarnings("deprecation") }
@Override
public boolean onPreferenceClick(Preference preference) { protected Dialog onCreateDialog(int id) {
showDialog(DIALOG_CHANGELOG); switch (id) {
return true; case DIALOG_CHANGELOG:
} return DialogHelper.showDialog(this, new ChangelogDialog());
}; case DIALOG_ABOUT:
return DialogHelper.showDialog(this, new AboutDialog());
private OnPreferenceClickListener onAboutClick = new OnPreferenceClickListener() { }
@SuppressWarnings("deprecation") return null;
@Override }
public boolean onPreferenceClick(Preference preference) {
showDialog(DIALOG_ABOUT);
return true;
}
};
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_CHANGELOG:
return DialogHelper.showDialog(this, new ChangelogDialog());
case DIALOG_ABOUT:
return DialogHelper.showDialog(this, new AboutDialog());
}
return null;
}
} }

70
app/src/main/java/org/transdroid/core/gui/settings/InterceptableEditTextPreference.java

@ -9,40 +9,40 @@ import androidx.preference.EditTextPreference;
public class InterceptableEditTextPreference extends EditTextPreference { public class InterceptableEditTextPreference extends EditTextPreference {
private OnPreferenceClickListener overrideClickListener = null; private OnPreferenceClickListener overrideClickListener = null;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public InterceptableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { public InterceptableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes); super(context, attrs, defStyleAttr, defStyleRes);
} }
public InterceptableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { public InterceptableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
} }
public InterceptableEditTextPreference(Context context, AttributeSet attrs) { public InterceptableEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
} }
public InterceptableEditTextPreference(Context context) { public InterceptableEditTextPreference(Context context) {
super(context); super(context);
} }
@Override @Override
public OnPreferenceClickListener getOnPreferenceClickListener() { public OnPreferenceClickListener getOnPreferenceClickListener() {
return overrideClickListener; return overrideClickListener;
} }
@Override @Override
public void setOnPreferenceClickListener(OnPreferenceClickListener onPreferenceClickListener) { public void setOnPreferenceClickListener(OnPreferenceClickListener onPreferenceClickListener) {
this.overrideClickListener = onPreferenceClickListener; this.overrideClickListener = onPreferenceClickListener;
} }
@Override @Override
protected void onClick() { protected void onClick() {
if (overrideClickListener == null || !overrideClickListener.onPreferenceClick(this)) { if (overrideClickListener == null || !overrideClickListener.onPreferenceClick(this)) {
super.onClick(); super.onClick();
} }
} }
} }

388
app/src/main/java/org/transdroid/core/gui/settings/KeyBoundPreferencesActivity.java

@ -40,201 +40,207 @@ import java.util.Map;
* and then call initXPreference for each contained preference. {@link #onPreferencesChanged()} can be overridden to * and then call initXPreference for each contained preference. {@link #onPreferencesChanged()} can be overridden to
* react to preference changes, e.g. when field availability should be updated (and where preference dependency isn't * react to preference changes, e.g. when field availability should be updated (and where preference dependency isn't
* enough). * enough).
*
* @author Eric Kok * @author Eric Kok
*/ */
@EActivity @EActivity
public abstract class KeyBoundPreferencesActivity extends PreferenceCompatActivity { public abstract class KeyBoundPreferencesActivity extends PreferenceCompatActivity {
@Extra @Extra
protected int key = -1; protected int key = -1;
private SharedPreferences sharedPrefs; private SharedPreferences sharedPrefs;
private Map<String, String> originalSummaries = new HashMap<>(); private Map<String, String> originalSummaries = new HashMap<>();
private OnSharedPreferenceChangeListener onPreferenceChangeListener = (sharedPreferences, key) -> {
/** showValueOnSummary(key);
* Should be called during the activity {@link #onCreate(android.os.Bundle)} (but after super.onCreate(Bundle)) to onPreferencesChanged();
* load the preferences for this screen from an XML resource. };
* @param preferencesResId The XML resource to read preferences from, which may contain embedded
* {@link PreferenceScreen} objects /**
* @param currentMaxKey The value of what is currently the last defined settings object, or -1 of no settings were * Should be called during the activity {@link #onCreate(android.os.Bundle)} (but after super.onCreate(Bundle)) to
* defined so far at all * load the preferences for this screen from an XML resource.
*/ *
protected final void init(int preferencesResId, int currentMaxKey) { * @param preferencesResId The XML resource to read preferences from, which may contain embedded
* {@link PreferenceScreen} objects
// Load the raw preferences to show in this screen * @param currentMaxKey The value of what is currently the last defined settings object, or -1 of no settings were
addPreferencesFromResource(preferencesResId); * defined so far at all
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); */
protected final void init(int preferencesResId, int currentMaxKey) {
// If no key was supplied (in the extra bundle) then use a new key instead
if (key < 0) { // Load the raw preferences to show in this screen
key = currentMaxKey + 1; addPreferencesFromResource(preferencesResId);
} sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
} // If no key was supplied (in the extra bundle) then use a new key instead
if (key < 0) {
protected void onResume() { key = currentMaxKey + 1;
super.onResume(); }
// Monitor preference changes
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener( }
onPreferenceChangeListener);
} protected void onResume() {
super.onResume();
protected void onPause() { // Monitor preference changes
super.onPause(); PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(
// Stop monitoring preference changes onPreferenceChangeListener);
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener( }
onPreferenceChangeListener);
} protected void onPause() {
super.onPause();
private OnSharedPreferenceChangeListener onPreferenceChangeListener = new OnSharedPreferenceChangeListener() { // Stop monitoring preference changes
@Override PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { onPreferenceChangeListener);
showValueOnSummary(key); }
onPreferencesChanged();
} /**
}; * Key-bound preference activities may override this method if they want to react to preference changes.
*/
/** protected void onPreferencesChanged() {
* Key-bound preference activities may override this method if they want to react to preference changes. }
*/
protected void onPreferencesChanged() { /**
} * Updates a preference that allows for text entry via a dialog. This is used for both string and integer values. No
* default value will be shown.
/** *
* Updates a preference that allows for text entry via a dialog. This is used for both string and integer values. No * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
* default value will be shown. * item_name_[key]
* @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under * @return The concrete {@link EditTextPreference} that is bound to this preference
* item_name_[key] */
* @return The concrete {@link EditTextPreference} that is bound to this preference protected final EditTextPreference initTextPreference(String baseName) {
*/ return initTextPreference(baseName, null);
protected final EditTextPreference initTextPreference(String baseName) { }
return initTextPreference(baseName, null);
} /**
* Updates a preference that allows for text entry via a dialog. This is used for both string and integer values.
/** *
* Updates a preference that allows for text entry via a dialog. This is used for both string and integer values. * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
* @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under * item_name_[key]
* item_name_[key] * @param defValue The default value for this preference, as shown when no value was yet stored
* @param defValue The default value for this preference, as shown when no value was yet stored * @return The concrete {@link EditTextPreference} that is bound to this preference
* @return The concrete {@link EditTextPreference} that is bound to this preference */
*/ protected final EditTextPreference initTextPreference(String baseName, String defValue) {
protected final EditTextPreference initTextPreference(String baseName, String defValue) { return initTextPreference(baseName, defValue, null);
return initTextPreference(baseName, defValue, null); }
}
/**
/** * Updates a preference (including dependency) that allows for text entry via a dialog. This is used for both string
* Updates a preference (including dependency) that allows for text entry via a dialog. This is used for both string * and integer values.
* and integer values. *
* @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
* item_name_[key] * item_name_[key]
* @param defValue The default value for this preference, as shown when no value was yet stored * @param defValue The default value for this preference, as shown when no value was yet stored
* @param dependency The base name of the preference to which this preference depends * @param dependency The base name of the preference to which this preference depends
* @return The concrete {@link EditTextPreference} that is bound to this preference * @return The concrete {@link EditTextPreference} that is bound to this preference
*/ */
protected final EditTextPreference initTextPreference(String baseName, String defValue, String dependency) { protected final EditTextPreference initTextPreference(String baseName, String defValue, String dependency) {
// Update the loaded Preference with the actual preference key to load/store with // Update the loaded Preference with the actual preference key to load/store with
EditTextPreference pref = (EditTextPreference) findPreference(baseName); EditTextPreference pref = (EditTextPreference) findPreference(baseName);
pref.setKey(baseName + "_" + key); pref.setKey(baseName + "_" + key);
pref.setDependency(dependency == null ? null : dependency + "_" + key); pref.setDependency(dependency == null ? null : dependency + "_" + key);
// Update the Preference by loading the current stored value into the EditText, if it exists // Update the Preference by loading the current stored value into the EditText, if it exists
pref.setText(sharedPrefs.getString(baseName + "_" + key, defValue)); pref.setText(sharedPrefs.getString(baseName + "_" + key, defValue));
// Remember the original descriptive summary and if we have a value, show that instead // Remember the original descriptive summary and if we have a value, show that instead
originalSummaries.put(baseName + "_" + key, pref.getSummary() == null ? null : pref.getSummary().toString()); originalSummaries.put(baseName + "_" + key, pref.getSummary() == null ? null : pref.getSummary().toString());
showValueOnSummary(baseName + "_" + key); showValueOnSummary(baseName + "_" + key);
return pref; return pref;
} }
/** /**
* Updates a preference that simply shows a check box. No default value will be shown. * Updates a preference that simply shows a check box. No default value will be shown.
* @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under *
* item_name_[key] * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
* @return The concrete {@link CheckBoxPreference} that is bound to this preference * item_name_[key]
*/ * @return The concrete {@link CheckBoxPreference} that is bound to this preference
protected final CheckBoxPreference initBooleanPreference(String baseName) { */
return initBooleanPreference(baseName, false); protected final CheckBoxPreference initBooleanPreference(String baseName) {
} return initBooleanPreference(baseName, false);
}
/**
* Updates a preference that simply shows a check box. /**
* @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under * Updates a preference that simply shows a check box.
* item_name_[key] *
* @param defValue The default value for this preference, as shown when no value was yet stored * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
* @return The concrete {@link CheckBoxPreference} that is bound to this preference * item_name_[key]
*/ * @param defValue The default value for this preference, as shown when no value was yet stored
protected final CheckBoxPreference initBooleanPreference(String baseName, boolean defValue) { * @return The concrete {@link CheckBoxPreference} that is bound to this preference
return initBooleanPreference(baseName, defValue, null); */
} protected final CheckBoxPreference initBooleanPreference(String baseName, boolean defValue) {
return initBooleanPreference(baseName, defValue, null);
/** }
* Updates a preference (including dependency) that simply shows a check box.
* @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under /**
* item_name_[key] * Updates a preference (including dependency) that simply shows a check box.
* @param defValue The default value for this preference, as shown when no value was yet stored *
* @param dependency The base name of the preference to which this preference depends * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
* @return The concrete {@link CheckBoxPreference} that is bound to this preference * item_name_[key]
*/ * @param defValue The default value for this preference, as shown when no value was yet stored
protected final CheckBoxPreference initBooleanPreference(String baseName, boolean defValue, String dependency) { * @param dependency The base name of the preference to which this preference depends
// Update the loaded Preference with the actual preference key to load/store with * @return The concrete {@link CheckBoxPreference} that is bound to this preference
CheckBoxPreference pref = (CheckBoxPreference) findPreference(baseName); */
pref.setKey(baseName + "_" + key); protected final CheckBoxPreference initBooleanPreference(String baseName, boolean defValue, String dependency) {
pref.setDependency(dependency == null ? null : dependency + "_" + key); // Update the loaded Preference with the actual preference key to load/store with
// Update the Preference by loading the current stored value into the Checkbox, if it exists CheckBoxPreference pref = (CheckBoxPreference) findPreference(baseName);
pref.setChecked(sharedPrefs.getBoolean(baseName + "_" + key, defValue)); pref.setKey(baseName + "_" + key);
return pref; pref.setDependency(dependency == null ? null : dependency + "_" + key);
} // Update the Preference by loading the current stored value into the Checkbox, if it exists
pref.setChecked(sharedPrefs.getBoolean(baseName + "_" + key, defValue));
/** return pref;
* Updates a preference that allows picking an item from a list. No default value will be shown. }
* @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
* item_name_[key] /**
* @return The concrete {@link ListPreference} that is bound to this preference * Updates a preference that allows picking an item from a list. No default value will be shown.
*/ *
protected final ListPreference initListPreference(String baseName) { * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
return initListPreference(baseName, null); * item_name_[key]
} * @return The concrete {@link ListPreference} that is bound to this preference
*/
/** protected final ListPreference initListPreference(String baseName) {
* Updates a preference that allows picking an item from a list. return initListPreference(baseName, null);
* @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under }
* item_name_[key]
* @param defValue The default value for this preference, as shown when no value was yet stored /**
* @return The concrete {@link ListPreference} that is bound to this preference * Updates a preference that allows picking an item from a list.
*/ *
protected final ListPreference initListPreference(String baseName, String defValue) { * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
// Update the loaded Preference with the actual preference key to load/store with * item_name_[key]
ListPreference pref = (ListPreference) findPreference(baseName); * @param defValue The default value for this preference, as shown when no value was yet stored
pref.setKey(baseName + "_" + key); * @return The concrete {@link ListPreference} that is bound to this preference
// Update the Preference by selecting the current stored value in the list, if it exists */
pref.setValue(sharedPrefs.getString(baseName + "_" + key, defValue)); protected final ListPreference initListPreference(String baseName, String defValue) {
// Remember the original descriptive summary and if we have a value, show that instead // Update the loaded Preference with the actual preference key to load/store with
originalSummaries.put(baseName + "_" + key, pref.getSummary() == null ? null : pref.getSummary().toString()); ListPreference pref = (ListPreference) findPreference(baseName);
showValueOnSummary(baseName + "_" + key); pref.setKey(baseName + "_" + key);
return pref; // Update the Preference by selecting the current stored value in the list, if it exists
} pref.setValue(sharedPrefs.getString(baseName + "_" + key, defValue));
// Remember the original descriptive summary and if we have a value, show that instead
protected void showValueOnSummary(String prefKey) { originalSummaries.put(baseName + "_" + key, pref.getSummary() == null ? null : pref.getSummary().toString());
Preference pref = findPreference(prefKey); showValueOnSummary(baseName + "_" + key);
if (sharedPrefs.contains(prefKey) return pref;
&& pref instanceof EditTextPreference }
&& !TextUtils.isEmpty(sharedPrefs.getString(prefKey, ""))
&& !isPasswordPref((EditTextPreference) pref)) { protected void showValueOnSummary(String prefKey) {
// Non-password edit preferences show the user-entered value Preference pref = findPreference(prefKey);
pref.setSummary(sharedPrefs.getString(prefKey, "")); if (sharedPrefs.contains(prefKey)
return; && pref instanceof EditTextPreference
} else if (sharedPrefs.contains(prefKey) && pref instanceof ListPreference && !TextUtils.isEmpty(sharedPrefs.getString(prefKey, ""))
&& ((ListPreference) pref).getValue() != null) { && !isPasswordPref((EditTextPreference) pref)) {
// List preferences show the selected list value // Non-password edit preferences show the user-entered value
ListPreference listPreference = (ListPreference) pref; pref.setSummary(sharedPrefs.getString(prefKey, ""));
pref.setSummary(listPreference.getEntries()[listPreference.findIndexOfValue(listPreference.getValue())]); return;
return; } else if (sharedPrefs.contains(prefKey) && pref instanceof ListPreference
} && ((ListPreference) pref).getValue() != null) {
if (originalSummaries.containsKey(prefKey)) // List preferences show the selected list value
pref.setSummary(originalSummaries.get(prefKey)); ListPreference listPreference = (ListPreference) pref;
} pref.setSummary(listPreference.getEntries()[listPreference.findIndexOfValue(listPreference.getValue())]);
return;
protected boolean isPasswordPref(EditTextPreference preference) { }
return preference.getKey().startsWith("server_pass_") || preference.getKey().startsWith("server_extrapass") if (originalSummaries.containsKey(prefKey))
|| preference.getKey().startsWith("server_ftppass"); pref.setSummary(originalSummaries.get(prefKey));
} }
protected boolean isPasswordPref(EditTextPreference preference) {
return preference.getKey().startsWith("server_pass_") || preference.getKey().startsWith("server_extrapass")
|| preference.getKey().startsWith("server_ftppass");
}
} }

413
app/src/main/java/org/transdroid/core/gui/settings/MainSettingsActivity.java

@ -19,13 +19,13 @@ package org.transdroid.core.gui.settings;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import androidx.preference.ListPreference; import androidx.preference.ListPreference;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.Preference.OnPreferenceClickListener; import androidx.preference.Preference.OnPreferenceClickListener;
@ -55,249 +55,210 @@ import java.util.List;
/** /**
* The main activity that provides access to all application settings. It shows the configured serves, web search sites and RSS feeds along with other * The main activity that provides access to all application settings. It shows the configured serves, web search sites and RSS feeds along with other
* general settings. * general settings.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EActivity @EActivity
public class MainSettingsActivity extends PreferenceCompatActivity { public class MainSettingsActivity extends PreferenceCompatActivity {
protected static final int DIALOG_ADDSEEDBOX = 0; protected static final int DIALOG_ADDSEEDBOX = 0;
@Bean @Bean
protected NavigationHelper navigationHelper; protected NavigationHelper navigationHelper;
@Bean @Bean
protected ApplicationSettings applicationSettings; protected ApplicationSettings applicationSettings;
@Bean @Bean
protected SearchHelper searchHelper; protected SearchHelper searchHelper;
protected SharedPreferences prefs; protected SharedPreferences prefs;
private OnPreferenceClickListener onAddServer = new OnPreferenceClickListener() { private OnPreferenceClickListener onAddServer = new OnPreferenceClickListener() {
@Override @Override
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {
if (navigationHelper.enableSeedboxes()) if (navigationHelper.enableSeedboxes())
showDialog(DIALOG_ADDSEEDBOX); showDialog(DIALOG_ADDSEEDBOX);
else else
ServerSettingsActivity_.intent(MainSettingsActivity.this).start(); ServerSettingsActivity_.intent(MainSettingsActivity.this).start();
return true; return true;
} }
}; };
private OnPreferenceClickListener onAddWebsearch = new OnPreferenceClickListener() { private OnPreferenceClickListener onAddWebsearch = preference -> {
@Override WebsearchSettingsActivity_.intent(MainSettingsActivity.this).start();
public boolean onPreferenceClick(Preference preference) { return true;
WebsearchSettingsActivity_.intent(MainSettingsActivity.this).start(); };
return true; private OnPreferenceClickListener onAddRssfeed = preference -> {
} RssfeedSettingsActivity_.intent(MainSettingsActivity.this).start();
}; return true;
private OnPreferenceClickListener onAddRssfeed = new OnPreferenceClickListener() { };
@Override private OnPreferenceClickListener onBackgroundSettings = preference -> {
public boolean onPreferenceClick(Preference preference) { NotificationSettingsActivity_.intent(MainSettingsActivity.this).start();
RssfeedSettingsActivity_.intent(MainSettingsActivity.this).start(); return true;
return true; };
} private OnPreferenceClickListener onSystemSettings = preference -> {
}; SystemSettingsActivity_.intent(MainSettingsActivity.this).start();
private OnPreferenceClickListener onBackgroundSettings = new OnPreferenceClickListener() { return true;
@Override };
public boolean onPreferenceClick(Preference preference) { private OnPreferenceClickListener onHelpSettings = preference -> {
NotificationSettingsActivity_.intent(MainSettingsActivity.this).start(); HelpSettingsActivity_.intent(MainSettingsActivity.this).start();
return true; return true;
} };
}; private OnPreferenceClickListener onDonate = preference -> {
private OnPreferenceClickListener onSystemSettings = new OnPreferenceClickListener() { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.donate_url))));
@Override return true;
public boolean onPreferenceClick(Preference preference) { };
SystemSettingsActivity_.intent(MainSettingsActivity.this).start(); private OnServerClickedListener onServerClicked = serverSetting -> ServerSettingsActivity_.intent(MainSettingsActivity.this).key(serverSetting.getOrder()).start();
return true; private OnSeedboxClickedListener onSeedboxClicked = (serverSetting, provider, seedboxOffset) -> {
} // NOTE: The seedboxOffset is the seedbox type-unique order that we need to supply uin the Extras bundle to
}; // edit this specific seedbox
private OnPreferenceClickListener onHelpSettings = new OnPreferenceClickListener() { startActivity(provider.getSettings().getSettingsActivityIntent(MainSettingsActivity.this).putExtra("key", seedboxOffset));
@Override };
public boolean onPreferenceClick(Preference preference) { private OnWebsearchClickedListener onWebsearchClicked = websearchSetting -> WebsearchSettingsActivity_.intent(MainSettingsActivity.this).key(websearchSetting.getOrder()).start();
HelpSettingsActivity_.intent(MainSettingsActivity.this).start(); private OnRssfeedClickedListener onRssfeedClicked = rssfeedSetting -> RssfeedSettingsActivity_.intent(MainSettingsActivity.this).key(rssfeedSetting.getOrder()).start();
return true; private OnClickListener onAddSeedbox = (dialog, which) -> {
} // Start the configuration activity for this specific chosen seedbox
}; if (which == 0)
private OnPreferenceClickListener onDonate = new OnPreferenceClickListener() { ServerSettingsActivity_.intent(MainSettingsActivity.this).start();
@Override else
public boolean onPreferenceClick(Preference preference) { startActivity(SeedboxProvider.values()[which - 1].getSettings().getSettingsActivityIntent(MainSettingsActivity.this));
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.donate_url)))); };
return true;
}
};
private OnServerClickedListener onServerClicked = new OnServerClickedListener() {
@Override
public void onServerClicked(ServerSetting serverSetting) {
ServerSettingsActivity_.intent(MainSettingsActivity.this).key(serverSetting.getOrder()).start();
}
};
private OnSeedboxClickedListener onSeedboxClicked = new OnSeedboxClickedListener() {
@Override
public void onSeedboxClicked(ServerSetting serverSetting, SeedboxProvider provider, int seedboxOffset) {
// NOTE: The seedboxOffset is the seedbox type-unique order that we need to supply uin the Extras bundle to
// edit this specific seedbox
startActivity(provider.getSettings().getSettingsActivityIntent(MainSettingsActivity.this).putExtra("key", seedboxOffset));
}
};
private OnWebsearchClickedListener onWebsearchClicked = new OnWebsearchClickedListener() {
@Override
public void onWebsearchClicked(WebsearchSetting websearchSetting) {
WebsearchSettingsActivity_.intent(MainSettingsActivity.this).key(websearchSetting.getOrder()).start();
}
};
private OnRssfeedClickedListener onRssfeedClicked = new OnRssfeedClickedListener() {
@Override
public void onRssfeedClicked(RssfeedSetting rssfeedSetting) {
RssfeedSettingsActivity_.intent(MainSettingsActivity.this).key(rssfeedSetting.getOrder()).start();
}
};
private OnClickListener onAddSeedbox = new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Start the configuration activity for this specific chosen seedbox
if (which == 0)
ServerSettingsActivity_.intent(MainSettingsActivity.this).start();
else
startActivity(SeedboxProvider.values()[which - 1].getSettings().getSettingsActivityIntent(MainSettingsActivity.this));
}
};
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Note: Settings are loaded in onResume() // Note: Settings are loaded in onResume()
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
boolean enableSearchUi = navigationHelper.enableSearchUi(); boolean enableSearchUi = navigationHelper.enableSearchUi();
boolean enableRssUi = navigationHelper.enableRssUi(); boolean enableRssUi = navigationHelper.enableRssUi();
boolean enableDonateLink = !getString(R.string.donate_url).isEmpty(); boolean enableDonateLink = !getString(R.string.donate_url).isEmpty();
// Load the preference menu and attach actions // Load the preference menu and attach actions
addPreferencesFromResource(R.xml.pref_main); addPreferencesFromResource(R.xml.pref_main);
prefs = getPreferenceManager().getSharedPreferences(); prefs = getPreferenceManager().getSharedPreferences();
findPreference("header_addserver").setOnPreferenceClickListener(onAddServer); findPreference("header_addserver").setOnPreferenceClickListener(onAddServer);
if (enableSearchUi) { if (enableSearchUi) {
findPreference("header_addwebsearch").setOnPreferenceClickListener(onAddWebsearch); findPreference("header_addwebsearch").setOnPreferenceClickListener(onAddWebsearch);
} }
if (enableRssUi) { if (enableRssUi) {
findPreference("header_addrssfeed").setOnPreferenceClickListener(onAddRssfeed); findPreference("header_addrssfeed").setOnPreferenceClickListener(onAddRssfeed);
} }
findPreference("header_background").setOnPreferenceClickListener(onBackgroundSettings); findPreference("header_background").setOnPreferenceClickListener(onBackgroundSettings);
findPreference("header_system").setOnPreferenceClickListener(onSystemSettings); findPreference("header_system").setOnPreferenceClickListener(onSystemSettings);
findPreference("header_help").setOnPreferenceClickListener(onHelpSettings); findPreference("header_help").setOnPreferenceClickListener(onHelpSettings);
if (enableDonateLink) { if (enableDonateLink) {
findPreference("header_donate").setOnPreferenceClickListener(onDonate); findPreference("header_donate").setOnPreferenceClickListener(onDonate);
} else { } else {
getPreferenceScreen().removePreference(findPreference("header_donate")); getPreferenceScreen().removePreference(findPreference("header_donate"));
} }
// Keep a list of the server codes and names (for default server selection) // Keep a list of the server codes and names (for default server selection)
List<String> serverCodes = new ArrayList<>(); List<String> serverCodes = new ArrayList<>();
List<String> serverNames = new ArrayList<>(); List<String> serverNames = new ArrayList<>();
serverCodes.add(Integer.toString(ApplicationSettings.DEFAULTSERVER_LASTUSED)); serverCodes.add(Integer.toString(ApplicationSettings.DEFAULTSERVER_LASTUSED));
serverCodes.add(Integer.toString(ApplicationSettings.DEFAULTSERVER_ASKONADD)); serverCodes.add(Integer.toString(ApplicationSettings.DEFAULTSERVER_ASKONADD));
serverNames.add(getString(R.string.pref_defaultserver_lastused)); serverNames.add(getString(R.string.pref_defaultserver_lastused));
serverNames.add(getString(R.string.pref_defaultserver_askonadd)); serverNames.add(getString(R.string.pref_defaultserver_askonadd));
// Add existing servers // Add existing servers
List<ServerSetting> servers = applicationSettings.getNormalServerSettings(); List<ServerSetting> servers = applicationSettings.getNormalServerSettings();
for (ServerSetting serverSetting : servers) { for (ServerSetting serverSetting : servers) {
getPreferenceScreen() getPreferenceScreen()
.addPreference(new ServerPreference(this).setServerSetting(serverSetting).setOnServerClickedListener(onServerClicked)); .addPreference(new ServerPreference(this).setServerSetting(serverSetting).setOnServerClickedListener(onServerClicked));
if (serverSetting.getUniqueIdentifier() != null) { if (serverSetting.getUniqueIdentifier() != null) {
serverCodes.add(Integer.toString(serverSetting.getOrder())); serverCodes.add(Integer.toString(serverSetting.getOrder()));
serverNames.add(serverSetting.getName()); serverNames.add(serverSetting.getName());
} }
} }
// Add seedboxes; serversOffset keeps an int to have all ServerSettings with unique ids, seedboxOffset is unique // Add seedboxes; serversOffset keeps an int to have all ServerSettings with unique ids, seedboxOffset is unique
// only per seedbox type // only per seedbox type
int orderOffset = servers.size(); int orderOffset = servers.size();
for (SeedboxProvider provider : SeedboxProvider.values()) { for (SeedboxProvider provider : SeedboxProvider.values()) {
int seedboxOffset = 0; int seedboxOffset = 0;
for (ServerSetting seedbox : provider.getSettings().getAllServerSettings(prefs, orderOffset)) { for (ServerSetting seedbox : provider.getSettings().getAllServerSettings(prefs, orderOffset)) {
getPreferenceScreen().addPreference(new SeedboxPreference(this).setProvider(provider).setServerSetting(seedbox) getPreferenceScreen().addPreference(new SeedboxPreference(this).setProvider(provider).setServerSetting(seedbox)
.setOnSeedboxClickedListener(onSeedboxClicked, seedboxOffset)); .setOnSeedboxClickedListener(onSeedboxClicked, seedboxOffset));
orderOffset++; orderOffset++;
seedboxOffset++; seedboxOffset++;
if (seedbox.getUniqueIdentifier() != null) { if (seedbox.getUniqueIdentifier() != null) {
serverCodes.add(Integer.toString(seedbox.getOrder())); serverCodes.add(Integer.toString(seedbox.getOrder()));
serverNames.add(seedbox.getName()); serverNames.add(seedbox.getName());
} }
} }
} }
// Allow selection of the default server // Allow selection of the default server
ListPreference defaultServerPreference = (ListPreference) findPreference("header_defaultserver"); ListPreference defaultServerPreference = (ListPreference) findPreference("header_defaultserver");
defaultServerPreference.setEntries(serverNames.toArray(new String[serverNames.size()])); defaultServerPreference.setEntries(serverNames.toArray(new String[0]));
defaultServerPreference.setEntryValues(serverCodes.toArray(new String[serverCodes.size()])); defaultServerPreference.setEntryValues(serverCodes.toArray(new String[0]));
// Add existing RSS feeds // Add existing RSS feeds
if (!enableRssUi) { if (!enableRssUi) {
// RSS should be disabled // RSS should be disabled
getPreferenceScreen().removePreference(findPreference("header_rssfeeds")); getPreferenceScreen().removePreference(findPreference("header_rssfeeds"));
} else { } else {
List<RssfeedSetting> rssfeeds = applicationSettings.getRssfeedSettings(); List<RssfeedSetting> rssfeeds = applicationSettings.getRssfeedSettings();
for (RssfeedSetting rssfeedSetting : rssfeeds) { for (RssfeedSetting rssfeedSetting : rssfeeds) {
getPreferenceScreen() getPreferenceScreen()
.addPreference(new RssfeedPreference(this).setRssfeedSetting(rssfeedSetting).setOnRssfeedClickedListener(onRssfeedClicked)); .addPreference(new RssfeedPreference(this).setRssfeedSetting(rssfeedSetting).setOnRssfeedClickedListener(onRssfeedClicked));
} }
} }
if (!enableSearchUi) { if (!enableSearchUi) {
// Search should be disabled // Search should be disabled
getPreferenceScreen().removePreference(findPreference("header_searchsites")); getPreferenceScreen().removePreference(findPreference("header_searchsites"));
return; return;
} }
// Add existing websearch sites // Add existing websearch sites
List<WebsearchSetting> websearches = applicationSettings.getWebsearchSettings(); List<WebsearchSetting> websearches = applicationSettings.getWebsearchSettings();
for (WebsearchSetting websearchSetting : websearches) { for (WebsearchSetting websearchSetting : websearches) {
getPreferenceScreen().addPreference( getPreferenceScreen().addPreference(
new WebsearchPreference(this).setWebsearchSetting(websearchSetting).setOnWebsearchClickedListener(onWebsearchClicked)); new WebsearchPreference(this).setWebsearchSetting(websearchSetting).setOnWebsearchClickedListener(onWebsearchClicked));
} }
// Construct list of all available search sites, in-app and web // Construct list of all available search sites, in-app and web
ListPreference setSite = (ListPreference) findPreference("header_setsearchsite"); ListPreference setSite = (ListPreference) findPreference("header_setsearchsite");
// Retrieve the available in-app search sites (using the Torrent Search package) // Retrieve the available in-app search sites (using the Torrent Search package)
List<SearchSite> searchsites = searchHelper.getAvailableSites(); List<SearchSite> searchsites = searchHelper.getAvailableSites();
if (searchsites == null) { if (searchsites == null) {
searchsites = new ArrayList<>(); searchsites = new ArrayList<>();
} }
List<String> siteNames = new ArrayList<>(websearches.size() + searchsites.size()); List<String> siteNames = new ArrayList<>(websearches.size() + searchsites.size());
List<String> siteValues = new ArrayList<>(websearches.size() + searchsites.size()); List<String> siteValues = new ArrayList<>(websearches.size() + searchsites.size());
for (SearchSite searchSite : searchsites) { for (SearchSite searchSite : searchsites) {
siteNames.add(searchSite.getName()); siteNames.add(searchSite.getName());
siteValues.add(searchSite.getKey()); siteValues.add(searchSite.getKey());
} }
for (WebsearchSetting websearch : websearches) { for (WebsearchSetting websearch : websearches) {
siteNames.add(websearch.getName()); siteNames.add(websearch.getName());
siteValues.add(websearch.getKey()); siteValues.add(websearch.getKey());
} }
// Supply the Preference list names and values // Supply the Preference list names and values
setSite.setEntries(siteNames.toArray(new String[siteNames.size()])); setSite.setEntries(siteNames.toArray(new String[0]));
setSite.setEntryValues(siteValues.toArray(new String[siteValues.size()])); setSite.setEntryValues(siteValues.toArray(new String[0]));
} }
@TargetApi(Build.VERSION_CODES.HONEYCOMB) @TargetApi(Build.VERSION_CODES.HONEYCOMB)
@OptionsItem(android.R.id.home) @OptionsItem(android.R.id.home)
protected void navigateUp() { protected void navigateUp() {
TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start(); TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
} }
@Override @Override
protected Dialog onCreateDialog(int id) { protected Dialog onCreateDialog(int id) {
switch (id) { if (id == DIALOG_ADDSEEDBOX) {
case DIALOG_ADDSEEDBOX: // Open dialog to pick one of the supported seedbox providers (or a normal server)
// Open dialog to pick one of the supported seedbox providers (or a normal server) String[] seedboxes = new String[SeedboxProvider.values().length + 1];
String[] seedboxes = new String[SeedboxProvider.values().length + 1]; seedboxes[0] = getString(R.string.pref_addserver_normal);
seedboxes[0] = getString(R.string.pref_addserver_normal); for (int i = 0; i < seedboxes.length - 1; i++) {
for (int i = 0; i < seedboxes.length - 1; i++) { seedboxes[i + 1] = getString(R.string.pref_seedbox_addseedbox, SeedboxProvider.values()[i].getSettings().getName());
seedboxes[i + 1] = getString(R.string.pref_seedbox_addseedbox, SeedboxProvider.values()[i].getSettings().getName()); }
} return new AlertDialog.Builder(this).setItems(seedboxes, onAddSeedbox).create();
return new AlertDialog.Builder(this).setItems(seedboxes, onAddSeedbox).create(); }
} return null;
return null; }
}
} }

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

Loading…
Cancel
Save