Browse Source

Adjust codestyle to an .editorconfig

pull/559/head
TacoTheDank 4 years ago
parent
commit
8204bd56a7
  1. 9
      .editorconfig
  2. 627
      app/src/main/AndroidManifest.xml
  3. 3
      app/src/main/java/androidx/preference/PreferenceManagerBinder.java
  4. 262
      app/src/main/java/org/transdroid/core/app/search/SearchHelper.java
  5. 183
      app/src/main/java/org/transdroid/core/app/search/SearchResult.java
  6. 65
      app/src/main/java/org/transdroid/core/app/search/SearchSite.java
  7. 1468
      app/src/main/java/org/transdroid/core/app/settings/ApplicationSettings.java
  8. 194
      app/src/main/java/org/transdroid/core/app/settings/NotificationSettings.java
  9. 182
      app/src/main/java/org/transdroid/core/app/settings/RssfeedSetting.java
  10. 596
      app/src/main/java/org/transdroid/core/app/settings/ServerSetting.java
  11. 631
      app/src/main/java/org/transdroid/core/app/settings/SettingsPersistence.java
  12. 3
      app/src/main/java/org/transdroid/core/app/settings/SettingsUtils.java
  13. 84
      app/src/main/java/org/transdroid/core/app/settings/SystemSettings.java
  14. 84
      app/src/main/java/org/transdroid/core/app/settings/WebsearchSetting.java
  15. 616
      app/src/main/java/org/transdroid/core/gui/DetailsActivity.java
  16. 1222
      app/src/main/java/org/transdroid/core/gui/DetailsFragment.java
  17. 62
      app/src/main/java/org/transdroid/core/gui/ServerPickerDialog.java
  18. 54
      app/src/main/java/org/transdroid/core/gui/ServerSelectionView.java
  19. 165
      app/src/main/java/org/transdroid/core/gui/ServerStatusView.java
  20. 29
      app/src/main/java/org/transdroid/core/gui/TorrentTasksExecutor.java
  21. 2475
      app/src/main/java/org/transdroid/core/gui/TorrentsActivity.java
  22. 864
      app/src/main/java/org/transdroid/core/gui/TorrentsFragment.java
  23. 35
      app/src/main/java/org/transdroid/core/gui/TransdroidApp.java
  24. 384
      app/src/main/java/org/transdroid/core/gui/lists/DetailsAdapter.java
  25. 435
      app/src/main/java/org/transdroid/core/gui/lists/LocalTorrent.java
  26. 554
      app/src/main/java/org/transdroid/core/gui/lists/MergeAdapter.java
  27. 15
      app/src/main/java/org/transdroid/core/gui/lists/PiecesMapView.java
  28. 3
      app/src/main/java/org/transdroid/core/gui/lists/SimpleListItem.java
  29. 171
      app/src/main/java/org/transdroid/core/gui/lists/SimpleListItemAdapter.java
  30. 60
      app/src/main/java/org/transdroid/core/gui/lists/SimpleListItemSpinnerAdapter.java
  31. 23
      app/src/main/java/org/transdroid/core/gui/lists/SimpleListItemView.java
  32. 93
      app/src/main/java/org/transdroid/core/gui/lists/SortByListItem.java
  33. 110
      app/src/main/java/org/transdroid/core/gui/lists/TorrentDetailsView.java
  34. 105
      app/src/main/java/org/transdroid/core/gui/lists/TorrentFilePriorityLayout.java
  35. 23
      app/src/main/java/org/transdroid/core/gui/lists/TorrentFileView.java
  36. 176
      app/src/main/java/org/transdroid/core/gui/lists/TorrentProgressBar.java
  37. 126
      app/src/main/java/org/transdroid/core/gui/lists/TorrentStatusLayout.java
  38. 85
      app/src/main/java/org/transdroid/core/gui/lists/TorrentView.java
  39. 90
      app/src/main/java/org/transdroid/core/gui/lists/TorrentsAdapter.java
  40. 158
      app/src/main/java/org/transdroid/core/gui/lists/ViewHolderAdapter.java
  41. 55
      app/src/main/java/org/transdroid/core/gui/log/DatabaseHelper.java
  42. 147
      app/src/main/java/org/transdroid/core/gui/log/ErrorLogEntry.java
  43. 98
      app/src/main/java/org/transdroid/core/gui/log/ErrorLogSender.java
  44. 63
      app/src/main/java/org/transdroid/core/gui/log/Log.java
  45. 54
      app/src/main/java/org/transdroid/core/gui/log/LogUncaughtExceptionHandler.java
  46. 130
      app/src/main/java/org/transdroid/core/gui/navigation/DialogHelper.java
  47. 140
      app/src/main/java/org/transdroid/core/gui/navigation/FilterListAdapter.java
  48. 75
      app/src/main/java/org/transdroid/core/gui/navigation/FilterListItemAdapter.java
  49. 17
      app/src/main/java/org/transdroid/core/gui/navigation/FilterListItemView.java
  50. 40
      app/src/main/java/org/transdroid/core/gui/navigation/FilterSeparatorView.java
  51. 221
      app/src/main/java/org/transdroid/core/gui/navigation/Label.java
  52. 40
      app/src/main/java/org/transdroid/core/gui/navigation/NavigationFilter.java
  53. 490
      app/src/main/java/org/transdroid/core/gui/navigation/NavigationHelper.java
  54. 3
      app/src/main/java/org/transdroid/core/gui/navigation/RefreshableActivity.java
  55. 195
      app/src/main/java/org/transdroid/core/gui/navigation/SelectionManagerMode.java
  56. 168
      app/src/main/java/org/transdroid/core/gui/navigation/SelectionModificationSpinner.java
  57. 117
      app/src/main/java/org/transdroid/core/gui/navigation/SetLabelDialog.java
  58. 61
      app/src/main/java/org/transdroid/core/gui/navigation/SetStorageLocationDialog.java
  59. 55
      app/src/main/java/org/transdroid/core/gui/navigation/SetTrackersDialog.java
  60. 161
      app/src/main/java/org/transdroid/core/gui/navigation/SetTransferRatesDialog.java
  61. 261
      app/src/main/java/org/transdroid/core/gui/navigation/StatusType.java
  62. 189
      app/src/main/java/org/transdroid/core/gui/remoterss/RemoteRssFragment.java
  63. 31
      app/src/main/java/org/transdroid/core/gui/remoterss/RemoteRssItemView.java
  64. 85
      app/src/main/java/org/transdroid/core/gui/remoterss/RemoteRssItemsAdapter.java
  65. 724
      app/src/main/java/org/transdroid/core/gui/rss/RssFeedsActivity.java
  66. 122
      app/src/main/java/org/transdroid/core/gui/rss/RssFeedsFragment.java
  67. 67
      app/src/main/java/org/transdroid/core/gui/rss/RssItemsActivity.java
  68. 352
      app/src/main/java/org/transdroid/core/gui/rss/RssItemsFragment.java
  69. 135
      app/src/main/java/org/transdroid/core/gui/rss/RssfeedLoader.java
  70. 59
      app/src/main/java/org/transdroid/core/gui/rss/RssfeedView.java
  71. 90
      app/src/main/java/org/transdroid/core/gui/rss/RssfeedsAdapter.java
  72. 71
      app/src/main/java/org/transdroid/core/gui/rss/RssitemStatusLayout.java
  73. 27
      app/src/main/java/org/transdroid/core/gui/rss/RssitemView.java
  74. 90
      app/src/main/java/org/transdroid/core/gui/rss/RssitemsAdapter.java
  75. 100
      app/src/main/java/org/transdroid/core/gui/search/BarcodeHelper.java
  76. 67
      app/src/main/java/org/transdroid/core/gui/search/FilePickerHelper.java
  77. 554
      app/src/main/java/org/transdroid/core/gui/search/SearchActivity.java
  78. 17
      app/src/main/java/org/transdroid/core/gui/search/SearchHistoryProvider.java
  79. 31
      app/src/main/java/org/transdroid/core/gui/search/SearchResultView.java
  80. 90
      app/src/main/java/org/transdroid/core/gui/search/SearchResultsAdapter.java
  81. 323
      app/src/main/java/org/transdroid/core/gui/search/SearchResultsFragment.java
  82. 22
      app/src/main/java/org/transdroid/core/gui/search/SearchSetting.java
  83. 17
      app/src/main/java/org/transdroid/core/gui/search/SearchSettingSelectionView.java
  84. 51
      app/src/main/java/org/transdroid/core/gui/search/SearchSettingsDropDownAdapter.java
  85. 39
      app/src/main/java/org/transdroid/core/gui/search/SearchSiteView.java
  86. 90
      app/src/main/java/org/transdroid/core/gui/search/SearchSitesAdapter.java
  87. 88
      app/src/main/java/org/transdroid/core/gui/search/SendIntentHelper.java
  88. 61
      app/src/main/java/org/transdroid/core/gui/search/UrlEntryDialog.java
  89. 41
      app/src/main/java/org/transdroid/core/gui/settings/AboutDialog.java
  90. 41
      app/src/main/java/org/transdroid/core/gui/settings/ChangelogDialog.java
  91. 158
      app/src/main/java/org/transdroid/core/gui/settings/HelpSettingsActivity.java
  92. 70
      app/src/main/java/org/transdroid/core/gui/settings/InterceptableEditTextPreference.java
  93. 392
      app/src/main/java/org/transdroid/core/gui/settings/KeyBoundPreferencesActivity.java
  94. 452
      app/src/main/java/org/transdroid/core/gui/settings/MainSettingsActivity.java
  95. 108
      app/src/main/java/org/transdroid/core/gui/settings/NotificationSettingsActivity.java
  96. 76
      app/src/main/java/org/transdroid/core/gui/settings/PreferenceCompatActivity.java
  97. 2
      app/src/main/java/org/transdroid/core/gui/settings/RingtonePreference.java
  98. 83
      app/src/main/java/org/transdroid/core/gui/settings/RssfeedPreference.java
  99. 81
      app/src/main/java/org/transdroid/core/gui/settings/RssfeedSettingsActivity.java
  100. 83
      app/src/main/java/org/transdroid/core/gui/settings/ServerPreference.java
  101. Some files were not shown because too many files have changed in this diff Show More

