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 @@ @@ -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 @@ @@ -1,44 +1,65 @@
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)
"Manage your torrents from your Android device"
<a href="https://transdroid.org/latest" target="_blank">
<img src="https://transdroid.org/images/getontransdroid.png" alt="Get it on transdroid.org" height="80"/></a>
<a href="https://f-droid.org/repository/browse/?fdid=org.transdroid.full" target="_blank">
<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="80"/></a>
<a href="https://play.google.com/store/apps/details?id=org.transdroid.lite" target="_blank">
<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>
<img src="http://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 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.
Manage torrents from your Android device.
<a href="https://transdroid.org/latest">
<img src="https://transdroid.org/images/getontransdroid.png"
alt="Get it on transdroid.org"
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"
alt="Get it on F-Droid"
height="80">
</a>
<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
=============
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
==============
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
============
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
=======
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
it under the terms of the GNU General Public License as published by
@ -51,39 +72,51 @@ License @@ -51,39 +72,51 @@ License
GNU General Public License for more details.
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:
* [Android Jetpack (AndroidX)](https://developer.android.com/jetpack)
The Android Open Source Project
Apache License, Version 2.0
* [AndroidAnnotations](http://androidannotations.org/)
Pierre-Yves Ricau (eBusinessInformations) et al.
Apache License, Version 2.0
* [ActionBar-PullToRefresh](https://github.com/chrisbanes/ActionBar-PullToRefresh)
Chris Banes
* [ORMLite](https://github.com/j256/ormlite-core) and [ORMLite Android](https://github.com/j256/ormlite-android)
Gray Watson
ISC License
* [Android Universal Image Loader](https://github.com/nostra13/Android-Universal-Image-Loader)
Sergey Tarasevich
Apache License, Version 2.0
* [Crouton](https://github.com/keyboardsurfer/Crouton)
Code: Benjamin Weiss (Neofonie Mobile Gmbh) et al.
Idea: Cyril Mottier
* [FloatingActionButton](https://github.com/zendesk/android-floating-action-button)
Oleksandr Melnykov, Zendesk
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
Apache OpenJPA
* MultipartEntity
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)
* [Base64](http://iharder.sourceforge.net/current/java/base64/)
Robert Harder
Public Domain
* [aXMLRPC](https://github.com/timroes/aXMLRPC)
* [aXMLRPC](https://github.com/gturri/aXMLRPC)
Tim Roes
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)
Daniel Nilsson and Sergey Margaritov
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
Creative Commons Attribution 3.0

2
app/build.gradle

@ -79,7 +79,7 @@ dependencies { @@ -79,7 +79,7 @@ dependencies {
// Android support
implementation 'androidx.appcompat:appcompat:1.1.0'
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'
// Other

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

@ -1,5 +1,4 @@ @@ -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.
Transdroid is free software: you can redistribute it and/or modify
@ -17,13 +16,13 @@ @@ -17,13 +16,13 @@
-->
<resources>
<!-- Used to enable checking for app updates in the background -->
<bool name="updatecheck_available">true</bool>
<!-- Used to enable the search UI -->
<bool name="search_available">true</bool>
<!-- Used to enable the RSS UI and background service -->
<bool name="rss_available">true</bool>
<!-- Used to allow adding of seedboxes via easy server setup -->
<bool name="seedboxes_available">true</bool>
<!-- Used to enable checking for app updates in the background -->
<bool name="updatecheck_available">true</bool>
<!-- Used to enable the search UI -->
<bool name="search_available">true</bool>
<!-- Used to enable the RSS UI and background service -->
<bool name="rss_available">true</bool>
<!-- Used to allow adding of seedboxes via easy server setup -->
<bool name="seedboxes_available">true</bool>
</resources>

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

@ -16,9 +16,9 @@ @@ -16,9 +16,9 @@
-->
<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_url">https://paypal.me/erickoknl</string>
<string name="donate_text">Donate with PayPal</string>
<string name="donate_url">https://paypal.me/erickoknl</string>
</resources>

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

@ -1,5 +1,4 @@ @@ -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.
Transdroid is free software: you can redistribute it and/or modify
@ -17,13 +16,13 @@ @@ -17,13 +16,13 @@
-->
<resources>
<!-- Used to enable checking for app updates in the background -->
<bool name="updatecheck_available">false</bool>
<!-- Used to enable the search UI -->
<bool name="search_available">false</bool>
<!-- Used to enable the RSS UI and background service -->
<bool name="rss_available">false</bool>
<!-- Used to allow adding of seedboxes via easy server setup -->
<bool name="seedboxes_available">true</bool>
<!-- Used to enable checking for app updates in the background -->
<bool name="updatecheck_available">false</bool>
<!-- Used to enable the search UI -->
<bool name="search_available">false</bool>
<!-- Used to enable the RSS UI and background service -->
<bool name="rss_available">false</bool>
<!-- Used to allow adding of seedboxes via easy server setup -->
<bool name="seedboxes_available">true</bool>
</resources>

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

@ -16,9 +16,9 @@ @@ -16,9 +16,9 @@
-->
<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_url"></string>
<string name="donate_text" />
<string name="donate_url" />
</resources>

628
app/src/main/AndroidManifest.xml

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

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

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

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

@ -16,6 +16,15 @@ @@ -16,6 +16,15 @@
*/
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.InputStream;
import java.io.UnsupportedEncodingException;
@ -23,146 +32,141 @@ import java.net.URLEncoder; @@ -23,146 +32,141 @@ import java.net.URLEncoder;
import java.util.ArrayList;
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)
public class SearchHelper {
static final int CURSOR_SEARCH_ID = 0;
static final int CURSOR_SEARCH_NAME = 1;
static final int CURSOR_SEARCH_TORRENTURL = 2;
static final int CURSOR_SEARCH_DETAILSURL = 3;
static final int CURSOR_SEARCH_SIZE = 4;
static final int CURSOR_SEARCH_ADDED = 5;
static final int CURSOR_SEARCH_SEEDERS = 6;
static final int CURSOR_SEARCH_LEECHERS = 7;
static final int CURSOR_SITE_ID = 0;
static final int CURSOR_SITE_CODE = 1;
static final int CURSOR_SITE_NAME = 2;
static final int CURSOR_SITE_RSSURL = 3;
static final int CURSOR_SITE_ISPRIVATE = 4;
@RootContext
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
*/
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
*/
public List<SearchSite> getAvailableSites() {
// Try to access the TorrentSitesProvider of the Torrent Search app
Uri uri = Uri.parse("content://org.transdroid.search.torrentsitesprovider/sites");
ContentProviderClient test = context.getContentResolver().acquireContentProviderClient(uri);
if (test == 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);
if (cursor == null) {
// The installed Torrent Search version is corrupt or incompatible
return null;
}
List<SearchSite> sites = new ArrayList<>();
if (cursor.moveToFirst()) {
do {
// Read the cursor fields into the SearchSite object
sites.add(new SearchSite(cursor.getInt(CURSOR_SITE_ID), cursor.getString(CURSOR_SITE_CODE), cursor
.getString(CURSOR_SITE_NAME), cursor.getString(CURSOR_SITE_RSSURL),
cursor.getColumnNames().length > 4 && cursor.getInt(CURSOR_SITE_ISPRIVATE) == 1));
} while (cursor.moveToNext());
}
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.
* @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 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
* there is no internet connection
*/
public ArrayList<SearchResult> search(String query, SearchSite site, SearchSortOrder sortBy) {
// Try to query the TorrentSearchProvider to search for torrents on the web
Uri uri = Uri.parse("content://org.transdroid.search.torrentsearchprovider/search/" + query);
Cursor cursor;
if (site == null) {
// If no explicit site was supplied, rely on the Torrent Search package's default
cursor = context.getContentResolver().query(uri, null, null, null, sortBy.name());
} else {
cursor = context.getContentResolver().query(uri, null, "SITE = ?", new String[] { site.getKey() },
sortBy.name());
}
if (cursor == null) {
// The content provider could not load any content (for example when there is no connection)
return null;
}
if (cursor.moveToFirst()) {
ArrayList<SearchResult> results = new ArrayList<>();
do {
// Read the cursor fields into the SearchResult object
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
.getString(CURSOR_SEARCH_SIZE), cursor.getLong(CURSOR_SEARCH_ADDED), cursor
.getString(CURSOR_SEARCH_SEEDERS), cursor.getString(CURSOR_SEARCH_LEECHERS)));
} while (cursor.moveToNext());
cursor.close();
return results;
}
// Torrent Search package is not yet installed
cursor.close();
return null;
}
/**
* 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
* example.
* @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
* @param url The full url of the torrent to download
* @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
*/
public InputStream getFile(String site, String url) throws FileNotFoundException {
try {
Uri uri = Uri.parse("content://org.transdroid.search.torrentsearchprovider/get/" + site + "/"
+ URLEncoder.encode(url, "UTF-8"));
return context.getContentResolver().openInputStream(uri);
} catch (UnsupportedEncodingException e) {
// Ignore
return null;
}
}
static final int CURSOR_SEARCH_ID = 0;
static final int CURSOR_SEARCH_NAME = 1;
static final int CURSOR_SEARCH_TORRENTURL = 2;
static final int CURSOR_SEARCH_DETAILSURL = 3;
static final int CURSOR_SEARCH_SIZE = 4;
static final int CURSOR_SEARCH_ADDED = 5;
static final int CURSOR_SEARCH_SEEDERS = 6;
static final int CURSOR_SEARCH_LEECHERS = 7;
static final int CURSOR_SITE_ID = 0;
static final int CURSOR_SITE_CODE = 1;
static final int CURSOR_SITE_NAME = 2;
static final int CURSOR_SITE_RSSURL = 3;
static final int CURSOR_SITE_ISPRIVATE = 4;
@RootContext
protected Context context;
/**
* 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
*/
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
*/
public List<SearchSite> getAvailableSites() {
// Try to access the TorrentSitesProvider of the Torrent Search app
Uri uri = Uri.parse("content://org.transdroid.search.torrentsitesprovider/sites");
ContentProviderClient test = context.getContentResolver().acquireContentProviderClient(uri);
if (test == 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);
if (cursor == null) {
// The installed Torrent Search version is corrupt or incompatible
return null;
}
List<SearchSite> sites = new ArrayList<>();
if (cursor.moveToFirst()) {
do {
// Read the cursor fields into the SearchSite object
sites.add(new SearchSite(cursor.getInt(CURSOR_SITE_ID), cursor.getString(CURSOR_SITE_CODE), cursor
.getString(CURSOR_SITE_NAME), cursor.getString(CURSOR_SITE_RSSURL),
cursor.getColumnNames().length > 4 && cursor.getInt(CURSOR_SITE_ISPRIVATE) == 1));
} while (cursor.moveToNext());
}
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.
*
* @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 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
* there is no internet connection
*/
public ArrayList<SearchResult> search(String query, SearchSite site, SearchSortOrder sortBy) {
// Try to query the TorrentSearchProvider to search for torrents on the web
Uri uri = Uri.parse("content://org.transdroid.search.torrentsearchprovider/search/" + query);
Cursor cursor;
if (site == null) {
// If no explicit site was supplied, rely on the Torrent Search package's default
cursor = context.getContentResolver().query(uri, null, null, null, sortBy.name());
} else {
cursor = context.getContentResolver().query(uri, null, "SITE = ?", new String[]{site.getKey()},
sortBy.name());
}
if (cursor == null) {
// The content provider could not load any content (for example when there is no connection)
return null;
}
if (cursor.moveToFirst()) {
ArrayList<SearchResult> results = new ArrayList<>();
do {
// Read the cursor fields into the SearchResult object
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
.getString(CURSOR_SEARCH_SIZE), cursor.getLong(CURSOR_SEARCH_ADDED), cursor
.getString(CURSOR_SEARCH_SEEDERS), cursor.getString(CURSOR_SEARCH_LEECHERS)));
} while (cursor.moveToNext());
cursor.close();
return results;
}
// Torrent Search package is not yet installed
cursor.close();
return null;
}
/**
* 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
* example.
*
* @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
* @param url The full url of the torrent to download
* @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
*/
public InputStream getFile(String site, String url) throws FileNotFoundException {
try {
Uri uri = Uri.parse("content://org.transdroid.search.torrentsearchprovider/get/" + site + "/"
+ URLEncoder.encode(url, "UTF-8"));
return context.getContentResolver().openInputStream(uri);
} catch (UnsupportedEncodingException e) {
// Ignore
return null;
}
}
public enum SearchSortOrder {
Combined, BySeeders
}
}

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

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

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

@ -21,48 +21,49 @@ import org.transdroid.core.gui.search.SearchSetting; @@ -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.
*
* @author Eric Kok
*/
public class SearchSite implements SimpleListItem, SearchSetting {
private final int id;
private final String key;
private final String name;
private final String rssFeedUrl;
private final boolean isPrivate;
private final int id;
private final String key;
private final String name;
private final String rssFeedUrl;
private final boolean isPrivate;
public SearchSite(int id, String key, String name, String rssFeedUrl, boolean isPrivate) {
this.id = id;
this.key = key;
this.name = name;
this.rssFeedUrl = rssFeedUrl;
this.isPrivate = isPrivate;
}
public SearchSite(int id, String key, String name, String rssFeedUrl, boolean isPrivate) {
this.id = id;
this.key = key;
this.name = name;
this.rssFeedUrl = rssFeedUrl;
this.isPrivate = isPrivate;
}
public int getId() {
return id;
}
public int getId() {
return id;
}
public String getKey() {
return key;
}
public String getKey() {
return key;
}
@Override
public String getName() {
return name;
}
@Override
public String getName() {
return name;
}
public String getRssFeedUrl() {
return rssFeedUrl;
}
public String getRssFeedUrl() {
return rssFeedUrl;
}
@Override
public String getBaseUrl() {
return rssFeedUrl;
}
@Override
public String getBaseUrl() {
return rssFeedUrl;
}
public boolean isPrivate() {
return isPrivate;
}
public boolean 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; @@ -30,103 +30,111 @@ import org.transdroid.R;
/**
* Allows instantiation of the settings specified in R.xml.pref_notifications.
*
* @author Eric Kok
*/
@EBean(scope = Scope.Singleton)
public class NotificationSettings {
private static final long MINIMUM_BACKGROUND_INTERVAL = 900_000; // 15 minutes
@RootContext
protected Context context;
private SharedPreferences prefs;
protected NotificationSettings(Context context) {
prefs = PreferenceManager.getDefaultSharedPreferences(context);
}
/**
* 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
*/
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
*/
public boolean isEnabledForTorrents() {
return prefs.getBoolean("notifications_enabled", true);
}
private String getRawInverval() {
return prefs.getString("notifications_interval", "10800");
}
/**
* Returns the interval between two server checks
* @return The interval, in milliseconds
*/
public Long getInvervalInMilliseconds() {
return Math.max(Long.parseLong(getRawInverval()) * 1000L, MINIMUM_BACKGROUND_INTERVAL);
}
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() {
String raw = getRawSound();
if (raw == null)
return null;
if (raw.equals(""))
return Settings.System.DEFAULT_NOTIFICATION_URI;
return Uri.parse(raw);
}
/**
* Whether the device should vibrate on a new notification
*/
public boolean shouldVibrate() {
return prefs.getBoolean("notifications_vibrate", false);
}
/**
* Returns the default vibrate pattern to use if the user enabled notification vibrations; check
* {@link #shouldVibrate()},
* @return A unique pattern for vibrations in Transdroid
*/
public long[] getDefaultVibratePattern() {
return new long[]{100, 100, 200, 300, 400, 700}; // Unique pattern?
}
private int getRawLedColour() {
return prefs.getInt("notifications_ledcolour", -1);
}
/**
* Returns the LED colour to use on a new notification
* @return The integer value of the user-specified or default colour
*/
public int getDesiredLedColour() {
int raw = getRawLedColour();
if (raw <= 0)
return context.getResources().getColor(R.color.ledgreen);
return raw;
}
/**
* 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);
}
private static final long MINIMUM_BACKGROUND_INTERVAL = 900_000; // 15 minutes
@RootContext
protected Context context;
private SharedPreferences prefs;
protected NotificationSettings(Context context) {
prefs = PreferenceManager.getDefaultSharedPreferences(context);
}
/**
* 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
*/
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
*/
public boolean isEnabledForTorrents() {
return prefs.getBoolean("notifications_enabled", true);
}
private String getRawInverval() {
return prefs.getString("notifications_interval", "10800");
}
/**
* Returns the interval between two server checks
*
* @return The interval, in milliseconds
*/
public Long getInvervalInMilliseconds() {
return Math.max(Long.parseLong(getRawInverval()) * 1000L, MINIMUM_BACKGROUND_INTERVAL);
}
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() {
String raw = getRawSound();
if (raw == null)
return null;
if (raw.equals(""))
return Settings.System.DEFAULT_NOTIFICATION_URI;
return Uri.parse(raw);
}
/**
* Whether the device should vibrate on a new notification
*/
public boolean shouldVibrate() {
return prefs.getBoolean("notifications_vibrate", false);
}
/**
* Returns the default vibrate pattern to use if the user enabled notification vibrations; check
* {@link #shouldVibrate()},
*
* @return A unique pattern for vibrations in Transdroid
*/
public long[] getDefaultVibratePattern() {
return new long[]{100, 100, 200, 300, 400, 700}; // Unique pattern?
}
private int getRawLedColour() {
return prefs.getInt("notifications_ledcolour", -1);
}
/**
* Returns the LED colour to use on a new notification
*
* @return The integer value of the user-specified or default colour
*/
public int getDesiredLedColour() {
int raw = getRawLedColour();
if (raw <= 0)
return context.getResources().getColor(R.color.ledgreen);
return raw;
}
/**
* 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 @@ @@ -16,107 +16,111 @@
*/
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 android.net.Uri;
import android.text.TextUtils;
import java.util.Date;
/**
* Represents a user-specified RSS feed.
*
* @author Eric Kok
*/
public class RssfeedSetting implements SimpleListItem {
private static final String DEFAULT_NAME = "Default";
private final int order;
private final String name;
private final String url;
private final boolean requiresAuth;
private final boolean alarm;
private final String excludeFilter;
private final String includeFilter;
private Date lastViewed;
private final String lastViewedItemUrl;
public RssfeedSetting(int order, String name, String baseUrl, boolean needsAuth, boolean alarm, String excludeFilter, String includeFilter, Date lastViewed,
String lastViewedItemUrl) {
this.order = order;
this.name = name;
this.url = baseUrl;
this.requiresAuth = needsAuth;
this.alarm = alarm;
this.excludeFilter = excludeFilter;
this.includeFilter = includeFilter;
this.lastViewed = lastViewed;
this.lastViewedItemUrl = lastViewedItemUrl;
}
public int getOrder() {
return order;
}
@Override
public String getName() {
if (!TextUtils.isEmpty(name))
return name;
if (!TextUtils.isEmpty(url)) {
String host = Uri.parse(url).getHost();
return host == null ? DEFAULT_NAME : host;
}
return DEFAULT_NAME;
}
public String getUrl() {
return url;
}
public boolean requiresExternalAuthentication() {
return requiresAuth;
}
public boolean shouldAlarmOnNewItems() {
return alarm;
}
public String getExcludeFilter() {
return excludeFilter;
}
public String getIncludeFilter() {
return includeFilter;
}
/**
* 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
* using {@link ApplicationSettings#getRssfeedSetting(int)}.
* @return The last new item's URL as URL-encoded string
*/
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
* manually loaded again using {@link ApplicationSettings#getRssfeedSetting(int)}.
* @return The last new item's URL as URL-encoded string
*/
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
*/
public String getHumanReadableIdentifier() {
String host = Uri.parse(url).getHost();
String path = Uri.parse(url).getPath();
return (host == null ? null : host + (path == null ? "" : path));
}
private static final String DEFAULT_NAME = "Default";
private final int order;
private final String name;
private final String url;
private final boolean requiresAuth;
private final boolean alarm;
private final String excludeFilter;
private final String includeFilter;
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,
String lastViewedItemUrl) {
this.order = order;
this.name = name;
this.url = baseUrl;
this.requiresAuth = needsAuth;
this.alarm = alarm;
this.excludeFilter = excludeFilter;
this.includeFilter = includeFilter;
this.lastViewed = lastViewed;
this.lastViewedItemUrl = lastViewedItemUrl;
}
public int getOrder() {
return order;
}
@Override
public String getName() {
if (!TextUtils.isEmpty(name))
return name;
if (!TextUtils.isEmpty(url)) {
String host = Uri.parse(url).getHost();
return host == null ? DEFAULT_NAME : host;
}
return DEFAULT_NAME;
}
public String getUrl() {
return url;
}
public boolean requiresExternalAuthentication() {
return requiresAuth;
}
public boolean shouldAlarmOnNewItems() {
return alarm;
}
public String getExcludeFilter() {
return excludeFilter;
}
public String getIncludeFilter() {
return includeFilter;
}
/**
* 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
* using {@link ApplicationSettings#getRssfeedSetting(int)}.
*
* @return The last new item's URL as URL-encoded string
*/
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
* manually loaded again using {@link ApplicationSettings#getRssfeedSetting(int)}.
*
* @return The last new item's URL as URL-encoded string
*/
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
*/
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; @@ -29,303 +29,309 @@ import org.transdroid.daemon.OS;
/**
* Represents a user-configured remote server.
*
* @author Eric Kok
*/
public class ServerSetting implements SimpleListItem {
private static final String DEFAULT_NAME = "Default";
private final int key;
private final String name;
private final Daemon type;
private final String address;
private final String localAddress;
private final int localPort;
private final String localNetwork;
private final int port;
private final String folder;
private final boolean useAuthentication;
private final String username;
private final String password;
private final String extraPass;
private final OS os;
private final String downloadDir;
private final String ftpUrl;
private final String ftpPassword;
private final int timeout;
private final boolean alarmOnFinishedDownload;
private final boolean alarmOnNewTorrent;
private final boolean ssl;
private final boolean localSsl;
private final boolean sslTrustAll;
private final String sslTrustKey;
private final String excludeFilter;
private final String includeFilter;
private final boolean isAutoGenerated;
/**
* 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 address The server domain name or IP address
* @param localAddress The server domain or IP address when connected to the server's local network
* @param localPort The port on which the server is running in the server's local network
* @param localNetwork The server's local network SSID
* @param port The port on which the server daemon is running
* @param sslTrustKey The specific key that will be accepted.
* @param folder The server folder (like a virtual sub-folder or an SCGI mount point)
* @param useAuthentication Whether to use basic authentication
* @param username The user name to provide during authentication
* @param password The password to provide during authentication
* @param extraPass The Deluge web interface password
* @param downloadDir The default download directory (which may also be used as base directory for file paths)
* @param ftpUrl The partial URL to connect to when requesting FTP-style transfers
* @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,
String password, String extraPass, OS os, String downloadDir, String ftpUrl, String ftpPassword, int timeout,
boolean alarmOnFinishedDownload, boolean alarmOnNewTorrent, String excludeFilter, String includeFilter,
boolean isAutoGenerated) {
this.key = key;
this.name = name;
this.type = type;
this.address = address;
this.localAddress = localAddress;
this.localPort = localPort;
this.localNetwork = localNetwork;
this.port = port;
this.ssl = ssl;
this.localSsl = localSsl;
this.sslTrustAll = sslTrustAll;
this.sslTrustKey = sslTrustKey;
this.folder = folder;
this.useAuthentication = useAuthentication;
this.username = username;
this.password = password;
this.extraPass = extraPass;
this.os = os;
this.downloadDir = downloadDir;
this.ftpUrl = ftpUrl;
this.ftpPassword = ftpPassword;
this.timeout = timeout;
this.alarmOnFinishedDownload = alarmOnFinishedDownload;
this.alarmOnNewTorrent = alarmOnNewTorrent;
this.excludeFilter = excludeFilter;
this.includeFilter = includeFilter;
this.isAutoGenerated = isAutoGenerated;
}
@Override
public String getName() {
if (!TextUtils.isEmpty(name)) {
return name;
}
if (!TextUtils.isEmpty(address)) {
String host = Uri.parse(address).getHost();
return host == null ? DEFAULT_NAME : host;
}
return DEFAULT_NAME;
}
public Daemon getType() {
return type;
}
public String getAddress() {
return address;
}
public String getLocalAddress() {
return localAddress;
}
public int getLocalPort() {
return localPort;
}
public String getLocalNetwork() {
return localNetwork;
}
public int getPort() {
return port;
}
public boolean getSsl() {
return ssl;
}
public boolean getLocalSsl() {
return localSsl;
}
public boolean getSslTrustAll() {
return sslTrustAll;
}
public String getSslTrustKey() {
return sslTrustKey;
}
public String getFolder() {
return folder;
}
public boolean shouldUseAuthentication() {
return useAuthentication;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getExtraPassword() {
return extraPass;
}
public OS getOS() {
return os;
}
public String getDownloadDir() {
return downloadDir;
}
public String getFtpUrl() {
return ftpUrl;
}
public String getFtpPassword() {
return ftpPassword;
}
public int getTimeoutInMilliseconds() {
return timeout * 1000;
}
public boolean shouldAlarmOnFinishedDownload() {
return alarmOnFinishedDownload;
}
public boolean shouldAlarmOnNewTorrent() {
return alarmOnNewTorrent;
}
public String getExcludeFilter() {
return excludeFilter;
}
public String getIncludeFilter() {
return includeFilter;
}
public boolean isAutoGenerated() {
return isAutoGenerated;
}
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
*/
public String getHumanReadableIdentifier() {
if (isAutoGenerated) {
// Hide the 'implementation details'; just give the username and server
return (this.shouldUseAuthentication() && !TextUtils.isEmpty(this.getUsername()) ?
this.getUsername() + "@" : "") + getAddress();
}
return (this.ssl ? "https://" : "http://") +
(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.
* @return A unique identifying string, based primarily on the configured address, port number, SSL settings and
* user name; returns null if the server is not yet fully identifiable (during configuration, for example)
*/
public String getUniqueIdentifier() {
if (getType() == null || getAddress() == null || getAddress().equals("")) {
return null;
}
return getType().toString() + "|" + getHumanReadableIdentifier();
}
@Override
public boolean equals(Object o) {
if (o instanceof ServerSetting) {
// Directly compare order numbers/unique keys
return ((ServerSetting) o).getOrder() == this.key;
} else if (o instanceof DaemonSettings) {
// Old-style DaemonSettings objects can be equal if they were constructed from a ServerSettings object:
// idString should reflect the local key/order
return ((DaemonSettings) o).getIdString().equals(Integer.toString(this.key));
}
// Other objects are never equal to this
return false;
}
@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
* @return An IDaemonAdapter instance of the specific torrent client daemon type
*/
public IDaemonAdapter createServerAdapter(String connectedToNetwork, Context context) {
return type.createAdapter(convertToDaemonSettings(connectedToNetwork, context));
}
/**
* Converts local server settings into an old-style {@link DaemonSettings} object.
* @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
*/
private DaemonSettings convertToDaemonSettings(String connectedToNetwork, Context caller) {
// The local integer key is converted to the idString string.
// 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).
if (!TextUtils.isEmpty(localNetwork)) {
Log_.getInstance_(caller)
.d("ServerSetting", "Creating adapter for " + name + " of type " + type.name() + ": connected to " +
connectedToNetwork + " and configured local network is " + localNetwork);
}
String addressToUse = address;
int portToUse = port;
boolean sslEnable = ssl;
if (!TextUtils.isEmpty(localNetwork) && !TextUtils.isEmpty(localAddress) &&
!TextUtils.isEmpty(connectedToNetwork)) {
String[] localNetworks = localNetwork.split("\\|");
for (String network : localNetworks) {
if (connectedToNetwork.equals(network)) {
addressToUse = localAddress;
portToUse = localPort;
sslEnable = localSsl;
break;
}
}
}
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);
}
private static final String DEFAULT_NAME = "Default";
private final int key;
private final String name;
private final Daemon type;
private final String address;
private final String localAddress;
private final int localPort;
private final String localNetwork;
private final int port;
private final String folder;
private final boolean useAuthentication;
private final String username;
private final String password;
private final String extraPass;
private final OS os;
private final String downloadDir;
private final String ftpUrl;
private final String ftpPassword;
private final int timeout;
private final boolean alarmOnFinishedDownload;
private final boolean alarmOnNewTorrent;
private final boolean ssl;
private final boolean localSsl;
private final boolean sslTrustAll;
private final String sslTrustKey;
private final String excludeFilter;
private final String includeFilter;
private final boolean isAutoGenerated;
/**
* 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 address The server domain name or IP address
* @param localAddress The server domain or IP address when connected to the server's local network
* @param localPort The port on which the server is running in the server's local network
* @param localNetwork The server's local network SSID
* @param port The port on which the server daemon is running
* @param sslTrustKey The specific key that will be accepted.
* @param folder The server folder (like a virtual sub-folder or an SCGI mount point)
* @param useAuthentication Whether to use basic authentication
* @param username The user name to provide during authentication
* @param password The password to provide during authentication
* @param extraPass The Deluge web interface password
* @param downloadDir The default download directory (which may also be used as base directory for file paths)
* @param ftpUrl The partial URL to connect to when requesting FTP-style transfers
* @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,
String password, String extraPass, OS os, String downloadDir, String ftpUrl, String ftpPassword, int timeout,
boolean alarmOnFinishedDownload, boolean alarmOnNewTorrent, String excludeFilter, String includeFilter,
boolean isAutoGenerated) {
this.key = key;
this.name = name;
this.type = type;
this.address = address;
this.localAddress = localAddress;
this.localPort = localPort;
this.localNetwork = localNetwork;
this.port = port;
this.ssl = ssl;
this.localSsl = localSsl;
this.sslTrustAll = sslTrustAll;
this.sslTrustKey = sslTrustKey;
this.folder = folder;
this.useAuthentication = useAuthentication;
this.username = username;
this.password = password;
this.extraPass = extraPass;
this.os = os;
this.downloadDir = downloadDir;
this.ftpUrl = ftpUrl;
this.ftpPassword = ftpPassword;
this.timeout = timeout;
this.alarmOnFinishedDownload = alarmOnFinishedDownload;
this.alarmOnNewTorrent = alarmOnNewTorrent;
this.excludeFilter = excludeFilter;
this.includeFilter = includeFilter;
this.isAutoGenerated = isAutoGenerated;
}
@Override
public String getName() {
if (!TextUtils.isEmpty(name)) {
return name;
}
if (!TextUtils.isEmpty(address)) {
String host = Uri.parse(address).getHost();
return host == null ? DEFAULT_NAME : host;
}
return DEFAULT_NAME;
}
public Daemon getType() {
return type;
}
public String getAddress() {
return address;
}
public String getLocalAddress() {
return localAddress;
}
public int getLocalPort() {
return localPort;
}
public String getLocalNetwork() {
return localNetwork;
}
public int getPort() {
return port;
}
public boolean getSsl() {
return ssl;
}
public boolean getLocalSsl() {
return localSsl;
}
public boolean getSslTrustAll() {
return sslTrustAll;
}
public String getSslTrustKey() {
return sslTrustKey;
}
public String getFolder() {
return folder;
}
public boolean shouldUseAuthentication() {
return useAuthentication;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getExtraPassword() {
return extraPass;
}
public OS getOS() {
return os;
}
public String getDownloadDir() {
return downloadDir;
}
public String getFtpUrl() {
return ftpUrl;
}
public String getFtpPassword() {
return ftpPassword;
}
public int getTimeoutInMilliseconds() {
return timeout * 1000;
}
public boolean shouldAlarmOnFinishedDownload() {
return alarmOnFinishedDownload;
}
public boolean shouldAlarmOnNewTorrent() {
return alarmOnNewTorrent;
}
public String getExcludeFilter() {
return excludeFilter;
}
public String getIncludeFilter() {
return includeFilter;
}
public boolean isAutoGenerated() {
return isAutoGenerated;
}
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
*/
public String getHumanReadableIdentifier() {
if (isAutoGenerated) {
// Hide the 'implementation details'; just give the username and server
return (this.shouldUseAuthentication() && !TextUtils.isEmpty(this.getUsername()) ?
this.getUsername() + "@" : "") + getAddress();
}
return (this.ssl ? "https://" : "http://") +
(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.
*
* @return A unique identifying string, based primarily on the configured address, port number, SSL settings and
* user name; returns null if the server is not yet fully identifiable (during configuration, for example)
*/
public String getUniqueIdentifier() {
if (getType() == null || getAddress() == null || getAddress().equals("")) {
return null;
}
return getType().toString() + "|" + getHumanReadableIdentifier();
}
@Override
public boolean equals(Object o) {
if (o instanceof ServerSetting) {
// Directly compare order numbers/unique keys
return ((ServerSetting) o).getOrder() == this.key;
} else if (o instanceof DaemonSettings) {
// Old-style DaemonSettings objects can be equal if they were constructed from a ServerSettings object:
// idString should reflect the local key/order
return ((DaemonSettings) o).getIdString().equals(Integer.toString(this.key));
}
// Other objects are never equal to this
return false;
}
@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
* @return An IDaemonAdapter instance of the specific torrent client daemon type
*/
public IDaemonAdapter createServerAdapter(String connectedToNetwork, Context context) {
return type.createAdapter(convertToDaemonSettings(connectedToNetwork, context));
}
/**
* Converts local server settings into an old-style {@link DaemonSettings} object.
*
* @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
*/
private DaemonSettings convertToDaemonSettings(String connectedToNetwork, Context caller) {
// The local integer key is converted to the idString string.
// 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).
if (!TextUtils.isEmpty(localNetwork)) {
Log_.getInstance_(caller)
.d("ServerSetting", "Creating adapter for " + name + " of type " + type.name() + ": connected to " +
connectedToNetwork + " and configured local network is " + localNetwork);
}
String addressToUse = address;
int portToUse = port;
boolean sslEnable = ssl;
if (!TextUtils.isEmpty(localNetwork) && !TextUtils.isEmpty(localAddress) &&
!TextUtils.isEmpty(connectedToNetwork)) {
String[] localNetworks = localNetwork.split("\\|");
for (String network : localNetworks) {
if (connectedToNetwork.equals(network)) {
addressToUse = localAddress;
portToUse = localPort;
sslEnable = localSsl;
break;
}
}
}
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; @@ -44,42 +44,44 @@ import java.io.OutputStream;
@EBean(scope = Scope.Singleton)
public class SettingsPersistence {
@Bean
protected ApplicationSettings applicationSettings;
@Bean
protected SystemSettings systemSettings;
public static final String DEFAULT_SETTINGS_DIR = Environment.getExternalStorageDirectory().toString()
+ "/Transdroid/";
public static final String DEFAULT_SETTINGS_FILENAME = "settings.json";
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.
* @param prefs The application-global preferences object to write settings to
* @param contents The JSON-encoded settings as raw String
* @throws JSONException Thrown when the file did not contain valid JSON content
*/
public void importSettingsAsString(SharedPreferences prefs, String contents) throws JSONException {
importSettings(prefs, new JSONObject(contents));
}
/**
* Synchronously reads the server, web searches, RSS feed, background service and system settings from a file in
* JSON format.
* @param prefs The application-global preferences object to write settings to
* @param settingsFile The local file to read the settings from
* @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 static final String DEFAULT_SETTINGS_DIR = Environment.getExternalStorageDirectory().toString()
+ "/Transdroid/";
public static final String DEFAULT_SETTINGS_FILENAME = "settings.json";
public static final File DEFAULT_SETTINGS_FILE = new File(DEFAULT_SETTINGS_DIR + DEFAULT_SETTINGS_FILENAME);
@Bean
protected ApplicationSettings applicationSettings;
@Bean
protected SystemSettings systemSettings;
/**
* 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 contents The JSON-encoded settings as raw String
* @throws JSONException Thrown when the file did not contain valid JSON content
*/
public void importSettingsAsString(SharedPreferences prefs, String contents) throws JSONException {
importSettings(prefs, new JSONObject(contents));
}
/**
* Synchronously reads the server, web searches, RSS feed, background service and system settings from a file in
* JSON format.
*
* @param prefs The application-global preferences object to write settings to
* @param settingsFile The local file to read the settings from
* @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 {
importSettingsFromStream(prefs, new FileInputStream(settingsFile));
}
}
/**
* Synchronously reads the server, web searches, RSS feed, background service and system settings from a stream (file) in
* 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
* @throws JSONException Thrown when the file did not contain valid JSON content
*/
@ -88,287 +90,289 @@ public class SettingsPersistence { @@ -88,287 +90,289 @@ public class SettingsPersistence {
importSettings(prefs, new JSONObject(raw));
}
public void importSettings(SharedPreferences prefs, JSONObject json) throws JSONException {
Editor editor = prefs.edit();
// Import servers
if (json.has("servers")) {
JSONArray servers = json.getJSONArray("servers");
for (int i = 0; i < servers.length(); i++) {
JSONObject server = servers.getJSONObject(i);
String postfix = Integer.toString(applicationSettings.getMaxOfAllServers() + 1 + i);
if (server.has("name"))
editor.putString("server_name_" + postfix, server.getString("name"));
if (server.has("type"))
editor.putString("server_type_" + postfix, server.getString("type"));
if (server.has("host"))
editor.putString("server_address_" + postfix, server.getString("host"));
if (server.has("local_network"))
editor.putString("server_localnetwork_" + postfix, server.getString("local_network"));
if (server.has("local_host"))
editor.putString("server_localaddress_" + postfix, server.getString("local_host"));
if (server.has("local_port"))
editor.putString("server_localport_" + postfix, server.getString("local_port"));
if (server.has("port"))
editor.putString("server_port_" + postfix, server.getString("port"));
if (server.has("ssl"))
editor.putBoolean("server_sslenabled_" + postfix, server.getBoolean("ssl"));
if (server.has("local_ssl"))
editor.putBoolean("server_localsslenabled_" + postfix, server.getBoolean("local_ssl"));
if (server.has("ssl_accept_all"))
editor.putBoolean("server_ssltrustall_" + postfix, server.getBoolean("ssl_accept_all"));
if (server.has("ssl_trust_key"))
editor.putString("server_ssltrustkey_" + postfix, server.getString("ssl_trust_key"));
if (server.has("folder"))
editor.putString("server_folder_" + postfix, server.getString("folder"));
if (server.has("use_auth"))
editor.putBoolean("server_disableauth_" + postfix, !server.getBoolean("use_auth"));
if (server.has("username"))
editor.putString("server_user_" + postfix, server.getString("username"));
if (server.has("password"))
editor.putString("server_pass_" + postfix, server.getString("password"));
if (server.has("extra_password"))
editor.putString("server_extrapass_" + postfix, server.getString("extra_password"));
if (server.has("os_type"))
editor.putString("server_os_" + postfix, server.getString("os_type"));
if (server.has("downloads_dir"))
editor.putString("server_downloaddir_" + postfix, server.getString("downloads_dir"));
if (server.has("base_ftp_url"))
editor.putString("server_ftpurl_" + postfix, server.getString("base_ftp_url"));
if (server.has("ftp_password"))
editor.putString("server_ftppass_" + postfix, server.getString("ftp_password"));
if (server.has("server_timeout"))
editor.putString("server_timeout_" + postfix, server.getString("server_timeout"));
if (server.has("download_alarm"))
editor.putBoolean("server_alarmfinished_" + postfix, server.getBoolean("download_alarm"));
if (server.has("new_torrent_alarm"))
editor.putBoolean("server_alarmnew_" + postfix, server.getBoolean("new_torrent_alarm"));
if (server.has("alarm_filter_exclude"))
editor.putString("server_alarmexclude_" + postfix, server.getString("alarm_filter_exclude"));
if (server.has("alarm_filter_include"))
editor.putString("server_alarminclude_" + postfix, server.getString("alarm_filter_include"));
}
}
// Import web search sites
if (json.has("websites")) {
JSONArray sites = json.getJSONArray("websites");
for (int i = 0; i < sites.length(); i++) {
JSONObject site = sites.getJSONObject(i);
String postfix = Integer.toString(applicationSettings.getMaxWebsearch() + 1 + i);
if (site.has("name"))
editor.putString("websearch_name_" + postfix, site.getString("name"));
if (site.has("url"))
editor.putString("websearch_baseurl_" + postfix, site.getString("url"));
if (site.has("cookies"))
editor.putString("websearch_cookies_" + postfix, site.getString("cookies"));
}
}
// Import RSS feeds
if (json.has("rssfeeds")) {
JSONArray feeds = json.getJSONArray("rssfeeds");
for (int i = 0; i < feeds.length(); i++) {
JSONObject feed = feeds.getJSONObject(i);
String postfix = Integer.toString(applicationSettings.getMaxRssfeed() + 1 + i);
if (feed.has("name"))
editor.putString("rssfeed_name_" + postfix, feed.getString("name"));
if (feed.has("url"))
editor.putString("rssfeed_url_" + postfix, feed.getString("url"));
if (feed.has("needs_auth"))
editor.putBoolean("rssfeed_reqauth_" + postfix, feed.getBoolean("needs_auth"));
if (feed.has("new_item_alarm"))
editor.putBoolean("rssfeed_alarmnew_" + postfix, feed.getBoolean("new_item_alarm"));
if (feed.has("alarm_filter_include"))
editor.putString("rssfeed_include_" + postfix, feed.getString("alarm_filter_include"));
if (feed.has("alarm_filter_exclude"))
editor.putString("rssfeed_exclude_" + postfix, feed.getString("alarm_filter_exclude"));
if (feed.has("last_seen_time"))
editor.putLong("rssfeed_lastviewed_" + postfix, feed.getLong("last_seen_time"));
if (feed.has("last_seen_item"))
editor.putString("rssfeed_lastvieweditemurl_" + postfix, feed.getString("last_seen_item"));
}
}
// Import background service and system settings
if (json.has("alarm_enabled_rss"))
editor.putBoolean("notifications_enabledrss", json.getBoolean("alarm_enabled_rss"));
if (json.has("alarm_enabled_torrents"))
editor.putBoolean("notifications_enabled", json.getBoolean("alarm_enabled_torrents"));
else if (json.has("alarm_enabled")) // Compat
editor.putBoolean("notifications_enabled", json.getBoolean("alarm_enabled"));
if (json.has("alarm_interval"))
editor.putString("notifications_interval", json.getString("alarm_interval"));
if (json.has("alarm_sound_uri"))
editor.putString("notifications_sound", json.getString("alarm_sound_uri"));
if (json.has("alarm_vibrate"))
editor.putBoolean("notifications_vibrate", json.getBoolean("alarm_vibrate"));
if (json.has("alarm_ledcolour"))
editor.putInt("notifications_ledcolour", json.getInt("alarm_ledcolour"));
if (json.has("alarm_adwnotifications"))
editor.putBoolean("notifications_adwnotify", json.getBoolean("alarm_adwnotifications"));
if (json.has("system_dormantasinactive"))
editor.putBoolean("system_dormantasinactive", json.getBoolean("system_dormantasinactive"));
if (json.has("system_autorefresh"))
editor.putString("system_autorefresh", json.getString("system_autorefresh"));
if (json.has("system_checkupdates"))
editor.putBoolean("system_checkupdates", json.getBoolean("system_checkupdates"));
if (json.has("system_usedarktheme"))
editor.putBoolean("system_usedarktheme", json.getBoolean("system_usedarktheme"));
editor.apply();
}
/**
* 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
*/
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.
* @param prefs The application-global preferences object to read settings from
* @param settingsFile The local file to read the settings from
* @throws JSONException Thrown when the JSON content could not be constructed properly
* @throws IOException Thrown when the settings file could not be created or written to
*/
public void exportSettingsToFile(SharedPreferences prefs, File settingsFile) throws JSONException, IOException {
if (settingsFile.exists()) {
settingsFile.delete();
}
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.
*
* @param prefs The application-global preferences object to read settings from
* @param settingsStream The stream to read the settings to
* @throws JSONException Thrown when the JSON content could not be constructed properly
* @throws IOException Thrown when the settings file could not be created or written to
*/
public void exportSettingsToStream(SharedPreferences prefs, OutputStream settingsStream) throws JSONException, IOException {
try {
JSONObject json = exportSettings(prefs);
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
JSONObject json = new JSONObject();
// Convert server settings into JSON
JSONArray servers = new JSONArray();
int i = 0;
String postfixi = "0";
while (prefs.contains("server_type_" + postfixi)) {
JSONObject server = new JSONObject();
server.put("name", prefs.getString("server_name_" + postfixi, null));
server.put("type", prefs.getString("server_type_" + postfixi, null));
server.put("host", prefs.getString("server_address_" + postfixi, null));
server.put("local_network", prefs.getString("server_localnetwork_" + postfixi, null));
server.put("local_host", prefs.getString("server_localaddress_" + postfixi, null));
server.put("local_port", prefs.getString("server_localport_" + postfixi, null));
server.put("port", prefs.getString("server_port_" + postfixi, null));
server.put("ssl", prefs.getBoolean("server_sslenabled_" + postfixi, false));
server.put("local_ssl", prefs.getBoolean("server_localsslenabled_" + postfixi, false));
server.put("ssl_accept_all", prefs.getBoolean("server_ssltrustall_" + postfixi, false));
server.put("ssl_trust_key", prefs.getString("server_ssltrustkey_" + postfixi, null));
server.put("folder", prefs.getString("server_folder_" + postfixi, null));
server.put("use_auth", !prefs.getBoolean("server_disableauth_" + postfixi, false));
server.put("username", prefs.getString("server_user_" + postfixi, null));
server.put("password", prefs.getString("server_pass_" + postfixi, null));
server.put("extra_password", prefs.getString("server_extrapass_" + postfixi, null));
server.put("os_type", prefs.getString("server_os_" + postfixi, null));
server.put("downloads_dir", prefs.getString("server_downloaddir_" + postfixi, null));
server.put("base_ftp_url", prefs.getString("server_ftpurl_" + postfixi, null));
server.put("ftp_password", prefs.getString("server_ftppass_" + postfixi, null));
server.put("server_timeout", prefs.getString("server_timeout_" + postfixi, null));
server.put("download_alarm", prefs.getBoolean("server_alarmfinished_" + postfixi, false));
server.put("new_torrent_alarm", prefs.getBoolean("server_alarmnew_" + postfixi, false));
server.put("alarm_filter_exclude", prefs.getString("server_alarmexclude_" + postfixi, null));
server.put("alarm_filter_include", prefs.getString("server_alarminclude_" + postfixi, null));
servers.put(server);
i++;
postfixi = Integer.toString(i);
}
json.put("servers", servers);
// Convert web search settings into JSON
JSONArray sites = new JSONArray();
int j = 0;
String postfixj = "0";
while (prefs.contains("websearch_baseurl_" + postfixj)) {
JSONObject site = new JSONObject();
site.put("name", prefs.getString("websearch_name_" + postfixj, null));
site.put("url", prefs.getString("websearch_baseurl_" + postfixj, null));
site.put("cookies", prefs.getString("websearch_cookies_" + postfixj, null));
sites.put(site);
j++;
postfixj = Integer.toString(j);
}
json.put("websites", sites);
// Convert RSS feed settings into JSON
JSONArray feeds = new JSONArray();
int k = 0;
String postfixk = "0";
while (prefs.contains("rssfeed_url_" + postfixk)) {
JSONObject feed = new JSONObject();
feed.put("name", prefs.getString("rssfeed_name_" + postfixk, null));
feed.put("url", prefs.getString("rssfeed_url_" + postfixk, null));
feed.put("needs_auth", prefs.getBoolean("rssfeed_reqauth_" + postfixk, false));
feed.put("new_item_alarm", prefs.getBoolean("rssfeed_alarmnew_" + postfixk, false));
feed.put("alarm_filter_exclude", prefs.getString("rssfeed_exclude_" + postfixk, null));
feed.put("alarm_filter_include", prefs.getString("rssfeed_include_" + postfixk, null));
feed.put("last_seen_time", prefs.getLong("rssfeed_lastviewed_" + postfixk, -1));
feed.put("last_seen_item", prefs.getString("rssfeed_lastvieweditemurl_" + postfixk, null));
feeds.put(feed);
k++;
postfixk = Integer.toString(k);
}
json.put("rssfeeds", feeds);
// Convert background service and system settings into JSON
json.put("alarm_enabled_rss", prefs.getBoolean("notifications_enabledrss", true));
json.put("alarm_enabled_torrents", prefs.getBoolean("notifications_enabled", true));
json.put("alarm_interval", prefs.getString("notifications_interval", null));
json.put("alarm_sound_uri", prefs.getString("notifications_sound", null));
json.put("alarm_vibrate", prefs.getBoolean("notifications_vibrate", false));
json.put("alarm_ledcolour", prefs.getInt("notifications_ledcolour", -1));
json.put("alarm_adwnotifications", prefs.getBoolean("notifications_adwnotify", false));
json.put("system_dormantasinactive", prefs.getBoolean("system_dormantasinactive", false));
json.put("system_autorefresh", prefs.getString("system_autorefresh", "0"));
json.put("system_usedarktheme", prefs.getBoolean("system_usedarktheme", false));
json.put("system_checkupdates", prefs.getBoolean("system_checkupdates", true));
return json;
}
public void importSettings(SharedPreferences prefs, JSONObject json) throws JSONException {
Editor editor = prefs.edit();
// Import servers
if (json.has("servers")) {
JSONArray servers = json.getJSONArray("servers");
for (int i = 0; i < servers.length(); i++) {
JSONObject server = servers.getJSONObject(i);
String postfix = Integer.toString(applicationSettings.getMaxOfAllServers() + 1 + i);
if (server.has("name"))
editor.putString("server_name_" + postfix, server.getString("name"));
if (server.has("type"))
editor.putString("server_type_" + postfix, server.getString("type"));
if (server.has("host"))
editor.putString("server_address_" + postfix, server.getString("host"));
if (server.has("local_network"))
editor.putString("server_localnetwork_" + postfix, server.getString("local_network"));
if (server.has("local_host"))
editor.putString("server_localaddress_" + postfix, server.getString("local_host"));
if (server.has("local_port"))
editor.putString("server_localport_" + postfix, server.getString("local_port"));
if (server.has("port"))
editor.putString("server_port_" + postfix, server.getString("port"));
if (server.has("ssl"))
editor.putBoolean("server_sslenabled_" + postfix, server.getBoolean("ssl"));
if (server.has("local_ssl"))
editor.putBoolean("server_localsslenabled_" + postfix, server.getBoolean("local_ssl"));
if (server.has("ssl_accept_all"))
editor.putBoolean("server_ssltrustall_" + postfix, server.getBoolean("ssl_accept_all"));
if (server.has("ssl_trust_key"))
editor.putString("server_ssltrustkey_" + postfix, server.getString("ssl_trust_key"));
if (server.has("folder"))
editor.putString("server_folder_" + postfix, server.getString("folder"));
if (server.has("use_auth"))
editor.putBoolean("server_disableauth_" + postfix, !server.getBoolean("use_auth"));
if (server.has("username"))
editor.putString("server_user_" + postfix, server.getString("username"));
if (server.has("password"))
editor.putString("server_pass_" + postfix, server.getString("password"));
if (server.has("extra_password"))
editor.putString("server_extrapass_" + postfix, server.getString("extra_password"));
if (server.has("os_type"))
editor.putString("server_os_" + postfix, server.getString("os_type"));
if (server.has("downloads_dir"))
editor.putString("server_downloaddir_" + postfix, server.getString("downloads_dir"));
if (server.has("base_ftp_url"))
editor.putString("server_ftpurl_" + postfix, server.getString("base_ftp_url"));
if (server.has("ftp_password"))
editor.putString("server_ftppass_" + postfix, server.getString("ftp_password"));
if (server.has("server_timeout"))
editor.putString("server_timeout_" + postfix, server.getString("server_timeout"));
if (server.has("download_alarm"))
editor.putBoolean("server_alarmfinished_" + postfix, server.getBoolean("download_alarm"));
if (server.has("new_torrent_alarm"))
editor.putBoolean("server_alarmnew_" + postfix, server.getBoolean("new_torrent_alarm"));
if (server.has("alarm_filter_exclude"))
editor.putString("server_alarmexclude_" + postfix, server.getString("alarm_filter_exclude"));
if (server.has("alarm_filter_include"))
editor.putString("server_alarminclude_" + postfix, server.getString("alarm_filter_include"));
}
}
// Import web search sites
if (json.has("websites")) {
JSONArray sites = json.getJSONArray("websites");
for (int i = 0; i < sites.length(); i++) {
JSONObject site = sites.getJSONObject(i);
String postfix = Integer.toString(applicationSettings.getMaxWebsearch() + 1 + i);
if (site.has("name"))
editor.putString("websearch_name_" + postfix, site.getString("name"));
if (site.has("url"))
editor.putString("websearch_baseurl_" + postfix, site.getString("url"));
if (site.has("cookies"))
editor.putString("websearch_cookies_" + postfix, site.getString("cookies"));
}
}
// Import RSS feeds
if (json.has("rssfeeds")) {
JSONArray feeds = json.getJSONArray("rssfeeds");
for (int i = 0; i < feeds.length(); i++) {
JSONObject feed = feeds.getJSONObject(i);
String postfix = Integer.toString(applicationSettings.getMaxRssfeed() + 1 + i);
if (feed.has("name"))
editor.putString("rssfeed_name_" + postfix, feed.getString("name"));
if (feed.has("url"))
editor.putString("rssfeed_url_" + postfix, feed.getString("url"));
if (feed.has("needs_auth"))
editor.putBoolean("rssfeed_reqauth_" + postfix, feed.getBoolean("needs_auth"));
if (feed.has("new_item_alarm"))
editor.putBoolean("rssfeed_alarmnew_" + postfix, feed.getBoolean("new_item_alarm"));
if (feed.has("alarm_filter_include"))
editor.putString("rssfeed_include_" + postfix, feed.getString("alarm_filter_include"));
if (feed.has("alarm_filter_exclude"))
editor.putString("rssfeed_exclude_" + postfix, feed.getString("alarm_filter_exclude"));
if (feed.has("last_seen_time"))
editor.putLong("rssfeed_lastviewed_" + postfix, feed.getLong("last_seen_time"));
if (feed.has("last_seen_item"))
editor.putString("rssfeed_lastvieweditemurl_" + postfix, feed.getString("last_seen_item"));
}
}
// Import background service and system settings
if (json.has("alarm_enabled_rss"))
editor.putBoolean("notifications_enabledrss", json.getBoolean("alarm_enabled_rss"));
if (json.has("alarm_enabled_torrents"))
editor.putBoolean("notifications_enabled", json.getBoolean("alarm_enabled_torrents"));
else if (json.has("alarm_enabled")) // Compat
editor.putBoolean("notifications_enabled", json.getBoolean("alarm_enabled"));
if (json.has("alarm_interval"))
editor.putString("notifications_interval", json.getString("alarm_interval"));
if (json.has("alarm_sound_uri"))
editor.putString("notifications_sound", json.getString("alarm_sound_uri"));
if (json.has("alarm_vibrate"))
editor.putBoolean("notifications_vibrate", json.getBoolean("alarm_vibrate"));
if (json.has("alarm_ledcolour"))
editor.putInt("notifications_ledcolour", json.getInt("alarm_ledcolour"));
if (json.has("alarm_adwnotifications"))
editor.putBoolean("notifications_adwnotify", json.getBoolean("alarm_adwnotifications"));
if (json.has("system_dormantasinactive"))
editor.putBoolean("system_dormantasinactive", json.getBoolean("system_dormantasinactive"));
if (json.has("system_autorefresh"))
editor.putString("system_autorefresh", json.getString("system_autorefresh"));
if (json.has("system_checkupdates"))
editor.putBoolean("system_checkupdates", json.getBoolean("system_checkupdates"));
if (json.has("system_usedarktheme"))
editor.putBoolean("system_usedarktheme", json.getBoolean("system_usedarktheme"));
editor.apply();
}
/**
* 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
*/
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.
*
* @param prefs The application-global preferences object to read settings from
* @param settingsFile The local file to read the settings from
* @throws JSONException Thrown when the JSON content could not be constructed properly
* @throws IOException Thrown when the settings file could not be created or written to
*/
public void exportSettingsToFile(SharedPreferences prefs, File settingsFile) throws JSONException, IOException {
if (settingsFile.exists()) {
settingsFile.delete();
}
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.
*
* @param prefs The application-global preferences object to read settings from
* @param settingsStream The stream to read the settings to
* @throws JSONException Thrown when the JSON content could not be constructed properly
* @throws IOException Thrown when the settings file could not be created or written to
*/
public void exportSettingsToStream(SharedPreferences prefs, OutputStream settingsStream) throws JSONException, IOException {
try {
JSONObject json = exportSettings(prefs);
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
JSONObject json = new JSONObject();
// Convert server settings into JSON
JSONArray servers = new JSONArray();
int i = 0;
String postfixi = "0";
while (prefs.contains("server_type_" + postfixi)) {
JSONObject server = new JSONObject();
server.put("name", prefs.getString("server_name_" + postfixi, null));
server.put("type", prefs.getString("server_type_" + postfixi, null));
server.put("host", prefs.getString("server_address_" + postfixi, null));
server.put("local_network", prefs.getString("server_localnetwork_" + postfixi, null));
server.put("local_host", prefs.getString("server_localaddress_" + postfixi, null));
server.put("local_port", prefs.getString("server_localport_" + postfixi, null));
server.put("port", prefs.getString("server_port_" + postfixi, null));
server.put("ssl", prefs.getBoolean("server_sslenabled_" + postfixi, false));
server.put("local_ssl", prefs.getBoolean("server_localsslenabled_" + postfixi, false));
server.put("ssl_accept_all", prefs.getBoolean("server_ssltrustall_" + postfixi, false));
server.put("ssl_trust_key", prefs.getString("server_ssltrustkey_" + postfixi, null));
server.put("folder", prefs.getString("server_folder_" + postfixi, null));
server.put("use_auth", !prefs.getBoolean("server_disableauth_" + postfixi, false));
server.put("username", prefs.getString("server_user_" + postfixi, null));
server.put("password", prefs.getString("server_pass_" + postfixi, null));
server.put("extra_password", prefs.getString("server_extrapass_" + postfixi, null));
server.put("os_type", prefs.getString("server_os_" + postfixi, null));
server.put("downloads_dir", prefs.getString("server_downloaddir_" + postfixi, null));
server.put("base_ftp_url", prefs.getString("server_ftpurl_" + postfixi, null));
server.put("ftp_password", prefs.getString("server_ftppass_" + postfixi, null));
server.put("server_timeout", prefs.getString("server_timeout_" + postfixi, null));
server.put("download_alarm", prefs.getBoolean("server_alarmfinished_" + postfixi, false));
server.put("new_torrent_alarm", prefs.getBoolean("server_alarmnew_" + postfixi, false));
server.put("alarm_filter_exclude", prefs.getString("server_alarmexclude_" + postfixi, null));
server.put("alarm_filter_include", prefs.getString("server_alarminclude_" + postfixi, null));
servers.put(server);
i++;
postfixi = Integer.toString(i);
}
json.put("servers", servers);
// Convert web search settings into JSON
JSONArray sites = new JSONArray();
int j = 0;
String postfixj = "0";
while (prefs.contains("websearch_baseurl_" + postfixj)) {
JSONObject site = new JSONObject();
site.put("name", prefs.getString("websearch_name_" + postfixj, null));
site.put("url", prefs.getString("websearch_baseurl_" + postfixj, null));
site.put("cookies", prefs.getString("websearch_cookies_" + postfixj, null));
sites.put(site);
j++;
postfixj = Integer.toString(j);
}
json.put("websites", sites);
// Convert RSS feed settings into JSON
JSONArray feeds = new JSONArray();
int k = 0;
String postfixk = "0";
while (prefs.contains("rssfeed_url_" + postfixk)) {
JSONObject feed = new JSONObject();
feed.put("name", prefs.getString("rssfeed_name_" + postfixk, null));
feed.put("url", prefs.getString("rssfeed_url_" + postfixk, null));
feed.put("needs_auth", prefs.getBoolean("rssfeed_reqauth_" + postfixk, false));
feed.put("new_item_alarm", prefs.getBoolean("rssfeed_alarmnew_" + postfixk, false));
feed.put("alarm_filter_exclude", prefs.getString("rssfeed_exclude_" + postfixk, null));
feed.put("alarm_filter_include", prefs.getString("rssfeed_include_" + postfixk, null));
feed.put("last_seen_time", prefs.getLong("rssfeed_lastviewed_" + postfixk, -1));
feed.put("last_seen_item", prefs.getString("rssfeed_lastvieweditemurl_" + postfixk, null));
feeds.put(feed);
k++;
postfixk = Integer.toString(k);
}
json.put("rssfeeds", feeds);
// Convert background service and system settings into JSON
json.put("alarm_enabled_rss", prefs.getBoolean("notifications_enabledrss", true));
json.put("alarm_enabled_torrents", prefs.getBoolean("notifications_enabled", true));
json.put("alarm_interval", prefs.getString("notifications_interval", null));
json.put("alarm_sound_uri", prefs.getString("notifications_sound", null));
json.put("alarm_vibrate", prefs.getBoolean("notifications_vibrate", false));
json.put("alarm_ledcolour", prefs.getInt("notifications_ledcolour", -1));
json.put("alarm_adwnotifications", prefs.getBoolean("notifications_adwnotify", false));
json.put("system_dormantasinactive", prefs.getBoolean("system_dormantasinactive", false));
json.put("system_autorefresh", prefs.getString("system_autorefresh", "0"));
json.put("system_usedarktheme", prefs.getBoolean("system_usedarktheme", false));
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 @@ @@ -1,7 +1,6 @@
package org.transdroid.core.app.settings;
import android.content.Context;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
@ -33,6 +32,6 @@ public class SettingsUtils { @@ -33,6 +32,6 @@ public class SettingsUtils {
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; @@ -29,58 +29,62 @@ import java.util.Date;
/**
* Allows instantiation of the settings specified in R.xml.pref_system.
*
* @author Eric Kok
*/
@EBean(scope = Scope.Singleton)
public class SystemSettings {
@RootContext
protected Context context;
private SharedPreferences prefs;
@RootContext
protected Context context;
private SharedPreferences prefs;
protected SystemSettings(Context context) {
prefs = PreferenceManager.getDefaultSharedPreferences(context);
}
protected SystemSettings(Context context) {
prefs = PreferenceManager.getDefaultSharedPreferences(context);
}
public boolean treatDormantAsInactive() {
return prefs.getBoolean("system_dormantasinactive", false);
}
public boolean treatDormantAsInactive() {
return prefs.getBoolean("system_dormantasinactive", false);
}
/**
* 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
*/
public long getRefreshIntervalMilliseconds() {
return Integer.parseInt(prefs.getString("system_autorefresh", "0")) * 1000;
}
/**
* 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
*/
public long getRefreshIntervalMilliseconds() {
return Integer.parseInt(prefs.getString("system_autorefresh", "0")) * 1000;
}
public boolean checkForUpdates() {
return prefs.getBoolean("system_checkupdates", true);
}
public boolean checkForUpdates() {
return prefs.getBoolean("system_checkupdates", true);
}
public boolean autoDarkTheme() {
return prefs.getBoolean("system_autodarktheme", true);
}
public boolean autoDarkTheme() {
return prefs.getBoolean("system_autodarktheme", true);
}
public boolean useDarkTheme() {
return prefs.getBoolean("system_usedarktheme", false);
}
public boolean useDarkTheme() {
return prefs.getBoolean("system_usedarktheme", false);
}
/**
* 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
*/
public Date getLastCheckedForAppUpdates() {
long lastChecked = prefs.getLong("system_lastappupdatecheck", -1L);
return lastChecked == -1 ? null : new Date(lastChecked);
}
/**
* 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
*/
public Date getLastCheckedForAppUpdates() {
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.
* @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();
}
/**
* 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
*/
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 @@ @@ -16,66 +16,67 @@
*/
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.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)
*
* @author Eric Kok
*/
public class WebsearchSetting implements SimpleListItem, SearchSetting {
private static final String DEFAULT_NAME = "Default";
public static final String KEY_PREFIX = "websearch_";
private final int order;
private final String name;
private final String baseUrl;
private final String cookies;
public static final String KEY_PREFIX = "websearch_";
private static final String DEFAULT_NAME = "Default";
private final int order;
private final String name;
private final String baseUrl;
private final String cookies;
public WebsearchSetting(int order, String name, String baseUrl, String cookies) {
this.order = order;
this.name = name;
this.baseUrl = baseUrl;
this.cookies = cookies;
}
public WebsearchSetting(int order, String name, String baseUrl, String cookies) {
this.order = order;
this.name = name;
this.baseUrl = baseUrl;
this.cookies = cookies;
}
public int getOrder() {
return order;
}
public int getOrder() {
return order;
}
@Override
public String getName() {
if (!TextUtils.isEmpty(name))
return name;
if (!TextUtils.isEmpty(baseUrl)) {
String host = Uri.parse(baseUrl).getHost();
return host == null? DEFAULT_NAME: host;
}
return DEFAULT_NAME;
}
@Override
public String getName() {
if (!TextUtils.isEmpty(name))
return name;
if (!TextUtils.isEmpty(baseUrl)) {
String host = Uri.parse(baseUrl).getHost();
return host == null ? DEFAULT_NAME : host;
}
return DEFAULT_NAME;
}
public String getBaseUrl() {
return baseUrl;
}
public String getBaseUrl() {
return baseUrl;
}
public String getCookies() {
return cookies;
}
public String getCookies() {
return cookies;
}
public String getKey() {
return KEY_PREFIX + getOrder();
}
public String getKey() {
return KEY_PREFIX + getOrder();
}
/**
* Returns a nicely formatted identifier containing (a portion of) the search base URL
* @return A string to identify this site's search URL
*/
public String getHumanReadableIdentifier() {
return Uri.parse(baseUrl).getHost();
}
/**
* Returns a nicely formatted identifier containing (a portion of) the search base URL
*
* @return A string to identify this site's search URL
*/
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; @@ -20,6 +20,7 @@ import android.annotation.TargetApi;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
@ -56,8 +57,6 @@ import org.transdroid.daemon.TorrentFile; @@ -56,8 +57,6 @@ import org.transdroid.daemon.TorrentFile;
import org.transdroid.daemon.task.DaemonTaskFailureResult;
import org.transdroid.daemon.task.DaemonTaskResult;
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.GetFileListTask;
import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
@ -74,6 +73,8 @@ import org.transdroid.daemon.task.SetLabelTask; @@ -74,6 +73,8 @@ import org.transdroid.daemon.task.SetLabelTask;
import org.transdroid.daemon.task.SetTrackersTask;
import org.transdroid.daemon.task.StartTask;
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.List;
@ -82,318 +83,318 @@ 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
* TorrentsActivity} directly. Task execution, such as loading of more details and updating file priorities, is performed in this activity via
* background methods.
*
* @author Eric Kok
*/
@EActivity(R.layout.activity_details)
@OptionsMenu(R.menu.activity_details)
public class DetailsActivity extends AppCompatActivity implements TorrentTasksExecutor, RefreshableActivity {
@Extra
@InstanceState
protected Torrent torrent;
@Extra
@InstanceState
protected ArrayList<Label> currentLabels;
// Settings
@Bean
protected Log log;
@Bean
protected NavigationHelper navigationHelper;
@Bean
protected ConnectivityHelper connectivityHelper;
@Bean
protected ApplicationSettings applicationSettings;
private IDaemonAdapter currentConnection = null;
// Details view components
@ViewById
protected Toolbar selectionToolbar;
@FragmentById(R.id.torrentdetails_fragment)
protected DetailsFragment fragmentDetails;
@Override
public void onCreate(Bundle savedInstanceState) {
SettingsUtils.applyDayNightTheme(this);
super.onCreate(savedInstanceState);
}
@AfterViews
protected void init() {
// We require a torrent to be specified; otherwise close the activity
if (torrent == null) {
finish();
return;
}
// Simple action bar with up, torrent name as title and refresh button
setSupportActionBar(selectionToolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(torrent.getName()));
// Connect to the last used server
ServerSetting lastUsed = applicationSettings.getLastUsedServer();
fragmentDetails.setCurrentServerSettings(lastUsed);
currentConnection = lastUsed.createServerAdapter(connectivityHelper.getConnectedNetworkName(), this);
// Show details and load fine stats and torrent files
fragmentDetails.updateTorrent(torrent);
fragmentDetails.updateLabels(currentLabels);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@OptionsItem(android.R.id.home)
protected void navigateUp() {
TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
}
@OptionsItem(R.id.action_refresh)
public void refreshScreen() {
fragmentDetails.updateIsLoading(true, null);
refreshTorrent();
refreshTorrentDetails(torrent);
refreshTorrentFiles(torrent);
}
@Background
protected void refreshTorrent() {
DaemonTaskResult result = RetrieveTask.create(currentConnection).execute(log);
if (result instanceof RetrieveTaskSuccessResult) {
onTorrentsRetrieved(((RetrieveTaskSuccessResult) result).getTorrents(), ((RetrieveTaskSuccessResult) result).getLabels());
} else {
onCommunicationError((DaemonTaskFailureResult) result, true);
}
}
@Background
public void refreshTorrentDetails(Torrent torrent) {
if (currentConnection == null) return;
if (!Daemon.supportsFineDetails(torrent.getDaemon())) {
return;
}
DaemonTaskResult result = GetTorrentDetailsTask.create(currentConnection, torrent).execute(log);
if (result instanceof GetTorrentDetailsTaskSuccessResult) {
onTorrentDetailsRetrieved(torrent, ((GetTorrentDetailsTaskSuccessResult) result).getTorrentDetails());
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
public void refreshTorrentFiles(Torrent torrent) {
if (currentConnection == null) return;
if (!Daemon.supportsFileListing(torrent.getDaemon())) {
return;
}
DaemonTaskResult result = GetFileListTask.create(currentConnection, torrent).execute(log);
if (result instanceof GetFileListTaskSuccessResult) {
onTorrentFilesRetrieved(torrent, ((GetFileListTaskSuccessResult) result).getFiles());
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void resumeTorrent(Torrent torrent) {
if (currentConnection == null) return;
torrent.mimicResume();
DaemonTaskResult result = ResumeTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_resumed, torrent.getName()));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void pauseTorrent(Torrent torrent) {
torrent.mimicPause();
DaemonTaskResult result = PauseTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_paused, torrent.getName()));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void startTorrent(Torrent torrent, boolean forced) {
torrent.mimicStart();
DaemonTaskResult result = StartTask.create(currentConnection, torrent, forced).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_started, torrent.getName()));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void stopTorrent(Torrent torrent) {
torrent.mimicStop();
DaemonTaskResult result = StopTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_stopped, torrent.getName()));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void removeTorrent(Torrent torrent, boolean withData) {
DaemonTaskResult result = RemoveTask.create(currentConnection, torrent, withData).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
// 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()));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@UiThread
protected void closeActivity(String closeText) {
setResult(RESULT_OK, new Intent().putExtra("torrent_removed", true).putExtra("affected_torrent", torrent));
finish();
if (closeText != null) {
SnackbarManager.show(Snackbar.with(this).text(closeText));
}
}
@Background
@Override
public void updateLabel(Torrent torrent, String newLabel) {
torrent.mimicNewLabel(newLabel);
DaemonTaskResult result = SetLabelTask.create(currentConnection, torrent, newLabel == null ? "" : newLabel).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_labelset, newLabel));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void toggleSequentialDownload(Torrent torrent, boolean sequentialState) {
torrent.mimicSequentialDownload(sequentialState);
String onState = getString(R.string.result_togglesequential_onstate);
String offState = getString(R.string.result_togglesequential_offstate);
String stateString = sequentialState ? onState : offState;
DaemonTaskResult result = ToggleSequentialDownloadTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglesequential, torrent.getName(), stateString));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void toggleFirstLastPieceDownload(Torrent torrent, boolean firstLastPieceState) {
torrent.mimicFirstLastPieceDownload(firstLastPieceState);
String onState = getString(R.string.result_togglefirstlastpiece_onstate);
String offState = getString(R.string.result_togglefirstlastpiece_offstate);
String stateString = firstLastPieceState ? onState : offState;
DaemonTaskResult result = ToggleFirstLastPieceDownloadTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglefirstlastpiece, torrent.getName(), stateString));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void forceRecheckTorrent(Torrent torrent) {
torrent.mimicCheckingStatus();
DaemonTaskResult result = ForceRecheckTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_recheckedstarted, torrent.getName()));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void updateTrackers(Torrent torrent, List<String> newTrackers) {
DaemonTaskResult result = SetTrackersTask.create(currentConnection, torrent, newTrackers).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_trackersupdated));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void updateLocation(Torrent torrent, String newLocation) {
DaemonTaskResult result = SetDownloadLocationTask.create(currentConnection, torrent, newLocation).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_locationset, newLocation));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void updatePriority(Torrent torrent, List<TorrentFile> files, Priority priority) {
DaemonTaskResult result = SetFilePriorityTask.create(currentConnection, torrent, priority, new ArrayList<>(files)).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_priotitiesset));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@UiThread
protected void onTaskSucceeded(DaemonTaskSuccessResult result, String successMessage) {
// 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));
// Refresh the screen as well
refreshTorrent();
refreshTorrentDetails(torrent);
SnackbarManager.show(Snackbar.with(this).text(successMessage).duration(Snackbar.SnackbarDuration.LENGTH_SHORT));
}
@UiThread
protected void onTorrentDetailsRetrieved(Torrent torrent, TorrentDetails torrentDetails) {
// Update the details fragment with the new fine details for the shown torrent
if (fragmentDetails.isResumed())
fragmentDetails.updateTorrentDetails(torrent, torrentDetails);
}
@UiThread
protected void onTorrentFilesRetrieved(Torrent torrent, List<TorrentFile> torrentFiles) {
// Update the details fragment with the newly retrieved list of files
if (fragmentDetails.isResumed())
fragmentDetails.updateTorrentFiles(torrent, new ArrayList<>(torrentFiles));
}
@UiThread
protected void onCommunicationError(DaemonTaskFailureResult result, boolean isCritical) {
log.i(this, result.getException().toString());
String error = getString(LocalTorrent.getResourceForDaemonException(result.getException()));
if (fragmentDetails.isResumed())
fragmentDetails.updateIsLoading(false, isCritical ? error : null);
SnackbarManager.show(Snackbar.with(this).text(getString(LocalTorrent.getResourceForDaemonException(result.getException())))
.colorResource(R.color.red));
}
@UiThread
protected void onTorrentsRetrieved(List<Torrent> torrents, List<org.transdroid.daemon.Label> labels) {
// Update the details fragment accordingly
if (fragmentDetails.isResumed()) {
fragmentDetails.updateIsLoading(false, null);
fragmentDetails.perhapsUpdateTorrent(torrents);
fragmentDetails.updateLabels(Label.convertToNavigationLabels(labels, getResources().getString(R.string.labels_unlabeled)));
}
}
@Extra
@InstanceState
protected Torrent torrent;
@Extra
@InstanceState
protected ArrayList<Label> currentLabels;
// Settings
@Bean
protected Log log;
@Bean
protected NavigationHelper navigationHelper;
@Bean
protected ConnectivityHelper connectivityHelper;
@Bean
protected ApplicationSettings applicationSettings;
// Details view components
@ViewById
protected Toolbar selectionToolbar;
@FragmentById(R.id.torrentdetails_fragment)
protected DetailsFragment fragmentDetails;
private IDaemonAdapter currentConnection = null;
@Override
public void onCreate(Bundle savedInstanceState) {
SettingsUtils.applyDayNightTheme(this);
super.onCreate(savedInstanceState);
}
@AfterViews
protected void init() {
// We require a torrent to be specified; otherwise close the activity
if (torrent == null) {
finish();
return;
}
// Simple action bar with up, torrent name as title and refresh button
setSupportActionBar(selectionToolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(torrent.getName()));
// Connect to the last used server
ServerSetting lastUsed = applicationSettings.getLastUsedServer();
fragmentDetails.setCurrentServerSettings(lastUsed);
currentConnection = lastUsed.createServerAdapter(connectivityHelper.getConnectedNetworkName(), this);
// Show details and load fine stats and torrent files
fragmentDetails.updateTorrent(torrent);
fragmentDetails.updateLabels(currentLabels);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@OptionsItem(android.R.id.home)
protected void navigateUp() {
TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
}
@OptionsItem(R.id.action_refresh)
public void refreshScreen() {
fragmentDetails.updateIsLoading(true, null);
refreshTorrent();
refreshTorrentDetails(torrent);
refreshTorrentFiles(torrent);
}
@Background
protected void refreshTorrent() {
DaemonTaskResult result = RetrieveTask.create(currentConnection).execute(log);
if (result instanceof RetrieveTaskSuccessResult) {
onTorrentsRetrieved(((RetrieveTaskSuccessResult) result).getTorrents(), ((RetrieveTaskSuccessResult) result).getLabels());
} else {
onCommunicationError((DaemonTaskFailureResult) result, true);
}
}
@Background
public void refreshTorrentDetails(Torrent torrent) {
if (currentConnection == null) return;
if (!Daemon.supportsFineDetails(torrent.getDaemon())) {
return;
}
DaemonTaskResult result = GetTorrentDetailsTask.create(currentConnection, torrent).execute(log);
if (result instanceof GetTorrentDetailsTaskSuccessResult) {
onTorrentDetailsRetrieved(torrent, ((GetTorrentDetailsTaskSuccessResult) result).getTorrentDetails());
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
public void refreshTorrentFiles(Torrent torrent) {
if (currentConnection == null) return;
if (!Daemon.supportsFileListing(torrent.getDaemon())) {
return;
}
DaemonTaskResult result = GetFileListTask.create(currentConnection, torrent).execute(log);
if (result instanceof GetFileListTaskSuccessResult) {
onTorrentFilesRetrieved(torrent, ((GetFileListTaskSuccessResult) result).getFiles());
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void resumeTorrent(Torrent torrent) {
if (currentConnection == null) return;
torrent.mimicResume();
DaemonTaskResult result = ResumeTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_resumed, torrent.getName()));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void pauseTorrent(Torrent torrent) {
torrent.mimicPause();
DaemonTaskResult result = PauseTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_paused, torrent.getName()));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void startTorrent(Torrent torrent, boolean forced) {
torrent.mimicStart();
DaemonTaskResult result = StartTask.create(currentConnection, torrent, forced).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_started, torrent.getName()));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void stopTorrent(Torrent torrent) {
torrent.mimicStop();
DaemonTaskResult result = StopTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_stopped, torrent.getName()));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void removeTorrent(Torrent torrent, boolean withData) {
DaemonTaskResult result = RemoveTask.create(currentConnection, torrent, withData).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
// 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()));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@UiThread
protected void closeActivity(String closeText) {
setResult(RESULT_OK, new Intent().putExtra("torrent_removed", true).putExtra("affected_torrent", torrent));
finish();
if (closeText != null) {
SnackbarManager.show(Snackbar.with(this).text(closeText));
}
}
@Background
@Override
public void updateLabel(Torrent torrent, String newLabel) {
torrent.mimicNewLabel(newLabel);
DaemonTaskResult result = SetLabelTask.create(currentConnection, torrent, newLabel == null ? "" : newLabel).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_labelset, newLabel));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void toggleSequentialDownload(Torrent torrent, boolean sequentialState) {
torrent.mimicSequentialDownload(sequentialState);
String onState = getString(R.string.result_togglesequential_onstate);
String offState = getString(R.string.result_togglesequential_offstate);
String stateString = sequentialState ? onState : offState;
DaemonTaskResult result = ToggleSequentialDownloadTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglesequential, torrent.getName(), stateString));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void toggleFirstLastPieceDownload(Torrent torrent, boolean firstLastPieceState) {
torrent.mimicFirstLastPieceDownload(firstLastPieceState);
String onState = getString(R.string.result_togglefirstlastpiece_onstate);
String offState = getString(R.string.result_togglefirstlastpiece_offstate);
String stateString = firstLastPieceState ? onState : offState;
DaemonTaskResult result = ToggleFirstLastPieceDownloadTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglefirstlastpiece, torrent.getName(), stateString));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void forceRecheckTorrent(Torrent torrent) {
torrent.mimicCheckingStatus();
DaemonTaskResult result = ForceRecheckTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_recheckedstarted, torrent.getName()));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void updateTrackers(Torrent torrent, List<String> newTrackers) {
DaemonTaskResult result = SetTrackersTask.create(currentConnection, torrent, newTrackers).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_trackersupdated));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void updateLocation(Torrent torrent, String newLocation) {
DaemonTaskResult result = SetDownloadLocationTask.create(currentConnection, torrent, newLocation).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_locationset, newLocation));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void updatePriority(Torrent torrent, List<TorrentFile> files, Priority priority) {
DaemonTaskResult result = SetFilePriorityTask.create(currentConnection, torrent, priority, new ArrayList<>(files)).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_priotitiesset));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@UiThread
protected void onTaskSucceeded(DaemonTaskSuccessResult result, String successMessage) {
// 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));
// Refresh the screen as well
refreshTorrent();
refreshTorrentDetails(torrent);
SnackbarManager.show(Snackbar.with(this).text(successMessage).duration(Snackbar.SnackbarDuration.LENGTH_SHORT));
}
@UiThread
protected void onTorrentDetailsRetrieved(Torrent torrent, TorrentDetails torrentDetails) {
// Update the details fragment with the new fine details for the shown torrent
if (fragmentDetails.isResumed())
fragmentDetails.updateTorrentDetails(torrent, torrentDetails);
}
@UiThread
protected void onTorrentFilesRetrieved(Torrent torrent, List<TorrentFile> torrentFiles) {
// Update the details fragment with the newly retrieved list of files
if (fragmentDetails.isResumed())
fragmentDetails.updateTorrentFiles(torrent, new ArrayList<>(torrentFiles));
}
@UiThread
protected void onCommunicationError(DaemonTaskFailureResult result, boolean isCritical) {
log.i(this, result.getException().toString());
String error = getString(LocalTorrent.getResourceForDaemonException(result.getException()));
if (fragmentDetails.isResumed())
fragmentDetails.updateIsLoading(false, isCritical ? error : null);
SnackbarManager.show(Snackbar.with(this).text(getString(LocalTorrent.getResourceForDaemonException(result.getException())))
.colorResource(R.color.red));
}
@UiThread
protected void onTorrentsRetrieved(List<Torrent> torrents, List<org.transdroid.daemon.Label> labels) {
// Update the details fragment accordingly
if (fragmentDetails.isResumed()) {
fragmentDetails.updateIsLoading(false, null);
fragmentDetails.perhapsUpdateTorrent(torrents);
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 @@ @@ -16,50 +16,47 @@
*/
package org.transdroid.core.gui;
import java.util.List;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.os.Bundle;
import androidx.fragment.app.DialogFragment;
import org.transdroid.R;
import org.transdroid.core.app.settings.ServerSetting;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import java.util.List;
public class ServerPickerDialog extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
String[] serverNames = getArguments().getStringArray("serverNames");
return new AlertDialog.Builder(getActivity()).setTitle(R.string.navigation_pickserver)
.setItems(serverNames, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (getActivity() != null && getActivity() instanceof TorrentsActivity)
((TorrentsActivity) getActivity()).switchServerAndAddFromIntent(which);
}
}).create();
}
/**
* Opens a dialog that allows the selection of a configured server (manual or seedbox). The calling activity will
* receive a callback on its switchServerAndAddFromIntent(int) method.
*
* @param activity The torrents activity from which the picker is started (and which received the callback)
* @param serverSettings The list of all available servers, of which their names will be offered to the user to pick
* from (and its position in the list is returned to the activity)
*/
public static void startServerPicker(final TorrentsActivity activity, List<ServerSetting> serverSettings) {
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.getSupportFragmentManager(), "serverpicker");
}
/**
* Opens a dialog that allows the selection of a configured server (manual or seedbox). The calling activity will
* receive a callback on its switchServerAndAddFromIntent(int) method.
* @param activity The torrents activity from which the picker is started (and which received the callback)
* @param serverSettings The list of all available servers, of which their names will be offered to the user to pick
* from (and its position in the list is returned to the activity)
*/
public static void startServerPicker(final TorrentsActivity activity, List<ServerSetting> serverSettings) {
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");
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
String[] serverNames = getArguments().getStringArray("serverNames");
return new AlertDialog.Builder(getActivity()).setTitle(R.string.navigation_pickserver)
.setItems(serverNames, (dialog, which) -> {
if (getActivity() != null && getActivity() instanceof TorrentsActivity)
((TorrentsActivity) getActivity()).switchServerAndAddFromIntent(which);
}).create();
}
}

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

@ -29,31 +29,33 @@ import org.transdroid.daemon.IDaemonAdapter; @@ -29,31 +29,33 @@ import org.transdroid.daemon.IDaemonAdapter;
@EViewGroup(R.layout.actionbar_serverselection)
public class ServerSelectionView extends RelativeLayout {
@ViewById
protected TextView filterText, serverText;
public ServerSelectionView(Context context) {
super(context);
}
public ServerSelectionView(TorrentsActivity activity) {
super(activity.torrentsToolbar.getContext());
}
/**
* Updates the name of the current connected server.
* @param currentServer The server currently connected to
*/
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
*/
public void updateCurrentFilter(NavigationFilter currentFilter) {
filterText.setText(currentFilter.getName());
}
@ViewById
protected TextView filterText, serverText;
public ServerSelectionView(Context context) {
super(context);
}
public ServerSelectionView(TorrentsActivity activity) {
super(activity.torrentsToolbar.getContext());
}
/**
* Updates the name of the current connected server.
*
* @param currentServer The server currently connected to
*/
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
*/
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; @@ -37,87 +37,84 @@ import java.util.List;
@EViewGroup(R.layout.actionbar_serverstatus)
public class ServerStatusView extends RelativeLayout implements OnRatesPickedListener {
@ViewById
protected TextView downcountText, upcountText, downcountSign, upcountSign, downspeedText, upspeedText;
@ViewById
protected View speedswrapperLayout;
private TorrentsActivity activity;
public ServerStatusView(Context context) {
super(context);
}
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
* @param supportsSetTransferRates Whether the connected torrent client supports setting of max transfer speeds
*/
public void updateStatus(List<Torrent> torrents, boolean dormantAsInactive, boolean supportsSetTransferRates) {
if (torrents == null) {
downcountText.setText(null);
upcountText.setText(null);
downspeedText.setText(null);
upspeedText.setText(null);
downcountSign.setVisibility(View.INVISIBLE);
upcountSign.setVisibility(View.INVISIBLE);
speedswrapperLayout.setOnClickListener(null);
return;
}
int downcount = 0, upcount = 0, downspeed = 0, upspeed = 0;
for (Torrent torrent : torrents) {
// Downloading torrents count towards downloads and uploads, seeding torrents towards uploads
if (torrent.isDownloading(dormantAsInactive)) {
downcount++;
upcount++;
} else if (torrent.isSeeding(dormantAsInactive)) {
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");
downcountSign.setVisibility(View.VISIBLE);
upcountSign.setVisibility(View.VISIBLE);
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 resetRates() {
activity.updateMaxSpeeds(null, null);
}
@Override
public void onInvalidNumber() {
SnackbarManager.show(Snackbar.with(activity).text(R.string.error_notanumber).colorResource(R.color.red));
}
@ViewById
protected TextView downcountText, upcountText, downcountSign, upcountSign, downspeedText, upspeedText;
@ViewById
protected View speedswrapperLayout;
private TorrentsActivity activity;
private OnClickListener onStartDownPickerClicked = v ->
SetTransferRatesDialog.show(getContext(), ServerStatusView.this);
public ServerStatusView(Context context) {
super(context);
}
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
* @param supportsSetTransferRates Whether the connected torrent client supports setting of max transfer speeds
*/
public void updateStatus(List<Torrent> torrents, boolean dormantAsInactive, boolean supportsSetTransferRates) {
if (torrents == null) {
downcountText.setText(null);
upcountText.setText(null);
downspeedText.setText(null);
upspeedText.setText(null);
downcountSign.setVisibility(View.INVISIBLE);
upcountSign.setVisibility(View.INVISIBLE);
speedswrapperLayout.setOnClickListener(null);
return;
}
int downcount = 0, upcount = 0, downspeed = 0, upspeed = 0;
for (Torrent torrent : torrents) {
// Downloading torrents count towards downloads and uploads, seeding torrents towards uploads
if (torrent.isDownloading(dormantAsInactive)) {
downcount++;
upcount++;
} else if (torrent.isSeeding(dormantAsInactive)) {
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");
downcountSign.setVisibility(View.VISIBLE);
upcountSign.setVisibility(View.VISIBLE);
if (supportsSetTransferRates)
speedswrapperLayout.setOnClickListener(onStartDownPickerClicked);
else
speedswrapperLayout.setBackgroundDrawable(null);
}
@Override
public void onRatesPicked(int maxDownloadSpeed, int maxUploadSpeed) {
activity.updateMaxSpeeds(maxDownloadSpeed, maxUploadSpeed);
}
@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));
}
}

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

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

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

@ -17,11 +17,10 @@ @@ -17,11 +17,10 @@
package org.transdroid.core.gui;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.evernote.android.job.JobConfig;
import com.evernote.android.job.JobManager;
import com.evernote.android.job.util.JobLogger;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EApplication;
import org.transdroid.core.gui.log.Log;
@ -30,21 +29,16 @@ import org.transdroid.core.service.ScheduledJobCreator; @@ -30,21 +29,16 @@ import org.transdroid.core.service.ScheduledJobCreator;
@EApplication
public class TransdroidApp extends Application {
@Bean
protected Log log;
@Override
public void onCreate() {
super.onCreate();
// Configure Android-Job
JobConfig.addLogger(new JobLogger() {
@Override
public void log(int priority, @NonNull String tag, @NonNull String message, @Nullable Throwable t) {
log.d(tag, message);
}
});
JobManager.create(this).addJobCreator(new ScheduledJobCreator());
}
@Bean
protected Log log;
@Override
public void onCreate() {
super.onCreate();
// Configure Android-Job
JobConfig.addLogger((priority, tag, message, 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 @@ @@ -16,215 +16,220 @@
*/
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.text.util.Linkify;
import android.view.View;
import android.view.ViewGroup;
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.
*
* @author Eric Kok
*/
public class DetailsAdapter extends MergeAdapter {
private ViewHolderAdapter torrentDetailsViewAdapter = null;
private TorrentDetailsView torrentDetailsView = null;
private ViewHolderAdapter piecesSeparatorAdapter = null;
private ViewHolderAdapter piecesMapViewAdapter = null;
private PiecesMapView piecesMapView = null;
private ViewHolderAdapter trackersSeparatorAdapter = null;
private SimpleListItemAdapter trackersAdapter = null;
private ViewHolderAdapter errorsSeparatorAdapter = null;
private SimpleListItemAdapter errorsAdapter = null;
private ViewHolderAdapter torrentFilesSeparatorAdapter = null;
private TorrentFilesAdapter torrentFilesAdapter = null;
public DetailsAdapter(Context context) {
// Immediately bind the adapters, or the MergeAdapter will not be able to determine the view types and instead
// display nothing at all
// Torrent details header
torrentDetailsView = TorrentDetailsView_.build(context);
torrentDetailsViewAdapter = new ViewHolderAdapter(torrentDetailsView);
torrentDetailsViewAdapter.setViewEnabled(false);
torrentDetailsViewAdapter.setViewVisibility(View.GONE);
addAdapter(torrentDetailsViewAdapter);
// Pieces map
piecesSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
context.getString(R.string.status_pieces)));
piecesSeparatorAdapter.setViewEnabled(false);
piecesSeparatorAdapter.setViewVisibility(View.GONE);
addAdapter(piecesSeparatorAdapter);
piecesMapView = new PiecesMapView(context);
piecesMapViewAdapter = new ViewHolderAdapter(piecesMapView);
piecesMapViewAdapter.setViewEnabled(false);
piecesMapViewAdapter.setViewVisibility(View.GONE);
addAdapter(piecesMapViewAdapter);
// Tracker errors
errorsSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
context.getString(R.string.status_errors)));
errorsSeparatorAdapter.setViewEnabled(false);
errorsSeparatorAdapter.setViewVisibility(View.GONE);
addAdapter(errorsSeparatorAdapter);
this.errorsAdapter = new SimpleListItemAdapter(context, new ArrayList<SimpleListItem>());
this.errorsAdapter.setAutoLinkMask(Linkify.WEB_URLS);
addAdapter(errorsAdapter);
// Trackers
trackersSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
context.getString(R.string.status_trackers)));
trackersSeparatorAdapter.setViewEnabled(false);
trackersSeparatorAdapter.setViewVisibility(View.GONE);
addAdapter(trackersSeparatorAdapter);
this.trackersAdapter = new SimpleListItemAdapter(context, new ArrayList<SimpleListItem>());
addAdapter(trackersAdapter);
// Torrent files
torrentFilesSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
context.getString(R.string.status_files)));
torrentFilesSeparatorAdapter.setViewEnabled(false);
torrentFilesSeparatorAdapter.setViewVisibility(View.GONE);
addAdapter(torrentFilesSeparatorAdapter);
this.torrentFilesAdapter = new TorrentFilesAdapter(context, new ArrayList<TorrentFile>());
addAdapter(torrentFilesAdapter);
}
/**
* Update the torrent data in the details header of this merge adapter
* @param torrent The torrent for which detailed data is shown
*/
public void updateTorrent(Torrent torrent) {
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
*/
public void updateTorrentFiles(List<TorrentFile> torrentFiles) {
if (torrentFiles == null) {
torrentFilesAdapter.update(new ArrayList<TorrentFile>());
torrentFilesSeparatorAdapter.setViewVisibility(View.GONE);
} 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
*/
public void updateTrackers(List<? extends SimpleListItem> trackers) {
if (trackers == null || trackers.isEmpty()) {
trackersAdapter.update(new ArrayList<SimpleListItemAdapter.SimpleStringItem>());
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) {
if (errors == null || errors.isEmpty()) {
errorsAdapter.update(new ArrayList<SimpleListItemAdapter.SimpleStringItem>());
errorsSeparatorAdapter.setViewVisibility(View.GONE);
} else {
errorsAdapter.update(errors);
errorsSeparatorAdapter.setViewVisibility(View.VISIBLE);
}
}
public void updatePieces(List<Integer> pieces) {
if (pieces == null || pieces.isEmpty()) {
piecesSeparatorAdapter.setViewEnabled(false);
piecesSeparatorAdapter.setViewVisibility(View.GONE);
piecesMapViewAdapter.setViewEnabled(false);
piecesMapViewAdapter.setViewVisibility(View.GONE);
} else {
piecesMapView.setPieces(pieces);
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);
updateErrors(null);
updateTrackers(null);
}
protected static class TorrentFilesAdapter extends BaseAdapter {
private final Context context;
private List<TorrentFile> items;
public TorrentFilesAdapter(Context context, List<TorrentFile> items) {
this.context = context;
this.items = items;
}
/**
* Allows updating of the full data list underlying this adapter, replacing all items
* @param newItems The new list of files to display
*/
public void update(List<TorrentFile> newItems) {
this.items = newItems;
notifyDataSetChanged();
}
@Override
public int getCount() {
return items.size();
}
@Override
public TorrentFile getItem(int position) {
return items.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TorrentFileView torrentFileView;
if (convertView == null) {
torrentFileView = TorrentFileView_.build(context);
} else {
torrentFileView = (TorrentFileView) convertView;
}
torrentFileView.bind(getItem(position));
return torrentFileView;
}
}
private ViewHolderAdapter torrentDetailsViewAdapter = null;
private TorrentDetailsView torrentDetailsView = null;
private ViewHolderAdapter piecesSeparatorAdapter = null;
private ViewHolderAdapter piecesMapViewAdapter = null;
private PiecesMapView piecesMapView = null;
private ViewHolderAdapter trackersSeparatorAdapter = null;
private SimpleListItemAdapter trackersAdapter = null;
private ViewHolderAdapter errorsSeparatorAdapter = null;
private SimpleListItemAdapter errorsAdapter = null;
private ViewHolderAdapter torrentFilesSeparatorAdapter = null;
private TorrentFilesAdapter torrentFilesAdapter = null;
public DetailsAdapter(Context context) {
// Immediately bind the adapters, or the MergeAdapter will not be able to determine the view types and instead
// display nothing at all
// Torrent details header
torrentDetailsView = TorrentDetailsView_.build(context);
torrentDetailsViewAdapter = new ViewHolderAdapter(torrentDetailsView);
torrentDetailsViewAdapter.setViewEnabled(false);
torrentDetailsViewAdapter.setViewVisibility(View.GONE);
addAdapter(torrentDetailsViewAdapter);
// Pieces map
piecesSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
context.getString(R.string.status_pieces)));
piecesSeparatorAdapter.setViewEnabled(false);
piecesSeparatorAdapter.setViewVisibility(View.GONE);
addAdapter(piecesSeparatorAdapter);
piecesMapView = new PiecesMapView(context);
piecesMapViewAdapter = new ViewHolderAdapter(piecesMapView);
piecesMapViewAdapter.setViewEnabled(false);
piecesMapViewAdapter.setViewVisibility(View.GONE);
addAdapter(piecesMapViewAdapter);
// Tracker errors
errorsSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
context.getString(R.string.status_errors)));
errorsSeparatorAdapter.setViewEnabled(false);
errorsSeparatorAdapter.setViewVisibility(View.GONE);
addAdapter(errorsSeparatorAdapter);
this.errorsAdapter = new SimpleListItemAdapter(context, new ArrayList<>());
this.errorsAdapter.setAutoLinkMask(Linkify.WEB_URLS);
addAdapter(errorsAdapter);
// Trackers
trackersSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
context.getString(R.string.status_trackers)));
trackersSeparatorAdapter.setViewEnabled(false);
trackersSeparatorAdapter.setViewVisibility(View.GONE);
addAdapter(trackersSeparatorAdapter);
this.trackersAdapter = new SimpleListItemAdapter(context, new ArrayList<>());
addAdapter(trackersAdapter);
// Torrent files
torrentFilesSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
context.getString(R.string.status_files)));
torrentFilesSeparatorAdapter.setViewEnabled(false);
torrentFilesSeparatorAdapter.setViewVisibility(View.GONE);
addAdapter(torrentFilesSeparatorAdapter);
this.torrentFilesAdapter = new TorrentFilesAdapter(context, new ArrayList<>());
addAdapter(torrentFilesAdapter);
}
/**
* Update the torrent data in the details header of this merge adapter
*
* @param torrent The torrent for which detailed data is shown
*/
public void updateTorrent(Torrent torrent) {
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
*/
public void updateTorrentFiles(List<TorrentFile> torrentFiles) {
if (torrentFiles == null) {
torrentFilesAdapter.update(new ArrayList<>());
torrentFilesSeparatorAdapter.setViewVisibility(View.GONE);
} 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
*/
public void updateTrackers(List<? extends SimpleListItem> trackers) {
if (trackers == null || trackers.isEmpty()) {
trackersAdapter.update(new ArrayList<SimpleListItemAdapter.SimpleStringItem>());
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) {
if (errors == null || errors.isEmpty()) {
errorsAdapter.update(new ArrayList<SimpleListItemAdapter.SimpleStringItem>());
errorsSeparatorAdapter.setViewVisibility(View.GONE);
} else {
errorsAdapter.update(errors);
errorsSeparatorAdapter.setViewVisibility(View.VISIBLE);
}
}
public void updatePieces(List<Integer> pieces) {
if (pieces == null || pieces.isEmpty()) {
piecesSeparatorAdapter.setViewEnabled(false);
piecesSeparatorAdapter.setViewVisibility(View.GONE);
piecesMapViewAdapter.setViewEnabled(false);
piecesMapViewAdapter.setViewVisibility(View.GONE);
} else {
piecesMapView.setPieces(pieces);
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);
updateErrors(null);
updateTrackers(null);
}
protected static class TorrentFilesAdapter extends BaseAdapter {
private final Context context;
private List<TorrentFile> items;
public TorrentFilesAdapter(Context context, List<TorrentFile> items) {
this.context = context;
this.items = items;
}
/**
* Allows updating of the full data list underlying this adapter, replacing all items
*
* @param newItems The new list of files to display
*/
public void update(List<TorrentFile> newItems) {
this.items = newItems;
notifyDataSetChanged();
}
@Override
public int getCount() {
return items.size();
}
@Override
public TorrentFile getItem(int position) {
return items.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TorrentFileView torrentFileView;
if (convertView == null) {
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 @@ @@ -16,7 +16,7 @@
*/
package org.transdroid.core.gui.lists;
import java.util.Locale;
import android.content.res.Resources;
import org.transdroid.R;
import org.transdroid.daemon.DaemonException;
@ -25,227 +25,233 @@ import org.transdroid.daemon.TorrentStatus; @@ -25,227 +25,233 @@ import org.transdroid.daemon.TorrentStatus;
import org.transdroid.daemon.util.FileSizeConverter;
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
* torrent statistics.
*
* @author Eric Kok
*/
public class LocalTorrent {
/**
* Creates the LocalTorrent object so that the translatable/formattable version of a Torrent can be used.
* @param torrent The Torrent object
* @return The torrent wrapped as LocalTorrent object
*/
public static LocalTorrent fromTorrent(Torrent torrent) {
return new LocalTorrent(torrent);
}
private final Torrent t;
private LocalTorrent(Torrent torrent) {
this.t = torrent;
}
private static final String DECIMAL_FORMATTER = "%.1f";
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
* size; so if you created the torrent yourself you will have downloaded 0 bytes, but the ratio will pretend you
* have 100%.
* @return A nicely formatted string containing the upload/download seed ratio
*/
public String getRatioString() {
long baseSize = t.getTotalSize();
if (t.getStatusCode() == TorrentStatus.Downloading) {
baseSize = t.getDownloadedEver();
}
if (baseSize <= 0) {
return String.format(Locale.getDefault(), DECIMAL_FORMATTER_2, 0d);
} else if (t.getRatio() == Double.POSITIVE_INFINITY) {
return "\u221E";
} else {
return String.format(Locale.getDefault(), DECIMAL_FORMATTER_2, t.getRatio());
}
}
/**
* Returns a formatted string indicating the current progress in terms of transferred bytes
* @param r The context resources, to access translations
* @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) {
switch (t.getStatusCode()) {
case Waiting:
case Error:
// Not downloading yet
return r.getString(R.string.status_waitingtodl, FileSizeConverter.getSize(t.getTotalSize()));
case Checking:
return r.getString(R.string.status_checking);
case Downloading:
// Downloading
return r.getString(
R.string.status_size1,
FileSizeConverter.getSize(t.getDownloadedEver()),
FileSizeConverter.getSize(t.getTotalSize()),
String.format(DECIMAL_FORMATTER, t.getDownloadedPercentage() * 100)
+ "%"
+ (!withAvailability ? "" : "/"
+ String.format(DECIMAL_FORMATTER, t.getAvailability() * 100) + "%"));
case Seeding:
case Paused:
case Queued:
// 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 either the expected time to download (ETA) or, when seeding, the ratio
* @param r The context resources, to access translations
* @return A string like '~ 34 seconds', or 'RATIO 8.2' or an empty string
*/
public String getProgressEtaRatioText(Resources r) {
switch (t.getStatusCode()) {
case Downloading:
// Downloading
return getRemainingTimeString(r, true, false);
case Seeding:
case Paused:
case Queued:
// Seeding or paused
return r.getString(R.string.status_ratio, getRatioString());
case Waiting:
case Checking:
case Error:
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) {
switch (t.getStatusCode()) {
case Waiting:
return r.getString(R.string.status_waiting);
case Checking:
return r.getString(R.string.status_checking);
case Downloading:
return r.getString(R.string.status_seeders, t.getSeedersConnected(), t.getSeedersKnown());
case Seeding:
return r.getString(R.string.status_leechers, t.getLeechersConnected(), t.getLeechersKnown());
case Paused:
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);
default:
return r.getString(R.string.status_unknown);
}
}
/**
* Returns a formatted string indicating current transfer speeds for the torrent
* @param r The context resources, to access translations
* @return A string like ' 28KB/s 1.8MB/s', or an empty string when not transferrring
*/
public String getProgressSpeedText(Resources r) {
switch (t.getStatusCode()) {
case Waiting:
case Checking:
case Paused:
case Queued:
return "";
case Downloading:
return r.getString(R.string.status_speed_down, FileSizeConverter.getSize(t.getRateDownload()) + "/s") + " "
+ r.getString(R.string.status_speed_up, FileSizeConverter.getSize(t.getRateUpload()) + "/s");
case Seeding:
return r.getString(R.string.status_speed_up, FileSizeConverter.getSize(t.getRateUpload()) + "/s");
default:
return "";
}
}
public String getProgressStatusEta(Resources r) {
switch (t.getStatusCode()) {
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());
case Downloading:
// Downloading
return r.getString(R.string.status_downloading).toUpperCase(Locale.getDefault()) + " ("
+ String.format(DECIMAL_FORMATTER, t.getDownloadedPercentage() * 100) + "%), "
+ getRemainingTimeString(r, false, true);
case Seeding:
return r.getString(R.string.status_seeding).toUpperCase(Locale.getDefault());
case Paused:
return r.getString(R.string.status_paused).toUpperCase(Locale.getDefault());
case Queued:
return r.getString(R.string.status_queued).toUpperCase(Locale.getDefault());
default:
return r.getString(R.string.status_unknown).toUpperCase(Locale.getDefault());
}
}
/**
* Returns a formatted string indicating the remaining download time
* @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) {
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));
}
/**
* Convert a DaemonException to a translatable human-readable error message
* @param e The exception that was thrown by the server
* @return A string resource ID to show to the user
*/
public static int getResourceForDaemonException(DaemonException e) {
switch (e.getType()) {
case MethodUnsupported:
return R.string.error_unsupported;
case ConnectionError:
return R.string.error_httperror;
case UnexpectedResponse:
return R.string.error_jsonresponseerror;
case ParsingFailed:
return R.string.error_jsonrequesterror;
case NotConnected:
return R.string.error_daemonnotconnected;
case AuthenticationFailure:
return R.string.error_401;
case FileAccessError:
return R.string.error_torrentfile;
default:
return R.string.error_httperror;
}
}
private static final String DECIMAL_FORMATTER = "%.1f";
private static final String DECIMAL_FORMATTER_2 = "%.2f";
private final Torrent t;
private LocalTorrent(Torrent torrent) {
this.t = torrent;
}
/**
* Creates the LocalTorrent object so that the translatable/formattable version of a Torrent can be used.
*
* @param torrent The Torrent object
* @return The torrent wrapped as LocalTorrent object
*/
public static LocalTorrent fromTorrent(Torrent torrent) {
return new LocalTorrent(torrent);
}
/**
* Convert a DaemonException to a translatable human-readable error message
*
* @param e The exception that was thrown by the server
* @return A string resource ID to show to the user
*/
public static int getResourceForDaemonException(DaemonException e) {
switch (e.getType()) {
case MethodUnsupported:
return R.string.error_unsupported;
case UnexpectedResponse:
return R.string.error_jsonresponseerror;
case ParsingFailed:
return R.string.error_jsonrequesterror;
case NotConnected:
return R.string.error_daemonnotconnected;
case AuthenticationFailure:
return R.string.error_401;
case FileAccessError:
return R.string.error_torrentfile;
case ConnectionError:
default:
return R.string.error_httperror;
}
}
/**
* Builds a string showing the upload/download seed ratio. If not downloading, it will base the ratio on the total
* size; so if you created the torrent yourself you will have downloaded 0 bytes, but the ratio will pretend you
* have 100%.
*
* @return A nicely formatted string containing the upload/download seed ratio
*/
public String getRatioString() {
long baseSize = t.getTotalSize();
if (t.getStatusCode() == TorrentStatus.Downloading) {
baseSize = t.getDownloadedEver();
}
if (baseSize <= 0) {
return String.format(Locale.getDefault(), DECIMAL_FORMATTER_2, 0d);
} else if (t.getRatio() == Double.POSITIVE_INFINITY) {
return "\u221E";
} else {
return String.format(Locale.getDefault(), DECIMAL_FORMATTER_2, t.getRatio());
}
}
/**
* Returns a formatted string indicating the current progress in terms of transferred bytes
*
* @param r The context resources, to access translations
* @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) {
switch (t.getStatusCode()) {
case Waiting:
case Error:
// Not downloading yet
return r.getString(R.string.status_waitingtodl, FileSizeConverter.getSize(t.getTotalSize()));
case Checking:
return r.getString(R.string.status_checking);
case Downloading:
// Downloading
return r.getString(
R.string.status_size1,
FileSizeConverter.getSize(t.getDownloadedEver()),
FileSizeConverter.getSize(t.getTotalSize()),
String.format(DECIMAL_FORMATTER, t.getDownloadedPercentage() * 100)
+ "%"
+ (!withAvailability ? "" : "/"
+ String.format(DECIMAL_FORMATTER, t.getAvailability() * 100) + "%"));
case Seeding:
case Paused:
case Queued:
// 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 either the expected time to download (ETA) or, when seeding, the ratio
*
* @param r The context resources, to access translations
* @return A string like '~ 34 seconds', or 'RATIO 8.2' or an empty string
*/
public String getProgressEtaRatioText(Resources r) {
switch (t.getStatusCode()) {
case Downloading:
// Downloading
return getRemainingTimeString(r, true, false);
case Seeding:
case Paused:
case Queued:
// Seeding or paused
return r.getString(R.string.status_ratio, getRatioString());
case Waiting:
case Checking:
case Error:
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) {
switch (t.getStatusCode()) {
case Waiting:
return r.getString(R.string.status_waiting);
case Checking:
return r.getString(R.string.status_checking);
case Downloading:
return r.getString(R.string.status_seeders, t.getSeedersConnected(), t.getSeedersKnown());
case Seeding:
return r.getString(R.string.status_leechers, t.getLeechersConnected(), t.getLeechersKnown());
case Paused:
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);
default:
return r.getString(R.string.status_unknown);
}
}
/**
* Returns a formatted string indicating current transfer speeds for the torrent
*
* @param r The context resources, to access translations
* @return A string like ' 28KB/s 1.8MB/s', or an empty string when not transferrring
*/
public String getProgressSpeedText(Resources r) {
switch (t.getStatusCode()) {
case Downloading:
return r.getString(R.string.status_speed_down, FileSizeConverter.getSize(t.getRateDownload()) + "/s") + " "
+ r.getString(R.string.status_speed_up, FileSizeConverter.getSize(t.getRateUpload()) + "/s");
case Seeding:
return r.getString(R.string.status_speed_up, FileSizeConverter.getSize(t.getRateUpload()) + "/s");
case Waiting:
case Checking:
case Paused:
case Queued:
default:
return "";
}
}
public String getProgressStatusEta(Resources r) {
switch (t.getStatusCode()) {
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());
case Downloading:
// Downloading
return r.getString(R.string.status_downloading).toUpperCase(Locale.getDefault()) + " ("
+ String.format(DECIMAL_FORMATTER, t.getDownloadedPercentage() * 100) + "%), "
+ getRemainingTimeString(r, false, true);
case Seeding:
return r.getString(R.string.status_seeding).toUpperCase(Locale.getDefault());
case Paused:
return r.getString(R.string.status_paused).toUpperCase(Locale.getDefault());
case Queued:
return r.getString(R.string.status_queued).toUpperCase(Locale.getDefault());
default:
return r.getString(R.string.status_unknown).toUpperCase(Locale.getDefault());
}
}
/**
* Returns a formatted string indicating the remaining download time
*
* @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) {
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 @@ @@ -16,8 +16,6 @@
*/
package org.transdroid.core.gui.lists;
import java.util.ArrayList;
import android.database.DataSetObserver;
import android.view.View;
import android.view.ViewGroup;
@ -27,288 +25,297 @@ import android.widget.ListAdapter; @@ -27,288 +25,297 @@ import android.widget.ListAdapter;
import android.widget.SectionIndexer;
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
* http://stackoverflow.com/questions/7964259/android-attaching-multiple-adapters-to-one-adapter and based on the Apache
* 2-licensed CommonsWare MergeAdapter.
*
* @author Eric Kok
* @author Alex Amiryan
* @author Mark Murphy
*/
public class MergeAdapter extends BaseAdapter implements SectionIndexer {
protected ArrayList<ListAdapter> pieces = new ArrayList<ListAdapter>();
protected String noItemsText;
/**
* Stock constructor, simply chaining to the superclass.
*/
public MergeAdapter() {
super();
}
/**
* Adds a new adapter to the roster of things to appear in the aggregate list.
* @param adapter Source for row views for this section
*/
public void addAdapter(ListAdapter adapter) {
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
*/
public Object getItem(int position) {
for (ListAdapter piece : pieces) {
int size = piece.getCount();
if (position < size) {
return (piece.getItem(position));
}
position -= size;
}
return (null);
}
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
*/
public ListAdapter getAdapter(int position) {
for (ListAdapter piece : pieces) {
int size = piece.getCount();
if (position < size) {
return (piece);
}
position -= size;
}
return (null);
}
/**
* How many items are in the data set represented by this {@link Adapter}.
*/
public int getCount() {
int total = 0;
for (ListAdapter piece : pieces) {
total += piece.getCount();
}
if (total == 0 && noItemsText != null) {
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() {
int total = 0;
for (ListAdapter piece : pieces) {
total += piece.getViewTypeCount();
}
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
public int getItemViewType(int position) {
int typeOffset = 0;
int result = -1;
for (ListAdapter piece : pieces) {
int size = piece.getCount();
if (position < size) {
result = typeOffset + piece.getItemViewType(position);
break;
}
position -= size;
typeOffset += piece.getViewTypeCount();
}
return (result);
}
/**
* Are all items in this {@link ListAdapter} enabled? If yes it means all items are selectable and clickable.
*/
@Override
public boolean areAllItemsEnabled() {
return (false);
}
/**
* Returns true if the item at the specified position is not a separator.
* @param position Position of the item whose data we want
*/
@Override
public boolean isEnabled(int position) {
for (ListAdapter piece : pieces) {
int size = piece.getCount();
if (position < size) {
return (piece.isEnabled(position));
}
position -= size;
}
return (false);
}
/**
* 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
* @param convertView View to recycle, if not null
* @param parent ViewGroup containing the returned View
*/
public View getView(int position, View convertView, ViewGroup parent) {
for (ListAdapter piece : pieces) {
int size = piece.getCount();
if (position < size) {
return (piece.getView(position, convertView, parent));
}
position -= size;
}
if (noItemsText != null) {
TextView text = new TextView(parent.getContext());
text.setText(noItemsText);
return text;
}
return (null);
}
/**
* Get the row id associated with the specified position in the list.
* @param position Position of the item whose data we want
*/
public long getItemId(int position) {
for (ListAdapter piece : pieces) {
int size = piece.getCount();
if (position < size) {
return (piece.getItemId(position));
}
position -= size;
}
return (-1);
}
public final int getPositionForSection(int section) {
int position = 0;
for (ListAdapter piece : pieces) {
if (piece instanceof SectionIndexer) {
Object[] sections = ((SectionIndexer) piece).getSections();
int numSections = 0;
if (sections != null) {
numSections = sections.length;
}
if (section < numSections) {
return (position + ((SectionIndexer) piece).getPositionForSection(section));
} else if (sections != null) {
section -= numSections;
}
}
position += piece.getCount();
}
return (0);
}
public final int getSectionForPosition(int position) {
int section = 0;
for (ListAdapter piece : pieces) {
int size = piece.getCount();
if (position < size) {
if (piece instanceof SectionIndexer) {
return (section + ((SectionIndexer) piece).getSectionForPosition(position));
}
return (0);
} else {
if (piece instanceof SectionIndexer) {
Object[] sections = ((SectionIndexer) piece).getSections();
if (sections != null) {
section += sections.length;
}
}
}
position -= size;
}
return (0);
}
public final Object[] getSections() {
ArrayList<Object> sections = new ArrayList<Object>();
for (ListAdapter piece : pieces) {
if (piece instanceof SectionIndexer) {
Object[] curSections = ((SectionIndexer) piece).getSections();
if (curSections != null) {
for (Object section : curSections) {
sections.add(section);
}
}
}
}
if (sections.size() == 0) {
return (null);
}
return (sections.toArray(new Object[0]));
}
private class CascadeDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
notifyDataSetChanged();
}
@Override
public void onInvalidated() {
notifyDataSetInvalidated();
}
}
protected ArrayList<ListAdapter> pieces = new ArrayList<>();
protected String noItemsText;
/**
* Stock constructor, simply chaining to the superclass.
*/
public MergeAdapter() {
super();
}
/**
* Adds a new adapter to the roster of things to appear in the aggregate list.
*
* @param adapter Source for row views for this section
*/
public void addAdapter(ListAdapter adapter) {
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
*/
public Object getItem(int position) {
for (ListAdapter piece : pieces) {
int size = piece.getCount();
if (position < size) {
return (piece.getItem(position));
}
position -= size;
}
return (null);
}
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
*/
public ListAdapter getAdapter(int position) {
for (ListAdapter piece : pieces) {
int size = piece.getCount();
if (position < size) {
return (piece);
}
position -= size;
}
return (null);
}
/**
* How many items are in the data set represented by this {@link Adapter}.
*/
public int getCount() {
int total = 0;
for (ListAdapter piece : pieces) {
total += piece.getCount();
}
if (total == 0 && noItemsText != null) {
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() {
int total = 0;
for (ListAdapter piece : pieces) {
total += piece.getViewTypeCount();
}
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
public int getItemViewType(int position) {
int typeOffset = 0;
int result = -1;
for (ListAdapter piece : pieces) {
int size = piece.getCount();
if (position < size) {
result = typeOffset + piece.getItemViewType(position);
break;
}
position -= size;
typeOffset += piece.getViewTypeCount();
}
return (result);
}
/**
* Are all items in this {@link ListAdapter} enabled? If yes it means all items are selectable and clickable.
*/
@Override
public boolean areAllItemsEnabled() {
return (false);
}
/**
* Returns true if the item at the specified position is not a separator.
*
* @param position Position of the item whose data we want
*/
@Override
public boolean isEnabled(int position) {
for (ListAdapter piece : pieces) {
int size = piece.getCount();
if (position < size) {
return (piece.isEnabled(position));
}
position -= size;
}
return (false);
}
/**
* 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
* @param convertView View to recycle, if not null
* @param parent ViewGroup containing the returned View
*/
public View getView(int position, View convertView, ViewGroup parent) {
for (ListAdapter piece : pieces) {
int size = piece.getCount();
if (position < size) {
return (piece.getView(position, convertView, parent));
}
position -= size;
}
if (noItemsText != null) {
TextView text = new TextView(parent.getContext());
text.setText(noItemsText);
return text;
}
return (null);
}
/**
* Get the row id associated with the specified position in the list.
*
* @param position Position of the item whose data we want
*/
public long getItemId(int position) {
for (ListAdapter piece : pieces) {
int size = piece.getCount();
if (position < size) {
return (piece.getItemId(position));
}
position -= size;
}
return (-1);
}
public final int getPositionForSection(int section) {
int position = 0;
for (ListAdapter piece : pieces) {
if (piece instanceof SectionIndexer) {
Object[] sections = ((SectionIndexer) piece).getSections();
int numSections = 0;
if (sections != null) {
numSections = sections.length;
}
if (section < numSections) {
return (position + ((SectionIndexer) piece).getPositionForSection(section));
} else if (sections != null) {
section -= numSections;
}
}
position += piece.getCount();
}
return (0);
}
public final int getSectionForPosition(int position) {
int section = 0;
for (ListAdapter piece : pieces) {
int size = piece.getCount();
if (position < size) {
if (piece instanceof SectionIndexer) {
return (section + ((SectionIndexer) piece).getSectionForPosition(position));
}
return (0);
} else {
if (piece instanceof SectionIndexer) {
Object[] sections = ((SectionIndexer) piece).getSections();
if (sections != null) {
section += sections.length;
}
}
}
position -= size;
}
return (0);
}
public final Object[] getSections() {
ArrayList<Object> sections = new ArrayList<>();
for (ListAdapter piece : pieces) {
if (piece instanceof SectionIndexer) {
Object[] curSections = ((SectionIndexer) piece).getSections();
if (curSections != null) {
sections.addAll(Arrays.asList(curSections));
}
}
}
if (sections.size() == 0) {
return (null);
}
return (sections.toArray(new Object[0]));
}
private class CascadeDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
notifyDataSetChanged();
}
@Override
public void onInvalidated() {
notifyDataSetInvalidated();
}
}
}

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

@ -1,27 +1,24 @@ @@ -1,27 +1,24 @@
package org.transdroid.core.gui.lists;
import org.transdroid.R;
import android.content.Context;
import android.view.View;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.View;
import org.transdroid.R;
import java.util.ArrayList;
import java.util.List;
import java.lang.Math;
class PiecesMapView extends View {
private final float scale = getContext().getResources().getDisplayMetrics().density;
private final int MINIMUM_HEIGHT = (int) (25 * scale);
private final int MINIMUM_PIECE_WIDTH = (int) (2 * scale);
private ArrayList<Integer> pieces = null;
private final Paint downloadingPaint = new Paint();
private final Paint donePaint = new Paint();
private final Paint partialDonePaint = new Paint();
private ArrayList<Integer> pieces = null;
public PiecesMapView(Context context) {
super(context);
@ -35,15 +32,15 @@ class PiecesMapView extends View { @@ -35,15 +32,15 @@ class PiecesMapView extends View {
}
public void setPieces(List<Integer> pieces) {
this.pieces = new ArrayList<Integer>(pieces);
this.pieces = new ArrayList<>(pieces);
invalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int ws = MeasureSpec.getSize(widthMeasureSpec);
int hs = Math.max(getHeight(), MINIMUM_HEIGHT);
setMeasuredDimension(ws, hs);
int ws = MeasureSpec.getSize(widthMeasureSpec);
int hs = Math.max(getHeight(), MINIMUM_HEIGHT);
setMeasuredDimension(ws, hs);
}
@Override
@ -62,10 +59,10 @@ class PiecesMapView extends View { @@ -62,10 +59,10 @@ class PiecesMapView extends View {
int pieceWidth;
pieceWidth = MINIMUM_PIECE_WIDTH;
piecesScaled = new ArrayList<Integer>();
piecesScaled = new ArrayList<>();
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
for (int i = 0; i < bucketCount; i++) {
@ -74,15 +71,15 @@ class PiecesMapView extends View { @@ -74,15 +71,15 @@ class PiecesMapView extends View {
int start = i * bucketSize;
// 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 downloadingCount = 0;
// loop pieces in bucket
for(int j = 0; j < bucket.size(); j++) {
for (int j = 0; j < bucket.size(); j++) {
// Count downloading pieces
if (bucket.get(j) == 1) {
downloadingCount++;
@ -114,10 +111,9 @@ class PiecesMapView extends View { @@ -114,10 +111,9 @@ class PiecesMapView extends View {
piecesScaled.add(state);
}
String scaledPiecesString = "";
for (int s : piecesScaled)
{
scaledPiecesString += s;
StringBuilder scaledPiecesString = new StringBuilder();
for (int s : piecesScaled) {
scaledPiecesString.append(s);
}
// 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; @@ -19,10 +19,11 @@ package org.transdroid.core.gui.lists;
/**
* Represents a filter item as shown in the navigation list or spinner.
*
* @author Eric Kok
*/
public interface SimpleListItem {
public String getName();
String getName();
}

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

@ -16,99 +16,102 @@ @@ -16,99 +16,102 @@
*/
package org.transdroid.core.gui.lists;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import java.util.ArrayList;
import java.util.List;
public class SimpleListItemAdapter extends BaseAdapter {
private final Context context;
private List<? extends SimpleListItem> items;
private int autoLinkMask = 0;
public SimpleListItemAdapter(Context context, List<? extends SimpleListItem> items) {
this.context = context;
this.items = 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
*/
public void update(List<? extends SimpleListItem> newItems) {
this.items = newItems;
notifyDataSetChanged();
}
public void setAutoLinkMask(int autoLinkMask) {
this.autoLinkMask = autoLinkMask;
}
@Override
public int getCount() {
return items.size();
}
@Override
public SimpleListItem getItem(int position) {
return items.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
SimpleListItemView filterItemView;
if (convertView == null || !(convertView instanceof SimpleListItemView)) {
filterItemView = SimpleListItemView_.build(context);
} else {
filterItemView = (SimpleListItemView) convertView;
}
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.
* @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}
* @param strings A list of string
* @return A list of SimpleStringItem objects representing the input strings
*/
public static List<SimpleStringItem> wrapStringsList(List<String> strings) {
ArrayList<SimpleStringItem> errors = new ArrayList<SimpleStringItem>();
if (strings != null) {
for (String string : strings) {
errors.add(new SimpleStringItem(string));
}
}
return errors;
}
private final String string;
public SimpleStringItem(String string) {
this.string = string;
}
@Override
public String getName() {
return this.string;
}
}
private final Context context;
private List<? extends SimpleListItem> items;
private int autoLinkMask = 0;
public SimpleListItemAdapter(Context context, List<? extends SimpleListItem> items) {
this.context = context;
this.items = 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
*/
public void update(List<? extends SimpleListItem> newItems) {
this.items = newItems;
notifyDataSetChanged();
}
public void setAutoLinkMask(int autoLinkMask) {
this.autoLinkMask = autoLinkMask;
}
@Override
public int getCount() {
return items.size();
}
@Override
public SimpleListItem getItem(int position) {
return items.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
SimpleListItemView filterItemView;
if (!(convertView instanceof SimpleListItemView)) {
filterItemView = SimpleListItemView_.build(context);
} else {
filterItemView = (SimpleListItemView) convertView;
}
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.
*
* @author Eric Kok
*/
public static class SimpleStringItem implements SimpleListItem {
private final String string;
public SimpleStringItem(String string) {
this.string = string;
}
/**
* Wraps a simple string of strings into a list of SimpleStringItem to add as data to a
* {@link SimpleListItemAdapter}
*
* @param strings A list of string
* @return A list of SimpleStringItem objects representing the input strings
*/
public static List<SimpleStringItem> wrapStringsList(List<String> strings) {
ArrayList<SimpleStringItem> errors = new ArrayList<>();
if (strings != null) {
for (String string : strings) {
errors.add(new SimpleStringItem(string));
}
}
return errors;
}
@Override
public String getName() {
return this.string;
}
}
}

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

@ -16,51 +16,53 @@ @@ -16,51 +16,53 @@
*/
package org.transdroid.core.gui.lists;
import java.util.List;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
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
* Spinner. The standard Android spinner resources are used for the layout.
*
* @author Eric Kok
*/
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
* 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 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);
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
}
/**
* 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.
*
* @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 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);
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
}
@Override
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
// TextView; this can then be filled with the SimpleListItem's name instead of the standard toString()
// implementation
TextView text = (TextView) super.getView(position, convertView, parent);
text.setText(getItem(position).getName());
return text;
}
@Override
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
// TextView; this can then be filled with the SimpleListItem's name instead of the standard toString()
// implementation
TextView text = (TextView) super.getView(position, convertView, parent);
text.setText(getItem(position).getName());
return text;
}
@Override
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
// TextView; this can then be filled with the SimpleListItem's name instead of the standard toString()
// implementation
TextView text = (TextView) super.getDropDownView(position, convertView, parent);
text.setText(getItem(position).getName());
return text;
}
@Override
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
// TextView; this can then be filled with the SimpleListItem's name instead of the standard toString()
// implementation
TextView text = (TextView) super.getDropDownView(position, convertView, parent);
text.setText(getItem(position).getName());
return text;
}
}

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

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

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

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

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

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

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

@ -16,9 +16,6 @@ @@ -16,9 +16,6 @@
*/
package org.transdroid.core.gui.lists;
import org.transdroid.R;
import org.transdroid.daemon.Priority;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
@ -26,75 +23,76 @@ import android.graphics.RectF; @@ -26,75 +23,76 @@ import android.graphics.RectF;
import android.util.AttributeSet;
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
* 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.
*
* @author Eric Kok
*/
public class TorrentFilePriorityLayout extends RelativeLayout {
private final float scale = getContext().getResources().getDisplayMetrics().density;
private final int WIDTH = (int) (6 * scale + 0.5f);
private Priority priority = null;
private final Paint offPaint = new Paint();
private final Paint lowPaint = new Paint();
private final Paint highPaint = new Paint();
private final Paint normalPaint = new Paint();
private final RectF fullRect = new RectF();
private final float scale = getContext().getResources().getDisplayMetrics().density;
private final int WIDTH = (int) (6 * scale + 0.5f);
private final Paint offPaint = new Paint();
private final Paint lowPaint = new Paint();
private final Paint highPaint = new Paint();
private final Paint normalPaint = new Paint();
private final RectF fullRect = new RectF();
private Priority priority = null;
public TorrentFilePriorityLayout(Context context) {
super(context);
initPaints();
setWillNotDraw(false);
}
public TorrentFilePriorityLayout(Context context) {
super(context);
initPaints();
setWillNotDraw(false);
}
public TorrentFilePriorityLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initPaints();
setWillNotDraw(false);
}
public TorrentFilePriorityLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initPaints();
setWillNotDraw(false);
}
private void initPaints() {
offPaint.setColor(getResources().getColor(R.color.file_off));
lowPaint.setColor(getResources().getColor(R.color.file_low));
normalPaint.setColor(getResources().getColor(R.color.file_normal));
highPaint.setColor(getResources().getColor(R.color.file_high));
}
private void initPaints() {
offPaint.setColor(getResources().getColor(R.color.file_off));
lowPaint.setColor(getResources().getColor(R.color.file_low));
normalPaint.setColor(getResources().getColor(R.color.file_normal));
highPaint.setColor(getResources().getColor(R.color.file_high));
}
public void setPriority(Priority priority) {
this.priority = priority;
this.invalidate();
}
public void setPriority(Priority priority) {
this.priority = priority;
this.invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int height = getHeight();
int width = WIDTH;
fullRect.set(0, 0, width, height);
fullRect.set(0, 0, WIDTH, getHeight());
if (priority == null) {
return;
}
if (priority == null) {
return;
}
switch (priority) {
case Low:
canvas.drawRect(fullRect, lowPaint);
break;
case Normal:
canvas.drawRect(fullRect, normalPaint);
break;
case High:
canvas.drawRect(fullRect, highPaint);
break;
default: // Off
canvas.drawRect(fullRect, offPaint);
break;
}
switch (priority) {
case Low:
canvas.drawRect(fullRect, lowPaint);
break;
case Normal:
canvas.drawRect(fullRect, normalPaint);
break;
case High:
canvas.drawRect(fullRect, highPaint);
break;
default: // Off
canvas.drawRect(fullRect, offPaint);
break;
}
}
}
}

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

@ -26,23 +26,24 @@ import org.transdroid.daemon.TorrentFile; @@ -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
*
* @author Eric Kok
*/
@EViewGroup(R.layout.list_item_torrentfile)
public class TorrentFileView extends TorrentFilePriorityLayout {
@ViewById
protected TextView nameText, progressText, sizesText;
@ViewById
protected TextView nameText, progressText, sizesText;
public TorrentFileView(Context context) {
super(context, null);
}
public TorrentFileView(Context context) {
super(context, null);
}
public void bind(TorrentFile torrentFile) {
nameText.setText(torrentFile.getName());
sizesText.setText(torrentFile.getDownloadedAndTotalSizeText());
progressText.setText(torrentFile.getProgressText());
setPriority(torrentFile.getPriority());
}
public void bind(TorrentFile torrentFile) {
nameText.setText(torrentFile.getName());
sizesText.setText(torrentFile.getDownloadedAndTotalSizeText());
progressText.setText(torrentFile.getProgressText());
setPriority(torrentFile.getPriority());
}
}

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

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

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

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

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

@ -29,55 +29,56 @@ import org.transdroid.daemon.TorrentStatus; @@ -29,55 +29,56 @@ import org.transdroid.daemon.TorrentStatus;
/**
* View that represents some {@link Torrent} object and displays progress, status, speeds, etc.
*
* @author Eric Kok
*/
@EViewGroup(R.layout.list_item_torrent)
public class TorrentView extends TorrentStatusLayout {
@ViewById
protected ImageView priorityImage;
@ViewById
protected TextView nameText, ratioText, progressText, speedText, peersText;
@ViewById
protected TorrentProgressBar torrentProgressbar;
@ViewById
protected ImageView priorityImage;
@ViewById
protected TextView nameText, ratioText, progressText, speedText, peersText;
@ViewById
protected TorrentProgressBar torrentProgressbar;
public TorrentView(Context context) {
super(context);
}
public TorrentView(Context context) {
super(context);
}
public void bind(Torrent torrent) {
LocalTorrent local = LocalTorrent.fromTorrent(torrent);
setStatus(torrent.getStatusCode());
nameText.setText(torrent.getName());
progressText.setText(local.getProgressSizeText(getResources(), false));
ratioText.setText(local.getProgressEtaRatioText(getResources()));
// TODO: Implement per-torrent priority and set priorityImage
priorityImage.setVisibility(View.INVISIBLE);
public void bind(Torrent torrent) {
LocalTorrent local = LocalTorrent.fromTorrent(torrent);
setStatus(torrent.getStatusCode());
nameText.setText(torrent.getName());
progressText.setText(local.getProgressSizeText(getResources(), false));
ratioText.setText(local.getProgressEtaRatioText(getResources()));
// TODO: Implement per-torrent priority and set priorityImage
priorityImage.setVisibility(View.INVISIBLE);
// Only show status bar, peers and speed fields if relevant, i.e. when downloading or actively seeding
if (torrent.getStatusCode() == TorrentStatus.Downloading ||
(torrent.getStatusCode() == TorrentStatus.Seeding && torrent.getRateUpload() > 0)) {
torrentProgressbar.setVisibility(View.VISIBLE);
torrentProgressbar.setProgress((int) (torrent.getDownloadedPercentage() * 100));
torrentProgressbar.setActive(torrent.canPause());
torrentProgressbar.setError(torrent.getStatusCode() == TorrentStatus.Error);
peersText.setVisibility(View.VISIBLE);
peersText.setText(local.getProgressConnectionText(getResources()));
speedText.setVisibility(View.VISIBLE);
speedText.setText(local.getProgressSpeedText(getResources()));
} else if (torrent.getPartDone() < 1) {
// Not active, but also not complete, so show the status bar
torrentProgressbar.setVisibility(View.VISIBLE);
torrentProgressbar.setProgress((int) (torrent.getDownloadedPercentage() * 100));
torrentProgressbar.setActive(torrent.canPause());
torrentProgressbar.setError(torrent.getStatusCode() == TorrentStatus.Error);
peersText.setVisibility(View.GONE);
speedText.setVisibility(View.GONE);
} else {
torrentProgressbar.setVisibility(View.GONE);
peersText.setVisibility(View.GONE);
speedText.setVisibility(View.GONE);
}
}
// Only show status bar, peers and speed fields if relevant, i.e. when downloading or actively seeding
if (torrent.getStatusCode() == TorrentStatus.Downloading ||
(torrent.getStatusCode() == TorrentStatus.Seeding && torrent.getRateUpload() > 0)) {
torrentProgressbar.setVisibility(View.VISIBLE);
torrentProgressbar.setProgress((int) (torrent.getDownloadedPercentage() * 100));
torrentProgressbar.setActive(torrent.canPause());
torrentProgressbar.setError(torrent.getStatusCode() == TorrentStatus.Error);
peersText.setVisibility(View.VISIBLE);
peersText.setText(local.getProgressConnectionText(getResources()));
speedText.setVisibility(View.VISIBLE);
speedText.setText(local.getProgressSpeedText(getResources()));
} else if (torrent.getPartDone() < 1) {
// Not active, but also not complete, so show the status bar
torrentProgressbar.setVisibility(View.VISIBLE);
torrentProgressbar.setProgress((int) (torrent.getDownloadedPercentage() * 100));
torrentProgressbar.setActive(torrent.canPause());
torrentProgressbar.setError(torrent.getStatusCode() == TorrentStatus.Error);
peersText.setVisibility(View.GONE);
speedText.setVisibility(View.GONE);
} else {
torrentProgressbar.setVisibility(View.GONE);
peersText.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; @@ -29,61 +29,62 @@ import java.util.ArrayList;
/**
* Adapter that contains a list of torrent objects to show.
*
* @author Eric Kok
*/
@EBean
public class TorrentsAdapter extends BaseAdapter {
private ArrayList<Torrent> torrents = null;
@RootContext
protected Context context;
@RootContext
protected Context context;
private ArrayList<Torrent> torrents = null;
/**
* Allows updating the full internal list of torrents at once, replacing the old list
* @param newTorrents The new list of torrent objects
*/
public void update(ArrayList<Torrent> newTorrents) {
this.torrents = newTorrents;
notifyDataSetChanged();
}
/**
* Allows updating the full internal list of torrents at once, replacing the old list
*
* @param newTorrents The new list of torrent objects
*/
public void update(ArrayList<Torrent> newTorrents) {
this.torrents = newTorrents;
notifyDataSetChanged();
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public int getCount() {
if (torrents == null) {
return 0;
}
return torrents.size();
}
@Override
public int getCount() {
if (torrents == null) {
return 0;
}
return torrents.size();
}
@Override
public Torrent getItem(int position) {
if (torrents == null) {
return null;
}
return torrents.get(position);
}
@Override
public Torrent getItem(int position) {
if (torrents == null) {
return null;
}
return torrents.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TorrentView torrentView;
if (convertView == null) {
torrentView = TorrentView_.build(context);
} else {
torrentView = (TorrentView) convertView;
}
torrentView.bind(getItem(position));
return torrentView;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TorrentView torrentView;
if (convertView == null) {
torrentView = TorrentView_.build(context);
} else {
torrentView = (TorrentView) convertView;
}
torrentView.bind(getItem(position));
return torrentView;
}
}

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

@ -28,96 +28,100 @@ import android.widget.ListView; @@ -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 list row accordingly. Use {@link #setViewEnabled(boolean)} to enable or disable this contained view for user
* interaction.
*
* @author Eric Kok
*/
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
* 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.
* @param view The view that will be wrapper in an adapter to show in a list view
*/
public ViewHolderAdapter(View view) {
this.view = view;
}
/**
* 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
* {@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
*/
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})
* accordingly. Use {@link View#GONE} to hide this adapter's view altogether.
* @param visibility The visibility to set on the contained view
*/
public void setViewVisibility(int visibility) {
view.setVisibility(visibility);
notifyDataSetChanged();
}
/**
* 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.
*
* @param visibility The visibility to set on the contained view
*/
public void setViewVisibility(int visibility) {
view.setVisibility(visibility);
notifyDataSetChanged();
}
/**
* 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.
* @param enabled Whether the contained view should be enabled
*/
public void setViewEnabled(boolean enabled) {
view.setEnabled(enabled);
notifyDataSetChanged();
}
/**
* 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.
*
* @param enabled Whether the contained view should be enabled
*/
public void setViewEnabled(boolean enabled) {
view.setEnabled(enabled);
notifyDataSetChanged();
}
/**
* Returns 1 if the contained view is {@link View#VISIBLE} or {@link View#INVISIBLE}, return 0 if {@link View#GONE}.
*/
@Override
public int getCount() {
return view.getVisibility() == View.VISIBLE ? 1 : 0;
}
/**
* Returns 1 if the contained view is {@link View#VISIBLE} or {@link View#INVISIBLE}, return 0 if {@link View#GONE}.
*/
@Override
public int getCount() {
return view.getVisibility() == View.VISIBLE ? 1 : 0;
}
/**
* Always directly returns the single contained view instance.
*/
@Override
public Object getItem(int position) {
return view;
}
/**
* Always directly returns the single contained view instance.
*/
@Override
public Object getItem(int position) {
return view;
}
/**
* Always returns the position directly as item id.
*/
@Override
public long getItemId(int position) {
return position;
}
/**
* Always returns the position directly as item id.
*/
@Override
public long getItemId(int position) {
return position;
}
/**
* Always directly returns the single contained view instance.
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return view;
}
/**
* Always directly returns the single contained view instance.
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return view;
}
/**
* Always returns true, as there is only one contained item and it is never changed.
*/
@Override
public boolean hasStableIds() {
return true;
}
/**
* Always returns true, as there is only one contained item and it is never changed.
*/
@Override
public boolean hasStableIds() {
return true;
}
/**
* Returns false, as the contained view can still be enabled and disabled.
*/
@Override
public boolean areAllItemsEnabled() {
return false;
}
/**
* Returns false, as the contained view can still be enabled and disabled.
*/
@Override
public boolean areAllItemsEnabled() {
return false;
}
/**
* Returns true if the contained view is enabled, returns false otherwise.
*/
@Override
public boolean isEnabled(int position) {
return view.isEnabled();
}
/**
* Returns true if the contained view is enabled, returns false otherwise.
*/
@Override
public boolean isEnabled(int position) {
return view.isEnabled();
}
}

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

@ -16,54 +16,56 @@ @@ -16,54 +16,56 @@
*/
package org.transdroid.core.gui.log;
import java.sql.SQLException;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import androidx.annotation.Keep;
import android.util.Log;
import androidx.annotation.Keep;
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
import java.sql.SQLException;
/**
* Helper to access the database to access persisting objects.
*
* @author Eric Kok
*/
public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
private static final String DATABASE_NAME = "transdroid.db";
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "transdroid.db";
private static final int DATABASE_VERSION = 1;
@Keep
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Keep
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource) {
try {
TableUtils.createTable(connectionSource, ErrorLogEntry.class);
} catch (SQLException e) {
Log.e(org.transdroid.core.gui.log.Log.LOG_NAME, "Could not create new table for ErrorLogEntry", e);
}
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource) {
try {
TableUtils.createTable(connectionSource, ErrorLogEntry.class);
} catch (SQLException e) {
Log.e(org.transdroid.core.gui.log.Log.LOG_NAME, "Could not create new table for ErrorLogEntry", e);
}
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource, int oldVersion,
int newVersion) {
try {
switch (oldVersion) {
case 1:
TableUtils.createTable(connectionSource, ErrorLogEntry.class);
/*case 1:
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource, int oldVersion,
int newVersion) {
try {
switch (oldVersion) {
case 1:
TableUtils.createTable(connectionSource, ErrorLogEntry.class);
/*case 2:
etc...*/
}
}
} catch (SQLException e) {
Log.e(org.transdroid.core.gui.log.Log.LOG_NAME, "Could not upgrade the table for ErrorLogEntry", e);
}
}
} catch (SQLException 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 @@ @@ -16,93 +16,92 @@
*/
package org.transdroid.core.gui.log;
import java.util.Date;
import android.os.Parcel;
import android.os.Parcelable;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
import java.util.Date;
/**
* Represents an error log entry to be registered in the database.
*
* @author Eric Kok
*/
@DatabaseTable(tableName = "ErrorLogEntry")
public class ErrorLogEntry implements Parcelable {
public static final String ID = "logId";
public static final String DATEANDTIME = "dateAndTime";
@DatabaseField(id = true, columnName = ID)
private Integer logId;
@DatabaseField(columnName = DATEANDTIME)
private Date dateAndTime;
@DatabaseField
private Integer priority;
@DatabaseField
private String tag;
@DatabaseField
private String message;
public ErrorLogEntry() {
}
public ErrorLogEntry(Integer priority, String tag, String message) {
this.dateAndTime = new Date();
this.priority = priority;
this.tag = tag;
this.message = message;
}
public Integer getLogId() {
return logId;
}
public Date getDateAndTime() {
return dateAndTime;
}
public Integer getPriority() {
return priority;
}
public String getTag() {
return tag;
}
public String getMessage() {
return message;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel out, int flags) {
out.writeInt(logId);
out.writeLong(dateAndTime.getTime());
out.writeInt(priority);
out.writeString(tag);
out.writeString(message);
}
public static final Parcelable.Creator<ErrorLogEntry> CREATOR = new Parcelable.Creator<ErrorLogEntry>() {
public ErrorLogEntry createFromParcel(Parcel in) {
return new ErrorLogEntry(in);
}
public ErrorLogEntry[] newArray(int size) {
return new ErrorLogEntry[size];
}
};
private ErrorLogEntry(Parcel in) {
logId = in.readInt();
dateAndTime = new Date(in.readLong());
priority = in.readInt();
tag = in.readString();
message = in.readString();
}
public static final String ID = "logId";
public static final String DATEANDTIME = "dateAndTime";
public static final Parcelable.Creator<ErrorLogEntry> CREATOR = new Parcelable.Creator<ErrorLogEntry>() {
public ErrorLogEntry createFromParcel(Parcel in) {
return new ErrorLogEntry(in);
}
public ErrorLogEntry[] newArray(int size) {
return new ErrorLogEntry[size];
}
};
@DatabaseField(id = true, columnName = ID)
private Integer logId;
@DatabaseField(columnName = DATEANDTIME)
private Date dateAndTime;
@DatabaseField
private Integer priority;
@DatabaseField
private String tag;
@DatabaseField
private String message;
public ErrorLogEntry() {
}
public ErrorLogEntry(Integer priority, String tag, String message) {
this.dateAndTime = new Date();
this.priority = priority;
this.tag = tag;
this.message = message;
}
private ErrorLogEntry(Parcel in) {
logId = in.readInt();
dateAndTime = new Date(in.readLong());
priority = in.readInt();
tag = in.readString();
message = in.readString();
}
public Integer getLogId() {
return logId;
}
public Date getDateAndTime() {
return dateAndTime;
}
public Integer getPriority() {
return priority;
}
public String getTag() {
return tag;
}
public String getMessage() {
return message;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel out, int flags) {
out.writeInt(logId);
out.writeLong(dateAndTime.getTime());
out.writeInt(priority);
out.writeString(tag);
out.writeString(message);
}
}

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

@ -16,8 +16,11 @@ @@ -16,8 +16,11 @@
*/
package org.transdroid.core.gui.log;
import java.sql.SQLException;
import java.util.List;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import com.j256.ormlite.dao.Dao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
@ -26,70 +29,67 @@ import org.transdroid.R; @@ -26,70 +29,67 @@ import org.transdroid.R;
import org.transdroid.core.app.settings.ServerSetting;
import org.transdroid.core.gui.navigation.NavigationHelper;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import com.j256.ormlite.dao.Dao;
import java.sql.SQLException;
import java.util.List;
@EBean
public class ErrorLogSender {
@Bean
protected Log log;
@Bean
protected NavigationHelper navigationHelper;
@OrmLiteDao(helper = DatabaseHelper.class)
protected Dao<ErrorLogEntry, Integer> errorLogDao;
@Bean
protected Log log;
@Bean
protected NavigationHelper navigationHelper;
@OrmLiteDao(helper = DatabaseHelper.class)
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
StringBuilder body = new StringBuilder();
body.append("Please describe your problem:\n\n\n");
body.append("\n");
body.append(navigationHelper.getAppNameAndVersion());
body.append("\n");
if (serverSetting == null) {
body.append("(No server settings)");
} else {
body.append(serverSetting.getType().toString());
body.append(" settings: ");
body.append(serverSetting.getHumanReadableIdentifier());
}
body.append("\n\nConnection and error log:");
// Prepare an email with error logging information
StringBuilder body = new StringBuilder();
body.append("Please describe your problem:\n\n\n");
body.append("\n");
body.append(navigationHelper.getAppNameAndVersion());
body.append("\n");
if (serverSetting == null) {
body.append("(No server settings)");
} else {
body.append(serverSetting.getType().toString());
body.append(" settings: ");
body.append(serverSetting.getHumanReadableIdentifier());
}
body.append("\n\nConnection and error log:");
// Print the individual error log messages as stored in the database
List<ErrorLogEntry> all = errorLogDao.queryBuilder().orderBy(ErrorLogEntry.ID, true).query();
for (ErrorLogEntry errorLogEntry : all) {
body.append("\n");
body.append(errorLogEntry.getLogId());
body.append(" -- ");
body.append(errorLogEntry.getDateAndTime());
body.append(" -- ");
body.append(errorLogEntry.getPriority());
body.append(" -- ");
body.append(errorLogEntry.getMessage());
}
// Print the individual error log messages as stored in the database
List<ErrorLogEntry> all = errorLogDao.queryBuilder().orderBy(ErrorLogEntry.ID, true).query();
for (ErrorLogEntry errorLogEntry : all) {
body.append("\n");
body.append(errorLogEntry.getLogId());
body.append(" -- ");
body.append(errorLogEntry.getDateAndTime());
body.append(" -- ");
body.append(errorLogEntry.getPriority());
body.append(" -- ");
body.append(errorLogEntry.getMessage());
}
Intent target = new Intent(Intent.ACTION_SEND);
target.setType("message/rfc822");
target.putExtra(Intent.EXTRA_EMAIL, new String[] { "transdroid@2312.nl" });
target.putExtra(Intent.EXTRA_SUBJECT, "Transdroid error report");
target.putExtra(Intent.EXTRA_TEXT, body.toString());
try {
callingActivity.startActivity(Intent.createChooser(target,
callingActivity.getString(R.string.pref_sendlog)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} catch (ActivityNotFoundException e) {
log.i(callingActivity, "Tried to send error log, but there is no email app installed.");
}
Intent target = new Intent(Intent.ACTION_SEND);
target.setType("message/rfc822");
target.putExtra(Intent.EXTRA_EMAIL, new String[]{"transdroid@2312.nl"});
target.putExtra(Intent.EXTRA_SUBJECT, "Transdroid error report");
target.putExtra(Intent.EXTRA_TEXT, body.toString());
try {
callingActivity.startActivity(Intent.createChooser(target,
callingActivity.getString(R.string.pref_sendlog)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} catch (ActivityNotFoundException e) {
log.i(callingActivity, "Tried to send error log, but there is no email app installed.");
}
} catch (SQLException e) {
log.e(callingActivity, "Cannot read the error log to build an error report to send: " + e.toString());
}
} catch (SQLException e) {
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; @@ -28,46 +28,47 @@ import java.util.Date;
/**
* Application-wide logging class that registers entries in the database (for a certain time).
*
* @author Eric Kok
*/
@EBean(scope = Scope.Singleton)
public class Log {
public static final String LOG_NAME = "Transdroid";
private static final long MAX_LOG_AGE = 15 * 60 * 1000; // 15 minutes
@OrmLiteDao(helper = DatabaseHelper.class)
Dao<ErrorLogEntry, Integer> errorLogDao;
public static final String LOG_NAME = "Transdroid";
private static final long MAX_LOG_AGE = 15 * 60 * 1000; // 15 minutes
@OrmLiteDao(helper = DatabaseHelper.class)
Dao<ErrorLogEntry, Integer> errorLogDao;
protected void log(Object object, int priority, String message) {
log(object instanceof String ? (String) object : object.getClass().getSimpleName(), priority, message);
}
protected void log(Object object, int priority, String message) {
log(object instanceof String ? (String) object : object.getClass().getSimpleName(), priority, message);
}
protected void log(String logName, int priority, String message) {
if (BuildConfig.DEBUG) {
android.util.Log.println(priority, LOG_NAME, message);
}
try {
// Store this log message to the database
errorLogDao.create(new ErrorLogEntry(priority, logName, message));
// Truncate the error log
DeleteBuilder<ErrorLogEntry, Integer> db = errorLogDao.deleteBuilder();
db.setWhere(db.where().le(ErrorLogEntry.DATEANDTIME, new Date(new Date().getTime() - MAX_LOG_AGE)));
errorLogDao.delete(db.prepare());
} catch (Exception e) {
android.util.Log.e(LOG_NAME, "Cannot write log message to database: " + e.toString());
}
}
protected void log(String logName, int priority, String message) {
if (BuildConfig.DEBUG) {
android.util.Log.println(priority, LOG_NAME, message);
}
try {
// Store this log message to the database
errorLogDao.create(new ErrorLogEntry(priority, logName, message));
// Truncate the error log
DeleteBuilder<ErrorLogEntry, Integer> db = errorLogDao.deleteBuilder();
db.setWhere(db.where().le(ErrorLogEntry.DATEANDTIME, new Date(new Date().getTime() - MAX_LOG_AGE)));
errorLogDao.delete(db.prepare());
} catch (Exception e) {
android.util.Log.e(LOG_NAME, "Cannot write log message to database: " + e.toString());
}
}
public void d(Object object, String msg) {
log(object, android.util.Log.DEBUG, msg);
}
public void d(Object object, String msg) {
log(object, android.util.Log.DEBUG, msg);
}
public void i(Object object, String msg) {
log(object, android.util.Log.DEBUG, msg);
}
public void i(Object object, String msg) {
log(object, android.util.Log.DEBUG, msg);
}
public void e(Object object, String msg) {
log(object, android.util.Log.ERROR, msg);
}
public void e(Object object, String 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; @@ -20,32 +20,32 @@ import android.content.Context;
public class LogUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private final Context context;
private final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;
public LogUncaughtExceptionHandler(Context context, Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler) {
this.context = context;
this.defaultUncaughtExceptionHandler = defaultUncaughtExceptionHandler;
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
// Write exception stack trace to the log
String prefix = "E: ";
Log_ log = Log_.getInstance_(context);
log.e(this, prefix + ex.toString());
if (ex.getCause() != null) {
for (StackTraceElement e : ex.getCause().getStackTrace()) {
log.e(this, prefix + e.toString());
}
}
for (StackTraceElement e : ex.getStackTrace()) {
log.e(this, prefix + e.toString());
}
// Rely on default Android exception handling
defaultUncaughtExceptionHandler.uncaughtException(thread, ex);
}
private final Context context;
private final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;
public LogUncaughtExceptionHandler(Context context, Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler) {
this.context = context;
this.defaultUncaughtExceptionHandler = defaultUncaughtExceptionHandler;
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
// Write exception stack trace to the log
String prefix = "E: ";
Log_ log = Log_.getInstance_(context);
log.e(this, prefix + ex.toString());
if (ex.getCause() != null) {
for (StackTraceElement e : ex.getCause().getStackTrace()) {
log.e(this, prefix + e.toString());
}
}
for (StackTraceElement e : ex.getStackTrace()) {
log.e(this, prefix + e.toString());
}
// Rely on default Android exception handling
defaultUncaughtExceptionHandler.uncaughtException(thread, ex);
}
}

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

@ -16,12 +16,6 @@ @@ -16,12 +16,6 @@
*/
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.Dialog;
import android.content.Context;
@ -32,82 +26,92 @@ import android.view.MenuInflater; @@ -32,82 +26,92 @@ import android.view.MenuInflater;
import android.view.MenuItem;
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
* {@link #showDialog(Context, DialogSpecification)} with in instance of the dialog specification that should be shown,
* from the calling activity's {@link Activity#onCreateDialog(int)}.
*
* @author Eric Kok
*/
@EActivity
public class DialogHelper extends Activity {
@Extra
protected DialogSpecification dialog;
@Extra
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
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(dialog.getDialogLayoutId());
// TODO getActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(dialog.getDialogLayoutId());
// TODO getActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater menuInflater = getMenuInflater();
menuInflater.inflate(dialog.getDialogMenuId(), menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
// 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();
return true;
}
return dialog.onMenuItemSelected(this, item.getItemId());
}
/**
* 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;
}
/**
* 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);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
// 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();
return true;
}
return dialog.onMenuItemSelected(this, item.getItemId());
}
/**
* 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);
}
/**
* 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());
}
}
}

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

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

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

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

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

@ -27,29 +27,31 @@ import org.transdroid.R; @@ -27,29 +27,31 @@ import org.transdroid.R;
/**
* A list item that shows a sub header or separator (in underlined Holo style).
*
* @author Eric Kok
*/
@EViewGroup(R.layout.list_item_separator)
public class FilterSeparatorView extends FrameLayout {
protected String text;
@ViewById
protected TextView separatorText;
public FilterSeparatorView(Context context) {
super(context);
}
/**
* 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
*/
public FilterSeparatorView setText(String text) {
separatorText.setText(text);
setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.WRAP_CONTENT, AbsListView.LayoutParams.WRAP_CONTENT));
return this;
}
protected String text;
@ViewById
protected TextView separatorText;
public FilterSeparatorView(Context context) {
super(context);
}
/**
* 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
*/
public FilterSeparatorView setText(String text) {
separatorText.setText(text);
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; @@ -29,118 +29,119 @@ import java.util.List;
/**
* Represents some label that is active or available on the server.
*
* @author Eric Kok
*/
public class Label implements SimpleListItem, NavigationFilter, Comparable<Label> {
private static String unnamedLabelText = null;
private final boolean isEmptyLabel;
private final String name;
private final int count;
private Label(String name, int count, boolean isEmptyLabel) {
this.name = name;
this.count = count;
this.isEmptyLabel = isEmptyLabel;
}
public Label(org.transdroid.daemon.Label daemonLabel) {
this(daemonLabel.getName(), daemonLabel.getCount(), false);
}
@Override
public String getName() {
if (TextUtils.isEmpty(this.name)) {
return unnamedLabelText;
}
return this.name;
}
@Override
public String getCode() {
// Use the class name and label name to provide a unique navigation filter code
return Label.class.getSimpleName() + "_" + name;
}
public int getCount() {
return count;
}
public boolean isEmptyLabel() {
return isEmptyLabel;
}
/**
* Returns true if the torrent label's name matches this (selected) label's name, false otherwise
* @param torrent The torrent to match against this label
* @param dormantAsInactive This property is ignored for label comparisons
*/
@Override
public boolean matches(Torrent torrent, boolean dormantAsInactive) {
if (isEmptyLabel) {
return TextUtils.isEmpty(torrent.getLabelName());
}
return torrent.getLabelName() != null && torrent.getLabelName().equals(name);
}
@Override
public int compareTo(Label another) {
return this.name.compareTo(another.getName());
}
/**
* 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
* @param unnamedLabel The text to show for the empty label (i.e. the unnamed label)
* @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) {
return null;
}
ArrayList<Label> localLabels = new ArrayList<>();
unnamedLabelText = unnamedLabel;
for (org.transdroid.daemon.Label label : daemonLabels) {
if (label != null && !TextUtils.isEmpty(label.getName())) {
localLabels.add(new Label(label));
}
}
Collections.sort(localLabels);
// force unlabelled to be at the top
localLabels.add(0, new Label(unnamedLabel, -1, true));
return localLabels;
}
private Label(Parcel in) {
this.name = in.readString();
this.count = in.readInt();
this.isEmptyLabel = in.readInt() == 1;
}
public static final Parcelable.Creator<Label> CREATOR = new Parcelable.Creator<Label>() {
public Label createFromParcel(Parcel in) {
return new Label(in);
}
public Label[] newArray(int size) {
return new Label[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(count);
dest.writeInt(isEmptyLabel ? 1 : 0);
}
public static final Parcelable.Creator<Label> CREATOR = new Parcelable.Creator<Label>() {
public Label createFromParcel(Parcel in) {
return new Label(in);
}
public Label[] newArray(int size) {
return new Label[size];
}
};
private static String unnamedLabelText = null;
private final boolean isEmptyLabel;
private final String name;
private final int count;
private Label(String name, int count, boolean isEmptyLabel) {
this.name = name;
this.count = count;
this.isEmptyLabel = isEmptyLabel;
}
public Label(org.transdroid.daemon.Label daemonLabel) {
this(daemonLabel.getName(), daemonLabel.getCount(), false);
}
private Label(Parcel in) {
this.name = in.readString();
this.count = in.readInt();
this.isEmptyLabel = in.readInt() == 1;
}
/**
* 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
* @param unnamedLabel The text to show for the empty label (i.e. the unnamed label)
* @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) {
return null;
}
ArrayList<Label> localLabels = new ArrayList<>();
unnamedLabelText = unnamedLabel;
for (org.transdroid.daemon.Label label : daemonLabels) {
if (label != null && !TextUtils.isEmpty(label.getName())) {
localLabels.add(new Label(label));
}
}
Collections.sort(localLabels);
// force unlabelled to be at the top
localLabels.add(0, new Label(unnamedLabel, -1, true));
return localLabels;
}
@Override
public String getName() {
if (TextUtils.isEmpty(this.name)) {
return unnamedLabelText;
}
return this.name;
}
@Override
public String getCode() {
// Use the class name and label name to provide a unique navigation filter code
return Label.class.getSimpleName() + "_" + name;
}
public int getCount() {
return count;
}
public boolean isEmptyLabel() {
return isEmptyLabel;
}
/**
* Returns true if the torrent label's name matches this (selected) label's name, false otherwise
*
* @param torrent The torrent to match against this label
* @param dormantAsInactive This property is ignored for label comparisons
*/
@Override
public boolean matches(Torrent torrent, boolean dormantAsInactive) {
if (isEmptyLabel) {
return TextUtils.isEmpty(torrent.getLabelName());
}
return torrent.getLabelName() != null && torrent.getLabelName().equals(name);
}
@Override
public int compareTo(Label another) {
return this.name.compareTo(another.getName());
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(count);
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; @@ -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
*
* @author Eric Kok
*/
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
* 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
* @return True if the torrent matches the filter and should be shown in the current screen, false otherwise
*/
boolean matches(Torrent torrent, boolean dormantAsInactive);
/**
* 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.
*
* @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
* @return True if the torrent matches the filter and should be shown in the current screen, false otherwise
*/
boolean matches(Torrent torrent, boolean dormantAsInactive);
/**
* Implementations should return a name that can be shown to indicate the active filter
* @return The name of the filter item as string
*/
String getName();
/**
* Implementations should return a name that can be shown to indicate the active filter
*
* @return The name of the filter item as string
*/
String getName();
/**
* 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
*/
String getCode();
/**
* 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
*/
String getCode();
}

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

@ -26,14 +26,13 @@ import android.content.pm.PackageManager; @@ -26,14 +26,13 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
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.SpannableString;
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.nostra13.universalimageloader.cache.disc.impl.ext.LruDiskCache;
import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator;
@ -54,250 +53,255 @@ import java.util.List; @@ -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
* errors.
*
* @author Eric Kok
*/
@SuppressLint("ResourceAsColor")
@EBean
public class NavigationHelper {
private static final int REQUEST_TORRENT_READ_PERMISSION = 0;
private static final int REQUEST_SETTINGS_READ_PERMISSION = 1;
private static final int REQUEST_SETTINGS_WRITE_PERMISSION = 2;
private static ImageLoader imageCache;
@RootContext
protected Context context;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
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);
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public boolean checkSettingsReadPermission(final Activity activity) {
return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT ||
checkPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_SETTINGS_READ_PERMISSION);
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public boolean checkSettingsWritePermission(final Activity activity) {
return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT ||
checkPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE, REQUEST_SETTINGS_WRITE_PERMISSION);
}
private boolean checkPermission(final Activity activity, final String permission, final int requestCode) {
if (hasPermission(permission))
// Permission already granted
return true;
if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {
// Never asked again: show a dialog with an explanation
activity.runOnUiThread(new Runnable() {
public void run() {
new MaterialDialog.Builder(context).content(R.string.permission_readtorrent).positiveText(android.R.string.ok)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode);
}
}).show();
}
});
return false;
}
// Permission not granted (and we asked for it already before)
ActivityCompat.requestPermissions(activity, new String[]{permission}, REQUEST_TORRENT_READ_PERMISSION);
return false;
}
private boolean hasPermission(String requiredPermission) {
return ContextCompat.checkSelfPermission(context, requiredPermission) == PackageManager.PERMISSION_GRANTED;
}
public Boolean handleTorrentReadPermissionResult(int requestCode, int[] grantResults) {
if (requestCode == REQUEST_TORRENT_READ_PERMISSION) {
// Return permission granting result
return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
}
return null;
}
public Boolean handleSettingsReadPermissionResult(int requestCode, int[] grantResults) {
if (requestCode == REQUEST_SETTINGS_READ_PERMISSION) {
// Return permission granting result
return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
}
return null;
}
public Boolean handleSettingsWritePermissionResult(int requestCode, int[] grantResults) {
if (requestCode == REQUEST_SETTINGS_WRITE_PERMISSION) {
// Return permission granting result
return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
}
return null;
}
/**
* Converts a string into a {@link Spannable} that displays the string in the Roboto Condensed font
* @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)
*/
public static SpannableString buildCondensedFontString(String string) {
if (string == null) {
return null;
}
SpannableString s = new SpannableString(string);
s.setSpan(new TypefaceSpan("sans-serif-condensed"), 0, s.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return s;
}
/**
* Analyses a torrent http or magnet URI and tries to come up with a reasonable human-readable name.
* @param rawTorrentUri The raw http:// or magnet: link to the torrent
* @return A best-guess, reasonably long name for the linked torrent
*/
public static String extractNameFromUri(Uri rawTorrentUri) {
if (rawTorrentUri.getScheme() == null) {
// Probably an incorrect URI; just return the whole thing
return rawTorrentUri.toString();
}
if (rawTorrentUri.getScheme().equals("magnet")) {
// Magnet links might have a dn (display name) parameter
String dn = getQueryParameter(rawTorrentUri, "dn");
if (dn != null && !dn.equals("")) {
return dn;
}
// If not, try to return the hash that is specified as xt (exact topci)
String xt = getQueryParameter(rawTorrentUri, "xt");
if (xt != null && !xt.equals("")) {
return xt;
}
}
if (rawTorrentUri.isHierarchical()) {
String path = rawTorrentUri.getPath();
if (path != null) {
if (path.contains("/")) {
path = path.substring(path.lastIndexOf("/"));
}
return path;
}
}
// No idea what to do with this; return as is
return rawTorrentUri.toString();
}
private static String getQueryParameter(Uri uri, String parameter) {
int start = uri.toString().indexOf(parameter + "=");
if (start >= 0) {
int begin = start + (parameter + "=").length();
int end = uri.toString().indexOf("&", begin);
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
*/
public ImageLoader getImageCache() {
if (imageCache == null) {
imageCache = ImageLoader.getInstance();
try {
LruDiskCache diskCache = new LruDiskCache(context.getCacheDir(), null, new Md5FileNameGenerator(), 640000, 25);
// @formatter:off
Builder imageCacheBuilder = new Builder(context)
.defaultDisplayImageOptions(
new DisplayImageOptions.Builder()
.cacheInMemory(true)
.cacheOnDisk(true)
.imageScaleType(ImageScaleType.IN_SAMPLE_INT)
.showImageForEmptyUri(R.drawable.ic_launcher).build())
.memoryCache(new UsingFreqLimitedMemoryCache(1024 * 1024))
.diskCache(diskCache);
imageCache.init(imageCacheBuilder.build());
// @formatter:on
} catch (IOException e) {
// The cache directory is always available on Android; ignore this exception
}
}
return imageCache;
}
public void forceOpenInBrowser(Uri link) {
Intent intent = new Intent(Intent.ACTION_VIEW).setData(link);
List<ResolveInfo> activities = context.getPackageManager().queryIntentActivities(intent, 0);
for (ResolveInfo resolveInfo : activities) {
if (activities.size() == 1 || (resolveInfo.isDefault && resolveInfo.activityInfo.packageName.equals(context.getPackageName()))) {
// There is a default browser; use this
intent.setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);
return;
}
}
// No default browser found: open chooser
try {
context.startActivity(Intent.createChooser(intent, "Open..."));
} 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).
* @return The app name and version, such as 'Transdroid 1.5.0 (180)'
*/
public String getAppNameAndVersion() {
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
* 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
*/
public boolean isSmallScreen() {
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
* version.
* @return True if search is enabled, false otherwise
*/
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
*/
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
*/
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
*/
public boolean enableUpdateChecker() {
return context.getResources().getBoolean(R.bool.updatecheck_available);
}
private static final int REQUEST_TORRENT_READ_PERMISSION = 0;
private static final int REQUEST_SETTINGS_READ_PERMISSION = 1;
private static final int REQUEST_SETTINGS_WRITE_PERMISSION = 2;
private static ImageLoader imageCache;
@RootContext
protected Context context;
/**
* Converts a string into a {@link Spannable} that displays the string in the Roboto Condensed font
*
* @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)
*/
public static SpannableString buildCondensedFontString(String string) {
if (string == null) {
return null;
}
SpannableString s = new SpannableString(string);
s.setSpan(new TypefaceSpan("sans-serif-condensed"), 0, s.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return s;
}
/**
* Analyses a torrent http or magnet URI and tries to come up with a reasonable human-readable name.
*
* @param rawTorrentUri The raw http:// or magnet: link to the torrent
* @return A best-guess, reasonably long name for the linked torrent
*/
public static String extractNameFromUri(Uri rawTorrentUri) {
if (rawTorrentUri.getScheme() == null) {
// Probably an incorrect URI; just return the whole thing
return rawTorrentUri.toString();
}
if (rawTorrentUri.getScheme().equals("magnet")) {
// Magnet links might have a dn (display name) parameter
String dn = getQueryParameter(rawTorrentUri, "dn");
if (dn != null && !dn.equals("")) {
return dn;
}
// If not, try to return the hash that is specified as xt (exact topci)
String xt = getQueryParameter(rawTorrentUri, "xt");
if (xt != null && !xt.equals("")) {
return xt;
}
}
if (rawTorrentUri.isHierarchical()) {
String path = rawTorrentUri.getPath();
if (path != null) {
if (path.contains("/")) {
path = path.substring(path.lastIndexOf("/"));
}
return path;
}
}
// No idea what to do with this; return as is
return rawTorrentUri.toString();
}
private static String getQueryParameter(Uri uri, String parameter) {
int start = uri.toString().indexOf(parameter + "=");
if (start >= 0) {
int begin = start + (parameter + "=").length();
int end = uri.toString().indexOf("&", begin);
return uri.toString().substring(begin, end >= 0 ? end : uri.toString().length());
}
return null;
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
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);
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public boolean checkSettingsReadPermission(final Activity activity) {
return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT ||
checkPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_SETTINGS_READ_PERMISSION);
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public boolean checkSettingsWritePermission(final Activity activity) {
return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT ||
checkPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE, REQUEST_SETTINGS_WRITE_PERMISSION);
}
private boolean checkPermission(final Activity activity, final String permission, final int requestCode) {
if (hasPermission(permission))
// Permission already granted
return true;
if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {
// Never asked again: show a dialog with an explanation
activity.runOnUiThread(() ->
new MaterialDialog.Builder(context)
.content(R.string.permission_readtorrent)
.positiveText(android.R.string.ok)
.onPositive((dialog, which) ->
ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode)).show());
return false;
}
// Permission not granted (and we asked for it already before)
ActivityCompat.requestPermissions(activity, new String[]{permission}, REQUEST_TORRENT_READ_PERMISSION);
return false;
}
private boolean hasPermission(String requiredPermission) {
return ContextCompat.checkSelfPermission(context, requiredPermission) == PackageManager.PERMISSION_GRANTED;
}
public Boolean handleTorrentReadPermissionResult(int requestCode, int[] grantResults) {
if (requestCode == REQUEST_TORRENT_READ_PERMISSION) {
// Return permission granting result
return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
}
return null;
}
public Boolean handleSettingsReadPermissionResult(int requestCode, int[] grantResults) {
if (requestCode == REQUEST_SETTINGS_READ_PERMISSION) {
// Return permission granting result
return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
}
return null;
}
public Boolean handleSettingsWritePermissionResult(int requestCode, int[] grantResults) {
if (requestCode == REQUEST_SETTINGS_WRITE_PERMISSION) {
// Return permission granting result
return grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
}
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
*/
public ImageLoader getImageCache() {
if (imageCache == null) {
imageCache = ImageLoader.getInstance();
try {
LruDiskCache diskCache = new LruDiskCache(context.getCacheDir(), null, new Md5FileNameGenerator(), 640000, 25);
// @formatter:off
Builder imageCacheBuilder = new Builder(context)
.defaultDisplayImageOptions(
new DisplayImageOptions.Builder()
.cacheInMemory(true)
.cacheOnDisk(true)
.imageScaleType(ImageScaleType.IN_SAMPLE_INT)
.showImageForEmptyUri(R.drawable.ic_launcher).build())
.memoryCache(new UsingFreqLimitedMemoryCache(1024 * 1024))
.diskCache(diskCache);
imageCache.init(imageCacheBuilder.build());
// @formatter:on
} catch (IOException e) {
// The cache directory is always available on Android; ignore this exception
}
}
return imageCache;
}
public void forceOpenInBrowser(Uri link) {
Intent intent = new Intent(Intent.ACTION_VIEW).setData(link);
List<ResolveInfo> activities = context.getPackageManager().queryIntentActivities(intent, 0);
for (ResolveInfo resolveInfo : activities) {
if (activities.size() == 1 || (resolveInfo.isDefault && resolveInfo.activityInfo.packageName.equals(context.getPackageName()))) {
// There is a default browser; use this
intent.setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);
return;
}
}
// No default browser found: open chooser
try {
context.startActivity(Intent.createChooser(intent, "Open..."));
} 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).
*
* @return The app name and version, such as 'Transdroid 1.5.0 (180)'
*/
public String getAppNameAndVersion() {
return context.getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME + " (" + 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
* 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
*/
public boolean isSmallScreen() {
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
* version.
*
* @return True if search is enabled, false otherwise
*/
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
*/
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
*/
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
*/
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; @@ -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.
*
* @author Eric Kok
*/
public interface RefreshableActivity {
void refreshScreen();
void refreshScreen();
}

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

@ -16,9 +16,6 @@ @@ -16,9 +16,6 @@
*/
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.util.SparseBooleanArray;
import android.view.ActionMode;
@ -28,119 +25,125 @@ import android.view.ViewGroup; @@ -28,119 +25,125 @@ import android.view.ViewGroup;
import android.widget.AbsListView.MultiChoiceModeListener;
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}
* 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.
*
* @author Eric Kok
*/
public class SelectionManagerMode implements MultiChoiceModeListener, OnModificationActionSelectedListener {
private final Context themedContext;
private final ListView managedList;
private final int titleTemplateResource;
private Class<?> onlyCheckClass = null;
private final Context themedContext;
private final ListView managedList;
private final int titleTemplateResource;
private Class<?> onlyCheckClass = null;
/**
* Instantiates the helper by binding it to a specific {@link ListView} and providing the text resource to display
* 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 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;
this.managedList = managedList;
this.titleTemplateResource = titleTemplateResource;
}
/**
* Instantiates the helper by binding it to a specific {@link ListView} and providing the text resource to display
* 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 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;
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
* every list view row can be checked.
* @param onlyCheckClass The {@link Class} instance to use to check list item types against
*/
public void setOnlyCheckClass(Class<?> onlyCheckClass) {
this.onlyCheckClass = onlyCheckClass;
}
/**
* 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.
*
* @param onlyCheckClass The {@link Class} instance to use to check list item types against
*/
public void setOnlyCheckClass(Class<?> onlyCheckClass) {
this.onlyCheckClass = onlyCheckClass;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Allow modification of selection through a spinner
SelectionModificationSpinner selectionSpinner = new SelectionModificationSpinner(themedContext);
selectionSpinner.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT));
selectionSpinner.setOnModificationActionSelectedListener(this);
mode.setCustomView(selectionSpinner);
return true;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Allow modification of selection through a spinner
SelectionModificationSpinner selectionSpinner = new SelectionModificationSpinner(themedContext);
selectionSpinner.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT));
selectionSpinner.setOnModificationActionSelectedListener(this);
mode.setCustomView(selectionSpinner);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
int checkedCount = 0;
for (int i = 0; i < managedList.getCheckedItemPositions().size(); i++) {
if (managedList.getCheckedItemPositions().valueAt(i)
&& (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(managedList
.getCheckedItemPositions().keyAt(i)))))
checkedCount++;
}
((SelectionModificationSpinner) mode.getCustomView()).updateTitle(themedContext.getResources()
.getQuantityString(titleTemplateResource, checkedCount, checkedCount));
}
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
int checkedCount = 0;
for (int i = 0; i < managedList.getCheckedItemPositions().size(); i++) {
if (managedList.getCheckedItemPositions().valueAt(i)
&& (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(managedList
.getCheckedItemPositions().keyAt(i)))))
checkedCount++;
}
((SelectionModificationSpinner) mode.getCustomView()).updateTitle(themedContext.getResources()
.getQuantityString(titleTemplateResource, checkedCount, checkedCount));
}
@Override
public void onDestroyActionMode(ActionMode mode) {
}
@Override
public void onDestroyActionMode(ActionMode mode) {
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return false;
}
/**
* Implements the {@link SelectionModificationSpinner}'s invert selection command by flipping the checked status for
* each (enabled) items in the {@link ListView}.
*/
@Override
public void invertSelection() {
SparseBooleanArray checked = managedList.getCheckedItemPositions();
for (int i = 0; i < managedList.getAdapter().getCount(); i++) {
if (managedList.getAdapter().isEnabled(i)
&& (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(i))))
managedList.setItemChecked(i, !checked.get(i, false));
}
}
/**
* Implements the {@link SelectionModificationSpinner}'s invert selection command by flipping the checked status for
* each (enabled) items in the {@link ListView}.
*/
@Override
public void invertSelection() {
SparseBooleanArray checked = managedList.getCheckedItemPositions();
for (int i = 0; i < managedList.getAdapter().getCount(); i++) {
if (managedList.getAdapter().isEnabled(i)
&& (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(i))))
managedList.setItemChecked(i, !checked.get(i, false));
}
}
/**
* Implements the {@link SelectionModificationSpinner}'s select all command by checking each (enabled) item in the
* {@link ListView}.
*/
@Override
public void selectAll() {
for (int i = 0; i < managedList.getAdapter().getCount(); i++) {
if (managedList.getAdapter().isEnabled(i)
&& (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(i))))
managedList.setItemChecked(i, true);
}
}
/**
* Implements the {@link SelectionModificationSpinner}'s select all command by checking each (enabled) item in the
* {@link ListView}.
*/
@Override
public void selectAll() {
for (int i = 0; i < managedList.getAdapter().getCount(); i++) {
if (managedList.getAdapter().isEnabled(i)
&& (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(i))))
managedList.setItemChecked(i, true);
}
}
/**
* Implements the {@link SelectionModificationSpinner}'s select finished command by checking each (enabled) item
* that represents something that is {@link Finishable} and indeed is finished;
*/
@Override
public void selectFinished() {
for (int i = 0; i < managedList.getAdapter().getCount(); i++) {
if (managedList.getAdapter().isEnabled(i)
&& (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(i)))
&& managedList.getItemAtPosition(i) instanceof Finishable)
managedList.setItemChecked(i, ((Finishable) managedList.getItemAtPosition(i)).isFinished());
}
}
/**
* Implements the {@link SelectionModificationSpinner}'s select finished command by checking each (enabled) item
* that represents something that is {@link Finishable} and indeed is finished;
*/
@Override
public void selectFinished() {
for (int i = 0; i < managedList.getAdapter().getCount(); i++) {
if (managedList.getAdapter().isEnabled(i)
&& (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(i)))
&& managedList.getItemAtPosition(i) instanceof Finishable)
managedList.setItemChecked(i, ((Finishable) managedList.getItemAtPosition(i)).isFinished());
}
}
}

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

@ -16,102 +16,109 @@ @@ -16,102 +16,109 @@
*/
package org.transdroid.core.gui.navigation;
import org.transdroid.R;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
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
* example be used to show the number of selected items.
*
* @author Eric Kok
*/
public class SelectionModificationSpinner extends Spinner {
private SelectionDropDownAdapter selectionAdapter;
private OnModificationActionSelectedListener onModificationActionSelected = null;
/**
* 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
*/
public SelectionModificationSpinner(Context context) {
super(context);
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
*/
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
*/
public void setOnModificationActionSelectedListener(OnModificationActionSelectedListener onModificationActionSelected) {
this.onModificationActionSelected = onModificationActionSelected;
}
@Override
public void setSelection(int position) {
if (position == 0) {
onModificationActionSelected.selectAll();
} else if (position == 1) {
onModificationActionSelected.selectFinished();
} 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> {
protected TextView titleView = null;
public SelectionDropDownAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1, new String[] {
context.getString(R.string.navigation_selectall),
context.getString(R.string.navigation_selectfinished),
context.getString(R.string.navigation_invertselection) });
titleView = new TextView(getContext());
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// This returns the singleton text view showing the title with the number of selected items
return titleView;
}
@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);
}
}
/**
* Interface to implement if an interface want to respond to selection modification actions.
*/
public interface OnModificationActionSelectedListener {
public void selectAll();
public void selectFinished();
public void invertSelection();
}
public class SelectionModificationSpinner extends AppCompatSpinner {
private SelectionDropDownAdapter selectionAdapter;
private OnModificationActionSelectedListener onModificationActionSelected = null;
/**
* 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
*/
public SelectionModificationSpinner(Context context) {
super(context);
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
*/
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
*/
public void setOnModificationActionSelectedListener(OnModificationActionSelectedListener onModificationActionSelected) {
this.onModificationActionSelected = onModificationActionSelected;
}
@Override
public void setSelection(int position) {
if (position == 0) {
onModificationActionSelected.selectAll();
} else if (position == 1) {
onModificationActionSelected.selectFinished();
} else if (position == 2) {
onModificationActionSelected.invertSelection();
}
super.setSelection(position);
}
/**
* Interface to implement if an interface want to respond to selection modification actions.
*/
public interface OnModificationActionSelectedListener {
void selectAll();
void selectFinished();
void invertSelection();
}
/**
* 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 static class SelectionDropDownAdapter extends ArrayAdapter<String> {
protected TextView titleView = null;
public SelectionDropDownAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1, new String[]{
context.getString(R.string.navigation_selectall),
context.getString(R.string.navigation_selectfinished),
context.getString(R.string.navigation_invertselection)});
titleView = new TextView(getContext());
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// This returns the singleton text view showing the title with the number of selected items
return titleView;
}
@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; @@ -20,8 +20,6 @@ import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.EditText;
import android.widget.ListView;
@ -37,71 +35,64 @@ import java.util.List; @@ -37,71 +35,64 @@ import java.util.List;
public class SetLabelDialog {
/**
* 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 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) {
/**
* 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 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) {
// Discard the empty label in this list before storing it locally
for (Iterator<Label> iter = currentLabels.iterator(); iter.hasNext(); ) {
if (iter.next().isEmptyLabel()) {
iter.remove();
}
}
// Discard the empty label in this list before storing it locally
for (Iterator<Label> iter = currentLabels.iterator(); iter.hasNext(); ) {
if (iter.next().isEmptyLabel()) {
iter.remove();
}
}
final View setLabelLayout = LayoutInflater.from(context).inflate(R.layout.dialog_setlabel, null);
final ListView labelsList = (ListView) setLabelLayout.findViewById(R.id.labels_list);
final EditText newLabelEdit = (EditText) setLabelLayout.findViewById(R.id.newlabel_edit);
final View setLabelLayout = LayoutInflater.from(context).inflate(R.layout.dialog_setlabel, null);
final ListView labelsList = (ListView) setLabelLayout.findViewById(R.id.labels_list);
final EditText newLabelEdit = (EditText) setLabelLayout.findViewById(R.id.newlabel_edit);
MaterialDialog.Builder builder = new MaterialDialog.Builder(context)
.customView(setLabelLayout, false)
.positiveText(R.string.status_update)
.neutralText(R.string.status_label_remove)
.negativeText(android.R.string.cancel)
.callback(new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
// User should have provided a new label
if (TextUtils.isEmpty(newLabelEdit.getText())) {
SnackbarManager.show(Snackbar.with(context).text(R.string.error_notalabel).colorResource(R.color.red));
return;
}
onLabelPickedListener.onLabelPicked(newLabelEdit.getText().toString());
}
MaterialDialog.Builder builder = new MaterialDialog.Builder(context)
.customView(setLabelLayout, false)
.positiveText(R.string.status_update)
.neutralText(R.string.status_label_remove)
.negativeText(android.R.string.cancel)
.onPositive((dialog, which) -> {
// User should have provided a new label
if (TextUtils.isEmpty(newLabelEdit.getText())) {
SnackbarManager.show(Snackbar.with(context).text(R.string.error_notalabel).colorResource(R.color.red));
return;
}
onLabelPickedListener.onLabelPicked(newLabelEdit.getText().toString());
})
.onNeutral((dialog, which) ->
onLabelPickedListener.onLabelPicked(null));
@Override
public void onNeutral(MaterialDialog dialog) {
onLabelPickedListener.onLabelPicked(null);
}
});
final MaterialDialog dialog = SettingsUtils
.applyDialogTheme(builder)
.build();
final MaterialDialog dialog = SettingsUtils
.applyDialogTheme(builder)
.build();
if (currentLabels.size() == 0) {
// Hide the list (and its label) if there are no labels yet
setLabelLayout.findViewById(R.id.pick_label).setVisibility(View.GONE);
labelsList.setVisibility(View.GONE);
} else {
labelsList.setAdapter(new FilterListItemAdapter(context, currentLabels));
labelsList.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
onLabelPickedListener.onLabelPicked(((Label) labelsList.getItemAtPosition(position)).getName());
dialog.dismiss();
}
});
}
if (currentLabels.size() == 0) {
// Hide the list (and its label) if there are no labels yet
setLabelLayout.findViewById(R.id.pick_label).setVisibility(View.GONE);
labelsList.setVisibility(View.GONE);
} else {
labelsList.setAdapter(new FilterListItemAdapter(context, currentLabels));
labelsList.setOnItemClickListener((parent, view, position, id) -> {
onLabelPickedListener.onLabelPicked(((Label) labelsList.getItemAtPosition(position)).getName());
dialog.dismiss();
});
}
dialog.show();
dialog.show();
}
}
public interface OnLabelPickedListener {
void onLabelPicked(String newLabel);
}
public interface OnLabelPickedListener {
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; @@ -28,35 +28,33 @@ import org.transdroid.core.app.settings.SettingsUtils;
public class SetStorageLocationDialog {
/**
* 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 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);
final EditText locationText = (EditText) locationLayout.findViewById(R.id.location_edit);
locationText.setText(currentLocation);
MaterialDialog.Builder builder = new MaterialDialog.Builder(context)
.customView(locationLayout, false)
.positiveText(R.string.status_update)
.negativeText(android.R.string.cancel)
.callback(new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
// User is done editing and requested to update given the text input
onStorageLocationUpdatedListener.onStorageLocationUpdated(locationText.getText().toString());
}
});
SettingsUtils
.applyDialogTheme(builder)
.show();
}
public interface OnStorageLocationUpdatedListener {
void onStorageLocationUpdated(String newLocation);
}
/**
* 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 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);
final EditText locationText = (EditText) locationLayout.findViewById(R.id.location_edit);
locationText.setText(currentLocation);
MaterialDialog.Builder builder = new MaterialDialog.Builder(context)
.customView(locationLayout, false)
.positiveText(R.string.status_update)
.negativeText(android.R.string.cancel)
.onPositive((dialog, which) -> {
// User is done editing and requested to update given the text input
onStorageLocationUpdatedListener.onStorageLocationUpdated(locationText.getText().toString());
});
SettingsUtils
.applyDialogTheme(builder)
.show();
}
public interface OnStorageLocationUpdatedListener {
void onStorageLocationUpdated(String newLocation);
}
}

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

@ -16,12 +16,13 @@ @@ -16,12 +16,13 @@
*/
package org.transdroid.core.gui.navigation;
import android.app.DialogFragment;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import androidx.fragment.app.DialogFragment;
import com.afollestad.materialdialogs.MaterialDialog;
import org.transdroid.R;
@ -32,32 +33,30 @@ import java.util.List; @@ -32,32 +33,30 @@ import java.util.List;
public class SetTrackersDialog extends DialogFragment {
/**
* 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 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);
final EditText trackersText = (EditText) trackersLayout.findViewById(R.id.trackers_edit);
trackersText.setText(currentTrackers);
MaterialDialog.Builder builder = new MaterialDialog.Builder(context)
.customView(trackersLayout, false)
.positiveText(R.string.status_update)
.negativeText(android.R.string.cancel)
.callback(new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
// User is done editing and requested to update given the text input
onTrackersUpdatedListener.onTrackersUpdated(Arrays.asList(trackersText.getText().toString().split("\n")));
}
});
SettingsUtils.applyDialogTheme(builder).show();
}
public interface OnTrackersUpdatedListener {
void onTrackersUpdated(List<String> updatedTrackers);
}
/**
* 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 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);
final EditText trackersText = (EditText) trackersLayout.findViewById(R.id.trackers_edit);
trackersText.setText(currentTrackers);
MaterialDialog.Builder builder = new MaterialDialog.Builder(context)
.customView(trackersLayout, false)
.positiveText(R.string.status_update)
.negativeText(android.R.string.cancel)
.onPositive((dialog, which) -> {
// User is done editing and requested to update given the text input
onTrackersUpdatedListener.onTrackersUpdated(Arrays.asList(trackersText.getText().toString().split("\n")));
});
SettingsUtils.applyDialogTheme(builder).show();
}
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; @@ -30,85 +30,78 @@ import org.transdroid.core.app.settings.SettingsUtils;
public class SetTransferRatesDialog {
/**
* A dialog fragment that allow picking of maximum download and upload transfer rates as well as the resetting of these values.
* @param context The activity context that opens (and owns) this dialog
* @param onRatesPickedListener The callback for results in this dialog (with newly selected values or a reset)
*/
public static void show(final Context context, final OnRatesPickedListener onRatesPickedListener) {
View transferRatesLayout = LayoutInflater.from(context).inflate(R.layout.dialog_transferrates, null);
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)
.customView(transferRatesLayout, false)
.positiveText(R.string.status_update)
.neutralText(R.string.status_maxspeed_reset)
.negativeText(android.R.string.cancel)
.callback(new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
int maxDown = -1, maxUp = -1;
try {
maxDown = Integer.parseInt(maxSpeedDown.getText().toString());
maxUp = Integer.parseInt(maxSpeedUp.getText().toString());
} catch (NumberFormatException e) {
// Impossible as we only input via the number buttons
}
if (maxDown <= 0 || maxUp <= 0) {
onRatesPickedListener.onInvalidNumber();
return;
}
onRatesPickedListener.onRatesPicked(maxDown, maxUp);
}
@Override
public void onNeutral(MaterialDialog dialog) {
onRatesPickedListener.resetRates();
}
});
MaterialDialog dialog = SettingsUtils.applyDialogTheme(builder).build();
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,
R.id.up6Button, R.id.up7Button, R.id.up8Button, R.id.up9Button, R.id.up0Button);
dialog.show();
}
private static void bindButtons(View transferRatesContent, View numberView, int... buttonResource) {
for (int i : buttonResource) {
// Keep the relevant number as reference in the view tag and bind the click listerner
transferRatesContent.findViewById(i).setTag(numberView);
transferRatesContent.findViewById(i).setOnClickListener(onNumberClicked);
}
}
private static OnClickListener onNumberClicked = new OnClickListener() {
@Override
public void onClick(View v) {
// 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("");
}
numberView.setText(numberView.getText().toString() + ((Button) v).getText().toString());
}
};
/**
* Listener interface to the user having picked or wanting to resets the current maximum transfer speeds;
*/
public interface OnRatesPickedListener {
void onRatesPicked(int maxDownloadSpeed, int maxUploadSpeed);
void resetRates();
void onInvalidNumber();
}
private static OnClickListener onNumberClicked = v -> {
// 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("");
}
numberView.setText(numberView.getText().toString() + ((Button) v).getText().toString());
};
/**
* A dialog fragment that allow picking of maximum download and upload transfer rates as well as the resetting of these values.
*
* @param context The activity context that opens (and owns) this dialog
* @param onRatesPickedListener The callback for results in this dialog (with newly selected values or a reset)
*/
public static void show(final Context context, final OnRatesPickedListener onRatesPickedListener) {
View transferRatesLayout = LayoutInflater.from(context).inflate(R.layout.dialog_transferrates, null);
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)
.customView(transferRatesLayout, false)
.positiveText(R.string.status_update)
.neutralText(R.string.status_maxspeed_reset)
.negativeText(android.R.string.cancel)
.onPositive((dialog, which) -> {
int maxDown = -1, maxUp = -1;
try {
maxDown = Integer.parseInt(maxSpeedDown.getText().toString());
maxUp = Integer.parseInt(maxSpeedUp.getText().toString());
} catch (NumberFormatException e) {
// Impossible as we only input via the number buttons
}
if (maxDown <= 0 || maxUp <= 0) {
onRatesPickedListener.onInvalidNumber();
return;
}
onRatesPickedListener.onRatesPicked(maxDown, maxUp);
})
.onNeutral((dialog, which) ->
onRatesPickedListener.resetRates());
MaterialDialog dialog = SettingsUtils.applyDialogTheme(builder).build();
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,
R.id.up6Button, R.id.up7Button, R.id.up8Button, R.id.up9Button, R.id.up0Button);
dialog.show();
}
private static void bindButtons(View transferRatesContent, View numberView, int... buttonResource) {
for (int i : buttonResource) {
// Keep the relevant number as reference in the view tag and bind the click listerner
transferRatesContent.findViewById(i).setTag(numberView);
transferRatesContent.findViewById(i).setOnClickListener(onNumberClicked);
}
}
/**
* Listener interface to the user having picked or wanting to resets the current maximum transfer speeds;
*/
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 @@ @@ -16,150 +16,154 @@
*/
package org.transdroid.core.gui.navigation;
import java.util.Arrays;
import java.util.List;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import org.transdroid.R;
import org.transdroid.core.gui.lists.SimpleListItem;
import org.transdroid.daemon.Torrent;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Arrays;
import java.util.List;
/**
* Enumeration of all status types, which filter the list of shown torrents based on transfer activity.
*
* @author Eric Kok
*/
public enum StatusType {
ShowAll {
public StatusTypeFilter getFilterItem(Context context) {
return new StatusTypeFilter(StatusType.ShowAll, context.getString(R.string.navigation_status_showall));
}
},
OnlyDownloading {
public StatusTypeFilter getFilterItem(Context context) {
return new StatusTypeFilter(StatusType.OnlyDownloading, context.getString(R.string.navigation_status_onlydown));
}
},
OnlyUploading {
public StatusTypeFilter getFilterItem(Context context) {
return new StatusTypeFilter(StatusType.OnlyUploading, context.getString(R.string.navigation_status_onlyup));
}
},
OnlyActive {
public StatusTypeFilter getFilterItem(Context context) {
return new StatusTypeFilter(StatusType.OnlyActive, context.getString(R.string.navigation_status_onlyactive));
}
},
OnlyInactive {
public StatusTypeFilter getFilterItem(Context context) {
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.
* @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);
}
/**
* 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
* @return A list of filter items for all available status types
*/
public static List<StatusTypeFilter> getAllStatusTypes(Context 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
*/
public abstract StatusTypeFilter getFilterItem(Context context);
public static class StatusTypeFilter implements SimpleListItem, NavigationFilter {
private final StatusType statusType;
private final String name;
StatusTypeFilter(StatusType statusType, String name) {
this.statusType = statusType;
this.name = name;
}
public StatusType getStatusType() {
return statusType;
}
@Override
public String getName() {
return name;
}
@Override
public String getCode() {
// Uses the class name and status type enum to provide a unique navigation filter code
return StatusTypeFilter.class.getSimpleName() + "_" + statusType.name();
}
/**
* Returns true if the torrent status matches this (selected) status type, false otherwise
* @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
*/
@Override
public boolean matches(Torrent torrent, boolean dormantAsInactive) {
switch (statusType) {
case OnlyDownloading:
return torrent.isDownloading(dormantAsInactive);
case OnlyUploading:
return torrent.isSeeding(dormantAsInactive);
case OnlyActive:
return torrent.isDownloading(dormantAsInactive)
|| torrent.isSeeding(dormantAsInactive);
case OnlyInactive:
return !torrent.isDownloading(dormantAsInactive) && !torrent.isSeeding(dormantAsInactive);
default:
return true;
}
}
private StatusTypeFilter(Parcel in) {
this.statusType = StatusType.valueOf(in.readString());
this.name = in.readString();
}
public static final Parcelable.Creator<StatusTypeFilter> CREATOR = new Parcelable.Creator<StatusTypeFilter>() {
public StatusTypeFilter createFromParcel(Parcel in) {
return new StatusTypeFilter(in);
}
public StatusTypeFilter[] newArray(int size) {
return new StatusTypeFilter[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(statusType.name());
dest.writeString(name);
}
}
ShowAll {
public StatusTypeFilter getFilterItem(Context context) {
return new StatusTypeFilter(StatusType.ShowAll, context.getString(R.string.navigation_status_showall));
}
},
OnlyDownloading {
public StatusTypeFilter getFilterItem(Context context) {
return new StatusTypeFilter(StatusType.OnlyDownloading, context.getString(R.string.navigation_status_onlydown));
}
},
OnlyUploading {
public StatusTypeFilter getFilterItem(Context context) {
return new StatusTypeFilter(StatusType.OnlyUploading, context.getString(R.string.navigation_status_onlyup));
}
},
OnlyActive {
public StatusTypeFilter getFilterItem(Context context) {
return new StatusTypeFilter(StatusType.OnlyActive, context.getString(R.string.navigation_status_onlyactive));
}
},
OnlyInactive {
public StatusTypeFilter getFilterItem(Context context) {
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.
*
* @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);
}
/**
* 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
* @return A list of filter items for all available status types
*/
public static List<StatusTypeFilter> getAllStatusTypes(Context 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
*/
public abstract StatusTypeFilter getFilterItem(Context context);
public static class StatusTypeFilter implements SimpleListItem, NavigationFilter {
public static final Parcelable.Creator<StatusTypeFilter> CREATOR = new Parcelable.Creator<StatusTypeFilter>() {
public StatusTypeFilter createFromParcel(Parcel in) {
return new StatusTypeFilter(in);
}
public StatusTypeFilter[] newArray(int size) {
return new StatusTypeFilter[size];
}
};
private final StatusType statusType;
private final String name;
StatusTypeFilter(StatusType statusType, String name) {
this.statusType = statusType;
this.name = name;
}
private StatusTypeFilter(Parcel in) {
this.statusType = StatusType.valueOf(in.readString());
this.name = in.readString();
}
public StatusType getStatusType() {
return statusType;
}
@Override
public String getName() {
return name;
}
@Override
public String getCode() {
// Uses the class name and status type enum to provide a unique navigation filter code
return StatusTypeFilter.class.getSimpleName() + "_" + statusType.name();
}
/**
* Returns true if the torrent status matches this (selected) status type, false otherwise
*
* @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
*/
@Override
public boolean matches(Torrent torrent, boolean dormantAsInactive) {
switch (statusType) {
case OnlyDownloading:
return torrent.isDownloading(dormantAsInactive);
case OnlyUploading:
return torrent.isSeeding(dormantAsInactive);
case OnlyActive:
return torrent.isDownloading(dormantAsInactive)
|| torrent.isSeeding(dormantAsInactive);
case OnlyInactive:
return !torrent.isDownloading(dormantAsInactive) && !torrent.isSeeding(dormantAsInactive);
default:
return true;
}
}
@Override
public int describeContents() {
return 0;
}
@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 @@ @@ -17,13 +17,14 @@
package org.transdroid.core.gui.remoterss;
import androidx.fragment.app.Fragment;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EFragment;
@ -44,102 +45,102 @@ import java.util.List; @@ -44,102 +45,102 @@ import java.util.List;
/**
* 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.
*
* @author Twig
*/
@EFragment(R.layout.fragment_remoterss)
public class RemoteRssFragment extends Fragment {
@Bean
protected Log log;
// Local data
protected ArrayList<RemoteRssItem> remoteRssItems;
// Views
@ViewById
protected View detailsContainer;
@ViewById(R.id.remoterss_filter)
protected Spinner remoteRssFilter;
@ViewById
protected ListView torrentsList;
@ViewById(R.id.remoterss_status_message)
protected TextView remoteRssStatusMessage;
@AfterViews
protected void init() {
// Inject menu options in the actions toolbar
setHasOptionsMenu(true);
// Set up details adapter
RemoteRssItemsAdapter adapter = new RemoteRssItemsAdapter(getActivity());
torrentsList.setAdapter(adapter);
torrentsList.setFastScrollEnabled(true);
}
@Override
public void onResume() {
super.onResume();
this.refreshScreen();
}
@OptionsItem(R.id.action_refresh)
protected void refreshScreen() {
RssFeedsActivity rssActivity = (RssFeedsActivity) getActivity();
rssActivity.refreshRemoteFeeds();
}
@OptionsItem(R.id.action_settings)
protected void openSettings() {
MainSettingsActivity_.intent(getActivity()).start();
}
/**
* Updates the UI with a new list of RSS items.
*/
public void updateRemoteItems(List<RemoteRssItem> remoteItems, boolean scrollToTop) {
RemoteRssItemsAdapter adapter = (RemoteRssItemsAdapter) torrentsList.getAdapter();
remoteRssItems = new ArrayList<>(remoteItems);
adapter.updateItems(remoteRssItems);
if (scrollToTop) {
torrentsList.smoothScrollToPosition(0);
}
// Show/hide a nice message if there are no items to show
if (remoteRssItems.size() > 0) {
remoteRssStatusMessage.setVisibility(View.GONE);
}
else {
remoteRssStatusMessage.setVisibility(View.VISIBLE);
remoteRssStatusMessage.setText(R.string.remoterss_no_files);
}
}
public void updateChannelFilters(List<RemoteRssChannel> feedLabels) {
List<String> labels = new ArrayList<>();
for (RemoteRssChannel feedLabel : feedLabels) {
labels.add(feedLabel.getName());
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(this.getContext(), android.R.layout.simple_spinner_dropdown_item, labels);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
remoteRssFilter.setAdapter(adapter);
}
/**
* When the user clicks on an item, prepare to download it.
*/
@ItemClick(resName = "torrents_list")
protected void detailsListClicked(int position) {
RemoteRssItemsAdapter adapter = (RemoteRssItemsAdapter) torrentsList.getAdapter();
RemoteRssItem item = (RemoteRssItem) adapter.getItem(position);
((RssFeedsActivity) getActivity()).downloadRemoteRssItem(item);
}
@ItemSelect(R.id.remoterss_filter)
protected void onFeedSelected(boolean selected, int position) {
((RssFeedsActivity) getActivity()).onFeedSelected(position);
}
@Bean
protected Log log;
// Local data
protected ArrayList<RemoteRssItem> remoteRssItems;
// Views
@ViewById
protected View detailsContainer;
@ViewById(R.id.remoterss_filter)
protected Spinner remoteRssFilter;
@ViewById
protected ListView torrentsList;
@ViewById(R.id.remoterss_status_message)
protected TextView remoteRssStatusMessage;
@AfterViews
protected void init() {
// Inject menu options in the actions toolbar
setHasOptionsMenu(true);
// Set up details adapter
RemoteRssItemsAdapter adapter = new RemoteRssItemsAdapter(getActivity());
torrentsList.setAdapter(adapter);
torrentsList.setFastScrollEnabled(true);
}
@Override
public void onResume() {
super.onResume();
this.refreshScreen();
}
@OptionsItem(R.id.action_refresh)
protected void refreshScreen() {
RssFeedsActivity rssActivity = (RssFeedsActivity) getActivity();
rssActivity.refreshRemoteFeeds();
}
@OptionsItem(R.id.action_settings)
protected void openSettings() {
MainSettingsActivity_.intent(getActivity()).start();
}
/**
* Updates the UI with a new list of RSS items.
*/
public void updateRemoteItems(List<RemoteRssItem> remoteItems, boolean scrollToTop) {
RemoteRssItemsAdapter adapter = (RemoteRssItemsAdapter) torrentsList.getAdapter();
remoteRssItems = new ArrayList<>(remoteItems);
adapter.updateItems(remoteRssItems);
if (scrollToTop) {
torrentsList.smoothScrollToPosition(0);
}
// Show/hide a nice message if there are no items to show
if (remoteRssItems.size() > 0) {
remoteRssStatusMessage.setVisibility(View.GONE);
} else {
remoteRssStatusMessage.setVisibility(View.VISIBLE);
remoteRssStatusMessage.setText(R.string.remoterss_no_files);
}
}
public void updateChannelFilters(List<RemoteRssChannel> feedLabels) {
List<String> labels = new ArrayList<>();
for (RemoteRssChannel feedLabel : feedLabels) {
labels.add(feedLabel.getName());
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(this.getContext(), android.R.layout.simple_spinner_dropdown_item, labels);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
remoteRssFilter.setAdapter(adapter);
}
/**
* When the user clicks on an item, prepare to download it.
*/
@ItemClick(resName = "torrents_list")
protected void detailsListClicked(int position) {
RemoteRssItemsAdapter adapter = (RemoteRssItemsAdapter) torrentsList.getAdapter();
RemoteRssItem item = (RemoteRssItem) adapter.getItem(position);
((RssFeedsActivity) getActivity()).downloadRemoteRssItem(item);
}
@ItemSelect(R.id.remoterss_filter)
protected void onFeedSelected(boolean selected, int 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; @@ -28,26 +28,27 @@ import org.transdroid.core.gui.remoterss.data.RemoteRssItem;
/**
* View that represents some {@link RemoteRssItem} object.
*
* @author Twig
*/
@EViewGroup(R.layout.list_item_remoterssitem)
public class RemoteRssItemView extends LinearLayout {
// Views
@ViewById
protected TextView nameText, dateText, labelText;
// Views
@ViewById
protected TextView nameText, dateText, labelText;
public RemoteRssItemView(Context context) {
super(context);
}
public RemoteRssItemView(Context context) {
super(context);
}
public void bind(RemoteRssItem item) {
labelText.setText(item.getSourceName());
nameText.setText(item.getName());
dateText.setText(
DateFormat.getDateFormat(getContext()).format(item.getTimestamp()) +
" " +
DateFormat.getTimeFormat(getContext()).format(item.getTimestamp())
);
}
public void bind(RemoteRssItem item) {
labelText.setText(item.getSourceName());
nameText.setText(item.getName());
dateText.setText(
DateFormat.getDateFormat(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; @@ -11,47 +11,46 @@ import java.util.ArrayList;
import java.util.List;
public class RemoteRssItemsAdapter extends BaseAdapter {
protected Context context;
protected List<RemoteRssItem> items;
public RemoteRssItemsAdapter(Context context) {
this.context = context;
items = new ArrayList<>();
}
@Override
public int getCount() {
return items.size();
}
@Override
public Object getItem(int position) {
return items.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
RemoteRssItemView itemView;
if (convertView == null) {
itemView = RemoteRssItemView_.build(context);
}
else {
itemView = (RemoteRssItemView) convertView;
}
itemView.bind((RemoteRssItem) getItem(position));
return itemView;
}
public void updateItems(List<RemoteRssItem> remoteItems) {
items = remoteItems;
notifyDataSetChanged();
}
protected Context context;
protected List<RemoteRssItem> items;
public RemoteRssItemsAdapter(Context context) {
this.context = context;
items = new ArrayList<>();
}
@Override
public int getCount() {
return items.size();
}
@Override
public Object getItem(int position) {
return items.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
RemoteRssItemView itemView;
if (convertView == null) {
itemView = RemoteRssItemView_.build(context);
} else {
itemView = (RemoteRssItemView) convertView;
}
itemView.bind((RemoteRssItem) getItem(position));
return itemView;
}
public void updateItems(List<RemoteRssItem> remoteItems) {
items = remoteItems;
notifyDataSetChanged();
}
}

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

@ -22,17 +22,18 @@ import android.net.Uri; @@ -22,17 +22,18 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
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.widget.Toolbar;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import com.nispok.snackbar.Snackbar;
import com.nispok.snackbar.SnackbarManager;
import com.nispok.snackbar.enums.SnackbarType;
@ -71,372 +72,364 @@ import org.transdroid.daemon.task.DaemonTaskSuccessResult; @@ -71,372 +72,364 @@ import org.transdroid.daemon.task.DaemonTaskSuccessResult;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
@EActivity(R.layout.activity_rssfeeds)
public class RssFeedsActivity extends AppCompatActivity {
// Settings and local data
@Bean
protected Log log;
@Bean
protected ApplicationSettings applicationSettings;
protected static final int RSS_FEEDS_LOCAL = 0;
protected static final int RSS_FEEDS_REMOTE = 1;
@FragmentById(R.id.rssfeeds_fragment)
protected RssFeedsFragment fragmentLocalFeeds;
@FragmentById(R.id.rssitems_fragment)
protected RssItemsFragment fragmentItems;
@FragmentById(R.id.remoterss_fragment)
protected RemoteRssFragment fragmentRemoteFeeds;
@ViewById(R.id.rssfeeds_toolbar)
protected Toolbar rssFeedsToolbar;
@ViewById(R.id.rssfeeds_tabs)
protected TabLayout tabLayout;
@ViewById(R.id.rssfeeds_pager)
protected ViewPager viewPager;
// remote RSS stuff
@NonConfigurationInstance
protected ArrayList<RemoteRssChannel> feeds;
@InstanceState
protected int selectedFilter;
@NonConfigurationInstance
protected ArrayList<RemoteRssItem> recentItems;
@Bean
protected ConnectivityHelper connectivityHelper;
protected class LayoutPagerAdapter extends PagerAdapter {
boolean hasRemoteRss;
String serverName;
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);
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
SettingsUtils.applyDayNightTheme(this);
super.onCreate(savedInstanceState);
}
@AfterViews
protected void init() {
setSupportActionBar(rssFeedsToolbar);
getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(getString(R.string.rss_feeds)));
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
IDaemonAdapter currentConnection = this.getCurrentConnection();
boolean hasRemoteRss = Daemon.supportsRemoteRssManagement(currentConnection.getType());
PagerAdapter pagerAdapter = new LayoutPagerAdapter(hasRemoteRss, currentConnection.getSettings().getName());
viewPager.setAdapter(pagerAdapter);
tabLayout.setupWithViewPager(viewPager);
// if local feeds dont have any entries but remote does, show it instead
int defaultTab = RSS_FEEDS_LOCAL;
if (hasRemoteRss && applicationSettings.getRssfeedSettings().size() == 0) {
if (currentConnection instanceof RemoteRssSupplier) {
RemoteRssSupplier remoteConnection = ((RemoteRssSupplier) (currentConnection));
boolean hasRemoteFeeds = false;
try {
hasRemoteFeeds = remoteConnection.getRemoteRssChannels(log).size() > 0;
} catch (DaemonException e) {}
if (hasRemoteFeeds) {
defaultTab = RSS_FEEDS_REMOTE;
}
}
}
viewPager.setCurrentItem(defaultTab);
if (!hasRemoteRss) {
tabLayout.setVisibility(View.GONE);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@OptionsItem(android.R.id.home)
protected void navigateUp() {
TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
}
/**
* Reload the RSS feed settings and start loading all the feeds. To be called from contained fragments.
*/
public void refreshFeeds() {
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()) {
RssfeedLoader loader = new RssfeedLoader(setting);
loaders.add(loader);
loadRssfeed(loader);
}
fragmentLocalFeeds.update(loaders);
}
/**
* 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
*/
@Background
protected void loadRssfeed(RssfeedLoader loader) {
try {
// Load and parse the feed
RssParser parser =
new RssParser(loader.getSetting().getUrl(), loader.getSetting().getExcludeFilter(), loader.getSetting().getIncludeFilter());
parser.parse();
handleRssfeedResult(loader, parser.getChannel(), false);
} catch (Exception e) {
// Catch any error that may occurred and register this failure
handleRssfeedResult(loader, null, true);
log.i(this, "RSS feed " + loader.getSetting().getUrl() + " error: " + e.toString());
}
}
/**
* 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
* @param hasError True if a connection error occurred in the loading of the feed; false otherwise
*/
@UiThread
protected void handleRssfeedResult(RssfeedLoader loader, Channel channel, boolean hasError) {
loader.update(channel, hasError);
fragmentLocalFeeds.notifyDataSetChanged();
}
/**
* 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
* @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) {
// 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);
}
protected static final int RSS_FEEDS_LOCAL = 0;
protected static final int RSS_FEEDS_REMOTE = 1;
// Settings and local data
@Bean
protected Log log;
@Bean
protected ApplicationSettings applicationSettings;
@FragmentById(R.id.rssfeeds_fragment)
protected RssFeedsFragment fragmentLocalFeeds;
@FragmentById(R.id.rssitems_fragment)
protected RssItemsFragment fragmentItems;
@FragmentById(R.id.remoterss_fragment)
protected RemoteRssFragment fragmentRemoteFeeds;
@ViewById(R.id.rssfeeds_toolbar)
protected Toolbar rssFeedsToolbar;
@ViewById(R.id.rssfeeds_tabs)
protected TabLayout tabLayout;
@ViewById(R.id.rssfeeds_pager)
protected ViewPager viewPager;
// remote RSS stuff
@NonConfigurationInstance
protected ArrayList<RemoteRssChannel> feeds;
@InstanceState
protected int selectedFilter;
@NonConfigurationInstance
protected ArrayList<RemoteRssItem> recentItems;
@Bean
protected ConnectivityHelper connectivityHelper;
@Override
public void onCreate(Bundle savedInstanceState) {
SettingsUtils.applyDayNightTheme(this);
super.onCreate(savedInstanceState);
}
@AfterViews
protected void init() {
setSupportActionBar(rssFeedsToolbar);
getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(getString(R.string.rss_feeds)));
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
IDaemonAdapter currentConnection = this.getCurrentConnection();
boolean hasRemoteRss = Daemon.supportsRemoteRssManagement(currentConnection.getType());
PagerAdapter pagerAdapter = new LayoutPagerAdapter(hasRemoteRss, currentConnection.getSettings().getName());
viewPager.setAdapter(pagerAdapter);
tabLayout.setupWithViewPager(viewPager);
// if local feeds dont have any entries but remote does, show it instead
int defaultTab = RSS_FEEDS_LOCAL;
if (hasRemoteRss && applicationSettings.getRssfeedSettings().size() == 0) {
if (currentConnection instanceof RemoteRssSupplier) {
RemoteRssSupplier remoteConnection = ((RemoteRssSupplier) (currentConnection));
boolean hasRemoteFeeds = false;
try {
hasRemoteFeeds = remoteConnection.getRemoteRssChannels(log).size() > 0;
} catch (DaemonException ignored) {
}
if (hasRemoteFeeds) {
defaultTab = RSS_FEEDS_REMOTE;
}
}
}
viewPager.setCurrentItem(defaultTab);
if (!hasRemoteRss) {
tabLayout.setVisibility(View.GONE);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@OptionsItem(android.R.id.home)
protected void navigateUp() {
TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
}
/**
* Reload the RSS feed settings and start loading all the feeds. To be called from contained fragments.
*/
public void refreshFeeds() {
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()) {
RssfeedLoader loader = new RssfeedLoader(setting);
loaders.add(loader);
loadRssfeed(loader);
}
fragmentLocalFeeds.update(loaders);
}
/**
* 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
*/
@Background
protected void loadRssfeed(RssfeedLoader loader) {
try {
// Load and parse the feed
RssParser parser =
new RssParser(loader.getSetting().getUrl(), loader.getSetting().getExcludeFilter(), loader.getSetting().getIncludeFilter());
parser.parse();
handleRssfeedResult(loader, parser.getChannel(), false);
} catch (Exception e) {
// Catch any error that may occurred and register this failure
handleRssfeedResult(loader, null, true);
log.i(this, "RSS feed " + loader.getSetting().getUrl() + " error: " + e.toString());
}
}
/**
* 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
* @param hasError True if a connection error occurred in the loading of the feed; false otherwise
*/
@UiThread
protected void handleRssfeedResult(RssfeedLoader loader, Channel channel, boolean hasError) {
loader.update(channel, hasError);
fragmentLocalFeeds.notifyDataSetChanged();
}
/**
* 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
* @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) {
// 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
public void refreshRemoteFeeds() {
// Connect to the last used server
IDaemonAdapter currentConnection = this.getCurrentConnection();
// remote rss not supported for this connection type
if (currentConnection instanceof RemoteRssSupplier == false) {
return;
}
try {
feeds = ((RemoteRssSupplier) (currentConnection)).getRemoteRssChannels(log);
// By default it displays the latest items within the last month.
recentItems = new ArrayList<>();
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MONTH, -1);
Date oneMonthAgo = calendar.getTime();
for (RemoteRssChannel feed : feeds) {
for (RemoteRssItem item : feed.getItems()) {
if (item.getTimestamp().after(oneMonthAgo)) {
recentItems.add(item);
}
}
}
// Sort by -newest
Collections.sort(recentItems, new Comparator<RemoteRssItem>() {
@Override
public int compare(RemoteRssItem lhs, RemoteRssItem rhs) {
return rhs.getTimestamp().compareTo(lhs.getTimestamp());
}
});
} catch (DaemonException e) {
onCommunicationError(e);
return;
}
// @UIThread
fragmentRemoteFeeds.updateRemoteItems(
selectedFilter == 0 ? recentItems : feeds.get(selectedFilter -1).getItems(),
false /* allow android to restore scroll position */ );
showRemoteChannelFilters();
}
@UiThread
protected void onCommunicationError(DaemonException daemonException) {
//noinspection ThrowableResultOfMethodCallIgnored
log.i(this, daemonException.toString());
String error = getString(LocalTorrent.getResourceForDaemonException(daemonException));
SnackbarManager.show(Snackbar.with(this).text(error).colorResource(R.color.red).type(SnackbarType.MULTI_LINE));
}
public void onFeedSelected(int position) {
selectedFilter = position;
if (position == 0) {
fragmentRemoteFeeds.updateRemoteItems(recentItems, true);
}
else {
RemoteRssChannel channel = feeds.get(selectedFilter -1);
fragmentRemoteFeeds.updateRemoteItems(channel.getItems(), true);
}
}
/**
* Download the item in a background thread and display success/fail accordingly.
*/
@Background
public void downloadRemoteRssItem(RemoteRssItem item) {
final RemoteRssSupplier supplier = (RemoteRssSupplier) this.getCurrentConnection();
try {
RemoteRssChannel channel = feeds.get(selectedFilter);
supplier.downloadRemoteRssItem(log, item, channel);
onTaskSucceeded(null, getString(R.string.result_added, item.getTitle()));
} catch (DaemonException e) {
onTaskFailed(getString(LocalTorrent.getResourceForDaemonException(e)));
}
}
@UiThread
protected void onTaskSucceeded(DaemonTaskSuccessResult result, String successMessage) {
SnackbarManager.show(Snackbar.with(this).text(successMessage));
}
@UiThread
protected void onTaskFailed(String message) {
SnackbarManager.show(Snackbar.with(this)
.text(message)
.colorResource(R.color.red)
.type(SnackbarType.MULTI_LINE)
);
}
private void showRemoteChannelFilters() {
List<RemoteRssChannel> feedLabels = new ArrayList<>(feeds.size() +1);
feedLabels.add(new RemoteRssChannel() {
@Override
public String getName() {
return getString(R.string.remoterss_filter_allrecent);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
}
});
feedLabels.addAll(feeds);
fragmentRemoteFeeds.updateChannelFilters(feedLabels);
}
public void refreshRemoteFeeds() {
// Connect to the last used server
IDaemonAdapter currentConnection = this.getCurrentConnection();
// remote rss not supported for this connection type
if (!(currentConnection instanceof RemoteRssSupplier)) {
return;
}
try {
feeds = ((RemoteRssSupplier) (currentConnection)).getRemoteRssChannels(log);
// By default it displays the latest items within the last month.
recentItems = new ArrayList<>();
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MONTH, -1);
Date oneMonthAgo = calendar.getTime();
for (RemoteRssChannel feed : feeds) {
for (RemoteRssItem item : feed.getItems()) {
if (item.getTimestamp().after(oneMonthAgo)) {
recentItems.add(item);
}
}
}
// Sort by -newest
Collections.sort(recentItems, (lhs, rhs) ->
rhs.getTimestamp().compareTo(lhs.getTimestamp()));
} catch (DaemonException e) {
onCommunicationError(e);
return;
}
// @UIThread
fragmentRemoteFeeds.updateRemoteItems(
selectedFilter == 0 ? recentItems : feeds.get(selectedFilter - 1).getItems(),
false /* allow android to restore scroll position */);
showRemoteChannelFilters();
}
@UiThread
protected void onCommunicationError(DaemonException daemonException) {
log.i(this, daemonException.toString());
String error = getString(LocalTorrent.getResourceForDaemonException(daemonException));
SnackbarManager.show(Snackbar.with(this).text(error).colorResource(R.color.red).type(SnackbarType.MULTI_LINE));
}
public void onFeedSelected(int position) {
selectedFilter = position;
if (position == 0) {
fragmentRemoteFeeds.updateRemoteItems(recentItems, true);
} else {
RemoteRssChannel channel = feeds.get(selectedFilter - 1);
fragmentRemoteFeeds.updateRemoteItems(channel.getItems(), true);
}
}
/**
* Download the item in a background thread and display success/fail accordingly.
*/
@Background
public void downloadRemoteRssItem(RemoteRssItem item) {
final RemoteRssSupplier supplier = (RemoteRssSupplier) this.getCurrentConnection();
try {
RemoteRssChannel channel = feeds.get(selectedFilter);
supplier.downloadRemoteRssItem(log, item, channel);
onTaskSucceeded(null, getString(R.string.result_added, item.getTitle()));
} catch (DaemonException e) {
onTaskFailed(getString(LocalTorrent.getResourceForDaemonException(e)));
}
}
@UiThread
protected void onTaskSucceeded(DaemonTaskSuccessResult result, String successMessage) {
SnackbarManager.show(Snackbar.with(this).text(successMessage));
}
@UiThread
protected void onTaskFailed(String message) {
SnackbarManager.show(Snackbar.with(this)
.text(message)
.colorResource(R.color.red)
.type(SnackbarType.MULTI_LINE)
);
}
private void showRemoteChannelFilters() {
List<RemoteRssChannel> feedLabels = new ArrayList<>(feeds.size() + 1);
feedLabels.add(new RemoteRssChannel() {
@Override
public String getName() {
return getString(R.string.remoterss_filter_allrecent);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
}
});
feedLabels.addAll(feeds);
fragmentRemoteFeeds.updateChannelFilters(feedLabels);
}
protected class LayoutPagerAdapter extends PagerAdapter {
boolean hasRemoteRss;
String serverName;
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 @@ @@ -16,13 +16,14 @@
*/
package org.transdroid.core.gui.rss;
import androidx.fragment.app.Fragment;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ListView;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EFragment;
@ -37,71 +38,72 @@ import java.util.List; @@ -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.
*
* @author Eric Kok
*/
@EFragment(R.layout.fragment_rssfeeds)
@OptionsMenu(R.menu.fragment_rssfeeds)
public class RssFeedsFragment extends Fragment {
// Views
@ViewById(R.id.rssfeeds_list)
protected ListView feedsList;
@Bean
protected RssfeedsAdapter rssfeedsAdapter;
@ViewById
protected TextView nosettingsText;
@AfterViews
protected void init() {
feedsList.setAdapter(rssfeedsAdapter);
}
public void update(List<RssfeedLoader> loaders) {
rssfeedsAdapter.update(loaders);
boolean hasSettings = !(loaders == null || loaders.size() == 0);
feedsList.setVisibility(hasSettings ? View.VISIBLE : View.GONE);
nosettingsText.setVisibility(hasSettings ? View.GONE : View.VISIBLE);
getActivity().invalidateOptionsMenu();
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
boolean hasFeeds = rssfeedsAdapter != null && rssfeedsAdapter.getCount() > 0;
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);
}
@OptionsItem(R.id.action_settings)
protected void openSettings() {
MainSettingsActivity_.intent(getActivity()).start();
}
protected RssFeedsActivity getRssActivity() {
return (RssFeedsActivity) getActivity();
}
@Override
public void onResume() {
super.onResume();
this.refreshScreen();
}
@OptionsItem(R.id.action_refresh)
protected void refreshScreen() {
getRssActivity().refreshFeeds();
}
@ItemClick(R.id.rssfeeds_list)
protected void onFeedClicked(RssfeedLoader loader) {
getRssActivity().openRssfeed(loader, true);
}
/**
* Notifies the contained list of RSS feeds that the underlying data has been changed.
*/
public void notifyDataSetChanged() {
rssfeedsAdapter.notifyDataSetChanged();
}
// Views
@ViewById(R.id.rssfeeds_list)
protected ListView feedsList;
@Bean
protected RssfeedsAdapter rssfeedsAdapter;
@ViewById
protected TextView nosettingsText;
@AfterViews
protected void init() {
feedsList.setAdapter(rssfeedsAdapter);
}
public void update(List<RssfeedLoader> loaders) {
rssfeedsAdapter.update(loaders);
boolean hasSettings = !(loaders == null || loaders.size() == 0);
feedsList.setVisibility(hasSettings ? View.VISIBLE : View.GONE);
nosettingsText.setVisibility(hasSettings ? View.GONE : View.VISIBLE);
getActivity().invalidateOptionsMenu();
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
boolean hasFeeds = rssfeedsAdapter != null && rssfeedsAdapter.getCount() > 0;
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);
}
@OptionsItem(R.id.action_settings)
protected void openSettings() {
MainSettingsActivity_.intent(getActivity()).start();
}
protected RssFeedsActivity getRssActivity() {
return (RssFeedsActivity) getActivity();
}
@Override
public void onResume() {
super.onResume();
this.refreshScreen();
}
@OptionsItem(R.id.action_refresh)
protected void refreshScreen() {
getRssActivity().refreshFeeds();
}
@ItemClick(R.id.rssfeeds_list)
protected void onFeedClicked(RssfeedLoader loader) {
getRssActivity().openRssfeed(loader, true);
}
/**
* Notifies the contained list of RSS feeds that the underlying data has been changed.
*/
public void notifyDataSetChanged() {
rssfeedsAdapter.notifyDataSetChanged();
}
}

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

@ -20,6 +20,7 @@ import android.annotation.TargetApi; @@ -20,6 +20,7 @@ import android.annotation.TargetApi;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
@ -38,45 +39,45 @@ import org.transdroid.core.rssparser.Channel; @@ -38,45 +39,45 @@ import org.transdroid.core.rssparser.Channel;
@EActivity(R.layout.activity_rssitems)
public class RssItemsActivity extends AppCompatActivity {
@Extra
protected Channel rssfeed = null;
@Extra
protected String rssfeedName;
@Extra
protected boolean requiresExternalAuthentication;
@Extra
protected Channel rssfeed = null;
@Extra
protected String rssfeedName;
@Extra
protected boolean requiresExternalAuthentication;
@FragmentById(R.id.rssitems_fragment)
protected RssItemsFragment fragmentItems;
@ViewById
protected Toolbar rssfeedsToolbar;
@FragmentById(R.id.rssitems_fragment)
protected RssItemsFragment fragmentItems;
@ViewById
protected Toolbar rssfeedsToolbar;
@Override
public void onCreate(Bundle savedInstanceState) {
SettingsUtils.applyDayNightTheme(this);
super.onCreate(savedInstanceState);
}
@Override
public void onCreate(Bundle savedInstanceState) {
SettingsUtils.applyDayNightTheme(this);
super.onCreate(savedInstanceState);
}
@AfterViews
protected void init() {
@AfterViews
protected void init() {
// We require an RSS feed to be specified; otherwise close the activity
if (rssfeed == null) {
finish();
return;
}
// We require an RSS feed to be specified; otherwise close the activity
if (rssfeed == null) {
finish();
return;
}
setSupportActionBar(rssfeedsToolbar);
getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(rssfeedName));
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setSupportActionBar(rssfeedsToolbar);
getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(rssfeedName));
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Get the intent extras and show them to the already loaded fragment
fragmentItems.update(rssfeed, false, requiresExternalAuthentication);
}
// Get the intent extras and show them to the already loaded fragment
fragmentItems.update(rssfeed, false, requiresExternalAuthentication);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@OptionsItem(android.R.id.home)
protected void navigateUp() {
TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@OptionsItem(android.R.id.home)
protected void navigateUp() {
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; @@ -23,8 +23,6 @@ import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.fragment.app.Fragment;
import androidx.appcompat.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.ActionMode;
import android.view.Menu;
@ -35,6 +33,9 @@ import android.widget.ListView; @@ -35,6 +33,9 @@ import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import com.nispok.snackbar.Snackbar;
import com.nispok.snackbar.SnackbarManager;
@ -57,184 +58,186 @@ import java.util.List; @@ -57,184 +58,186 @@ import java.util.List;
/**
* Fragment that lists the items in a specific RSS feed
*
* @author Eric Kok
*/
@EFragment(R.layout.fragment_rssitems)
public class RssItemsFragment extends Fragment {
@InstanceState
protected Channel rssFeed = null;
@InstanceState
protected boolean hasError = false;
@InstanceState
protected boolean requiresExternalAuthentication = false;
@Bean
protected NavigationHelper navigationHelper;
// Views
@ViewById(R.id.rssitems_list)
protected ListView rssItemsList;
private MultiChoiceModeListener onItemsSelected = new MultiChoiceModeListener() {
SelectionManagerMode selectionManagerMode;
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Show contextual action bar to add items in batch mode
mode.getMenuInflater().inflate(R.menu.fragment_rssitems_cab, menu);
Context themedContext = ((AppCompatActivity) getActivity()).getSupportActionBar().getThemedContext();
selectionManagerMode = new SelectionManagerMode(themedContext, rssItemsList, R.plurals.rss_itemsselected);
selectionManagerMode.onCreateActionMode(mode, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return selectionManagerMode.onPrepareActionMode(mode, menu);
}
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
// Get checked torrents
List<Item> checked = new ArrayList<>();
for (int i = 0; i < rssItemsList.getCheckedItemPositions().size(); i++) {
if (rssItemsList.getCheckedItemPositions().valueAt(i)) {
checked.add(rssitemsAdapter.getItem(rssItemsList.getCheckedItemPositions().keyAt(i)));
}
}
int itemId = item.getItemId();
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
Intent intent = new Intent("org.transdroid.ADD_MULTIPLE");
String[] urls = new String[checked.size()];
String[] titles = new String[checked.size()];
for (int i = 0; i < checked.size(); i++) {
urls[i] = checked.get(i).getTheLink();
titles[i] = checked.get(i).getTitle();
}
intent.putExtra("TORRENT_URLS", urls);
intent.putExtra("TORRENT_TITLES", titles);
startActivity(intent);
mode.finish();
return true;
} else if (itemId == R.id.action_copytoclipboard) {
StringBuilder names = new StringBuilder();
for (int f = 0; f < checked.size(); f++) {
if (f != 0) {
names.append("\n");
}
names.append(checked.get(f).getTitle());
}
ClipboardManager clipboardManager = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
clipboardManager.setPrimaryClip(ClipData.newPlainText("Transdroid", names.toString()));
mode.finish();
return true;
} else {
// The other items only operate on one (the first) selected item
if (checked.size() < 1) {
return false;
}
final Item first = checked.get(0);
if (itemId == R.id.action_showdetails) {
// Show a dialog box with the RSS item description text
new AlertDialog.Builder(getActivity()).setMessage(first.getDescription())
.setPositiveButton(R.string.action_close, null).show();
} else if (itemId == R.id.action_openwebsite) {
// Open the browser to show the website contained in the item's link tag
Toast.makeText(getActivity(), getString(R.string.search_openingdetails, first.getTitle()), Toast.LENGTH_LONG).show();
if (!TextUtils.isEmpty(first.getLink())) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(first.getLink())));
} else {
// No URL was specified in the RSS feed item link tag (or no link tag was present)
SnackbarManager.show(Snackbar.with(getActivity()).text(R.string.error_no_link).colorResource(R.color.red));
}
} else if (itemId == R.id.action_useassearch) {
// Use the RSS item title to start a new search (mimicking the search manager style)
Intent search = SearchActivity_.intent(getActivity()).get();
search.setAction(Intent.ACTION_SEARCH);
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
public void onDestroyActionMode(ActionMode mode) {
selectionManagerMode.onDestroyActionMode(mode);
}
};
@Bean
protected RssitemsAdapter rssitemsAdapter;
@ViewById
protected TextView emptyText;
@AfterViews
protected void init() {
// Set up the list adapter, which allows multi-select
rssItemsList.setAdapter(rssitemsAdapter);
rssItemsList.setMultiChoiceModeListener(onItemsSelected);
update(rssFeed, hasError, requiresExternalAuthentication);
}
/**
* 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 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;
rssitemsAdapter.update(channel);
rssItemsList.setVisibility(View.GONE);
emptyText.setVisibility(View.VISIBLE);
if (hasError) {
emptyText.setText(R.string.rss_error);
return;
}
if (channel == null) {
emptyText.setText(R.string.rss_noselection);
return;
}
if (channel.getItems().size() == 0) {
emptyText.setText(R.string.rss_empty);
return;
}
rssItemsList.setVisibility(View.VISIBLE);
emptyText.setVisibility(View.INVISIBLE);
}
@ItemClick(resName = "rssitems_list")
protected void onItemClicked(Item item) {
if (requiresExternalAuthentication) {
// Redirect to the browser, as this feed requires cookie authentication which we piggy-back on using the browser cookies
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();
i.setData(item.getTheLinkUri());
i.putExtra("TORRENT_TITLE", item.getTitle());
startActivity(i);
}
@InstanceState
protected Channel rssFeed = null;
@InstanceState
protected boolean hasError = false;
@InstanceState
protected boolean requiresExternalAuthentication = false;
@Bean
protected NavigationHelper navigationHelper;
// Views
@ViewById(R.id.rssitems_list)
protected ListView rssItemsList;
@Bean
protected RssitemsAdapter rssitemsAdapter;
@ViewById
protected TextView emptyText;
private MultiChoiceModeListener onItemsSelected = new MultiChoiceModeListener() {
SelectionManagerMode selectionManagerMode;
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Show contextual action bar to add items in batch mode
mode.getMenuInflater().inflate(R.menu.fragment_rssitems_cab, menu);
Context themedContext = ((AppCompatActivity) getActivity()).getSupportActionBar().getThemedContext();
selectionManagerMode = new SelectionManagerMode(themedContext, rssItemsList, R.plurals.rss_itemsselected);
selectionManagerMode.onCreateActionMode(mode, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return selectionManagerMode.onPrepareActionMode(mode, menu);
}
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
// Get checked torrents
List<Item> checked = new ArrayList<>();
for (int i = 0; i < rssItemsList.getCheckedItemPositions().size(); i++) {
if (rssItemsList.getCheckedItemPositions().valueAt(i)) {
checked.add(rssitemsAdapter.getItem(rssItemsList.getCheckedItemPositions().keyAt(i)));
}
}
int itemId = item.getItemId();
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
Intent intent = new Intent("org.transdroid.ADD_MULTIPLE");
String[] urls = new String[checked.size()];
String[] titles = new String[checked.size()];
for (int i = 0; i < checked.size(); i++) {
urls[i] = checked.get(i).getTheLink();
titles[i] = checked.get(i).getTitle();
}
intent.putExtra("TORRENT_URLS", urls);
intent.putExtra("TORRENT_TITLES", titles);
startActivity(intent);
mode.finish();
return true;
} else if (itemId == R.id.action_copytoclipboard) {
StringBuilder names = new StringBuilder();
for (int f = 0; f < checked.size(); f++) {
if (f != 0) {
names.append("\n");
}
names.append(checked.get(f).getTitle());
}
ClipboardManager clipboardManager = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
clipboardManager.setPrimaryClip(ClipData.newPlainText("Transdroid", names.toString()));
mode.finish();
return true;
} else {
// The other items only operate on one (the first) selected item
if (checked.size() < 1) {
return false;
}
final Item first = checked.get(0);
if (itemId == R.id.action_showdetails) {
// Show a dialog box with the RSS item description text
new AlertDialog.Builder(getActivity()).setMessage(first.getDescription())
.setPositiveButton(R.string.action_close, null).show();
} else if (itemId == R.id.action_openwebsite) {
// Open the browser to show the website contained in the item's link tag
Toast.makeText(getActivity(), getString(R.string.search_openingdetails, first.getTitle()), Toast.LENGTH_LONG).show();
if (!TextUtils.isEmpty(first.getLink())) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(first.getLink())));
} else {
// No URL was specified in the RSS feed item link tag (or no link tag was present)
SnackbarManager.show(Snackbar.with(getActivity()).text(R.string.error_no_link).colorResource(R.color.red));
}
} else if (itemId == R.id.action_useassearch) {
// Use the RSS item title to start a new search (mimicking the search manager style)
Intent search = SearchActivity_.intent(getActivity()).get();
search.setAction(Intent.ACTION_SEARCH);
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
public void onDestroyActionMode(ActionMode mode) {
selectionManagerMode.onDestroyActionMode(mode);
}
};
@AfterViews
protected void init() {
// Set up the list adapter, which allows multi-select
rssItemsList.setAdapter(rssitemsAdapter);
rssItemsList.setMultiChoiceModeListener(onItemsSelected);
update(rssFeed, hasError, requiresExternalAuthentication);
}
/**
* 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 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;
rssitemsAdapter.update(channel);
rssItemsList.setVisibility(View.GONE);
emptyText.setVisibility(View.VISIBLE);
if (hasError) {
emptyText.setText(R.string.rss_error);
return;
}
if (channel == null) {
emptyText.setText(R.string.rss_noselection);
return;
}
if (channel.getItems().size() == 0) {
emptyText.setText(R.string.rss_empty);
return;
}
rssItemsList.setVisibility(View.VISIBLE);
emptyText.setVisibility(View.INVISIBLE);
}
@ItemClick(resName = "rssitems_list")
protected void onItemClicked(Item item) {
if (requiresExternalAuthentication) {
// Redirect to the browser, as this feed requires cookie authentication which we piggy-back on using the browser cookies
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();
i.setData(item.getTheLinkUri());
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; @@ -21,89 +21,84 @@ import org.transdroid.core.rssparser.Channel;
import org.transdroid.core.rssparser.Item;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
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
* indication of a connection error.
*
* @author Eric Kok
*/
public class RssfeedLoader {
private final RssfeedSetting setting;
private Channel channel = null;
private int newCount = -1;
private boolean hasError = false;
private final RssfeedSetting setting;
private Channel channel = null;
private int newCount = -1;
private boolean hasError = false;
public RssfeedLoader(RssfeedSetting setting) {
this.setting = setting;
}
public RssfeedLoader(RssfeedSetting setting) {
this.setting = setting;
}
public void update(Channel channel, boolean hasError) {
this.channel = channel;
this.hasError = hasError;
if (channel == null || channel.getItems() == null || hasError) {
this.hasError = true;
newCount = -1;
return;
}
// Peek if this feed properly supports publish dates
boolean usePublishDate = false;
if (channel.getItems().size() > 0) {
Date pubDate = channel.getItems().get(0).getPubdate();
usePublishDate = pubDate != null && pubDate.getTime() > 0;
}
if (usePublishDate) {
// Count the number of new items, based on the date that this RSS feed was last viewed by the user
newCount = 0;
List<Item> items = channel.getItems();
// Reverse-order sort the items on their published date
Collections.sort(items, new Comparator<Item>() {
@Override
public int compare(Item lhs, Item rhs) {
return 0 - lhs.getPubdate().compareTo(rhs.getPubdate());
}
});
for (Item item : items) {
if (item.getPubdate() == null || setting.getLastViewed() == null || item.getPubdate().after(setting.getLastViewed())) {
newCount++;
item.setIsNew(true);
} else {
item.setIsNew(false);
}
}
} else {
// Use the url of the last RSS item the last time the feed was viewed by the user to count new items
newCount = 0;
boolean isNew = true;
for (Item item : channel.getItems()) {
if (item.getTheLink() != null && setting.getLastViewedItemUrl() != null && item.getTheLink().equals(setting.getLastViewedItemUrl())) {
isNew = false;
}
if (isNew) {
newCount++;
}
item.setIsNew(isNew);
}
}
}
public void update(Channel channel, boolean hasError) {
this.channel = channel;
this.hasError = hasError;
if (channel == null || channel.getItems() == null || hasError) {
this.hasError = true;
newCount = -1;
return;
}
// Peek if this feed properly supports publish dates
boolean usePublishDate = false;
if (channel.getItems().size() > 0) {
Date pubDate = channel.getItems().get(0).getPubdate();
usePublishDate = pubDate != null && pubDate.getTime() > 0;
}
newCount = 0;
if (usePublishDate) {
// 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();
// Reverse-order sort the items on their published date
Collections.sort(items, (lhs, rhs) ->
-lhs.getPubdate().compareTo(rhs.getPubdate()));
for (Item item : items) {
if (item.getPubdate() == null || setting.getLastViewed() == null || item.getPubdate().after(setting.getLastViewed())) {
newCount++;
item.setIsNew(true);
} else {
item.setIsNew(false);
}
}
} else {
// 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()) {
if (item.getTheLink() != null && setting.getLastViewedItemUrl() != null && item.getTheLink().equals(setting.getLastViewedItemUrl())) {
isNew = false;
}
if (isNew) {
newCount++;
}
item.setIsNew(isNew);
}
}
}
public Channel getChannel() {
return channel;
}
public Channel getChannel() {
return channel;
}
public RssfeedSetting getSetting() {
return setting;
}
public RssfeedSetting getSetting() {
return setting;
}
public int getNewCount() {
return newCount;
}
public int getNewCount() {
return newCount;
}
public boolean hasError() {
return hasError;
}
public boolean 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; @@ -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
* items are available.
*
* @author Eric Kok
*/
@EViewGroup(R.layout.list_item_rssfeed)
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
protected NavigationHelper navigationHelper;
@Bean
protected NavigationHelper navigationHelper;
// Views
@ViewById
protected ImageView faviconImage;
@ViewById
protected TextView nameText, newcountText;
@ViewById
protected ProgressBar loadingProgress;
// Views
@ViewById
protected ImageView faviconImage;
@ViewById
protected TextView nameText, newcountText;
@ViewById
protected ProgressBar loadingProgress;
public RssfeedView(Context context) {
super(context);
}
public RssfeedView(Context 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
nameText.setText(rssfeedLoader.getSetting().getName());
if (rssfeedLoader.hasError() || rssfeedLoader.getChannel() != null) {
loadingProgress.setVisibility(View.GONE);
newcountText.setVisibility(View.VISIBLE);
newcountText.setText(rssfeedLoader.hasError() ? "?" : Integer.toString(rssfeedLoader.getNewCount()));
} else {
loadingProgress.setVisibility(View.VISIBLE);
newcountText.setVisibility(View.GONE);
}
// Show the RSS feed name and either a loading indicator or the number of new items
nameText.setText(rssfeedLoader.getSetting().getName());
if (rssfeedLoader.hasError() || rssfeedLoader.getChannel() != null) {
loadingProgress.setVisibility(View.GONE);
newcountText.setVisibility(View.VISIBLE);
newcountText.setText(rssfeedLoader.hasError() ? "?" : Integer.toString(rssfeedLoader.getNewCount()));
} else {
loadingProgress.setVisibility(View.VISIBLE);
newcountText.setVisibility(View.GONE);
}
// Clear and then asynchronously load the RSS feed site' favicon
// Uses the g.etfv.co service to resolve the favicon of any feed URL
faviconImage.setImageDrawable(null);
navigationHelper.getImageCache().displayImage(String.format(GRABICON_URL, rssfeedLoader.getSetting().getUrl()), faviconImage);
// Clear and then asynchronously load the RSS feed site' favicon
// Uses the g.etfv.co service to resolve the favicon of any feed URL
faviconImage.setImageDrawable(null);
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; @@ -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}.
*
* @author Eric Kok
*/
@EBean
public class RssfeedsAdapter extends BaseAdapter {
private List<RssfeedLoader> loaders = null;
@RootContext
protected Context context;
@RootContext
protected Context context;
private List<RssfeedLoader> loaders = null;
/**
* 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
*/
public void update(List<RssfeedLoader> loaders) {
this.loaders = loaders;
notifyDataSetChanged();
}
/**
* 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
*/
public void update(List<RssfeedLoader> loaders) {
this.loaders = loaders;
notifyDataSetChanged();
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public int getCount() {
if (loaders == null) {
return 0;
}
return loaders.size();
}
@Override
public int getCount() {
if (loaders == null) {
return 0;
}
return loaders.size();
}
@Override
public RssfeedLoader getItem(int position) {
if (loaders == null) {
return null;
}
return loaders.get(position);
}
@Override
public RssfeedLoader getItem(int position) {
if (loaders == null) {
return null;
}
return loaders.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
RssfeedView rssfeedView;
if (convertView == null) {
rssfeedView = RssfeedView_.build(context);
} else {
rssfeedView = (RssfeedView) convertView;
}
rssfeedView.bind(getItem(position));
return rssfeedView;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
RssfeedView rssfeedView;
if (convertView == null) {
rssfeedView = RssfeedView_.build(context);
} else {
rssfeedView = (RssfeedView) convertView;
}
rssfeedView.bind(getItem(position));
return rssfeedView;
}
}

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

@ -28,53 +28,52 @@ import org.transdroid.R; @@ -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
* status, that is, if the item is new to the user or was viewed earlier.
*
* @author Eric Kok
*/
public class RssitemStatusLayout extends RelativeLayout {
private final float scale = getContext().getResources().getDisplayMetrics().density;
private final int WIDTH = (int) (6 * scale + 0.5f);
private final Paint oldPaint = new Paint();
private final Paint newPaint = new Paint();
private final RectF fullRect = new RectF();
private Boolean isNew = null;
private final float scale = getContext().getResources().getDisplayMetrics().density;
private final int WIDTH = (int) (6 * scale + 0.5f);
private final Paint oldPaint = new Paint();
private final Paint newPaint = new Paint();
private final RectF fullRect = new RectF();
private Boolean isNew = null;
public RssitemStatusLayout(Context context) {
super(context);
initPaints();
setWillNotDraw(false);
}
public RssitemStatusLayout(Context context) {
super(context);
initPaints();
setWillNotDraw(false);
}
public RssitemStatusLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initPaints();
setWillNotDraw(false);
}
public RssitemStatusLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initPaints();
setWillNotDraw(false);
}
private void initPaints() {
oldPaint.setColor(getResources().getColor(R.color.file_off)); // Grey
newPaint.setColor(getResources().getColor(R.color.file_normal)); // Normal green
}
private void initPaints() {
oldPaint.setColor(getResources().getColor(R.color.file_off)); // Grey
newPaint.setColor(getResources().getColor(R.color.file_normal)); // Normal green
}
public void setIsNew(Boolean isNew) {
this.isNew = isNew;
this.invalidate();
}
public void setIsNew(Boolean isNew) {
this.isNew = isNew;
this.invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int height = getHeight();
int width = WIDTH;
fullRect.set(0, 0, width, height);
fullRect.set(0, 0, WIDTH, getHeight());
if (isNew == null) {
return;
}
if (isNew == null) {
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; @@ -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.
*
* @author Eric Kok
*/
@EViewGroup(R.layout.list_item_rssitem)
public class RssitemView extends RssitemStatusLayout {
// Views
@ViewById
protected TextView nameText, dateText;
// Views
@ViewById
protected TextView nameText, dateText;
public RssitemView(Context context) {
super(context);
}
public RssitemView(Context context) {
super(context);
}
public void bind(Item rssitem) {
public void bind(Item rssitem) {
nameText.setText(rssitem.getTitle());
dateText.setText(rssitem.getPubdate() == null ? "" : DateUtils
.getRelativeDateTimeString(getContext(), rssitem.getPubdate().getTime(), DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS,
DateUtils.FORMAT_ABBREV_MONTH));
setIsNew(rssitem.isNew());
nameText.setText(rssitem.getTitle());
dateText.setText(rssitem.getPubdate() == null ? "" : DateUtils
.getRelativeDateTimeString(getContext(), rssitem.getPubdate().getTime(), DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS,
DateUtils.FORMAT_ABBREV_MONTH));
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; @@ -28,61 +28,62 @@ import org.transdroid.core.rssparser.Item;
/**
* Adapter that contains a list of {@link Item}s in an RSS feed.
*
* @author Eric Kok
*/
@EBean
public class RssitemsAdapter extends BaseAdapter {
private Channel rssfeed = null;
@RootContext
protected Context context;
@RootContext
protected Context context;
private Channel rssfeed = null;
/**
* Allows updating the full RSS feed (channel and contained items), replacing the old data
* @param rssfeed The new RSS feed contents
*/
public void update(Channel rssfeed) {
this.rssfeed = rssfeed;
notifyDataSetChanged();
}
/**
* Allows updating the full RSS feed (channel and contained items), replacing the old data
*
* @param rssfeed The new RSS feed contents
*/
public void update(Channel rssfeed) {
this.rssfeed = rssfeed;
notifyDataSetChanged();
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public int getCount() {
if (rssfeed == null) {
return 0;
}
return rssfeed.getItems().size();
}
@Override
public int getCount() {
if (rssfeed == null) {
return 0;
}
return rssfeed.getItems().size();
}
@Override
public Item getItem(int position) {
if (rssfeed == null) {
return null;
}
return rssfeed.getItems().get(position);
}
@Override
public Item getItem(int position) {
if (rssfeed == null) {
return null;
}
return rssfeed.getItems().get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
RssitemView rssitemView;
if (convertView == null) {
rssitemView = RssitemView_.build(context);
} else {
rssitemView = (RssitemView) convertView;
}
rssitemView.bind(getItem(position));
return rssitemView;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
RssitemView rssitemView;
if (convertView == null) {
rssitemView = RssitemView_.build(context);
} else {
rssitemView = (RssitemView) convertView;
}
rssitemView.bind(getItem(position));
return rssitemView;
}
}

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

@ -20,8 +20,6 @@ import android.annotation.SuppressLint; @@ -20,8 +20,6 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.net.Uri;
@ -31,57 +29,56 @@ import java.lang.ref.WeakReference; @@ -31,57 +29,56 @@ import java.lang.ref.WeakReference;
public class BarcodeHelper {
// A 'random' ID to identify QR-encoded settings scan intents
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");
// A 'random' ID to identify QR-encoded settings scan intents
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");
/**
* Call this to start a bar code scanner intent. The calling activity will receive an Intent result with the given
* 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 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)
startBarcodeIntent(activity, new Intent("com.google.zxing.client.android.SCAN"), requestCode);
}
/**
* Call this to start a bar code scanner intent. The calling activity will receive an Intent result with the given
* 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 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)
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
* receive an Intent result with ID {@link #ACTIVITY_BARCODE_QRSETTINGS}. From there the returned intent will
* 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 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)
Intent encodeIntent = new Intent("com.google.zxing.client.android.ENCODE");
encodeIntent.putExtra("ENCODE_TYPE", "TEXT_TYPE");
encodeIntent.putExtra("ENCODE_DATA", content);
encodeIntent.putExtra("ENCODE_SHOW_CONTENTS", false);
startBarcodeIntent(activity, encodeIntent, -1);
}
/**
* 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
* 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 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)
Intent encodeIntent = new Intent("com.google.zxing.client.android.ENCODE");
encodeIntent.putExtra("ENCODE_TYPE", "TEXT_TYPE");
encodeIntent.putExtra("ENCODE_DATA", content);
encodeIntent.putExtra("ENCODE_SHOW_CONTENTS", false);
startBarcodeIntent(activity, encodeIntent, -1);
}
@SuppressLint("ValidFragment")
private static void startBarcodeIntent(final Activity activity, final Intent intent, int requestCode) {
try {
activity.startActivityForResult(intent, requestCode);
} catch (Exception e) {
// 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);
new AlertDialog.Builder(activity).setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(activity.getString(R.string.search_barcodescannernotfound))
.setPositiveButton(android.R.string.yes, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (intentStartContext.get() != null)
intentStartContext.get().startActivity(new Intent(Intent.ACTION_VIEW, SCANNER_MARKET_URI));
}
}).setNegativeButton(android.R.string.no, null).show();
}
}
@SuppressLint("ValidFragment")
private static void startBarcodeIntent(final Activity activity, final Intent intent, int requestCode) {
try {
activity.startActivityForResult(intent, requestCode);
} catch (Exception e) {
// Can't start the bar code scanner, for example with a SecurityException or when ZXing is not present
final WeakReference<Context> intentStartContext = new WeakReference<>(activity);
new AlertDialog.Builder(activity).setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(activity.getString(R.string.search_barcodescannernotfound))
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
if (intentStartContext.get() != null)
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 @@ @@ -16,54 +16,50 @@
*/
package org.transdroid.core.gui.search;
import org.transdroid.R;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.net.Uri;
import org.transdroid.R;
import java.lang.ref.WeakReference;
public class FilePickerHelper {
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 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");
/**
* 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.
* @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) {
try {
// Start a file manager that can handle the file/* file/* intents
activity.startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT).setType("application/x-bittorrent"),
ACTIVITY_FILEPICKER);
} catch (Exception e1) {
try {
// Start a file manager that can handle the PICK_FILE intent (specifically IO File Manager)
activity.startActivityForResult(new Intent("org.openintents.action.PICK_FILE"), ACTIVITY_FILEPICKER);
} catch (Exception e2) {
// Can't start the file manager, for example with a SecurityException or when IO File Manager is not present
final WeakReference<Context> intentStartContext = new WeakReference<Context>(activity);
new AlertDialog.Builder(activity).setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(activity.getString(R.string.search_filemanagernotfound))
.setPositiveButton(android.R.string.yes, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (intentStartContext.get() != null)
intentStartContext.get().startActivity(new Intent(Intent.ACTION_VIEW, FILEMANAGER_MARKET_URI));
}
}).setNegativeButton(android.R.string.no, null).show();
}
}
}
/**
* 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.
*
* @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) {
try {
// Start a file manager that can handle the file/* file/* intents
activity.startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT).setType("application/x-bittorrent"),
ACTIVITY_FILEPICKER);
} catch (Exception e1) {
try {
// Start a file manager that can handle the PICK_FILE intent (specifically IO File Manager)
activity.startActivityForResult(new Intent("org.openintents.action.PICK_FILE"), ACTIVITY_FILEPICKER);
} catch (Exception e2) {
// Can't start the file manager, for example with a SecurityException or when IO File Manager is not present
final WeakReference<Context> intentStartContext = new WeakReference<>(activity);
new AlertDialog.Builder(activity).setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(activity.getString(R.string.search_filemanagernotfound))
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
if (intentStartContext.get() != null)
intentStartContext.get().startActivity(new Intent(Intent.ACTION_VIEW, FILEMANAGER_MARKET_URI));
}).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; @@ -23,9 +23,6 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
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.MenuItem;
import android.view.View;
@ -36,6 +33,9 @@ import android.widget.SearchView; @@ -36,6 +33,9 @@ import android.widget.SearchView;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
@ -58,284 +58,282 @@ import java.util.List; @@ -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
* search sites on the left (e.g. on tablets) or allows switching between search sites via the action bar spinner.
*
* @author Eric Kok
*/
@EActivity(R.layout.activity_search)
public class SearchActivity extends AppCompatActivity {
@ViewById
protected Toolbar searchToolbar;
@ViewById
protected Spinner sitesSpinner;
@FragmentById(R.id.searchresults_fragment)
protected SearchResultsFragment fragmentResults;
@ViewById
protected ListView searchsitesList;
@ViewById
protected TextView installmoduleText;
@Bean
protected ApplicationSettings applicationSettings;
@Bean
protected SearchHelper searchHelper;
@SystemService
protected SearchManager searchManager;
private MenuItem searchMenu = null;
private SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SearchHistoryProvider.AUTHORITY, SearchHistoryProvider.MODE);
private List<SearchSetting> searchSites;
private SearchSetting lastUsedSite;
private String lastUsedQuery;
@Override
public void onCreate(Bundle savedInstanceState) {
SettingsUtils.applyDayNightTheme(this);
super.onCreate(savedInstanceState);
}
@AfterViews
protected void init() {
searchToolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material);
searchToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TorrentsActivity_.intent(SearchActivity.this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
}
});
setSupportActionBar(searchToolbar);
// Get the user query, as coming from the standard SearchManager
handleIntent(getIntent());
if (!searchHelper.isTorrentSearchInstalled()) {
// The module install text will be shown instead (in onPrepareOptionsMenu)
return;
}
// Load sites and find the last used (or set as default) search site
searchSites = applicationSettings.getSearchSettings();
lastUsedSite = applicationSettings.getLastUsedSearchSite();
int lastUsedPosition = -1;
if (lastUsedSite != null) {
for (int i = 0; i < searchSites.size(); i++) {
if (searchSites.get(i).getKey().equals(lastUsedSite.getKey())) {
lastUsedPosition = i;
break;
}
}
}
// 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
SearchSitesAdapter searchSitesAdapter = SearchSitesAdapter_.getInstance_(this);
searchSitesAdapter.update(searchSites);
searchsitesList.setAdapter(searchSitesAdapter);
searchsitesList.setOnItemClickListener(onSearchSiteClicked);
// Select the last used site and start the search
if (lastUsedPosition >= 0) {
searchsitesList.setItemChecked(lastUsedPosition, true);
lastUsedSite = searchSites.get(lastUsedPosition);
refreshSearch();
} else {
fragmentResults.clearResults();
}
} else {
// Use the action bar spinner to select sites
if (getSupportActionBar() != null)
getSupportActionBar().setTitle("");
sitesSpinner.setVisibility(View.VISIBLE);
sitesSpinner.setAdapter(new SearchSettingsDropDownAdapter(searchToolbar.getContext(), searchSites));
sitesSpinner.setOnItemSelectedListener(onSearchSiteSelected);
// Select the last used site; this also starts the search!
if (lastUsedPosition >= 0) {
sitesSpinner.setSelection(lastUsedPosition);
lastUsedSite = searchSites.get(lastUsedPosition);
refreshSearch();
} else {
fragmentResults.clearResults();
}
}
invalidateOptionsMenu();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
// Manually insert the actions into the main torrent and secondary actions toolbars
searchToolbar.inflateMenu(R.menu.activity_search);
// Add an expandable SearchView to the action bar
MenuItem item = menu.findItem(R.id.action_search);
final SearchView searchView = new SearchView(searchToolbar.getContext());
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
searchView.setQueryRefinementEnabled(true);
searchView.setIconified(false);
searchView.setIconifiedByDefault(false);
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);
final SearchSortOrder sortOrder = applicationSettings.getLastUsedSearchSortOrder();
if (sortOrder == SearchSortOrder.BySeeders) {
sortBySeeders.setChecked(true);
} else {
sortByAdded.setChecked(true);
}
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
boolean searchInstalled = searchHelper.isTorrentSearchInstalled();
searchToolbar.getMenu().findItem(R.id.action_search).setVisible(searchInstalled);
searchToolbar.getMenu().findItem(R.id.action_refresh).setVisible(searchInstalled);
searchToolbar.getMenu().findItem(R.id.action_downloadsearch).setVisible(!searchInstalled);
if (searchsitesList != null) {
searchsitesList.setVisibility(searchInstalled ? View.VISIBLE : View.GONE);
}
if (searchInstalled) {
getFragmentManager().beginTransaction().show(fragmentResults).commit();
} else {
getFragmentManager().beginTransaction().hide(fragmentResults).commit();
}
installmoduleText.setVisibility(searchInstalled ? View.GONE : View.VISIBLE);
return true;
}
@Override
protected void onNewIntent(Intent intent) {
handleIntent(intent);
refreshSearch();
}
private void handleIntent(Intent intent) {
lastUsedQuery = parseQuery(intent);
// Is this actually a full HTTP URL? Then redirect this request to add the URL directly
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
Intent i = TorrentsActivity_.intent(this).get();
i.setData(Uri.parse(lastUsedQuery));
startActivity(i);
finish();
}
}
@Override
public boolean onSearchRequested() {
if (searchMenu != null) {
searchMenu.expandActionView();
}
return true;
}
private OnItemClickListener onSearchSiteClicked = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
lastUsedSite = searchSites.get(position);
refreshSearch();
}
};
private AdapterView.OnItemSelectedListener onSearchSiteSelected = new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
lastUsedSite = searchSites.get(position);
refreshSearch();
}
@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
*/
private String parseQuery(Intent intent) {
String query = null;
if (intent.getAction().equals(Intent.ACTION_SEARCH)) {
query = intent.getStringExtra(SearchManager.QUERY).trim();
} else if (intent.getAction().equals(Intent.ACTION_SEND)) {
query = SendIntentHelper.cleanUpText(intent).trim();
}
if (query != null && query.length() > 0) {
// Remember this search query to later show as a suggestion
suggestions.saveRecentQuery(query, null);
return query;
}
return null;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@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() {
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
WebsearchSetting websearch = (WebsearchSetting) lastUsedSite;
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(websearch.getBaseUrl().replace("%s", lastUsedQuery))));
finish();
} else if (lastUsedSite instanceof SearchSite) {
// Save the search site currently used to search for future usage
applicationSettings.setLastUsedSearchSite(lastUsedSite);
// Update the activity title (only shown on large devices)
if (sitesSpinner == null && getSupportActionBar() != null)
getSupportActionBar()
.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_sort_added)
protected void sortByDateAdded() {
if (applicationSettings.getLastUsedSearchSortOrder() == SearchSortOrder.Combined) {
return;
}
invalidateOptionsMenu();
applicationSettings.setLastUsedSearchSortOrder(SearchSortOrder.Combined);
refreshSearch();
}
@OptionsItem(R.id.action_sort_seeders)
protected void sortBySeeders() {
if (applicationSettings.getLastUsedSearchSortOrder() == SearchSortOrder.BySeeders) {
return;
}
invalidateOptionsMenu();
applicationSettings.setLastUsedSearchSortOrder(SearchSortOrder.BySeeders);
refreshSearch();
}
@ViewById
protected Toolbar searchToolbar;
@ViewById
protected Spinner sitesSpinner;
@FragmentById(R.id.searchresults_fragment)
protected SearchResultsFragment fragmentResults;
@ViewById
protected ListView searchsitesList;
@ViewById
protected TextView installmoduleText;
@Bean
protected ApplicationSettings applicationSettings;
@Bean
protected SearchHelper searchHelper;
@SystemService
protected SearchManager searchManager;
private MenuItem searchMenu = null;
private SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SearchHistoryProvider.AUTHORITY, SearchHistoryProvider.MODE);
private List<SearchSetting> searchSites;
private SearchSetting lastUsedSite;
private String lastUsedQuery;
private OnItemClickListener onSearchSiteClicked = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
lastUsedSite = searchSites.get(position);
refreshSearch();
}
};
private AdapterView.OnItemSelectedListener onSearchSiteSelected = new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
lastUsedSite = searchSites.get(position);
refreshSearch();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
SettingsUtils.applyDayNightTheme(this);
super.onCreate(savedInstanceState);
}
@AfterViews
protected void init() {
searchToolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material);
searchToolbar.setNavigationOnClickListener(v ->
TorrentsActivity_.intent(SearchActivity.this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start());
setSupportActionBar(searchToolbar);
// Get the user query, as coming from the standard SearchManager
handleIntent(getIntent());
if (!searchHelper.isTorrentSearchInstalled()) {
// The module install text will be shown instead (in onPrepareOptionsMenu)
return;
}
// Load sites and find the last used (or set as default) search site
searchSites = applicationSettings.getSearchSettings();
lastUsedSite = applicationSettings.getLastUsedSearchSite();
int lastUsedPosition = -1;
if (lastUsedSite != null) {
for (int i = 0; i < searchSites.size(); i++) {
if (searchSites.get(i).getKey().equals(lastUsedSite.getKey())) {
lastUsedPosition = i;
break;
}
}
}
// 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
SearchSitesAdapter searchSitesAdapter = SearchSitesAdapter_.getInstance_(this);
searchSitesAdapter.update(searchSites);
searchsitesList.setAdapter(searchSitesAdapter);
searchsitesList.setOnItemClickListener(onSearchSiteClicked);
// Select the last used site and start the search
if (lastUsedPosition >= 0) {
searchsitesList.setItemChecked(lastUsedPosition, true);
lastUsedSite = searchSites.get(lastUsedPosition);
refreshSearch();
} else {
fragmentResults.clearResults();
}
} else {
// Use the action bar spinner to select sites
if (getSupportActionBar() != null)
getSupportActionBar().setTitle("");
sitesSpinner.setVisibility(View.VISIBLE);
sitesSpinner.setAdapter(new SearchSettingsDropDownAdapter(searchToolbar.getContext(), searchSites));
sitesSpinner.setOnItemSelectedListener(onSearchSiteSelected);
// Select the last used site; this also starts the search!
if (lastUsedPosition >= 0) {
sitesSpinner.setSelection(lastUsedPosition);
lastUsedSite = searchSites.get(lastUsedPosition);
refreshSearch();
} else {
fragmentResults.clearResults();
}
}
invalidateOptionsMenu();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
// Manually insert the actions into the main torrent and secondary actions toolbars
searchToolbar.inflateMenu(R.menu.activity_search);
// Add an expandable SearchView to the action bar
MenuItem item = menu.findItem(R.id.action_search);
final SearchView searchView = new SearchView(searchToolbar.getContext());
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
searchView.setQueryRefinementEnabled(true);
searchView.setIconified(false);
searchView.setIconifiedByDefault(false);
item.setActionView(searchView);
searchMenu = item;
final MenuItem sortBySeeders = menu.findItem(R.id.action_sort_seeders);
final MenuItem sortByAdded = menu.findItem(R.id.action_sort_added);
final SearchSortOrder sortOrder = applicationSettings.getLastUsedSearchSortOrder();
if (sortOrder == SearchSortOrder.BySeeders) {
sortBySeeders.setChecked(true);
} else {
sortByAdded.setChecked(true);
}
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
boolean searchInstalled = searchHelper.isTorrentSearchInstalled();
searchToolbar.getMenu().findItem(R.id.action_search).setVisible(searchInstalled);
searchToolbar.getMenu().findItem(R.id.action_refresh).setVisible(searchInstalled);
searchToolbar.getMenu().findItem(R.id.action_downloadsearch).setVisible(!searchInstalled);
if (searchsitesList != null) {
searchsitesList.setVisibility(searchInstalled ? View.VISIBLE : View.GONE);
}
if (searchInstalled) {
getSupportFragmentManager().beginTransaction().show(fragmentResults).commit();
} else {
getSupportFragmentManager().beginTransaction().hide(fragmentResults).commit();
}
installmoduleText.setVisibility(searchInstalled ? View.GONE : View.VISIBLE);
return true;
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
refreshSearch();
}
private void handleIntent(Intent intent) {
lastUsedQuery = parseQuery(intent);
// Is this actually a full HTTP URL? Then redirect this request to add the URL directly
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
Intent i = TorrentsActivity_.intent(this).get();
i.setData(Uri.parse(lastUsedQuery));
startActivity(i);
finish();
}
}
@Override
public boolean onSearchRequested() {
if (searchMenu != null) {
searchMenu.expandActionView();
}
return true;
}
/**
* Extracts the query string from the search {@link Intent}
*
* @return The query string that was entered by the user
*/
private String parseQuery(Intent intent) {
String query = null;
if (intent.getAction().equals(Intent.ACTION_SEARCH)) {
query = intent.getStringExtra(SearchManager.QUERY).trim();
} else if (intent.getAction().equals(Intent.ACTION_SEND)) {
query = SendIntentHelper.cleanUpText(intent).trim();
}
if (query != null && query.length() > 0) {
// Remember this search query to later show as a suggestion
suggestions.saveRecentQuery(query, null);
return query;
}
return null;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@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() {
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
WebsearchSetting websearch = (WebsearchSetting) lastUsedSite;
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(websearch.getBaseUrl().replace("%s", lastUsedQuery))));
finish();
} else if (lastUsedSite instanceof SearchSite) {
// Save the search site currently used to search for future usage
applicationSettings.setLastUsedSearchSite(lastUsedSite);
// Update the activity title (only shown on large devices)
if (sitesSpinner == null && getSupportActionBar() != null)
getSupportActionBar()
.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_sort_added)
protected void sortByDateAdded() {
if (applicationSettings.getLastUsedSearchSortOrder() == SearchSortOrder.Combined) {
return;
}
invalidateOptionsMenu();
applicationSettings.setLastUsedSearchSortOrder(SearchSortOrder.Combined);
refreshSearch();
}
@OptionsItem(R.id.action_sort_seeders)
protected void sortBySeeders() {
if (applicationSettings.getLastUsedSearchSortOrder() == SearchSortOrder.BySeeders) {
return;
}
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; @@ -24,19 +24,20 @@ import org.transdroid.BuildConfig;
/**
* Provides search suggestions by simply returning previous user entries.
*
* @author Eric Kok
*/
public class SearchHistoryProvider extends SearchRecentSuggestionsProvider {
public final static String AUTHORITY = BuildConfig.APPLICATION_ID + ".search.SearchHistoryProvider";
public final static int MODE = DATABASE_MODE_QUERIES;
public final static String AUTHORITY = BuildConfig.APPLICATION_ID + ".search.SearchHistoryProvider";
public final static int MODE = DATABASE_MODE_QUERIES;
public SearchHistoryProvider() {
setupSuggestions(AUTHORITY, MODE);
}
public SearchHistoryProvider() {
setupSuggestions(AUTHORITY, MODE);
}
public static void clearHistory(Context context) {
new SearchRecentSuggestions(context, AUTHORITY, MODE).clearHistory();
}
public static void clearHistory(Context context) {
new SearchRecentSuggestions(context, AUTHORITY, MODE).clearHistory();
}
}

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

@ -16,41 +16,42 @@ @@ -16,41 +16,42 @@
*/
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.text.format.DateUtils;
import android.widget.RelativeLayout;
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
*
* @author Eric Kok
*/
@EViewGroup(resName = "list_item_searchresult")
public class SearchResultView extends RelativeLayout {
// Views
@ViewById
protected TextView nameText, seedersText, leechersText, sizeText, dateText;
// Views
@ViewById
protected TextView nameText, seedersText, leechersText, sizeText, dateText;
public SearchResultView(Context context) {
super(context);
}
public SearchResultView(Context context) {
super(context);
}
public void bind(SearchResult result) {
public void bind(SearchResult result) {
nameText.setText(result.getName());
sizeText.setText(result.getSize());
dateText.setText(result.getAddedOn() == null ? "" : DateUtils.getRelativeDateTimeString(getContext(), result
.getAddedOn().getTime(), DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS,
DateUtils.FORMAT_ABBREV_MONTH));
seedersText.setText(getContext().getString(R.string.search_seeders, result.getSeeders()));
leechersText.setText(getContext().getString(R.string.search_leechers, result.getLeechers()));
nameText.setText(result.getName());
sizeText.setText(result.getSize());
dateText.setText(result.getAddedOn() == null ? "" : DateUtils.getRelativeDateTimeString(getContext(), result
.getAddedOn().getTime(), DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS,
DateUtils.FORMAT_ABBREV_MONTH));
seedersText.setText(getContext().getString(R.string.search_seeders, result.getSeeders()));
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; @@ -29,61 +29,62 @@ import java.util.List;
/**
* Adapter that contains a list of {@link SearchResult}s.
*
* @author Eric Kok
*/
@EBean
public class SearchResultsAdapter extends BaseAdapter {
private List<SearchResult> results = null;
@RootContext
protected Context context;
@RootContext
protected Context context;
private List<SearchResult> results = null;
/**
* Allows updating the search results, replacing the old data
* @param results The new list of search results
*/
public void update(List<SearchResult> results) {
this.results = results;
notifyDataSetChanged();
}
/**
* Allows updating the search results, replacing the old data
*
* @param results The new list of search results
*/
public void update(List<SearchResult> results) {
this.results = results;
notifyDataSetChanged();
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public int getCount() {
if (results == null) {
return 0;
}
return results.size();
}
@Override
public int getCount() {
if (results == null) {
return 0;
}
return results.size();
}
@Override
public SearchResult getItem(int position) {
if (results == null) {
return null;
}
return results.get(position);
}
@Override
public SearchResult getItem(int position) {
if (results == null) {
return null;
}
return results.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
SearchResultView rssitemView;
if (convertView == null) {
rssitemView = SearchResultView_.build(context);
} else {
rssitemView = (SearchResultView) convertView;
}
rssitemView.bind(getItem(position));
return rssitemView;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
SearchResultView rssitemView;
if (convertView == null) {
rssitemView = SearchResultView_.build(context);
} else {
rssitemView = (SearchResultView) convertView;
}
rssitemView.bind(getItem(position));
return rssitemView;
}
}

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

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

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

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

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

@ -27,34 +27,35 @@ import java.util.List; @@ -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.
*
* @author Eric Kok
*/
public class SearchSettingsDropDownAdapter extends FilterListItemAdapter {
private final Context context;
public SearchSettingsDropDownAdapter(Context context, List<? extends SimpleListItem> items) {
super(context, items);
this.context = context;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// This returns the item to show in the action bar spinner
SearchSettingSelectionView filterItemView;
if (convertView == null || !(convertView instanceof SearchSettingSelectionView)) {
filterItemView = SearchSettingSelectionView_.build(context);
} else {
filterItemView = (SearchSettingSelectionView) convertView;
}
filterItemView.bind((SearchSetting) getItem(position));
return filterItemView;
}
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
// This returns the item to show in the drop down list
return super.getView(position, convertView, parent);
}
private final Context context;
public SearchSettingsDropDownAdapter(Context context, List<? extends SimpleListItem> items) {
super(context, items);
this.context = context;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// This returns the item to show in the action bar spinner
SearchSettingSelectionView filterItemView;
if (!(convertView instanceof SearchSettingSelectionView)) {
filterItemView = SearchSettingSelectionView_.build(context);
} else {
filterItemView = (SearchSettingSelectionView) convertView;
}
filterItemView.bind((SearchSetting) getItem(position));
return filterItemView;
}
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
// This returns the item to show in the drop down list
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; @@ -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
* items are available.
*
* @author Eric Kok
*/
@EViewGroup(R.layout.list_item_searchsite)
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
protected NavigationHelper navigationHelper;
@Bean
protected NavigationHelper navigationHelper;
// Views
@ViewById
protected ImageView faviconImage;
@ViewById
protected TextView nameText;
// Views
@ViewById
protected ImageView faviconImage;
@ViewById
protected TextView nameText;
public SearchSiteView(Context context) {
super(context);
}
public SearchSiteView(Context 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
nameText.setText(rssfeedLoader.getName());
// Clear and then asynchronously load the site's favicon
// Uses the g.etfv.co service to resolve the favicon of any URL
faviconImage.setImageDrawable(null);
navigationHelper.getImageCache().displayImage(String.format(GETFVO_URL, rssfeedLoader.getBaseUrl()), faviconImage);
// Show the RSS feed name and either a loading indicator or the number of new items
nameText.setText(rssfeedLoader.getName());
// Clear and then asynchronously load the site's favicon
// Uses the g.etfv.co service to resolve the favicon of any URL
faviconImage.setImageDrawable(null);
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; @@ -30,61 +30,62 @@ import java.util.List;
/**
* Adapter that contains a list of {@link SearchSetting}s, either {@link SearchSite} or {@link WebsearchSetting}.
*
* @author Eric Kok
*/
@EBean
public class SearchSitesAdapter extends BaseAdapter {
private List<SearchSetting> sites = null;
@RootContext
protected Context context;
@RootContext
protected Context context;
private List<SearchSetting> sites = null;
/**
* 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
*/
public void update(List<SearchSetting> sites) {
this.sites = sites;
notifyDataSetChanged();
}
/**
* 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
*/
public void update(List<SearchSetting> sites) {
this.sites = sites;
notifyDataSetChanged();
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public int getCount() {
if (sites == null) {
return 0;
}
return sites.size();
}
@Override
public int getCount() {
if (sites == null) {
return 0;
}
return sites.size();
}
@Override
public SearchSetting getItem(int position) {
if (sites == null) {
return null;
}
return sites.get(position);
}
@Override
public SearchSetting getItem(int position) {
if (sites == null) {
return null;
}
return sites.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
SearchSiteView rssfeedView;
if (convertView == null) {
rssfeedView = SearchSiteView_.build(context);
} else {
rssfeedView = (SearchSiteView) convertView;
}
rssfeedView.bind(getItem(position));
return rssfeedView;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
SearchSiteView rssfeedView;
if (convertView == null) {
rssfeedView = SearchSiteView_.build(context);
} else {
rssfeedView = (SearchSiteView) convertView;
}
rssfeedView.bind(getItem(position));
return rssfeedView;
}
}

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

@ -21,57 +21,59 @@ import android.content.Intent; @@ -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
* applications, i.e. the EXTRA_TEXT send by some known applications.
*
* @author Eric Kok
*/
public class SendIntentHelper {
private static final String SOUNDHOUND1 = "Just used #SoundHound to find ";
private static final String SOUNDHOUND1_END = " http://";
private static final String SHAZAM = "I just used Shazam to discover ";
private static final String SHAZAM_END = ". http://";
private static final String YOUTUBE_ID = "Watch \"";
private static final String YOUTUBE_START = "\"";
private static final String YOUTUBE_END = "\"";
private static final String SOUNDHOUND1 = "Just used #SoundHound to find ";
private static final String SOUNDHOUND1_END = " http://";
private static final String SHAZAM = "I just used Shazam to discover ";
private static final String SHAZAM_END = ". http://";
private static final String YOUTUBE_ID = "Watch \"";
private static final String YOUTUBE_START = "\"";
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
* 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
*/
public static String cleanUpText(Intent intent) {
/**
* 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.
*
* @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) {
if (intent == null || !intent.hasExtra(Intent.EXTRA_TEXT)) {
return null;
}
String text = intent.getStringExtra(Intent.EXTRA_TEXT);
try {
if (intent == null || !intent.hasExtra(Intent.EXTRA_TEXT)) {
return null;
}
String text = intent.getStringExtra(Intent.EXTRA_TEXT);
try {
// Soundhound song/artist share
if (text.startsWith(SOUNDHOUND1)) {
return cutOut(text, SOUNDHOUND1, SOUNDHOUND1_END).replace(" by ", " ");
}
// Shazam song share
if (text.startsWith(SHAZAM)) {
return cutOut(text, SHAZAM, SHAZAM_END).replace(" by ", " ");
}
// YouTube app share (stores title in EXTRA_SUBJECT)
if (intent.hasExtra(Intent.EXTRA_SUBJECT)) {
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
if (subject.startsWith(YOUTUBE_ID)) {
return cutOut(subject, YOUTUBE_START, YOUTUBE_END);
}
}
// Soundhound song/artist share
if (text.startsWith(SOUNDHOUND1)) {
return cutOut(text, SOUNDHOUND1, SOUNDHOUND1_END).replace(" by ", " ");
}
// Shazam song share
if (text.startsWith(SHAZAM)) {
return cutOut(text, SHAZAM, SHAZAM_END).replace(" by ", " ");
}
// YouTube app share (stores title in EXTRA_SUBJECT)
if (intent.hasExtra(Intent.EXTRA_SUBJECT)) {
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
if (subject.startsWith(YOUTUBE_ID)) {
return cutOut(subject, YOUTUBE_START, YOUTUBE_END);
}
}
} catch (Exception e) {
// Ignore any errors in parsing; just return the raw text
}
return text;
}
} catch (Exception e) {
// Ignore any errors in parsing; just return the raw text
}
return text;
}
private static String cutOut(String text, String start, String end) {
int startAt = text.indexOf(start) + start.length();
return text.substring(startAt, text.indexOf(end, startAt));
}
private static String cutOut(String text, String start, String end) {
int startAt = text.indexOf(start) + start.length();
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; @@ -19,7 +19,6 @@ package org.transdroid.core.gui.search;
import android.content.ClipboardManager;
import android.content.Context;
import android.net.Uri;
import android.text.InputType;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
@ -33,35 +32,36 @@ import org.transdroid.core.gui.navigation.NavigationHelper; @@ -33,35 +32,36 @@ import org.transdroid.core.gui.navigation.NavigationHelper;
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
* TorrentsActivity#addTorrentByUrl(String, String) method}.
* @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);
final EditText urlEdit = (EditText) inputLayout.findViewById(R.id.url_edit);
ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboard.hasPrimaryClip() && clipboard.getPrimaryClip().getItemCount() > 0) {
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() {
@Override
public void onPositive(MaterialDialog dialog) {
String url = urlEdit.getText().toString();
Uri uri = Uri.parse(url);
if (!TextUtils.isEmpty(url)) {
String title = NavigationHelper.extractNameFromUri(uri);
if (uri.getScheme() != null && uri.getScheme().equals("magnet")) {
activity.addTorrentByMagnetUrl(url, title);
} else {
activity.addTorrentByUrl(url, title);
}
}
}
}).show();
}
/**
* 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}.
*
* @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);
final EditText urlEdit = (EditText) inputLayout.findViewById(R.id.url_edit);
ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboard.hasPrimaryClip() && clipboard.getPrimaryClip().getItemCount() > 0) {
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)
.onPositive((dialog, which) -> {
String url = urlEdit.getText().toString();
Uri uri = Uri.parse(url);
if (!TextUtils.isEmpty(url)) {
String title = NavigationHelper.extractNameFromUri(uri);
if (uri.getScheme() != null && uri.getScheme().equals("magnet")) {
activity.addTorrentByMagnetUrl(url, title);
} else {
activity.addTorrentByUrl(url, title);
}
}
}).show();
}
}

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

@ -16,38 +16,39 @@ @@ -16,38 +16,39 @@
*/
package org.transdroid.core.gui.settings;
import org.transdroid.R;
import org.transdroid.core.gui.navigation.DialogHelper;
import android.app.Activity;
import android.content.Intent;
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.
*
* @author Eric Kok
*/
public class AboutDialog implements DialogHelper.DialogSpecification {
private static final long serialVersionUID = -4711432869714292985L;
@Override
public int getDialogLayoutId() {
return R.layout.dialog_about;
}
@Override
public int getDialogMenuId() {
return R.menu.dialog_about;
}
@Override
public boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId) {
if (selectedItemId == R.id.action_visitwebsite) {
ownerActivity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://transdroid.org")));
return true;
}
return false;
}
private static final long serialVersionUID = -4711432869714292985L;
@Override
public int getDialogLayoutId() {
return R.layout.dialog_about;
}
@Override
public int getDialogMenuId() {
return R.menu.dialog_about;
}
@Override
public boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId) {
if (selectedItemId == R.id.action_visitwebsite) {
ownerActivity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://transdroid.org")));
return true;
}
return false;
}
}

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

@ -16,38 +16,39 @@ @@ -16,38 +16,39 @@
*/
package org.transdroid.core.gui.settings;
import org.transdroid.R;
import org.transdroid.core.gui.navigation.DialogHelper;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import org.transdroid.R;
import org.transdroid.core.gui.navigation.DialogHelper;
/**
* Fragment that shows recent app changes.
*
* @author Eric Kok
*/
public class ChangelogDialog implements DialogHelper.DialogSpecification {
private static final long serialVersionUID = -4563410777022941124L;
@Override
public int getDialogLayoutId() {
return R.layout.dialog_changelog;
}
@Override
public int getDialogMenuId() {
return R.menu.dialog_about;
}
@Override
public boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId) {
if (selectedItemId == R.id.action_visitwebsite) {
ownerActivity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://transdroid.org/about/changelog/")));
return true;
}
return false;
}
private static final long serialVersionUID = -4563410777022941124L;
@Override
public int getDialogLayoutId() {
return R.layout.dialog_changelog;
}
@Override
public int getDialogMenuId() {
return R.menu.dialog_about;
}
@Override
public boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId) {
if (selectedItemId == R.id.action_visitwebsite) {
ownerActivity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://transdroid.org/about/changelog/")));
return true;
}
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; @@ -39,84 +39,69 @@ import org.transdroid.core.gui.navigation.NavigationHelper;
@EActivity
public class HelpSettingsActivity extends PreferenceCompatActivity {
protected static final int DIALOG_CHANGELOG = 0;
protected static final int DIALOG_ABOUT = 1;
protected static final String INSTALLHELP_URI = "http://www.transdroid.org/download/";
@Bean
protected NavigationHelper navigationHelper;
@Bean
protected ApplicationSettings applicationSettings;
@Bean
protected ErrorLogSender errorLogSender;
@Bean
protected SettingsPersistence settingsPersistence;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Just load the system-related preferences from XML
addPreferencesFromResource(R.xml.pref_help);
// Handle outgoing links and preference changes
findPreference("system_sendlog").setOnPreferenceClickListener(onSendLogClick);
findPreference("system_installhelp").setOnPreferenceClickListener(onInstallHelpClick);
findPreference("system_changelog").setOnPreferenceClickListener(onChangeLogClick);
findPreference("system_about").setTitle(getString(R.string.pref_about, getString(R.string.app_name)));
findPreference("system_about").setOnPreferenceClickListener(onAboutClick);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@OptionsItem(android.R.id.home)
protected void navigateUp() {
MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
}
private OnPreferenceClickListener onSendLogClick = new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
errorLogSender.collectAndSendLog(HelpSettingsActivity.this, applicationSettings.getLastUsedServer());
return true;
}
};
private OnPreferenceClickListener onInstallHelpClick = new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(INSTALLHELP_URI)));
return true;
}
};
private OnPreferenceClickListener onChangeLogClick = new OnPreferenceClickListener() {
@SuppressWarnings("deprecation")
@Override
public boolean onPreferenceClick(Preference preference) {
showDialog(DIALOG_CHANGELOG);
return true;
}
};
private OnPreferenceClickListener onAboutClick = new OnPreferenceClickListener() {
@SuppressWarnings("deprecation")
@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;
}
protected static final int DIALOG_CHANGELOG = 0;
protected static final int DIALOG_ABOUT = 1;
protected static final String INSTALLHELP_URI = "http://www.transdroid.org/download/";
@Bean
protected NavigationHelper navigationHelper;
@Bean
protected ApplicationSettings applicationSettings;
@Bean
protected ErrorLogSender errorLogSender;
@Bean
protected SettingsPersistence settingsPersistence;
private OnPreferenceClickListener onSendLogClick = new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
errorLogSender.collectAndSendLog(HelpSettingsActivity.this, applicationSettings.getLastUsedServer());
return true;
}
};
private OnPreferenceClickListener onInstallHelpClick = preference -> {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(INSTALLHELP_URI)));
return true;
};
private OnPreferenceClickListener onChangeLogClick = preference -> {
showDialog(DIALOG_CHANGELOG);
return true;
};
private OnPreferenceClickListener onAboutClick = preference -> {
showDialog(DIALOG_ABOUT);
return true;
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Just load the system-related preferences from XML
addPreferencesFromResource(R.xml.pref_help);
// Handle outgoing links and preference changes
findPreference("system_sendlog").setOnPreferenceClickListener(onSendLogClick);
findPreference("system_installhelp").setOnPreferenceClickListener(onInstallHelpClick);
findPreference("system_changelog").setOnPreferenceClickListener(onChangeLogClick);
findPreference("system_about").setTitle(getString(R.string.pref_about, getString(R.string.app_name)));
findPreference("system_about").setOnPreferenceClickListener(onAboutClick);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@OptionsItem(android.R.id.home)
protected void navigateUp() {
MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
}
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; @@ -9,40 +9,40 @@ import androidx.preference.EditTextPreference;
public class InterceptableEditTextPreference extends EditTextPreference {
private OnPreferenceClickListener overrideClickListener = null;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public InterceptableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public InterceptableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public InterceptableEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public InterceptableEditTextPreference(Context context) {
super(context);
}
@Override
public OnPreferenceClickListener getOnPreferenceClickListener() {
return overrideClickListener;
}
@Override
public void setOnPreferenceClickListener(OnPreferenceClickListener onPreferenceClickListener) {
this.overrideClickListener = onPreferenceClickListener;
}
@Override
protected void onClick() {
if (overrideClickListener == null || !overrideClickListener.onPreferenceClick(this)) {
super.onClick();
}
}
private OnPreferenceClickListener overrideClickListener = null;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public InterceptableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public InterceptableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public InterceptableEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public InterceptableEditTextPreference(Context context) {
super(context);
}
@Override
public OnPreferenceClickListener getOnPreferenceClickListener() {
return overrideClickListener;
}
@Override
public void setOnPreferenceClickListener(OnPreferenceClickListener onPreferenceClickListener) {
this.overrideClickListener = onPreferenceClickListener;
}
@Override
protected void onClick() {
if (overrideClickListener == null || !overrideClickListener.onPreferenceClick(this)) {
super.onClick();
}
}
}

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

@ -40,201 +40,207 @@ import java.util.Map; @@ -40,201 +40,207 @@ import java.util.Map;
* 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
* enough).
*
* @author Eric Kok
*/
@EActivity
public abstract class KeyBoundPreferencesActivity extends PreferenceCompatActivity {
@Extra
protected int key = -1;
private SharedPreferences sharedPrefs;
private Map<String, String> originalSummaries = new HashMap<>();
/**
* Should be called during the activity {@link #onCreate(android.os.Bundle)} (but after super.onCreate(Bundle)) to
* 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
* defined so far at all
*/
protected final void init(int preferencesResId, int currentMaxKey) {
// Load the raw preferences to show in this screen
addPreferencesFromResource(preferencesResId);
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
// If no key was supplied (in the extra bundle) then use a new key instead
if (key < 0) {
key = currentMaxKey + 1;
}
}
protected void onResume() {
super.onResume();
// Monitor preference changes
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(
onPreferenceChangeListener);
}
protected void onPause() {
super.onPause();
// Stop monitoring preference changes
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(
onPreferenceChangeListener);
}
private OnSharedPreferenceChangeListener onPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
showValueOnSummary(key);
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.
* @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 EditTextPreference} that is bound to this preference
*/
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.
* @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 EditTextPreference} that is bound to this preference
*/
protected final EditTextPreference initTextPreference(String baseName, String defValue) {
return initTextPreference(baseName, defValue, null);
}
/**
* Updates a preference (including dependency) 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
* item_name_[key]
* @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
* @return The concrete {@link EditTextPreference} that is bound to this preference
*/
protected final EditTextPreference initTextPreference(String baseName, String defValue, String dependency) {
// Update the loaded Preference with the actual preference key to load/store with
EditTextPreference pref = (EditTextPreference) findPreference(baseName);
pref.setKey(baseName + "_" + key);
pref.setDependency(dependency == null ? null : dependency + "_" + key);
// Update the Preference by loading the current stored value into the EditText, if it exists
pref.setText(sharedPrefs.getString(baseName + "_" + key, defValue));
// 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());
showValueOnSummary(baseName + "_" + key);
return pref;
}
/**
* 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]
* @return The concrete {@link CheckBoxPreference} that is bound to this preference
*/
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
* item_name_[key]
* @param defValue The default value for this preference, as shown when no value was yet stored
* @return The concrete {@link CheckBoxPreference} that is bound to this preference
*/
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]
* @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
* @return The concrete {@link CheckBoxPreference} that is bound to this preference
*/
protected final CheckBoxPreference initBooleanPreference(String baseName, boolean defValue, String dependency) {
// Update the loaded Preference with the actual preference key to load/store with
CheckBoxPreference pref = (CheckBoxPreference) findPreference(baseName);
pref.setKey(baseName + "_" + key);
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
*/
protected final ListPreference initListPreference(String baseName) {
return initListPreference(baseName, null);
}
/**
* Updates a preference that allows picking an item from a list.
* @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
*/
protected final ListPreference initListPreference(String baseName, String defValue) {
// Update the loaded Preference with the actual preference key to load/store with
ListPreference pref = (ListPreference) findPreference(baseName);
pref.setKey(baseName + "_" + key);
// 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
originalSummaries.put(baseName + "_" + key, pref.getSummary() == null ? null : pref.getSummary().toString());
showValueOnSummary(baseName + "_" + key);
return pref;
}
protected void showValueOnSummary(String prefKey) {
Preference pref = findPreference(prefKey);
if (sharedPrefs.contains(prefKey)
&& pref instanceof EditTextPreference
&& !TextUtils.isEmpty(sharedPrefs.getString(prefKey, ""))
&& !isPasswordPref((EditTextPreference) pref)) {
// Non-password edit preferences show the user-entered value
pref.setSummary(sharedPrefs.getString(prefKey, ""));
return;
} else if (sharedPrefs.contains(prefKey) && pref instanceof ListPreference
&& ((ListPreference) pref).getValue() != null) {
// List preferences show the selected list value
ListPreference listPreference = (ListPreference) pref;
pref.setSummary(listPreference.getEntries()[listPreference.findIndexOfValue(listPreference.getValue())]);
return;
}
if (originalSummaries.containsKey(prefKey))
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");
}
@Extra
protected int key = -1;
private SharedPreferences sharedPrefs;
private Map<String, String> originalSummaries = new HashMap<>();
private OnSharedPreferenceChangeListener onPreferenceChangeListener = (sharedPreferences, key) -> {
showValueOnSummary(key);
onPreferencesChanged();
};
/**
* Should be called during the activity {@link #onCreate(android.os.Bundle)} (but after super.onCreate(Bundle)) to
* 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
* defined so far at all
*/
protected final void init(int preferencesResId, int currentMaxKey) {
// Load the raw preferences to show in this screen
addPreferencesFromResource(preferencesResId);
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
// If no key was supplied (in the extra bundle) then use a new key instead
if (key < 0) {
key = currentMaxKey + 1;
}
}
protected void onResume() {
super.onResume();
// Monitor preference changes
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(
onPreferenceChangeListener);
}
protected void onPause() {
super.onPause();
// Stop monitoring preference changes
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(
onPreferenceChangeListener);
}
/**
* 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.
*
* @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 EditTextPreference} that is bound to this preference
*/
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.
*
* @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 EditTextPreference} that is bound to this preference
*/
protected final EditTextPreference initTextPreference(String baseName, String defValue) {
return initTextPreference(baseName, defValue, null);
}
/**
* Updates a preference (including dependency) 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
* item_name_[key]
* @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
* @return The concrete {@link EditTextPreference} that is bound to this preference
*/
protected final EditTextPreference initTextPreference(String baseName, String defValue, String dependency) {
// Update the loaded Preference with the actual preference key to load/store with
EditTextPreference pref = (EditTextPreference) findPreference(baseName);
pref.setKey(baseName + "_" + key);
pref.setDependency(dependency == null ? null : dependency + "_" + key);
// Update the Preference by loading the current stored value into the EditText, if it exists
pref.setText(sharedPrefs.getString(baseName + "_" + key, defValue));
// 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());
showValueOnSummary(baseName + "_" + key);
return pref;
}
/**
* 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]
* @return The concrete {@link CheckBoxPreference} that is bound to this preference
*/
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
* item_name_[key]
* @param defValue The default value for this preference, as shown when no value was yet stored
* @return The concrete {@link CheckBoxPreference} that is bound to this preference
*/
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]
* @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
* @return The concrete {@link CheckBoxPreference} that is bound to this preference
*/
protected final CheckBoxPreference initBooleanPreference(String baseName, boolean defValue, String dependency) {
// Update the loaded Preference with the actual preference key to load/store with
CheckBoxPreference pref = (CheckBoxPreference) findPreference(baseName);
pref.setKey(baseName + "_" + key);
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
*/
protected final ListPreference initListPreference(String baseName) {
return initListPreference(baseName, null);
}
/**
* Updates a preference that allows picking an item from a list.
*
* @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
*/
protected final ListPreference initListPreference(String baseName, String defValue) {
// Update the loaded Preference with the actual preference key to load/store with
ListPreference pref = (ListPreference) findPreference(baseName);
pref.setKey(baseName + "_" + key);
// 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
originalSummaries.put(baseName + "_" + key, pref.getSummary() == null ? null : pref.getSummary().toString());
showValueOnSummary(baseName + "_" + key);
return pref;
}
protected void showValueOnSummary(String prefKey) {
Preference pref = findPreference(prefKey);
if (sharedPrefs.contains(prefKey)
&& pref instanceof EditTextPreference
&& !TextUtils.isEmpty(sharedPrefs.getString(prefKey, ""))
&& !isPasswordPref((EditTextPreference) pref)) {
// Non-password edit preferences show the user-entered value
pref.setSummary(sharedPrefs.getString(prefKey, ""));
return;
} else if (sharedPrefs.contains(prefKey) && pref instanceof ListPreference
&& ((ListPreference) pref).getValue() != null) {
// List preferences show the selected list value
ListPreference listPreference = (ListPreference) pref;
pref.setSummary(listPreference.getEntries()[listPreference.findIndexOfValue(listPreference.getValue())]);
return;
}
if (originalSummaries.containsKey(prefKey))
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; @@ -19,13 +19,13 @@ package org.transdroid.core.gui.settings;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.Preference.OnPreferenceClickListener;
@ -55,249 +55,210 @@ import java.util.List; @@ -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
* general settings.
*
* @author Eric Kok
*/
@EActivity
public class MainSettingsActivity extends PreferenceCompatActivity {
protected static final int DIALOG_ADDSEEDBOX = 0;
@Bean
protected NavigationHelper navigationHelper;
@Bean
protected ApplicationSettings applicationSettings;
@Bean
protected SearchHelper searchHelper;
protected SharedPreferences prefs;
private OnPreferenceClickListener onAddServer = new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
if (navigationHelper.enableSeedboxes())
showDialog(DIALOG_ADDSEEDBOX);
else
ServerSettingsActivity_.intent(MainSettingsActivity.this).start();
return true;
}
};
private OnPreferenceClickListener onAddWebsearch = new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
WebsearchSettingsActivity_.intent(MainSettingsActivity.this).start();
return true;
}
};
private OnPreferenceClickListener onAddRssfeed = new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
RssfeedSettingsActivity_.intent(MainSettingsActivity.this).start();
return true;
}
};
private OnPreferenceClickListener onBackgroundSettings = new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
NotificationSettingsActivity_.intent(MainSettingsActivity.this).start();
return true;
}
};
private OnPreferenceClickListener onSystemSettings = new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
SystemSettingsActivity_.intent(MainSettingsActivity.this).start();
return true;
}
};
private OnPreferenceClickListener onHelpSettings = new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
HelpSettingsActivity_.intent(MainSettingsActivity.this).start();
return true;
}
};
private OnPreferenceClickListener onDonate = new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
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));
}
};
protected static final int DIALOG_ADDSEEDBOX = 0;
@Bean
protected NavigationHelper navigationHelper;
@Bean
protected ApplicationSettings applicationSettings;
@Bean
protected SearchHelper searchHelper;
protected SharedPreferences prefs;
private OnPreferenceClickListener onAddServer = new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
if (navigationHelper.enableSeedboxes())
showDialog(DIALOG_ADDSEEDBOX);
else
ServerSettingsActivity_.intent(MainSettingsActivity.this).start();
return true;
}
};
private OnPreferenceClickListener onAddWebsearch = preference -> {
WebsearchSettingsActivity_.intent(MainSettingsActivity.this).start();
return true;
};
private OnPreferenceClickListener onAddRssfeed = preference -> {
RssfeedSettingsActivity_.intent(MainSettingsActivity.this).start();
return true;
};
private OnPreferenceClickListener onBackgroundSettings = preference -> {
NotificationSettingsActivity_.intent(MainSettingsActivity.this).start();
return true;
};
private OnPreferenceClickListener onSystemSettings = preference -> {
SystemSettingsActivity_.intent(MainSettingsActivity.this).start();
return true;
};
private OnPreferenceClickListener onHelpSettings = preference -> {
HelpSettingsActivity_.intent(MainSettingsActivity.this).start();
return true;
};
private OnPreferenceClickListener onDonate = preference -> {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.donate_url))));
return true;
};
private OnServerClickedListener onServerClicked = serverSetting -> ServerSettingsActivity_.intent(MainSettingsActivity.this).key(serverSetting.getOrder()).start();
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
startActivity(provider.getSettings().getSettingsActivityIntent(MainSettingsActivity.this).putExtra("key", seedboxOffset));
};
private OnWebsearchClickedListener onWebsearchClicked = websearchSetting -> WebsearchSettingsActivity_.intent(MainSettingsActivity.this).key(websearchSetting.getOrder()).start();
private OnRssfeedClickedListener onRssfeedClicked = rssfeedSetting -> RssfeedSettingsActivity_.intent(MainSettingsActivity.this).key(rssfeedSetting.getOrder()).start();
private OnClickListener onAddSeedbox = (dialog, 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
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Note: Settings are loaded in onResume()
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Note: Settings are loaded in onResume()
}
@Override
protected void onResume() {
super.onResume();
@Override
protected void onResume() {
super.onResume();
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
boolean enableSearchUi = navigationHelper.enableSearchUi();
boolean enableRssUi = navigationHelper.enableRssUi();
boolean enableDonateLink = !getString(R.string.donate_url).isEmpty();
boolean enableSearchUi = navigationHelper.enableSearchUi();
boolean enableRssUi = navigationHelper.enableRssUi();
boolean enableDonateLink = !getString(R.string.donate_url).isEmpty();
// Load the preference menu and attach actions
addPreferencesFromResource(R.xml.pref_main);
prefs = getPreferenceManager().getSharedPreferences();
findPreference("header_addserver").setOnPreferenceClickListener(onAddServer);
if (enableSearchUi) {
findPreference("header_addwebsearch").setOnPreferenceClickListener(onAddWebsearch);
}
if (enableRssUi) {
findPreference("header_addrssfeed").setOnPreferenceClickListener(onAddRssfeed);
}
findPreference("header_background").setOnPreferenceClickListener(onBackgroundSettings);
findPreference("header_system").setOnPreferenceClickListener(onSystemSettings);
findPreference("header_help").setOnPreferenceClickListener(onHelpSettings);
if (enableDonateLink) {
findPreference("header_donate").setOnPreferenceClickListener(onDonate);
} else {
getPreferenceScreen().removePreference(findPreference("header_donate"));
}
// Load the preference menu and attach actions
addPreferencesFromResource(R.xml.pref_main);
prefs = getPreferenceManager().getSharedPreferences();
findPreference("header_addserver").setOnPreferenceClickListener(onAddServer);
if (enableSearchUi) {
findPreference("header_addwebsearch").setOnPreferenceClickListener(onAddWebsearch);
}
if (enableRssUi) {
findPreference("header_addrssfeed").setOnPreferenceClickListener(onAddRssfeed);
}
findPreference("header_background").setOnPreferenceClickListener(onBackgroundSettings);
findPreference("header_system").setOnPreferenceClickListener(onSystemSettings);
findPreference("header_help").setOnPreferenceClickListener(onHelpSettings);
if (enableDonateLink) {
findPreference("header_donate").setOnPreferenceClickListener(onDonate);
} else {
getPreferenceScreen().removePreference(findPreference("header_donate"));
}
// Keep a list of the server codes and names (for default server selection)
List<String> serverCodes = new ArrayList<>();
List<String> serverNames = new ArrayList<>();
serverCodes.add(Integer.toString(ApplicationSettings.DEFAULTSERVER_LASTUSED));
serverCodes.add(Integer.toString(ApplicationSettings.DEFAULTSERVER_ASKONADD));
serverNames.add(getString(R.string.pref_defaultserver_lastused));
serverNames.add(getString(R.string.pref_defaultserver_askonadd));
// Keep a list of the server codes and names (for default server selection)
List<String> serverCodes = new ArrayList<>();
List<String> serverNames = new ArrayList<>();
serverCodes.add(Integer.toString(ApplicationSettings.DEFAULTSERVER_LASTUSED));
serverCodes.add(Integer.toString(ApplicationSettings.DEFAULTSERVER_ASKONADD));
serverNames.add(getString(R.string.pref_defaultserver_lastused));
serverNames.add(getString(R.string.pref_defaultserver_askonadd));
// Add existing servers
List<ServerSetting> servers = applicationSettings.getNormalServerSettings();
for (ServerSetting serverSetting : servers) {
getPreferenceScreen()
.addPreference(new ServerPreference(this).setServerSetting(serverSetting).setOnServerClickedListener(onServerClicked));
if (serverSetting.getUniqueIdentifier() != null) {
serverCodes.add(Integer.toString(serverSetting.getOrder()));
serverNames.add(serverSetting.getName());
}
}
// Add seedboxes; serversOffset keeps an int to have all ServerSettings with unique ids, seedboxOffset is unique
// only per seedbox type
int orderOffset = servers.size();
for (SeedboxProvider provider : SeedboxProvider.values()) {
int seedboxOffset = 0;
for (ServerSetting seedbox : provider.getSettings().getAllServerSettings(prefs, orderOffset)) {
getPreferenceScreen().addPreference(new SeedboxPreference(this).setProvider(provider).setServerSetting(seedbox)
.setOnSeedboxClickedListener(onSeedboxClicked, seedboxOffset));
orderOffset++;
seedboxOffset++;
if (seedbox.getUniqueIdentifier() != null) {
serverCodes.add(Integer.toString(seedbox.getOrder()));
serverNames.add(seedbox.getName());
}
}
}
// Allow selection of the default server
ListPreference defaultServerPreference = (ListPreference) findPreference("header_defaultserver");
defaultServerPreference.setEntries(serverNames.toArray(new String[serverNames.size()]));
defaultServerPreference.setEntryValues(serverCodes.toArray(new String[serverCodes.size()]));
// Add existing servers
List<ServerSetting> servers = applicationSettings.getNormalServerSettings();
for (ServerSetting serverSetting : servers) {
getPreferenceScreen()
.addPreference(new ServerPreference(this).setServerSetting(serverSetting).setOnServerClickedListener(onServerClicked));
if (serverSetting.getUniqueIdentifier() != null) {
serverCodes.add(Integer.toString(serverSetting.getOrder()));
serverNames.add(serverSetting.getName());
}
}
// Add seedboxes; serversOffset keeps an int to have all ServerSettings with unique ids, seedboxOffset is unique
// only per seedbox type
int orderOffset = servers.size();
for (SeedboxProvider provider : SeedboxProvider.values()) {
int seedboxOffset = 0;
for (ServerSetting seedbox : provider.getSettings().getAllServerSettings(prefs, orderOffset)) {
getPreferenceScreen().addPreference(new SeedboxPreference(this).setProvider(provider).setServerSetting(seedbox)
.setOnSeedboxClickedListener(onSeedboxClicked, seedboxOffset));
orderOffset++;
seedboxOffset++;
if (seedbox.getUniqueIdentifier() != null) {
serverCodes.add(Integer.toString(seedbox.getOrder()));
serverNames.add(seedbox.getName());
}
}
}
// Allow selection of the default server
ListPreference defaultServerPreference = (ListPreference) findPreference("header_defaultserver");
defaultServerPreference.setEntries(serverNames.toArray(new String[0]));
defaultServerPreference.setEntryValues(serverCodes.toArray(new String[0]));
// Add existing RSS feeds
if (!enableRssUi) {
// RSS should be disabled
getPreferenceScreen().removePreference(findPreference("header_rssfeeds"));
} else {
List<RssfeedSetting> rssfeeds = applicationSettings.getRssfeedSettings();
for (RssfeedSetting rssfeedSetting : rssfeeds) {
getPreferenceScreen()
.addPreference(new RssfeedPreference(this).setRssfeedSetting(rssfeedSetting).setOnRssfeedClickedListener(onRssfeedClicked));
}
}
// Add existing RSS feeds
if (!enableRssUi) {
// RSS should be disabled
getPreferenceScreen().removePreference(findPreference("header_rssfeeds"));
} else {
List<RssfeedSetting> rssfeeds = applicationSettings.getRssfeedSettings();
for (RssfeedSetting rssfeedSetting : rssfeeds) {
getPreferenceScreen()
.addPreference(new RssfeedPreference(this).setRssfeedSetting(rssfeedSetting).setOnRssfeedClickedListener(onRssfeedClicked));
}
}
if (!enableSearchUi) {
// Search should be disabled
getPreferenceScreen().removePreference(findPreference("header_searchsites"));
return;
}
if (!enableSearchUi) {
// Search should be disabled
getPreferenceScreen().removePreference(findPreference("header_searchsites"));
return;
}
// Add existing websearch sites
List<WebsearchSetting> websearches = applicationSettings.getWebsearchSettings();
for (WebsearchSetting websearchSetting : websearches) {
getPreferenceScreen().addPreference(
new WebsearchPreference(this).setWebsearchSetting(websearchSetting).setOnWebsearchClickedListener(onWebsearchClicked));
}
// Add existing websearch sites
List<WebsearchSetting> websearches = applicationSettings.getWebsearchSettings();
for (WebsearchSetting websearchSetting : websearches) {
getPreferenceScreen().addPreference(
new WebsearchPreference(this).setWebsearchSetting(websearchSetting).setOnWebsearchClickedListener(onWebsearchClicked));
}
// Construct list of all available search sites, in-app and web
ListPreference setSite = (ListPreference) findPreference("header_setsearchsite");
// Retrieve the available in-app search sites (using the Torrent Search package)
List<SearchSite> searchsites = searchHelper.getAvailableSites();
if (searchsites == null) {
searchsites = new ArrayList<>();
}
List<String> siteNames = new ArrayList<>(websearches.size() + searchsites.size());
List<String> siteValues = new ArrayList<>(websearches.size() + searchsites.size());
for (SearchSite searchSite : searchsites) {
siteNames.add(searchSite.getName());
siteValues.add(searchSite.getKey());
}
for (WebsearchSetting websearch : websearches) {
siteNames.add(websearch.getName());
siteValues.add(websearch.getKey());
}
// Supply the Preference list names and values
setSite.setEntries(siteNames.toArray(new String[siteNames.size()]));
setSite.setEntryValues(siteValues.toArray(new String[siteValues.size()]));
// Construct list of all available search sites, in-app and web
ListPreference setSite = (ListPreference) findPreference("header_setsearchsite");
// Retrieve the available in-app search sites (using the Torrent Search package)
List<SearchSite> searchsites = searchHelper.getAvailableSites();
if (searchsites == null) {
searchsites = new ArrayList<>();
}
List<String> siteNames = new ArrayList<>(websearches.size() + searchsites.size());
List<String> siteValues = new ArrayList<>(websearches.size() + searchsites.size());
for (SearchSite searchSite : searchsites) {
siteNames.add(searchSite.getName());
siteValues.add(searchSite.getKey());
}
for (WebsearchSetting websearch : websearches) {
siteNames.add(websearch.getName());
siteValues.add(websearch.getKey());
}
// Supply the Preference list names and values
setSite.setEntries(siteNames.toArray(new String[0]));
setSite.setEntryValues(siteValues.toArray(new String[0]));
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@OptionsItem(android.R.id.home)
protected void navigateUp() {
TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@OptionsItem(android.R.id.home)
protected void navigateUp() {
TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_ADDSEEDBOX:
// Open dialog to pick one of the supported seedbox providers (or a normal server)
String[] seedboxes = new String[SeedboxProvider.values().length + 1];
seedboxes[0] = getString(R.string.pref_addserver_normal);
for (int i = 0; i < seedboxes.length - 1; i++) {
seedboxes[i + 1] = getString(R.string.pref_seedbox_addseedbox, SeedboxProvider.values()[i].getSettings().getName());
}
return new AlertDialog.Builder(this).setItems(seedboxes, onAddSeedbox).create();
}
return null;
}
@Override
protected Dialog onCreateDialog(int id) {
if (id == DIALOG_ADDSEEDBOX) {
// Open dialog to pick one of the supported seedbox providers (or a normal server)
String[] seedboxes = new String[SeedboxProvider.values().length + 1];
seedboxes[0] = getString(R.string.pref_addserver_normal);
for (int i = 0; i < seedboxes.length - 1; i++) {
seedboxes[i + 1] = getString(R.string.pref_seedbox_addseedbox, SeedboxProvider.values()[i].getSettings().getName());
}
return new AlertDialog.Builder(this).setItems(seedboxes, onAddSeedbox).create();
}
return null;
}
}

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

Loading…
Cancel
Save