9
.editorconfig

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

627
app/src/main/AndroidManifest.xml

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

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

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

@ -21,6 +21,7 @@ import java.util.List;
import android.app.Dialog; import android.app.Dialog;
import android.app.DialogFragment; import android.app.DialogFragment;
import android.os.Bundle; import android.os.Bundle;
import org.transdroid.R; import org.transdroid.R;
import org.transdroid.core.app.settings.ServerSetting; import org.transdroid.core.app.settings.ServerSetting;
@ -30,36 +31,37 @@ import android.content.DialogInterface.OnClickListener;
public class ServerPickerDialog extends DialogFragment { public class ServerPickerDialog extends DialogFragment {
@Override @Override
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
String[] serverNames = getArguments().getStringArray("serverNames"); String[] serverNames = getArguments().getStringArray("serverNames");
return new AlertDialog.Builder(getActivity()).setTitle(R.string.navigation_pickserver) return new AlertDialog.Builder(getActivity()).setTitle(R.string.navigation_pickserver)
.setItems(serverNames, new OnClickListener() { .setItems(serverNames, new OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
if (getActivity() != null && getActivity() instanceof TorrentsActivity) if (getActivity() != null && getActivity() instanceof TorrentsActivity)
((TorrentsActivity) getActivity()).switchServerAndAddFromIntent(which); ((TorrentsActivity) getActivity()).switchServerAndAddFromIntent(which);
} }
}).create(); }).create();
} }
/** /**
* Opens a dialog that allows the selection of a configured server (manual or seedbox). The calling activity will * 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. * 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 * @param activity The torrents activity from which the picker is started (and which received the callback)
* from (and its position in the list is returned to the activity) * @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()]; public static void startServerPicker(final TorrentsActivity activity, List<ServerSetting> serverSettings) {
for (int i = 0; i < serverSettings.size(); i++) { final String[] serverNames = new String[serverSettings.size()];
serverNames[i] = serverSettings.get(i).getName(); for (int i = 0; i < serverSettings.size(); i++) {
} serverNames[i] = serverSettings.get(i).getName();
ServerPickerDialog dialog = new ServerPickerDialog(); }
Bundle arguments = new Bundle(); ServerPickerDialog dialog = new ServerPickerDialog();
arguments.putStringArray("serverNames", serverNames); Bundle arguments = new Bundle();
dialog.setArguments(arguments); arguments.putStringArray("serverNames", serverNames);
dialog.show(activity.getFragmentManager(), "serverpicker"); dialog.setArguments(arguments);
} dialog.show(activity.getFragmentManager(), "serverpicker");
}
} }

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

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

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

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

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

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

2475
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

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

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

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

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

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

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

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

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

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

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

@ -41,9 +41,9 @@ class PiecesMapView extends View {
@Override @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int ws = MeasureSpec.getSize(widthMeasureSpec); int ws = MeasureSpec.getSize(widthMeasureSpec);
int hs = Math.max(getHeight(), MINIMUM_HEIGHT); int hs = Math.max(getHeight(), MINIMUM_HEIGHT);
setMeasuredDimension(ws, hs); setMeasuredDimension(ws, hs);
} }
@Override @Override
@ -65,7 +65,7 @@ class PiecesMapView extends View {
piecesScaled = new ArrayList<Integer>(); piecesScaled = new ArrayList<Integer>();
int bucketCount = (int) Math.ceil((double) width / (double) pieceWidth); int bucketCount = (int) Math.ceil((double) width / (double) pieceWidth);
int bucketSize = (int) Math.floor((double)this.pieces.size() / (double) bucketCount); int bucketSize = (int) Math.floor((double) this.pieces.size() / (double) bucketCount);
// loop buckets // loop buckets
for (int i = 0; i < bucketCount; i++) { for (int i = 0; i < bucketCount; i++) {
@ -74,7 +74,7 @@ class PiecesMapView extends View {
int start = i * bucketSize; int start = i * bucketSize;
// If this is the last bucket, throw the remainder of the pieces array into it // If this is the last bucket, throw the remainder of the pieces array into it
int end = (i == bucketCount-1) ? this.pieces.size() : (i+1) * bucketSize; int end = (i == bucketCount - 1) ? this.pieces.size() : (i + 1) * bucketSize;
ArrayList<Integer> bucket = new ArrayList<Integer>(this.pieces.subList(start, end)); ArrayList<Integer> bucket = new ArrayList<Integer>(this.pieces.subList(start, end));
@ -82,7 +82,7 @@ class PiecesMapView extends View {
int downloadingCount = 0; int downloadingCount = 0;
// loop pieces in bucket // loop pieces in bucket
for(int j = 0; j < bucket.size(); j++) { for (int j = 0; j < bucket.size(); j++) {
// Count downloading pieces // Count downloading pieces
if (bucket.get(j) == 1) { if (bucket.get(j) == 1) {
downloadingCount++; downloadingCount++;
@ -115,8 +115,7 @@ class PiecesMapView extends View {
} }
String scaledPiecesString = ""; String scaledPiecesString = "";
for (int s : piecesScaled) for (int s : piecesScaled) {
{
scaledPiecesString += s; scaledPiecesString += s;
} }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -36,78 +36,82 @@ import android.view.Window;
* Helper class that show a dialog either as pop-up or as full screen activity. Should be used by calling * Helper class that show a dialog either as pop-up or as full screen activity. Should be used by calling
* {@link #showDialog(Context, DialogSpecification)} with in instance of the dialog specification that should be shown, * {@link #showDialog(Context, DialogSpecification)} with in instance of the dialog specification that should be shown,
* from the calling activity's {@link Activity#onCreateDialog(int)}. * from the calling activity's {@link Activity#onCreateDialog(int)}.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EActivity @EActivity
public class DialogHelper extends Activity { public class DialogHelper extends Activity {
@Extra @Extra
protected DialogSpecification dialog; protected DialogSpecification dialog;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(dialog.getDialogLayoutId()); setContentView(dialog.getDialogLayoutId());
// TODO getActionBar().setDisplayHomeAsUpEnabled(true); // TODO getActionBar().setDisplayHomeAsUpEnabled(true);
} }
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater menuInflater = getMenuInflater(); MenuInflater menuInflater = getMenuInflater();
menuInflater.inflate(dialog.getDialogMenuId(), menu); menuInflater.inflate(dialog.getDialogMenuId(), menu);
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) { if (item.getItemId() == android.R.id.home) {
// Action bar up button clicked; navigate up all the way back to the torrents activity // Action bar up button clicked; navigate up all the way back to the torrents activity
TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start(); TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
return true; return true;
} }
return dialog.onMenuItemSelected(this, item.getItemId()); return dialog.onMenuItemSelected(this, item.getItemId());
} }
/** /**
* Call this from {@link Activity#onCreateDialog(int)}, supplying an instance of the {@link DialogSpecification} * Call this from {@link Activity#onCreateDialog(int)}, supplying an instance of the {@link DialogSpecification}
* that should be shown to the user. * 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 * @param context The activity that calls this method and which will own the constructed dialog
* @return Either an instance of a {@link Dialog} that the activity should further control or null if the dialog * @param dialog An instance of the specification for the dialog that needs to be shown
* will instead be opened as a full screen activity * @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) { */
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()) // If the device is large (i.e. a tablet) then return a dialog to show
return new PopupDialog(context, dialog); 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(); // This is a small device; create a full screen dialog (which is just an activity)
return null; 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. /**
*/ * 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) { protected static class PopupDialog extends Dialog {
super(context); public PopupDialog(Context context, DialogSpecification dialog) {
requestWindowFeature(Window.FEATURE_NO_TITLE); super(context);
setContentView(dialog.getDialogLayoutId()); 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 * Specification for some dialog that can be show to the user, consisting of a custom layout and possibly an action
* screen activity. Use only for unimportant actions. * 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(); public interface DialogSpecification extends Serializable {
int getDialogMenuId(); int getDialogLayoutId();
boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId);
} int getDialogMenuId();
boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId);
}
} }

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

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

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

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

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

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

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

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

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

@ -29,118 +29,121 @@ import java.util.List;
/** /**
* Represents some label that is active or available on the server. * Represents some label that is active or available on the server.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class Label implements SimpleListItem, NavigationFilter, Comparable<Label> { public class Label implements SimpleListItem, NavigationFilter, Comparable<Label> {
private static String unnamedLabelText = null; private static String unnamedLabelText = null;
private final boolean isEmptyLabel; private final boolean isEmptyLabel;
private final String name; private final String name;
private final int count; private final int count;
private Label(String name, int count, boolean isEmptyLabel) { private Label(String name, int count, boolean isEmptyLabel) {
this.name = name; this.name = name;
this.count = count; this.count = count;
this.isEmptyLabel = isEmptyLabel; this.isEmptyLabel = isEmptyLabel;
} }
public Label(org.transdroid.daemon.Label daemonLabel) { public Label(org.transdroid.daemon.Label daemonLabel) {
this(daemonLabel.getName(), daemonLabel.getCount(), false); this(daemonLabel.getName(), daemonLabel.getCount(), false);
} }
@Override @Override
public String getName() { public String getName() {
if (TextUtils.isEmpty(this.name)) { if (TextUtils.isEmpty(this.name)) {
return unnamedLabelText; return unnamedLabelText;
} }
return this.name; return this.name;
} }
@Override @Override
public String getCode() { public String getCode() {
// Use the class name and label name to provide a unique navigation filter code // Use the class name and label name to provide a unique navigation filter code
return Label.class.getSimpleName() + "_" + name; return Label.class.getSimpleName() + "_" + name;
} }
public int getCount() { public int getCount() {
return count; return count;
} }
public boolean isEmptyLabel() { public boolean isEmptyLabel() {
return isEmptyLabel; return isEmptyLabel;
} }
/** /**
* Returns true if the torrent label's name matches this (selected) label's name, false otherwise * 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 * @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) { @Override
if (isEmptyLabel) { public boolean matches(Torrent torrent, boolean dormantAsInactive) {
return TextUtils.isEmpty(torrent.getLabelName()); if (isEmptyLabel) {
} return TextUtils.isEmpty(torrent.getLabelName());
return torrent.getLabelName() != null && torrent.getLabelName().equals(name); }
} return torrent.getLabelName() != null && torrent.getLabelName().equals(name);
}
@Override
public int compareTo(Label another) { @Override
return this.name.compareTo(another.getName()); 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 * 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 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 * @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)
public static ArrayList<Label> convertToNavigationLabels(List<org.transdroid.daemon.Label> daemonLabels, String unnamedLabel) { * @return A label items that can be used in a filter list such as the action bar spinner
if (daemonLabels == null) { */
return null; public static ArrayList<Label> convertToNavigationLabels(List<org.transdroid.daemon.Label> daemonLabels, String unnamedLabel) {
} if (daemonLabels == null) {
ArrayList<Label> localLabels = new ArrayList<>(); return null;
unnamedLabelText = unnamedLabel; }
ArrayList<Label> localLabels = new ArrayList<>();
for (org.transdroid.daemon.Label label : daemonLabels) { unnamedLabelText = unnamedLabel;
if (label != null && !TextUtils.isEmpty(label.getName())) {
localLabels.add(new Label(label)); 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 Collections.sort(localLabels);
localLabels.add(0, new Label(unnamedLabel, -1, true));
// force unlabelled to be at the top
return localLabels; localLabels.add(0, new Label(unnamedLabel, -1, true));
}
return localLabels;
private Label(Parcel in) { }
this.name = in.readString();
this.count = in.readInt(); private Label(Parcel in) {
this.isEmptyLabel = in.readInt() == 1; 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 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];
} public Label[] newArray(int size) {
}; return new Label[size];
}
@Override };
public int describeContents() {
return 0; @Override
} public int describeContents() {
return 0;
@Override }
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name); @Override
dest.writeInt(count); public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(isEmptyLabel ? 1 : 0); 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;
/** /**
* Represents a filter, used in the app navigation, that can check if some torrent matches the user-set filter * Represents a filter, used in the app navigation, that can check if some torrent matches the user-set filter
*
* @author Eric Kok * @author Eric Kok
*/ */
public interface NavigationFilter extends Parcelable { public interface NavigationFilter extends Parcelable {
/** /**
* Implementations should check if the supplied torrent matches the filter; for example a label filter should return true if the torrent's label * Implementations should check if the supplied torrent matches the filter; for example a label filter should return true if the torrent's label
* equals this items label name. * equals this items label name.
* @param torrent The torrent to check for matches *
* @param dormantAsInactive If true, dormant (0KB/s, so no data transfer) torrents are never actively downloading or seeding * @param torrent The torrent to check for matches
* @return True if the torrent matches the filter and should be shown in the current screen, false otherwise * @param dormantAsInactive If true, dormant (0KB/s, so no data transfer) torrents are never actively downloading or seeding
*/ * @return True if the torrent matches the filter and should be shown in the current screen, false otherwise
boolean matches(Torrent torrent, boolean dormantAsInactive); */
boolean matches(Torrent torrent, boolean dormantAsInactive);
/** /**
* Implementations should return a name that can be shown to indicate the active filter * Implementations should return a name that can be shown to indicate the active filter
* @return The name of the filter item as string *
*/ * @return The name of the filter item as string
String getName(); */
String getName();
/** /**
* Implementations should return a code that (within reasonable expectations) uniquely identifies it in the list of navigation filters * Implementations should return a code that (within reasonable expectations) uniquely identifies it in the list of navigation filters
* @return The code to uniquely identify this specific navigation filter, such as the name with a class name prefix *
*/ * @return The code to uniquely identify this specific navigation filter, such as the name with a class name prefix
String getCode(); */
String getCode();
} }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

108
app/src/main/java/org/transdroid/core/gui/settings/NotificationSettingsActivity.java

@ -36,59 +36,59 @@ import org.transdroid.core.service.ServerCheckerJob;
@EActivity @EActivity
public class NotificationSettingsActivity extends PreferenceCompatActivity implements OnSharedPreferenceChangeListener { public class NotificationSettingsActivity extends PreferenceCompatActivity implements OnSharedPreferenceChangeListener {
@Bean @Bean
protected NotificationSettings notificationSettings; protected NotificationSettings notificationSettings;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Load the notification-related preferences from XML and update availability thereof // Load the notification-related preferences from XML and update availability thereof
addPreferencesFromResource(R.xml.pref_notifications); addPreferencesFromResource(R.xml.pref_notifications);
boolean disabled = !notificationSettings.isEnabledForRss() && !notificationSettings.isEnabledForTorrents(); boolean disabled = !notificationSettings.isEnabledForRss() && !notificationSettings.isEnabledForTorrents();
updatePrefsEnabled(disabled); updatePrefsEnabled(disabled);
} }
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
((RingtonePreference) findPreference("notifications_sound")).onActivityResult(requestCode, resultCode, data); ((RingtonePreference) findPreference("notifications_sound")).onActivityResult(requestCode, resultCode, data);
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
// Start/stop the background service appropriately // Start/stop the background service appropriately
getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
} }
@Override @Override
protected void onPause() { protected void onPause() {
super.onPause(); super.onPause();
getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
} }
@TargetApi(Build.VERSION_CODES.HONEYCOMB) @TargetApi(Build.VERSION_CODES.HONEYCOMB)
@OptionsItem(android.R.id.home) @OptionsItem(android.R.id.home)
protected void navigateUp() { protected void navigateUp() {
MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start(); MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
} }
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
ServerCheckerJob.schedule(getApplicationContext()); ServerCheckerJob.schedule(getApplicationContext());
RssCheckerJob.schedule(getApplicationContext()); RssCheckerJob.schedule(getApplicationContext());
} }
private void updatePrefsEnabled(boolean disabled) { private void updatePrefsEnabled(boolean disabled) {
findPreference("notifications_interval").setEnabled(!disabled); findPreference("notifications_interval").setEnabled(!disabled);
findPreference("notifications_sound").setEnabled(!disabled); findPreference("notifications_sound").setEnabled(!disabled);
findPreference("notifications_vibrate").setEnabled(!disabled); findPreference("notifications_vibrate").setEnabled(!disabled);
findPreference("notifications_ledcolour").setEnabled(!disabled); findPreference("notifications_ledcolour").setEnabled(!disabled);
findPreference("notifications_adwnotify").setEnabled(!disabled); findPreference("notifications_adwnotify").setEnabled(!disabled);
} }
} }

76
app/src/main/java/org/transdroid/core/gui/settings/PreferenceCompatActivity.java

@ -22,58 +22,58 @@ public class PreferenceCompatActivity extends AppCompatActivity implements AppCo
public PreferenceManager getPreferenceManager() { public PreferenceManager getPreferenceManager() {
return fragment.getPreferenceManager(); return fragment.getPreferenceManager();
} }
public PreferenceScreen getPreferenceScreen() { public PreferenceScreen getPreferenceScreen() {
return fragment.getPreferenceScreen(); return fragment.getPreferenceScreen();
} }
public Preference findPreference(CharSequence key) { public Preference findPreference(CharSequence key) {
return fragment.findPreference(key); return fragment.findPreference(key);
} }
@Override @Override
public boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref) { public boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref) {
LowerPreferencesFragment lowerFragment = new LowerPreferencesFragment(pref); LowerPreferencesFragment lowerFragment = new LowerPreferencesFragment(pref);
getSupportFragmentManager().beginTransaction().replace(android.R.id.content, lowerFragment).addToBackStack("lower").commit(); getSupportFragmentManager().beginTransaction().replace(android.R.id.content, lowerFragment).addToBackStack("lower").commit();
return true; return true;
} }
public static class RootPreferencesFragment extends PreferenceFragmentCompat { public static class RootPreferencesFragment extends PreferenceFragmentCompat {
private int preferencesResId; private int preferencesResId;
public RootPreferencesFragment(int preferencesResId) { public RootPreferencesFragment(int preferencesResId) {
this.preferencesResId = preferencesResId; this.preferencesResId = preferencesResId;
} }
@Override @Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(preferencesResId); addPreferencesFromResource(preferencesResId);
}
} }
}
public static class LowerPreferencesFragment extends PreferenceFragmentCompat { public static class LowerPreferencesFragment extends PreferenceFragmentCompat {
private PreferenceScreen prefs; private PreferenceScreen prefs;
public LowerPreferencesFragment() { public LowerPreferencesFragment() {
} }
public LowerPreferencesFragment(PreferenceScreen prefs) { public LowerPreferencesFragment(PreferenceScreen prefs) {
this.prefs = prefs; this.prefs = prefs;
} }
@Override @Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
if (prefs != null) { if (prefs != null) {
// Update the already loaded preferences with this fragment's manager to handle dialog clicks, etc. // Update the already loaded preferences with this fragment's manager to handle dialog clicks, etc.
for (int i = 0; i < prefs.getPreferenceCount(); i++) { for (int i = 0; i < prefs.getPreferenceCount(); i++) {
PreferenceManagerBinder.bind(prefs.getPreference(i), getPreferenceManager()); PreferenceManagerBinder.bind(prefs.getPreference(i), getPreferenceManager());
}
setPreferenceScreen(prefs);
prefs = null;
} }
setPreferenceScreen(prefs);
prefs = null;
} }
} }
} }
}

2
app/src/main/java/org/transdroid/core/gui/settings/RingtonePreference.java

@ -62,7 +62,7 @@ public class RingtonePreference extends Preference {
public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_RINGTONE && resultCode == Activity.RESULT_OK && data != null && data.getExtras() != null) { if (requestCode == REQUEST_RINGTONE && resultCode == Activity.RESULT_OK && data != null && data.getExtras() != null) {
Uri ringtone = (Uri) data.getExtras().get(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); Uri ringtone = (Uri) data.getExtras().get(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
getSharedPreferences().edit().putString(getKey(), ringtone == null? "": ringtone.toString()).apply(); getSharedPreferences().edit().putString(getKey(), ringtone == null ? "" : ringtone.toString()).apply();
} }
} }
} }

83
app/src/main/java/org/transdroid/core/gui/settings/RssfeedPreference.java

@ -24,55 +24,58 @@ import org.transdroid.core.app.settings.RssfeedSetting;
/** /**
* Represents a {@link RssfeedSetting} in a preferences screen. * Represents a {@link RssfeedSetting} in a preferences screen.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class RssfeedPreference extends Preference { public class RssfeedPreference extends Preference {
private static final int ORDER_START = 201; private static final int ORDER_START = 201;
private RssfeedSetting rssfeedSetting; private RssfeedSetting rssfeedSetting;
private OnRssfeedClickedListener onRssfeedClickedListener = null; private OnRssfeedClickedListener onRssfeedClickedListener = null;
public RssfeedPreference(Context context) { public RssfeedPreference(Context context) {
super(context); super(context);
setOnPreferenceClickListener(onPreferenceClicked); setOnPreferenceClickListener(onPreferenceClicked);
setIconSpaceReserved(false); setIconSpaceReserved(false);
} }
/** /**
* Set the RSS feed settings object that is bound to this preference item * Set the RSS feed settings object that is bound to this preference item
* @param rssfeedSetting The RSS feed settings *
* @return Itself, for method chaining * @param rssfeedSetting The RSS feed settings
*/ * @return Itself, for method chaining
public RssfeedPreference setRssfeedSetting(RssfeedSetting rssfeedSetting) { */
this.rssfeedSetting = rssfeedSetting; public RssfeedPreference setRssfeedSetting(RssfeedSetting rssfeedSetting) {
setTitle(rssfeedSetting.getName()); this.rssfeedSetting = rssfeedSetting;
setSummary(rssfeedSetting.getHumanReadableIdentifier()); setTitle(rssfeedSetting.getName());
setOrder(ORDER_START + rssfeedSetting.getOrder()); setSummary(rssfeedSetting.getHumanReadableIdentifier());
return this; setOrder(ORDER_START + rssfeedSetting.getOrder());
} return this;
}
/** /**
* Set a listener that will be notified of click events on this preference * Set a listener that will be notified of click events on this preference
* @param onRssfeedClickedListener The click listener to register *
* @return Itself, for method chaining * @param onRssfeedClickedListener The click listener to register
*/ * @return Itself, for method chaining
public RssfeedPreference setOnRssfeedClickedListener(OnRssfeedClickedListener onRssfeedClickedListener) { */
this.onRssfeedClickedListener = onRssfeedClickedListener; public RssfeedPreference setOnRssfeedClickedListener(OnRssfeedClickedListener onRssfeedClickedListener) {
return this; this.onRssfeedClickedListener = onRssfeedClickedListener;
} return this;
}
private OnPreferenceClickListener onPreferenceClicked = new OnPreferenceClickListener() { private OnPreferenceClickListener onPreferenceClicked = new OnPreferenceClickListener() {
@Override @Override
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {
if (onRssfeedClickedListener != null) if (onRssfeedClickedListener != null)
onRssfeedClickedListener.onRssfeedClicked(rssfeedSetting); onRssfeedClickedListener.onRssfeedClicked(rssfeedSetting);
return true; return true;
} }
}; };
public interface OnRssfeedClickedListener { public interface OnRssfeedClickedListener {
void onRssfeedClicked(RssfeedSetting rssfeedSetting); void onRssfeedClicked(RssfeedSetting rssfeedSetting);
} }
} }

81
app/src/main/java/org/transdroid/core/gui/settings/RssfeedSettingsActivity.java

@ -34,58 +34,59 @@ import org.transdroid.core.app.settings.ApplicationSettings_;
/** /**
* Activity that allows for a configuration of some RSS feed. The key can be supplied to update an existing RSS feed setting instead of creating a new * Activity that allows for a configuration of some RSS feed. The key can be supplied to update an existing RSS feed setting instead of creating a new
* one. * one.
*
* @author Eric Kok * @author Eric Kok
*/ */
@EActivity @EActivity
@OptionsMenu(R.menu.activity_deleteableprefs) @OptionsMenu(R.menu.activity_deleteableprefs)
public class RssfeedSettingsActivity extends KeyBoundPreferencesActivity { public class RssfeedSettingsActivity extends KeyBoundPreferencesActivity {
private static final int DIALOG_CONFIRMREMOVE = 0; private static final int DIALOG_CONFIRMREMOVE = 0;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Load the raw preferences to show in this screen // Load the raw preferences to show in this screen
init(R.xml.pref_rssfeed, ApplicationSettings_.getInstance_(this).getMaxRssfeed()); init(R.xml.pref_rssfeed, ApplicationSettings_.getInstance_(this).getMaxRssfeed());
initTextPreference("rssfeed_name"); initTextPreference("rssfeed_name");
initTextPreference("rssfeed_url"); initTextPreference("rssfeed_url");
initBooleanPreference("rssfeed_alarmnew"); initBooleanPreference("rssfeed_alarmnew");
initTextPreference("rssfeed_exclude"); initTextPreference("rssfeed_exclude");
initTextPreference("rssfeed_include"); initTextPreference("rssfeed_include");
// TODO: Replace this for cookies support like web searches // TODO: Replace this for cookies support like web searches
initBooleanPreference("rssfeed_reqauth"); initBooleanPreference("rssfeed_reqauth");
} }
@TargetApi(Build.VERSION_CODES.HONEYCOMB) @TargetApi(Build.VERSION_CODES.HONEYCOMB)
@OptionsItem(android.R.id.home) @OptionsItem(android.R.id.home)
protected void navigateUp() { protected void navigateUp() {
MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start(); MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@OptionsItem(R.id.action_removesettings) @OptionsItem(R.id.action_removesettings)
protected void removeSettings() { protected void removeSettings() {
showDialog(DIALOG_CONFIRMREMOVE); showDialog(DIALOG_CONFIRMREMOVE);
} }
@Override @Override
protected Dialog onCreateDialog(int id) { protected Dialog onCreateDialog(int id) {
switch (id) { switch (id) {
case DIALOG_CONFIRMREMOVE: case DIALOG_CONFIRMREMOVE:
return new AlertDialog.Builder(this).setMessage(R.string.pref_confirmremove) return new AlertDialog.Builder(this).setMessage(R.string.pref_confirmremove)
.setPositiveButton(android.R.string.ok, new OnClickListener() { .setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
ApplicationSettings_.getInstance_(RssfeedSettingsActivity.this).removeRssfeedSettings(key); ApplicationSettings_.getInstance_(RssfeedSettingsActivity.this).removeRssfeedSettings(key);
finish(); finish();
} }
}).setNegativeButton(android.R.string.cancel, null).create(); }).setNegativeButton(android.R.string.cancel, null).create();
} }
return null; return null;
} }
} }

83
app/src/main/java/org/transdroid/core/gui/settings/ServerPreference.java

@ -24,55 +24,58 @@ import org.transdroid.core.app.settings.ServerSetting;
/** /**
* Represents a {@link ServerSetting} in a preferences screen. * Represents a {@link ServerSetting} in a preferences screen.
*
* @author Eric Kok * @author Eric Kok
*/ */
public class ServerPreference extends Preference { public class ServerPreference extends Preference {
private static final int ORDER_START = 1; private static final int ORDER_START = 1;
protected ServerSetting serverSetting; protected ServerSetting serverSetting;
private OnServerClickedListener onServerClickedListener = null; private OnServerClickedListener onServerClickedListener = null;
public ServerPreference(Context context) { public ServerPreference(Context context) {
super(context); super(context);
setOnPreferenceClickListener(onPreferenceClicked); setOnPreferenceClickListener(onPreferenceClicked);
setIconSpaceReserved(false); setIconSpaceReserved(false);
} }
/** /**
* Set the server settings object that is bound to this preference item * Set the server settings object that is bound to this preference item
* @param serverSetting The server settings *
* @return Itself, for method chaining * @param serverSetting The server settings
*/ * @return Itself, for method chaining
public ServerPreference setServerSetting(ServerSetting serverSetting) { */
this.serverSetting = serverSetting; public ServerPreference setServerSetting(ServerSetting serverSetting) {
setTitle(serverSetting.getName()); this.serverSetting = serverSetting;
setSummary(serverSetting.getHumanReadableIdentifier()); setTitle(serverSetting.getName());
setOrder(ORDER_START + serverSetting.getOrder()); setSummary(serverSetting.getHumanReadableIdentifier());
return this; setOrder(ORDER_START + serverSetting.getOrder());
} return this;
}
/** /**
* Set a listener that will be notified of click events on this preference * Set a listener that will be notified of click events on this preference
* @param onServerClickedListener The click listener to register *
* @return Itself, for method chaining * @param onServerClickedListener The click listener to register
*/ * @return Itself, for method chaining
public ServerPreference setOnServerClickedListener(OnServerClickedListener onServerClickedListener) { */
this.onServerClickedListener = onServerClickedListener; public ServerPreference setOnServerClickedListener(OnServerClickedListener onServerClickedListener) {
return this; this.onServerClickedListener = onServerClickedListener;
} return this;
}
private OnPreferenceClickListener onPreferenceClicked = new OnPreferenceClickListener() { private OnPreferenceClickListener onPreferenceClicked = new OnPreferenceClickListener() {
@Override @Override
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {
if (onServerClickedListener != null) if (onServerClickedListener != null)
onServerClickedListener.onServerClicked(serverSetting); onServerClickedListener.onServerClicked(serverSetting);
return true; return true;
} }
}; };
public interface OnServerClickedListener { public interface OnServerClickedListener {
public void onServerClicked(ServerSetting serverSetting); public void onServerClicked(ServerSetting serverSetting);
} }
} }

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

Loading…
Cancel
Save