├── .gitignore ├── .travis.yml ├── License.md ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hitherejoe │ │ └── androidtvboilerplate │ │ ├── ContentActivityTest.java │ │ ├── SearchContentActivityTest.java │ │ └── util │ │ └── CustomMatchers.java │ ├── commonTest │ └── java │ │ └── com │ │ └── hitherejoe │ │ └── androidtvboilerplate │ │ └── test │ │ └── common │ │ ├── TestDataFactory.java │ │ ├── injection │ │ ├── component │ │ │ └── TestComponent.java │ │ └── module │ │ │ └── ApplicationTestModule.java │ │ └── rules │ │ └── TestComponentRule.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── hitherejoe │ │ │ └── androidtvboilerplate │ │ │ ├── AndroidTvBoilerplateApplication.java │ │ │ ├── data │ │ │ ├── DataManager.java │ │ │ ├── local │ │ │ │ └── PreferencesHelper.java │ │ │ ├── model │ │ │ │ └── Cat.java │ │ │ ├── recommendations │ │ │ │ ├── RecommendationReceiver.java │ │ │ │ └── UpdateRecommendationsService.java │ │ │ └── remote │ │ │ │ └── AndroidTvBoilerplateService.java │ │ │ ├── injection │ │ │ ├── ActivityContext.java │ │ │ ├── ApplicationContext.java │ │ │ ├── PerActivity.java │ │ │ ├── component │ │ │ │ ├── ActivityComponent.java │ │ │ │ └── ApplicationComponent.java │ │ │ └── module │ │ │ │ ├── ActivityModule.java │ │ │ │ └── ApplicationModule.java │ │ │ ├── ui │ │ │ ├── base │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── BasePresenter.java │ │ │ │ ├── MvpView.java │ │ │ │ └── Presenter.java │ │ │ ├── common │ │ │ │ └── CardPresenter.java │ │ │ ├── content │ │ │ │ ├── ContentActivity.java │ │ │ │ ├── ContentFragment.java │ │ │ │ ├── ContentMvpView.java │ │ │ │ └── ContentPresenter.java │ │ │ └── search │ │ │ │ ├── SearchContentActivity.java │ │ │ │ ├── SearchContentFragment.java │ │ │ │ ├── SearchContentMvpView.java │ │ │ │ └── SearchContentPresenter.java │ │ │ └── util │ │ │ ├── NetworkUtil.java │ │ │ ├── ToastFactory.java │ │ │ └── ViewUtils.java │ └── res │ │ ├── drawable-xhdpi │ │ ├── banner.png │ │ ├── banner_browse.png │ │ └── card_default.png │ │ ├── layout │ │ ├── activity_main.xml │ │ └── activity_search.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── hitherejoe │ └── androidtvboilerplate │ ├── data │ └── DataManagerTest.java │ ├── ui │ ├── content │ │ └── ContentPresenterTest.java │ └── search │ │ └── SearchContentPresenterTest.java │ └── util │ ├── DefaultConfig.java │ └── RxSchedulersOverrideRule.java ├── build.gradle ├── config └── quality │ ├── checkstyle │ └── checkstyle-config.xml │ ├── findbugs │ └── android-exclude-filter.xml │ ├── pmd │ └── pmd-ruleset.xml │ └── quality.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── browse_fragment.png ├── search_fragment.png └── web_banner.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | .DS_Store 5 | /build 6 | .idea/ 7 | *iml 8 | *.iml 9 | */build -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | android: 3 | components: 4 | - platform-tools 5 | - tools 6 | 7 | # The BuildTools version used by your project 8 | - build-tools-23.0.2 9 | - android-23 10 | - extra-android-m2repository 11 | - extra-google-m2repository 12 | - extra-android-support 13 | - extra-android-leanback 14 | - extra-google-google_play_services 15 | 16 | before_script: 17 | - chmod +x gradlew 18 | #Build, and run tests 19 | script: "./gradlew build testDebug" 20 | sudo: false 21 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | ========================== 3 | 4 | Version 3, 29 June 2007 5 | 6 | Copyright © 2007 Free Software Foundation, Inc. <> 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this license 9 | document, but changing it is not allowed. 10 | 11 | ## Preamble 12 | 13 | The GNU General Public License is a free, copyleft license for software and other 14 | kinds of works. 15 | 16 | The licenses for most software and other practical works are designed to take away 17 | your freedom to share and change the works. By contrast, the GNU General Public 18 | License is intended to guarantee your freedom to share and change all versions of a 19 | program--to make sure it remains free software for all its users. We, the Free 20 | Software Foundation, use the GNU General Public License for most of our software; it 21 | applies also to any other work released this way by its authors. You can apply it to 22 | your programs, too. 23 | 24 | When we speak of free software, we are referring to freedom, not price. Our General 25 | Public Licenses are designed to make sure that you have the freedom to distribute 26 | copies of free software (and charge for them if you wish), that you receive source 27 | code or can get it if you want it, that you can change the software or use pieces of 28 | it in new free programs, and that you know you can do these things. 29 | 30 | To protect your rights, we need to prevent others from denying you these rights or 31 | asking you to surrender the rights. Therefore, you have certain responsibilities if 32 | you distribute copies of the software, or if you modify it: responsibilities to 33 | respect the freedom of others. 34 | 35 | For example, if you distribute copies of such a program, whether gratis or for a fee, 36 | you must pass on to the recipients the same freedoms that you received. You must make 37 | sure that they, too, receive or can get the source code. And you must show them these 38 | terms so they know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: (1) assert 41 | copyright on the software, and (2) offer you this License giving you legal permission 42 | to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains that there is 45 | no warranty for this free software. For both users' and authors' sake, the GPL 46 | requires that modified versions be marked as changed, so that their problems will not 47 | be attributed erroneously to authors of previous versions. 48 | 49 | Some devices are designed to deny users access to install or run modified versions of 50 | the software inside them, although the manufacturer can do so. This is fundamentally 51 | incompatible with the aim of protecting users' freedom to change the software. The 52 | systematic pattern of such abuse occurs in the area of products for individuals to 53 | use, which is precisely where it is most unacceptable. Therefore, we have designed 54 | this version of the GPL to prohibit the practice for those products. If such problems 55 | arise substantially in other domains, we stand ready to extend this provision to 56 | those domains in future versions of the GPL, as needed to protect the freedom of 57 | users. 58 | 59 | Finally, every program is threatened constantly by software patents. States should 60 | not allow patents to restrict development and use of software on general-purpose 61 | computers, but in those that do, we wish to avoid the special danger that patents 62 | applied to a free program could make it effectively proprietary. To prevent this, the 63 | GPL assures that patents cannot be used to render the program non-free. 64 | 65 | The precise terms and conditions for copying, distribution and modification follow. 66 | 67 | ## TERMS AND CONDITIONS 68 | 69 | ### 0. Definitions. 70 | 71 | “This License” refers to version 3 of the GNU General Public License. 72 | 73 | “Copyright” also means copyright-like laws that apply to other kinds of 74 | works, such as semiconductor masks. 75 | 76 | “The Program” refers to any copyrightable work licensed under this 77 | License. Each licensee is addressed as “you”. “Licensees” and 78 | “recipients” may be individuals or organizations. 79 | 80 | To “modify” a work means to copy from or adapt all or part of the work in 81 | a fashion requiring copyright permission, other than the making of an exact copy. The 82 | resulting work is called a “modified version” of the earlier work or a 83 | work “based on” the earlier work. 84 | 85 | A “covered work” means either the unmodified Program or a work based on 86 | the Program. 87 | 88 | To “propagate” a work means to do anything with it that, without 89 | permission, would make you directly or secondarily liable for infringement under 90 | applicable copyright law, except executing it on a computer or modifying a private 91 | copy. Propagation includes copying, distribution (with or without modification), 92 | making available to the public, and in some countries other activities as well. 93 | 94 | To “convey” a work means any kind of propagation that enables other 95 | parties to make or receive copies. Mere interaction with a user through a computer 96 | network, with no transfer of a copy, is not conveying. 97 | 98 | An interactive user interface displays “Appropriate Legal Notices” to the 99 | extent that it includes a convenient and prominently visible feature that (1) 100 | displays an appropriate copyright notice, and (2) tells the user that there is no 101 | warranty for the work (except to the extent that warranties are provided), that 102 | licensees may convey the work under this License, and how to view a copy of this 103 | License. If the interface presents a list of user commands or options, such as a 104 | menu, a prominent item in the list meets this criterion. 105 | 106 | ### 1. Source Code. 107 | 108 | The “source code” for a work means the preferred form of the work for 109 | making modifications to it. “Object code” means any non-source form of a 110 | work. 111 | 112 | A “Standard Interface” means an interface that either is an official 113 | standard defined by a recognized standards body, or, in the case of interfaces 114 | specified for a particular programming language, one that is widely used among 115 | developers working in that language. 116 | 117 | The “System Libraries” of an executable work include anything, other than 118 | the work as a whole, that (a) is included in the normal form of packaging a Major 119 | Component, but which is not part of that Major Component, and (b) serves only to 120 | enable use of the work with that Major Component, or to implement a Standard 121 | Interface for which an implementation is available to the public in source code form. 122 | A “Major Component”, in this context, means a major essential component 123 | (kernel, window system, and so on) of the specific operating system (if any) on which 124 | the executable work runs, or a compiler used to produce the work, or an object code 125 | interpreter used to run it. 126 | 127 | The “Corresponding Source” for a work in object code form means all the 128 | source code needed to generate, install, and (for an executable work) run the object 129 | code and to modify the work, including scripts to control those activities. However, 130 | it does not include the work's System Libraries, or general-purpose tools or 131 | generally available free programs which are used unmodified in performing those 132 | activities but which are not part of the work. For example, Corresponding Source 133 | includes interface definition files associated with source files for the work, and 134 | the source code for shared libraries and dynamically linked subprograms that the work 135 | is specifically designed to require, such as by intimate data communication or 136 | control flow between those subprograms and other parts of the work. 137 | 138 | The Corresponding Source need not include anything that users can regenerate 139 | automatically from other parts of the Corresponding Source. 140 | 141 | The Corresponding Source for a work in source code form is that same work. 142 | 143 | ### 2. Basic Permissions. 144 | 145 | All rights granted under this License are granted for the term of copyright on the 146 | Program, and are irrevocable provided the stated conditions are met. This License 147 | explicitly affirms your unlimited permission to run the unmodified Program. The 148 | output from running a covered work is covered by this License only if the output, 149 | given its content, constitutes a covered work. This License acknowledges your rights 150 | of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not convey, without 153 | conditions so long as your license otherwise remains in force. You may convey covered 154 | works to others for the sole purpose of having them make modifications exclusively 155 | for you, or provide you with facilities for running those works, provided that you 156 | comply with the terms of this License in conveying all material for which you do not 157 | control copyright. Those thus making or running the covered works for you must do so 158 | exclusively on your behalf, under your direction and control, on terms that prohibit 159 | them from making any copies of your copyrighted material outside their relationship 160 | with you. 161 | 162 | Conveying under any other circumstances is permitted solely under the conditions 163 | stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 164 | 165 | ### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 166 | 167 | No covered work shall be deemed part of an effective technological measure under any 168 | applicable law fulfilling obligations under article 11 of the WIPO copyright treaty 169 | adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention 170 | of such measures. 171 | 172 | When you convey a covered work, you waive any legal power to forbid circumvention of 173 | technological measures to the extent such circumvention is effected by exercising 174 | rights under this License with respect to the covered work, and you disclaim any 175 | intention to limit operation or modification of the work as a means of enforcing, 176 | against the work's users, your or third parties' legal rights to forbid circumvention 177 | of technological measures. 178 | 179 | ### 4. Conveying Verbatim Copies. 180 | 181 | You may convey verbatim copies of the Program's source code as you receive it, in any 182 | medium, provided that you conspicuously and appropriately publish on each copy an 183 | appropriate copyright notice; keep intact all notices stating that this License and 184 | any non-permissive terms added in accord with section 7 apply to the code; keep 185 | intact all notices of the absence of any warranty; and give all recipients a copy of 186 | this License along with the Program. 187 | 188 | You may charge any price or no price for each copy that you convey, and you may offer 189 | support or warranty protection for a fee. 190 | 191 | ### 5. Conveying Modified Source Versions. 192 | 193 | You may convey a work based on the Program, or the modifications to produce it from 194 | the Program, in the form of source code under the terms of section 4, provided that 195 | you also meet all of these conditions: 196 | 197 | * **a)** The work must carry prominent notices stating that you modified it, and giving a 198 | relevant date. 199 | * **b)** The work must carry prominent notices stating that it is released under this 200 | License and any conditions added under section 7. This requirement modifies the 201 | requirement in section 4 to “keep intact all notices”. 202 | * **c)** You must license the entire work, as a whole, under this License to anyone who 203 | comes into possession of a copy. This License will therefore apply, along with any 204 | applicable section 7 additional terms, to the whole of the work, and all its parts, 205 | regardless of how they are packaged. This License gives no permission to license the 206 | work in any other way, but it does not invalidate such permission if you have 207 | separately received it. 208 | * **d)** If the work has interactive user interfaces, each must display Appropriate Legal 209 | Notices; however, if the Program has interactive interfaces that do not display 210 | Appropriate Legal Notices, your work need not make them do so. 211 | 212 | A compilation of a covered work with other separate and independent works, which are 213 | not by their nature extensions of the covered work, and which are not combined with 214 | it such as to form a larger program, in or on a volume of a storage or distribution 215 | medium, is called an “aggregate” if the compilation and its resulting 216 | copyright are not used to limit the access or legal rights of the compilation's users 217 | beyond what the individual works permit. Inclusion of a covered work in an aggregate 218 | does not cause this License to apply to the other parts of the aggregate. 219 | 220 | ### 6. Conveying Non-Source Forms. 221 | 222 | You may convey a covered work in object code form under the terms of sections 4 and 223 | 5, provided that you also convey the machine-readable Corresponding Source under the 224 | terms of this License, in one of these ways: 225 | 226 | * **a)** Convey the object code in, or embodied in, a physical product (including a 227 | physical distribution medium), accompanied by the Corresponding Source fixed on a 228 | durable physical medium customarily used for software interchange. 229 | * **b)** Convey the object code in, or embodied in, a physical product (including a 230 | physical distribution medium), accompanied by a written offer, valid for at least 231 | three years and valid for as long as you offer spare parts or customer support for 232 | that product model, to give anyone who possesses the object code either (1) a copy of 233 | the Corresponding Source for all the software in the product that is covered by this 234 | License, on a durable physical medium customarily used for software interchange, for 235 | a price no more than your reasonable cost of physically performing this conveying of 236 | source, or (2) access to copy the Corresponding Source from a network server at no 237 | charge. 238 | * **c)** Convey individual copies of the object code with a copy of the written offer to 239 | provide the Corresponding Source. This alternative is allowed only occasionally and 240 | noncommercially, and only if you received the object code with such an offer, in 241 | accord with subsection 6b. 242 | * **d)** Convey the object code by offering access from a designated place (gratis or for 243 | a charge), and offer equivalent access to the Corresponding Source in the same way 244 | through the same place at no further charge. You need not require recipients to copy 245 | the Corresponding Source along with the object code. If the place to copy the object 246 | code is a network server, the Corresponding Source may be on a different server 247 | (operated by you or a third party) that supports equivalent copying facilities, 248 | provided you maintain clear directions next to the object code saying where to find 249 | the Corresponding Source. Regardless of what server hosts the Corresponding Source, 250 | you remain obligated to ensure that it is available for as long as needed to satisfy 251 | these requirements. 252 | * **e)** Convey the object code using peer-to-peer transmission, provided you inform 253 | other peers where the object code and Corresponding Source of the work are being 254 | offered to the general public at no charge under subsection 6d. 255 | 256 | A separable portion of the object code, whose source code is excluded from the 257 | Corresponding Source as a System Library, need not be included in conveying the 258 | object code work. 259 | 260 | A “User Product” is either (1) a “consumer product”, which 261 | means any tangible personal property which is normally used for personal, family, or 262 | household purposes, or (2) anything designed or sold for incorporation into a 263 | dwelling. In determining whether a product is a consumer product, doubtful cases 264 | shall be resolved in favor of coverage. For a particular product received by a 265 | particular user, “normally used” refers to a typical or common use of 266 | that class of product, regardless of the status of the particular user or of the way 267 | in which the particular user actually uses, or expects or is expected to use, the 268 | product. A product is a consumer product regardless of whether the product has 269 | substantial commercial, industrial or non-consumer uses, unless such uses represent 270 | the only significant mode of use of the product. 271 | 272 | “Installation Information” for a User Product means any methods, 273 | procedures, authorization keys, or other information required to install and execute 274 | modified versions of a covered work in that User Product from a modified version of 275 | its Corresponding Source. The information must suffice to ensure that the continued 276 | functioning of the modified object code is in no case prevented or interfered with 277 | solely because modification has been made. 278 | 279 | If you convey an object code work under this section in, or with, or specifically for 280 | use in, a User Product, and the conveying occurs as part of a transaction in which 281 | the right of possession and use of the User Product is transferred to the recipient 282 | in perpetuity or for a fixed term (regardless of how the transaction is 283 | characterized), the Corresponding Source conveyed under this section must be 284 | accompanied by the Installation Information. But this requirement does not apply if 285 | neither you nor any third party retains the ability to install modified object code 286 | on the User Product (for example, the work has been installed in ROM). 287 | 288 | The requirement to provide Installation Information does not include a requirement to 289 | continue to provide support service, warranty, or updates for a work that has been 290 | modified or installed by the recipient, or for the User Product in which it has been 291 | modified or installed. Access to a network may be denied when the modification itself 292 | materially and adversely affects the operation of the network or violates the rules 293 | and protocols for communication across the network. 294 | 295 | Corresponding Source conveyed, and Installation Information provided, in accord with 296 | this section must be in a format that is publicly documented (and with an 297 | implementation available to the public in source code form), and must require no 298 | special password or key for unpacking, reading or copying. 299 | 300 | ### 7. Additional Terms. 301 | 302 | “Additional permissions” are terms that supplement the terms of this 303 | License by making exceptions from one or more of its conditions. Additional 304 | permissions that are applicable to the entire Program shall be treated as though they 305 | were included in this License, to the extent that they are valid under applicable 306 | law. If additional permissions apply only to part of the Program, that part may be 307 | used separately under those permissions, but the entire Program remains governed by 308 | this License without regard to the additional permissions. 309 | 310 | When you convey a copy of a covered work, you may at your option remove any 311 | additional permissions from that copy, or from any part of it. (Additional 312 | permissions may be written to require their own removal in certain cases when you 313 | modify the work.) You may place additional permissions on material, added by you to a 314 | covered work, for which you have or can give appropriate copyright permission. 315 | 316 | Notwithstanding any other provision of this License, for material you add to a 317 | covered work, you may (if authorized by the copyright holders of that material) 318 | supplement the terms of this License with terms: 319 | 320 | * **a)** Disclaiming warranty or limiting liability differently from the terms of 321 | sections 15 and 16 of this License; or 322 | * **b)** Requiring preservation of specified reasonable legal notices or author 323 | attributions in that material or in the Appropriate Legal Notices displayed by works 324 | containing it; or 325 | * **c)** Prohibiting misrepresentation of the origin of that material, or requiring that 326 | modified versions of such material be marked in reasonable ways as different from the 327 | original version; or 328 | * **d)** Limiting the use for publicity purposes of names of licensors or authors of the 329 | material; or 330 | * **e)** Declining to grant rights under trademark law for use of some trade names, 331 | trademarks, or service marks; or 332 | * **f)** Requiring indemnification of licensors and authors of that material by anyone 333 | who conveys the material (or modified versions of it) with contractual assumptions of 334 | liability to the recipient, for any liability that these contractual assumptions 335 | directly impose on those licensors and authors. 336 | 337 | All other non-permissive additional terms are considered “further 338 | restrictions” within the meaning of section 10. If the Program as you received 339 | it, or any part of it, contains a notice stating that it is governed by this License 340 | along with a term that is a further restriction, you may remove that term. If a 341 | license document contains a further restriction but permits relicensing or conveying 342 | under this License, you may add to a covered work material governed by the terms of 343 | that license document, provided that the further restriction does not survive such 344 | relicensing or conveying. 345 | 346 | If you add terms to a covered work in accord with this section, you must place, in 347 | the relevant source files, a statement of the additional terms that apply to those 348 | files, or a notice indicating where to find the applicable terms. 349 | 350 | Additional terms, permissive or non-permissive, may be stated in the form of a 351 | separately written license, or stated as exceptions; the above requirements apply 352 | either way. 353 | 354 | ### 8. Termination. 355 | 356 | You may not propagate or modify a covered work except as expressly provided under 357 | this License. Any attempt otherwise to propagate or modify it is void, and will 358 | automatically terminate your rights under this License (including any patent licenses 359 | granted under the third paragraph of section 11). 360 | 361 | However, if you cease all violation of this License, then your license from a 362 | particular copyright holder is reinstated (a) provisionally, unless and until the 363 | copyright holder explicitly and finally terminates your license, and (b) permanently, 364 | if the copyright holder fails to notify you of the violation by some reasonable means 365 | prior to 60 days after the cessation. 366 | 367 | Moreover, your license from a particular copyright holder is reinstated permanently 368 | if the copyright holder notifies you of the violation by some reasonable means, this 369 | is the first time you have received notice of violation of this License (for any 370 | work) from that copyright holder, and you cure the violation prior to 30 days after 371 | your receipt of the notice. 372 | 373 | Termination of your rights under this section does not terminate the licenses of 374 | parties who have received copies or rights from you under this License. If your 375 | rights have been terminated and not permanently reinstated, you do not qualify to 376 | receive new licenses for the same material under section 10. 377 | 378 | ### 9. Acceptance Not Required for Having Copies. 379 | 380 | You are not required to accept this License in order to receive or run a copy of the 381 | Program. Ancillary propagation of a covered work occurring solely as a consequence of 382 | using peer-to-peer transmission to receive a copy likewise does not require 383 | acceptance. However, nothing other than this License grants you permission to 384 | propagate or modify any covered work. These actions infringe copyright if you do not 385 | accept this License. Therefore, by modifying or propagating a covered work, you 386 | indicate your acceptance of this License to do so. 387 | 388 | ### 10. Automatic Licensing of Downstream Recipients. 389 | 390 | Each time you convey a covered work, the recipient automatically receives a license 391 | from the original licensors, to run, modify and propagate that work, subject to this 392 | License. You are not responsible for enforcing compliance by third parties with this 393 | License. 394 | 395 | An “entity transaction” is a transaction transferring control of an 396 | organization, or substantially all assets of one, or subdividing an organization, or 397 | merging organizations. If propagation of a covered work results from an entity 398 | transaction, each party to that transaction who receives a copy of the work also 399 | receives whatever licenses to the work the party's predecessor in interest had or 400 | could give under the previous paragraph, plus a right to possession of the 401 | Corresponding Source of the work from the predecessor in interest, if the predecessor 402 | has it or can get it with reasonable efforts. 403 | 404 | You may not impose any further restrictions on the exercise of the rights granted or 405 | affirmed under this License. For example, you may not impose a license fee, royalty, 406 | or other charge for exercise of rights granted under this License, and you may not 407 | initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging 408 | that any patent claim is infringed by making, using, selling, offering for sale, or 409 | importing the Program or any portion of it. 410 | 411 | ### 11. Patents. 412 | 413 | A “contributor” is a copyright holder who authorizes use under this 414 | License of the Program or a work on which the Program is based. The work thus 415 | licensed is called the contributor's “contributor version”. 416 | 417 | A contributor's “essential patent claims” are all patent claims owned or 418 | controlled by the contributor, whether already acquired or hereafter acquired, that 419 | would be infringed by some manner, permitted by this License, of making, using, or 420 | selling its contributor version, but do not include claims that would be infringed 421 | only as a consequence of further modification of the contributor version. For 422 | purposes of this definition, “control” includes the right to grant patent 423 | sublicenses in a manner consistent with the requirements of this License. 424 | 425 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent license 426 | under the contributor's essential patent claims, to make, use, sell, offer for sale, 427 | import and otherwise run, modify and propagate the contents of its contributor 428 | version. 429 | 430 | In the following three paragraphs, a “patent license” is any express 431 | agreement or commitment, however denominated, not to enforce a patent (such as an 432 | express permission to practice a patent or covenant not to sue for patent 433 | infringement). To “grant” such a patent license to a party means to make 434 | such an agreement or commitment not to enforce a patent against the party. 435 | 436 | If you convey a covered work, knowingly relying on a patent license, and the 437 | Corresponding Source of the work is not available for anyone to copy, free of charge 438 | and under the terms of this License, through a publicly available network server or 439 | other readily accessible means, then you must either (1) cause the Corresponding 440 | Source to be so available, or (2) arrange to deprive yourself of the benefit of the 441 | patent license for this particular work, or (3) arrange, in a manner consistent with 442 | the requirements of this License, to extend the patent license to downstream 443 | recipients. “Knowingly relying” means you have actual knowledge that, but 444 | for the patent license, your conveying the covered work in a country, or your 445 | recipient's use of the covered work in a country, would infringe one or more 446 | identifiable patents in that country that you have reason to believe are valid. 447 | 448 | If, pursuant to or in connection with a single transaction or arrangement, you 449 | convey, or propagate by procuring conveyance of, a covered work, and grant a patent 450 | license to some of the parties receiving the covered work authorizing them to use, 451 | propagate, modify or convey a specific copy of the covered work, then the patent 452 | license you grant is automatically extended to all recipients of the covered work and 453 | works based on it. 454 | 455 | A patent license is “discriminatory” if it does not include within the 456 | scope of its coverage, prohibits the exercise of, or is conditioned on the 457 | non-exercise of one or more of the rights that are specifically granted under this 458 | License. You may not convey a covered work if you are a party to an arrangement with 459 | a third party that is in the business of distributing software, under which you make 460 | payment to the third party based on the extent of your activity of conveying the 461 | work, and under which the third party grants, to any of the parties who would receive 462 | the covered work from you, a discriminatory patent license (a) in connection with 463 | copies of the covered work conveyed by you (or copies made from those copies), or (b) 464 | primarily for and in connection with specific products or compilations that contain 465 | the covered work, unless you entered into that arrangement, or that patent license 466 | was granted, prior to 28 March 2007. 467 | 468 | Nothing in this License shall be construed as excluding or limiting any implied 469 | license or other defenses to infringement that may otherwise be available to you 470 | under applicable patent law. 471 | 472 | ### 12. No Surrender of Others' Freedom. 473 | 474 | If conditions are imposed on you (whether by court order, agreement or otherwise) 475 | that contradict the conditions of this License, they do not excuse you from the 476 | conditions of this License. If you cannot convey a covered work so as to satisfy 477 | simultaneously your obligations under this License and any other pertinent 478 | obligations, then as a consequence you may not convey it at all. For example, if you 479 | agree to terms that obligate you to collect a royalty for further conveying from 480 | those to whom you convey the Program, the only way you could satisfy both those terms 481 | and this License would be to refrain entirely from conveying the Program. 482 | 483 | ### 13. Use with the GNU Affero General Public License. 484 | 485 | Notwithstanding any other provision of this License, you have permission to link or 486 | combine any covered work with a work licensed under version 3 of the GNU Affero 487 | General Public License into a single combined work, and to convey the resulting work. 488 | The terms of this License will continue to apply to the part which is the covered 489 | work, but the special requirements of the GNU Affero General Public License, section 490 | 13, concerning interaction through a network will apply to the combination as such. 491 | 492 | ### 14. Revised Versions of this License. 493 | 494 | The Free Software Foundation may publish revised and/or new versions of the GNU 495 | General Public License from time to time. Such new versions will be similar in spirit 496 | to the present version, but may differ in detail to address new problems or concerns. 497 | 498 | Each version is given a distinguishing version number. If the Program specifies that 499 | a certain numbered version of the GNU General Public License “or any later 500 | version” applies to it, you have the option of following the terms and 501 | conditions either of that numbered version or of any later version published by the 502 | Free Software Foundation. If the Program does not specify a version number of the GNU 503 | General Public License, you may choose any version ever published by the Free 504 | Software Foundation. 505 | 506 | If the Program specifies that a proxy can decide which future versions of the GNU 507 | General Public License can be used, that proxy's public statement of acceptance of a 508 | version permanently authorizes you to choose that version for the Program. 509 | 510 | Later license versions may give you additional or different permissions. However, no 511 | additional obligations are imposed on any author or copyright holder as a result of 512 | your choosing to follow a later version. 513 | 514 | ### 15. Disclaimer of Warranty. 515 | 516 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 517 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 518 | PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER 519 | EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 520 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE 521 | QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 522 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 523 | 524 | ### 16. Limitation of Liability. 525 | 526 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY 527 | COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS 528 | PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, 529 | INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 530 | PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE 531 | OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE 532 | WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 533 | POSSIBILITY OF SUCH DAMAGES. 534 | 535 | ### 17. Interpretation of Sections 15 and 16. 536 | 537 | If the disclaimer of warranty and limitation of liability provided above cannot be 538 | given local legal effect according to their terms, reviewing courts shall apply local 539 | law that most closely approximates an absolute waiver of all civil liability in 540 | connection with the Program, unless a warranty or assumption of liability accompanies 541 | a copy of the Program in return for a fee. 542 | 543 | END OF TERMS AND CONDITIONS 544 | 545 | ## How to Apply These Terms to Your New Programs 546 | 547 | If you develop a new program, and you want it to be of the greatest possible use to 548 | the public, the best way to achieve this is to make it free software which everyone 549 | can redistribute and change under these terms. 550 | 551 | To do so, attach the following notices to the program. It is safest to attach them 552 | to the start of each source file to most effectively state the exclusion of warranty; 553 | and each file should have at least the “copyright” line and a pointer to 554 | where the full notice is found. 555 | 556 | 557 | Copyright (C) 558 | 559 | This program is free software: you can redistribute it and/or modify 560 | it under the terms of the GNU General Public License as published by 561 | the Free Software Foundation, either version 3 of the License, or 562 | (at your option) any later version. 563 | 564 | This program is distributed in the hope that it will be useful, 565 | but WITHOUT ANY WARRANTY; without even the implied warranty of 566 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 567 | GNU General Public License for more details. 568 | 569 | You should have received a copy of the GNU General Public License 570 | along with this program. If not, see . 571 | 572 | Also add information on how to contact you by electronic and paper mail. 573 | 574 | If the program does terminal interaction, make it output a short notice like this 575 | when it starts in an interactive mode: 576 | 577 | Copyright (C) 578 | This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. 579 | This is free software, and you are welcome to redistribute it 580 | under certain conditions; type 'show c' for details. 581 | 582 | The hypothetical commands 'show w' and 'show c' should show the appropriate parts of 583 | the General Public License. Of course, your program's commands might be different; 584 | for a GUI interface, you would use an “about box”. 585 | 586 | You should also get your employer (if you work as a programmer) or school, if any, to 587 | sign a “copyright disclaimer” for the program, if necessary. For more 588 | information on this, and how to apply and follow the GNU GPL, see 589 | <>. 590 | 591 | The GNU General Public License does not permit incorporating your program into 592 | proprietary programs. If your program is a subroutine library, you may consider it 593 | more useful to permit linking proprietary applications with the library. If this is 594 | what you want to do, use the GNU Lesser General Public License instead of this 595 | License. But first, please read 596 | <>. 597 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Android TV Boilerplate 2 | [![Build Status](https://travis-ci.org/hitherejoe/Vineyard.svg?branch=master)](https://travis-ci.org/hitherejoe/AndroidTvBoilerplate) 3 | ======================= 4 | 5 |

6 | Loading Card 7 |

8 | 9 | This is an Android TV Boilerplate project which should make it easy for you to get started when 10 | wanting to create your own application for the Android TV platform! 11 | 12 | The project is setup using: 13 | 14 | - MVP architecture 15 | - Functional tests with [Espresso](http://google.github.io/android-testing-support-library/docs/espresso) 16 | - Unit tests with [Mockito](http://mockito.org/) 17 | - [Checkstyle](http://checkstyle.sourceforge.net/), [FindBugs](http://findbugs.sourceforge.net/) and [PMD](https://pmd.github.io/) 18 | - [Leanback Library](http://developer.android.com/tools/support-library/features.html#v17-leanback) 19 | - [Recommendation Library](http://developer.android.com/tools/support-library/features.html#recommendation) 20 | - [RxJava](https://github.com/ReactiveX/RxJava) and [RxAndroid](https://github.com/ReactiveX/RxAndroid) 21 | - [Retrofit](http://square.github.io/retrofit/) and [OkHttp](https://github.com/square/okhttp) 22 | - [Dagger 2](http://google.github.io/dagger/) 23 | - [Butterknife](https://github.com/JakeWharton/butterknife) 24 | - [Timber] (https://github.com/JakeWharton/timber) 25 | - [Mockito](http://mockito.org/) 26 | - [Glide](https://github.com/bumptech/glide) 27 | 28 | The boilerplate currently has two core screens implemented and ready to feed data into: 29 | 30 | ##Browse 31 | 32 |

33 | Loading Card 34 |

35 | 36 | ##Search 37 |

38 | Loading Card 39 |

40 | 41 | #Check 42 | 43 | To check the code style and run unit tests: 44 | 45 | ```./gradlew check``` 46 | 47 | #Building 48 | 49 | To build, install and run a debug version, run this from the root of the project: 50 | 51 | ```./gradlew assembleDebug``` 52 | 53 | #Unit Tests 54 | 55 | To run the unit tests for the application: 56 | 57 | ```./gradlew testDebugUnitTest``` 58 | 59 | #User Interface Tests 60 | 61 | To run the user interface tests for the application: 62 | 63 | ```./gradlew connectedDebugAndroidTest``` 64 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *iml 3 | *.iml 4 | .idea -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply from: '../config/quality/quality.gradle' 3 | apply plugin: 'com.neenbedankt.android-apt' 4 | 5 | android { 6 | compileSdkVersion 23 7 | buildToolsVersion "23.0.2" 8 | 9 | defaultConfig { 10 | applicationId "com.hitherejoe.tvboilerplate" 11 | minSdkVersion 21 12 | targetSdkVersion 23 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | versionCode 1 15 | versionName "1.0" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | sourceSets { 25 | def commonTestDir = 'src/commonTest/java' 26 | test { 27 | java.srcDir commonTestDir 28 | } 29 | androidTest { 30 | java.srcDir commonTestDir 31 | } 32 | } 33 | 34 | //Needed because of this https://github.com/square/okio/issues/58 35 | lintOptions { 36 | warning 'InvalidPackage' 37 | } 38 | } 39 | 40 | dependencies { 41 | final SUPPORT_LIBRARY_VERSION = '23.1.1' 42 | final DAGGER_VERSION = '2.0.2' 43 | final HAMCREST_VERSION = '1.3' 44 | final MOCKITO_VERSION = '1.10.19' 45 | final DEXMAKER_VERSION = '1.4' 46 | final ESPRESSO_VERSION = '2.2.1' 47 | final RUNNER_VERSION = '0.4' 48 | final RETROFIT_VERSION = '2.0.0-beta2' 49 | 50 | def daggerCompiler = "com.google.dagger:dagger-compiler:$DAGGER_VERSION" 51 | def jUnit = "junit:junit:4.12" 52 | def mockito = "org.mockito:mockito-core:1.10.19" 53 | 54 | compile fileTree(dir: 'libs', include: ['*.jar']) 55 | 56 | compile "com.android.support:leanback-v17:$SUPPORT_LIBRARY_VERSION" 57 | compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION" 58 | compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION" 59 | compile "com.android.support:recommendation:$SUPPORT_LIBRARY_VERSION" 60 | compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION" 61 | 62 | compile "com.squareup.retrofit:retrofit:$RETROFIT_VERSION" 63 | compile "com.squareup.retrofit:converter-gson:$RETROFIT_VERSION" 64 | compile "com.squareup.retrofit:adapter-rxjava:$RETROFIT_VERSION" 65 | compile 'com.squareup.okhttp:logging-interceptor:2.6.0' 66 | compile 'com.squareup.okhttp:okhttp-urlconnection:2.5.0' 67 | compile 'com.squareup.okhttp:okhttp:2.5.0' 68 | 69 | compile 'com.github.bumptech.glide:glide:3.6.1' 70 | compile 'io.reactivex:rxandroid:1.1.0' 71 | compile 'io.reactivex:rxjava:1.1.0' 72 | compile 'com.jakewharton:butterknife:7.0.1' 73 | compile 'com.jakewharton.timber:timber:4.1.0' 74 | 75 | compile "com.google.dagger:dagger:$DAGGER_VERSION" 76 | provided 'org.glassfish:javax.annotation:10.0-b28' //Required by Dagger2 77 | 78 | // Instrumentation test dependencies 79 | androidTestCompile jUnit 80 | androidTestCompile mockito 81 | androidTestCompile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION" 82 | androidTestCompile("com.android.support.test.espresso:espresso-contrib:$ESPRESSO_VERSION") { 83 | exclude group: 'com.android.support', module: 'appcompat' 84 | exclude group: 'com.android.support', module: 'support-v4' 85 | exclude group: 'com.android.support', module: 'recyclerview-v7' 86 | } 87 | androidTestCompile "com.android.support.test.espresso:espresso-core:$ESPRESSO_VERSION" 88 | androidTestCompile "com.android.support.test:runner:$RUNNER_VERSION" 89 | androidTestCompile "com.android.support.test:rules:$RUNNER_VERSION" 90 | androidTestCompile "com.crittercism.dexmaker:dexmaker:$DEXMAKER_VERSION" 91 | androidTestCompile "com.crittercism.dexmaker:dexmaker-dx:$DEXMAKER_VERSION" 92 | androidTestCompile "com.crittercism.dexmaker:dexmaker-mockito:$DEXMAKER_VERSION" 93 | 94 | testCompile jUnit 95 | testCompile mockito 96 | testCompile "org.hamcrest:hamcrest-core:$HAMCREST_VERSION" 97 | testCompile "org.hamcrest:hamcrest-library:$HAMCREST_VERSION" 98 | testCompile "org.hamcrest:hamcrest-integration:$HAMCREST_VERSION" 99 | testCompile "org.mockito:mockito-core:$MOCKITO_VERSION" 100 | testCompile 'org.robolectric:robolectric:3.0' 101 | 102 | // APT dependencies 103 | apt daggerCompiler 104 | testApt daggerCompiler 105 | androidTestApt daggerCompiler 106 | } 107 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By card_default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.4.1/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hitherejoe/androidtvboilerplate/ContentActivityTest.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate; 2 | 3 | import android.support.test.InstrumentationRegistry; 4 | import android.support.test.espresso.contrib.RecyclerViewActions; 5 | import android.support.test.rule.ActivityTestRule; 6 | import android.support.test.runner.AndroidJUnit4; 7 | 8 | import com.hitherejoe.androidtvboilerplate.data.model.Cat; 9 | import com.hitherejoe.androidtvboilerplate.test.common.TestDataFactory; 10 | import com.hitherejoe.androidtvboilerplate.test.common.rules.TestComponentRule; 11 | import com.hitherejoe.androidtvboilerplate.ui.content.ContentActivity; 12 | 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.junit.rules.RuleChain; 16 | import org.junit.rules.TestRule; 17 | import org.junit.runner.RunWith; 18 | 19 | import java.util.List; 20 | 21 | import rx.Single; 22 | 23 | import static android.support.test.espresso.Espresso.onView; 24 | import static android.support.test.espresso.action.ViewActions.click; 25 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 26 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 27 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 28 | import static com.hitherejoe.androidtvboilerplate.util.CustomMatchers.withItemText; 29 | import static org.mockito.Matchers.anyListOf; 30 | import static org.mockito.Mockito.when; 31 | 32 | @RunWith(AndroidJUnit4.class) 33 | public class ContentActivityTest { 34 | 35 | public final TestComponentRule component = 36 | new TestComponentRule(InstrumentationRegistry.getTargetContext()); 37 | public final ActivityTestRule main = 38 | new ActivityTestRule<>(ContentActivity.class, false, false); 39 | 40 | @Rule 41 | public final TestRule chain = RuleChain.outerRule(component).around(main); 42 | 43 | @Test 44 | public void postsDisplayAndAreBrowseable() { 45 | List mockcats = TestDataFactory.makeCats(5); 46 | stubDataManagerGetCats(Single.just(mockcats)); 47 | main.launchActivity(null); 48 | 49 | onView(withId(R.id.browse_headers)) 50 | .perform(RecyclerViewActions.actionOnItemAtPosition(0, click())); 51 | 52 | for (int i = 0; i < mockcats.size(); i++) { 53 | checkItemAtPosition(mockcats.get(i), i); 54 | } 55 | } 56 | 57 | private void checkItemAtPosition(Cat cat, int position) { 58 | if (position > 0) { 59 | onView(withItemText(cat.name, R.id.browse_container_dock)).perform(click()); 60 | } 61 | onView(withItemText(cat.name, R.id.browse_container_dock)) 62 | .check(matches(isDisplayed())); 63 | onView(withItemText(cat.description, R.id.browse_container_dock)) 64 | .check(matches(isDisplayed())); 65 | } 66 | 67 | private void stubDataManagerGetCats(Single> single) { 68 | when(component.getMockDataManager().getCats(anyListOf(Cat.class))) 69 | .thenReturn(single); 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hitherejoe/androidtvboilerplate/SearchContentActivityTest.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate; 2 | 3 | import android.support.test.InstrumentationRegistry; 4 | import android.support.test.rule.ActivityTestRule; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import com.hitherejoe.androidtvboilerplate.data.model.Cat; 8 | import com.hitherejoe.androidtvboilerplate.test.common.TestDataFactory; 9 | import com.hitherejoe.androidtvboilerplate.test.common.rules.TestComponentRule; 10 | import com.hitherejoe.androidtvboilerplate.ui.search.SearchContentActivity; 11 | 12 | import org.junit.Rule; 13 | import org.junit.Test; 14 | import org.junit.rules.RuleChain; 15 | import org.junit.rules.TestRule; 16 | import org.junit.runner.RunWith; 17 | 18 | import java.util.List; 19 | 20 | import rx.Single; 21 | 22 | import static android.support.test.espresso.Espresso.onView; 23 | import static android.support.test.espresso.action.ViewActions.click; 24 | import static android.support.test.espresso.action.ViewActions.replaceText; 25 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 26 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 27 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 28 | import static com.hitherejoe.androidtvboilerplate.util.CustomMatchers.withItemText; 29 | import static org.mockito.Matchers.anyListOf; 30 | import static org.mockito.Mockito.when; 31 | 32 | @RunWith(AndroidJUnit4.class) 33 | public class SearchContentActivityTest { 34 | 35 | public final TestComponentRule component = 36 | new TestComponentRule(InstrumentationRegistry.getTargetContext()); 37 | public final ActivityTestRule main = 38 | new ActivityTestRule<>(SearchContentActivity.class, false, false); 39 | 40 | @Rule 41 | public final TestRule chain = RuleChain.outerRule(component).around(main); 42 | 43 | @Test 44 | public void searchResultsDisplayAndAreScrollable() { 45 | main.launchActivity(null); 46 | 47 | List mockCats = TestDataFactory.makeCats(5); 48 | stubDataManagerGetCats(Single.just(mockCats)); 49 | 50 | onView(withId(R.id.lb_search_text_editor)) 51 | .perform(replaceText("cat")); 52 | 53 | for (int i = 0; i < mockCats.size(); i++) { 54 | checkItemAtPosition(mockCats.get(i)); 55 | } 56 | } 57 | 58 | private void checkItemAtPosition(Cat cat) { 59 | onView(withItemText(cat.name, R.id.lb_results_frame)).perform(click()); 60 | onView(withItemText(cat.description, R.id.lb_results_frame)).check(matches(isDisplayed())); 61 | } 62 | 63 | private void stubDataManagerGetCats(Single> single) { 64 | when(component.getMockDataManager().getCats(anyListOf(Cat.class))) 65 | .thenReturn(single); 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hitherejoe/androidtvboilerplate/util/CustomMatchers.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.util; 2 | 3 | import android.text.TextUtils; 4 | import android.view.View; 5 | 6 | import org.hamcrest.Description; 7 | import org.hamcrest.Matcher; 8 | import org.hamcrest.TypeSafeMatcher; 9 | 10 | import static android.support.test.espresso.core.deps.guava.base.Preconditions.checkArgument; 11 | import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; 12 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 13 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 14 | import static org.hamcrest.Matchers.allOf; 15 | 16 | public class CustomMatchers { 17 | 18 | public static Matcher withItemText(final String itemText, final int parentId) { 19 | checkArgument(!TextUtils.isEmpty(itemText), "itemText cannot be null or empty"); 20 | return new TypeSafeMatcher() { 21 | @Override 22 | public boolean matchesSafely(View item) { 23 | return allOf(isDescendantOfA(withId(parentId)), 24 | withText(itemText)).matches(item); 25 | } 26 | 27 | @Override 28 | public void describeTo(Description description) { 29 | description.appendText("is isDescendantOfA RecyclerView with text " + itemText); 30 | } 31 | }; 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/commonTest/java/com/hitherejoe/androidtvboilerplate/test/common/TestDataFactory.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.test.common; 2 | 3 | import com.hitherejoe.androidtvboilerplate.data.model.Cat; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.UUID; 8 | 9 | public class TestDataFactory { 10 | 11 | public static String generateRandomString() { 12 | return UUID.randomUUID().toString().substring(0, 5); 13 | } 14 | 15 | public static Cat makeCat(String unique) { 16 | Cat cat = new Cat(); 17 | cat.name = "Name " + unique; 18 | cat.description = "Description " + unique; 19 | cat.imageUrl = generateRandomString(); 20 | return cat; 21 | } 22 | 23 | public static List makeCats(int count) { 24 | List cats = new ArrayList<>(); 25 | for (int i = 0; i < count; i++) { 26 | cats.add(makeCat(String.valueOf(i))); 27 | } 28 | return cats; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/commonTest/java/com/hitherejoe/androidtvboilerplate/test/common/injection/component/TestComponent.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.test.common.injection.component; 2 | 3 | import com.hitherejoe.androidtvboilerplate.injection.component.ApplicationComponent; 4 | import com.hitherejoe.androidtvboilerplate.test.common.injection.module.ApplicationTestModule; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import dagger.Component; 9 | 10 | @Singleton 11 | @Component(modules = ApplicationTestModule.class) 12 | public interface TestComponent extends ApplicationComponent { 13 | 14 | } -------------------------------------------------------------------------------- /app/src/commonTest/java/com/hitherejoe/androidtvboilerplate/test/common/injection/module/ApplicationTestModule.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.test.common.injection.module; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.hitherejoe.androidtvboilerplate.data.DataManager; 7 | import com.hitherejoe.androidtvboilerplate.data.local.PreferencesHelper; 8 | import com.hitherejoe.androidtvboilerplate.data.remote.AndroidTvBoilerplateService; 9 | import com.hitherejoe.androidtvboilerplate.injection.ApplicationContext; 10 | 11 | import javax.inject.Singleton; 12 | 13 | import dagger.Module; 14 | import dagger.Provides; 15 | import rx.subscriptions.CompositeSubscription; 16 | 17 | import static org.mockito.Mockito.mock; 18 | 19 | /** 20 | * Provides application-level dependencies for an app running on a testing environment 21 | * This allows injecting mocks if necessary. 22 | */ 23 | @Module 24 | public class ApplicationTestModule { 25 | 26 | private final Application mApplication; 27 | 28 | public ApplicationTestModule(Application application) { 29 | mApplication = application; 30 | } 31 | 32 | @Provides 33 | Application provideApplication() { 34 | return mApplication; 35 | } 36 | 37 | @Provides 38 | @ApplicationContext 39 | Context provideContext() { 40 | return mApplication; 41 | } 42 | 43 | @Provides 44 | CompositeSubscription provideCompositeSubscription() { 45 | return new CompositeSubscription(); 46 | } 47 | 48 | /************* MOCKS *************/ 49 | 50 | @Provides 51 | @Singleton 52 | DataManager provideDataManager() { 53 | return mock(DataManager.class); 54 | } 55 | 56 | @Provides 57 | @Singleton 58 | PreferencesHelper providePreferencesHelper() { 59 | return mock(PreferencesHelper.class); 60 | } 61 | 62 | @Provides 63 | @Singleton 64 | AndroidTvBoilerplateService provideAndroidTvBoilerplateService() { 65 | return mock(AndroidTvBoilerplateService.class); 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /app/src/commonTest/java/com/hitherejoe/androidtvboilerplate/test/common/rules/TestComponentRule.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.test.common.rules; 2 | 3 | import android.content.Context; 4 | 5 | import com.hitherejoe.androidtvboilerplate.AndroidTvBoilerplateApplication; 6 | import com.hitherejoe.androidtvboilerplate.data.DataManager; 7 | import com.hitherejoe.androidtvboilerplate.test.common.injection.component.DaggerTestComponent; 8 | import com.hitherejoe.androidtvboilerplate.test.common.injection.component.TestComponent; 9 | import com.hitherejoe.androidtvboilerplate.test.common.injection.module.ApplicationTestModule; 10 | 11 | import org.junit.rules.TestRule; 12 | import org.junit.runner.Description; 13 | import org.junit.runners.model.Statement; 14 | 15 | /** 16 | * Test rule that creates and sets a Dagger TestComponent into the application overriding the 17 | * existing application component. 18 | * Use this rule in your test case in order for the app to use mock dependencies. 19 | * It also exposes some of the dependencies so they can be easily accessed from the tests, e.g. to 20 | * stub mocks etc. 21 | */ 22 | public class TestComponentRule implements TestRule { 23 | 24 | private final TestComponent mTestComponent; 25 | private final Context mContext; 26 | 27 | public TestComponentRule(Context context) { 28 | mContext = context; 29 | AndroidTvBoilerplateApplication application = AndroidTvBoilerplateApplication.get(context); 30 | mTestComponent = DaggerTestComponent.builder() 31 | .applicationTestModule(new ApplicationTestModule(application)) 32 | .build(); 33 | } 34 | 35 | public Context getContext() { 36 | return mContext; 37 | } 38 | 39 | public DataManager getMockDataManager() { 40 | return mTestComponent.dataManager(); 41 | } 42 | 43 | @Override 44 | public Statement apply(final Statement base, Description description) { 45 | return new Statement() { 46 | @Override 47 | public void evaluate() throws Throwable { 48 | AndroidTvBoilerplateApplication application = 49 | AndroidTvBoilerplateApplication.get(mContext); 50 | application.setComponent(mTestComponent); 51 | base.evaluate(); 52 | application.setComponent(null); 53 | } 54 | }; 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 16 | 17 | 20 | 21 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/AndroidTvBoilerplateApplication.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.hitherejoe.androidtvboilerplate.injection.component.ApplicationComponent; 7 | import com.hitherejoe.androidtvboilerplate.injection.component.DaggerApplicationComponent; 8 | import com.hitherejoe.androidtvboilerplate.injection.module.ApplicationModule; 9 | 10 | import timber.log.Timber; 11 | 12 | public class AndroidTvBoilerplateApplication extends Application { 13 | 14 | ApplicationComponent mApplicationComponent; 15 | 16 | @Override 17 | public void onCreate() { 18 | super.onCreate(); 19 | if (BuildConfig.DEBUG) Timber.plant(new Timber.DebugTree()); 20 | 21 | mApplicationComponent = DaggerApplicationComponent.builder() 22 | .applicationModule(new ApplicationModule(this)) 23 | .build(); 24 | } 25 | 26 | public static AndroidTvBoilerplateApplication get(Context context) { 27 | return (AndroidTvBoilerplateApplication) context.getApplicationContext(); 28 | } 29 | 30 | // Needed to replace the component with a test specific one 31 | public void setComponent(ApplicationComponent applicationComponent) { 32 | mApplicationComponent = applicationComponent; 33 | } 34 | 35 | public ApplicationComponent getComponent() { 36 | if (mApplicationComponent == null) { 37 | mApplicationComponent = DaggerApplicationComponent.builder() 38 | .applicationModule(new ApplicationModule(this)) 39 | .build(); 40 | } 41 | return mApplicationComponent; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/data/DataManager.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.data; 2 | 3 | import com.hitherejoe.androidtvboilerplate.data.local.PreferencesHelper; 4 | import com.hitherejoe.androidtvboilerplate.data.model.Cat; 5 | import com.hitherejoe.androidtvboilerplate.data.remote.AndroidTvBoilerplateService; 6 | 7 | import java.util.List; 8 | 9 | import javax.inject.Inject; 10 | import javax.inject.Singleton; 11 | 12 | import rx.Single; 13 | 14 | @Singleton 15 | public class DataManager { 16 | 17 | private final AndroidTvBoilerplateService mTvAndroidTvBoilerplateService; 18 | private final PreferencesHelper mPreferencesHelper; 19 | 20 | @Inject 21 | public DataManager(PreferencesHelper preferencesHelper, 22 | AndroidTvBoilerplateService androidTvBoilerplateService) { 23 | mPreferencesHelper = preferencesHelper; 24 | mTvAndroidTvBoilerplateService = androidTvBoilerplateService; 25 | } 26 | 27 | public PreferencesHelper getPreferencesHelper() { 28 | return mPreferencesHelper; 29 | } 30 | 31 | public Single> getCats(List cats) { 32 | // This just for example, usually here we'd make an API request and not pass a useless 33 | // list of cats back that we passed in! 34 | return Single.just(cats); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/data/local/PreferencesHelper.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.data.local; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.support.annotation.Nullable; 6 | 7 | import com.hitherejoe.androidtvboilerplate.injection.ApplicationContext; 8 | 9 | import javax.inject.Inject; 10 | import javax.inject.Singleton; 11 | 12 | @Singleton 13 | public class PreferencesHelper { 14 | 15 | private final SharedPreferences mPref; 16 | 17 | public static final String PREF_FILE_NAME = "tv_boilerplate_pref_file"; 18 | private static final String PREF_KEY_ACCESS_TOKEN = "PREF_KEY_ACCESS_TOKEN"; 19 | 20 | @Inject 21 | public PreferencesHelper(@ApplicationContext Context context) { 22 | mPref = context.getSharedPreferences(PREF_FILE_NAME, Context.MODE_PRIVATE); 23 | } 24 | 25 | public void clear() { 26 | mPref.edit().clear().apply(); 27 | } 28 | 29 | public void putAccessToken(String accessToken) { 30 | mPref.edit().putString(PREF_KEY_ACCESS_TOKEN, accessToken).apply(); 31 | } 32 | 33 | @Nullable 34 | public String getAccessToken() { 35 | return mPref.getString(PREF_KEY_ACCESS_TOKEN, null); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/data/model/Cat.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.data.model; 2 | 3 | public class Cat { 4 | public String name; 5 | public String description; 6 | public String imageUrl; 7 | 8 | public Cat() { 9 | 10 | } 11 | 12 | public Cat(String name, String description, String imageUrl) { 13 | this.name = name; 14 | this.description = description; 15 | this.imageUrl = imageUrl; 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/data/recommendations/RecommendationReceiver.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.data.recommendations; 2 | 3 | import android.app.AlarmManager; 4 | import android.app.PendingIntent; 5 | import android.content.BroadcastReceiver; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | 9 | import timber.log.Timber; 10 | 11 | public class RecommendationReceiver extends BroadcastReceiver { 12 | private static final long INITIAL_DELAY = 5000; 13 | 14 | @Override 15 | public void onReceive(Context context, Intent intent) { 16 | if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { 17 | scheduleRecommendationUpdate(context); 18 | } 19 | } 20 | 21 | private void scheduleRecommendationUpdate(Context context) { 22 | Timber.i("Scheduling recommendations update..."); 23 | 24 | AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 25 | Intent recommendationIntent = new Intent(context, UpdateRecommendationsService.class); 26 | PendingIntent alarmIntent = PendingIntent.getService(context, 0, recommendationIntent, 0); 27 | 28 | alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, 29 | INITIAL_DELAY, 30 | AlarmManager.INTERVAL_HALF_HOUR, 31 | alarmIntent); 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/data/recommendations/UpdateRecommendationsService.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.data.recommendations; 2 | 3 | import android.app.IntentService; 4 | import android.content.Intent; 5 | 6 | import timber.log.Timber; 7 | 8 | public class UpdateRecommendationsService extends IntentService { 9 | private static final String TAG = "UpdateRecommendationsService"; 10 | private static final int MAX_RECOMMENDATIONS = 3; 11 | 12 | public UpdateRecommendationsService() { 13 | super(TAG); 14 | } 15 | 16 | @Override 17 | protected void onHandleIntent(Intent intent) { 18 | Timber.i("Retrieving popular posts for recommendations..."); 19 | // fetch and add recommendations 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/data/remote/AndroidTvBoilerplateService.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.data.remote; 2 | 3 | import com.squareup.okhttp.Interceptor; 4 | import com.squareup.okhttp.OkHttpClient; 5 | import com.squareup.okhttp.Response; 6 | import com.squareup.okhttp.logging.HttpLoggingInterceptor; 7 | 8 | import java.io.IOException; 9 | 10 | import retrofit.GsonConverterFactory; 11 | import retrofit.Retrofit; 12 | import retrofit.RxJavaCallAdapterFactory; 13 | 14 | public interface AndroidTvBoilerplateService { 15 | 16 | String ENDPOINT = "https://your.endpoint.com/"; 17 | 18 | /******** 19 | * Helper class that sets up a new services 20 | *******/ 21 | class Creator { 22 | public static AndroidTvBoilerplateService newVineyardService() { 23 | OkHttpClient client = new OkHttpClient(); 24 | client.interceptors().add(new Interceptor() { 25 | @Override 26 | public Response intercept(Chain chain) throws IOException { 27 | Response response = chain.proceed(chain.request()); 28 | // Catch unauthorised error 29 | return response; 30 | } 31 | }); 32 | 33 | HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); 34 | interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); 35 | client.interceptors().add(interceptor); 36 | 37 | Retrofit retrofit = new Retrofit.Builder() 38 | .baseUrl(AndroidTvBoilerplateService.ENDPOINT) 39 | .client(client) 40 | .addConverterFactory(GsonConverterFactory.create()) 41 | .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) 42 | .build(); 43 | return retrofit.create(AndroidTvBoilerplateService.class); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/injection/ActivityContext.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.injection; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Qualifier; 7 | 8 | @Qualifier 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface ActivityContext { 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/injection/ApplicationContext.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.injection; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Qualifier; 7 | 8 | @Qualifier 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface ApplicationContext { 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/injection/PerActivity.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.injection; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Scope; 7 | 8 | /** 9 | * A scoping annotation to permit objects whose lifetime should 10 | * conform to the life of the Activity to be memorised in the 11 | * correct component. 12 | */ 13 | @Scope 14 | @Retention(RetentionPolicy.RUNTIME) 15 | public @interface PerActivity { 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/injection/component/ActivityComponent.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.injection.component; 2 | 3 | import com.hitherejoe.androidtvboilerplate.injection.PerActivity; 4 | import com.hitherejoe.androidtvboilerplate.injection.module.ActivityModule; 5 | import com.hitherejoe.androidtvboilerplate.ui.content.ContentFragment; 6 | import com.hitherejoe.androidtvboilerplate.ui.search.SearchContentFragment; 7 | 8 | import dagger.Component; 9 | 10 | /** 11 | * This component inject dependencies to all Activities across the application 12 | */ 13 | @PerActivity 14 | @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) 15 | public interface ActivityComponent { 16 | 17 | void inject(ContentFragment contentFragment); 18 | void inject(SearchContentFragment searchContentFragment); 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/injection/component/ApplicationComponent.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.injection.component; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.hitherejoe.androidtvboilerplate.data.DataManager; 7 | import com.hitherejoe.androidtvboilerplate.data.local.PreferencesHelper; 8 | import com.hitherejoe.androidtvboilerplate.injection.ApplicationContext; 9 | import com.hitherejoe.androidtvboilerplate.injection.module.ApplicationModule; 10 | 11 | import javax.inject.Singleton; 12 | 13 | import dagger.Component; 14 | import rx.subscriptions.CompositeSubscription; 15 | 16 | @Singleton 17 | @Component(modules = ApplicationModule.class) 18 | public interface ApplicationComponent { 19 | 20 | @ApplicationContext 21 | Context context(); 22 | Application application(); 23 | PreferencesHelper preferencesHelper(); 24 | DataManager dataManager(); 25 | CompositeSubscription compositeSubscription(); 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/injection/module/ActivityModule.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.injection.module; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | 6 | import com.hitherejoe.androidtvboilerplate.injection.ActivityContext; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | 11 | @Module 12 | public class ActivityModule { 13 | 14 | private Activity mActivity; 15 | 16 | public ActivityModule(Activity activity) { 17 | mActivity = activity; 18 | } 19 | 20 | @Provides 21 | Activity provideActivity() { 22 | return mActivity; 23 | } 24 | 25 | @Provides 26 | @ActivityContext 27 | Context providesContext() { 28 | return mActivity; 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/injection/module/ApplicationModule.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.injection.module; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.hitherejoe.androidtvboilerplate.data.remote.AndroidTvBoilerplateService; 7 | import com.hitherejoe.androidtvboilerplate.injection.ApplicationContext; 8 | 9 | import javax.inject.Singleton; 10 | 11 | import dagger.Module; 12 | import dagger.Provides; 13 | import rx.subscriptions.CompositeSubscription; 14 | 15 | /** 16 | * Provide application-level dependencies. Mainly singleton object that can be injected from 17 | * anywhere in the app. 18 | */ 19 | @Module 20 | public class ApplicationModule { 21 | protected final Application mApplication; 22 | 23 | public ApplicationModule(Application application) { 24 | mApplication = application; 25 | } 26 | 27 | @Provides 28 | @ApplicationContext 29 | Context provideContext() { 30 | return mApplication; 31 | } 32 | 33 | @Provides 34 | @Singleton 35 | Application provideApplication() { 36 | return mApplication; 37 | } 38 | 39 | @Provides 40 | CompositeSubscription provideCompositeSubscription() { 41 | return new CompositeSubscription(); 42 | } 43 | 44 | @Provides 45 | @Singleton 46 | AndroidTvBoilerplateService provideVineyardService() { 47 | return AndroidTvBoilerplateService.Creator.newVineyardService(); 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/ui/base/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.base; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.MenuItem; 6 | 7 | import com.hitherejoe.androidtvboilerplate.AndroidTvBoilerplateApplication; 8 | import com.hitherejoe.androidtvboilerplate.injection.component.ActivityComponent; 9 | import com.hitherejoe.androidtvboilerplate.injection.component.DaggerActivityComponent; 10 | import com.hitherejoe.androidtvboilerplate.injection.module.ActivityModule; 11 | 12 | public class BaseActivity extends Activity { 13 | 14 | private ActivityComponent mActivityComponent; 15 | 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | } 20 | 21 | @Override 22 | public boolean onOptionsItemSelected(MenuItem item) { 23 | switch (item.getItemId()) { 24 | case android.R.id.home: 25 | finish(); 26 | return true; 27 | default: 28 | return super.onOptionsItemSelected(item); 29 | } 30 | } 31 | 32 | public ActivityComponent activityComponent() { 33 | if (mActivityComponent == null) { 34 | mActivityComponent = DaggerActivityComponent.builder() 35 | .activityModule(new ActivityModule(this)) 36 | .applicationComponent(AndroidTvBoilerplateApplication.get(this).getComponent()) 37 | .build(); 38 | } 39 | return mActivityComponent; 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/ui/base/BasePresenter.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.base; 2 | 3 | /** 4 | * Base class that implements the Presenter interface and provides a base implementation for 5 | * attachView() and detachView(). It also handles keeping a reference to the mvpView that 6 | * can be accessed from the children classes by calling getMvpView(). 7 | */ 8 | public class BasePresenter implements Presenter { 9 | 10 | private T mMvpView; 11 | 12 | @Override 13 | public void attachView(T mvpView) { 14 | mMvpView = mvpView; 15 | } 16 | 17 | @Override 18 | public void detachView() { 19 | mMvpView = null; 20 | } 21 | 22 | public boolean isViewAttached() { 23 | return mMvpView != null; 24 | } 25 | 26 | public T getMvpView() { 27 | return mMvpView; 28 | } 29 | 30 | public void checkViewAttached() { 31 | if (!isViewAttached()) throw new MvpViewNotAttachedException(); 32 | } 33 | 34 | public static class MvpViewNotAttachedException extends RuntimeException { 35 | public MvpViewNotAttachedException() { 36 | super("Please call Presenter.attachView(MvpView) before" + 37 | " requesting data to the Presenter"); 38 | } 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/ui/base/MvpView.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.base; 2 | 3 | /** 4 | * Base interface that any class that wants to act as a View in the MVP (Model View Presenter) 5 | * pattern must implement. Generally this interface will be extended by a more specific interface 6 | * that then usually will be implemented by an Activity or Fragment. 7 | */ 8 | public interface MvpView { 9 | 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/ui/base/Presenter.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.base; 2 | 3 | /** 4 | * Every presenter in the app must either implement this interface or extend BasePresenter 5 | * indicating the MvpView type that wants to be attached with. 6 | */ 7 | public interface Presenter { 8 | 9 | void attachView(V mvpView); 10 | 11 | void detachView(); 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/ui/common/CardPresenter.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.common; 2 | 3 | import android.content.Context; 4 | import android.graphics.drawable.Drawable; 5 | import android.support.v17.leanback.widget.ImageCardView; 6 | import android.support.v17.leanback.widget.Presenter; 7 | import android.support.v4.content.ContextCompat; 8 | import android.view.ViewGroup; 9 | 10 | import com.bumptech.glide.Glide; 11 | import com.hitherejoe.androidtvboilerplate.R; 12 | import com.hitherejoe.androidtvboilerplate.data.model.Cat; 13 | 14 | public class CardPresenter extends Presenter { 15 | 16 | private static final int CARD_WIDTH = 300; 17 | private static final int CARD_HEIGHT = 300; 18 | 19 | private int mSelectedBackgroundColor = -1; 20 | private int mDefaultBackgroundColor = -1; 21 | private Drawable mDefaultCardImage; 22 | 23 | @Override 24 | public ViewHolder onCreateViewHolder(ViewGroup parent) { 25 | Context context = parent.getContext(); 26 | mDefaultBackgroundColor = ContextCompat.getColor(context, R.color.primary); 27 | mSelectedBackgroundColor = ContextCompat.getColor(context, R.color.primary_dark); 28 | mDefaultCardImage = ContextCompat.getDrawable(context, R.drawable.card_default); 29 | 30 | ImageCardView cardView = new ImageCardView(parent.getContext()) { 31 | @Override 32 | public void setSelected(boolean selected) { 33 | updateCardBackgroundColor(this, selected); 34 | super.setSelected(selected); 35 | } 36 | }; 37 | 38 | cardView.setFocusable(true); 39 | cardView.setFocusableInTouchMode(true); 40 | updateCardBackgroundColor(cardView, false); 41 | return new ViewHolder(cardView); 42 | } 43 | 44 | private void updateCardBackgroundColor(ImageCardView view, boolean selected) { 45 | int color = selected ? mSelectedBackgroundColor : mDefaultBackgroundColor; 46 | view.setBackgroundColor(color); 47 | view.findViewById(R.id.info_field).setBackgroundColor(color); 48 | } 49 | 50 | @Override 51 | public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { 52 | Cat cat = (Cat) item; 53 | 54 | ImageCardView cardView = (ImageCardView) viewHolder.view; 55 | cardView.setTitleText(cat.name); 56 | cardView.setContentText(cat.description); 57 | 58 | cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT); 59 | 60 | Glide.with(cardView.getContext()) 61 | .load(cat.imageUrl) 62 | .error(mDefaultCardImage) 63 | .into(cardView.getMainImageView()); 64 | } 65 | 66 | @Override 67 | public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { 68 | ImageCardView cardView = (ImageCardView) viewHolder.view; 69 | cardView.setBadgeImage(null); 70 | cardView.setMainImage(null); 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/ui/content/ContentActivity.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.content; 2 | 3 | import android.os.Bundle; 4 | import android.widget.FrameLayout; 5 | 6 | import com.hitherejoe.androidtvboilerplate.R; 7 | import com.hitherejoe.androidtvboilerplate.ui.base.BaseActivity; 8 | import com.hitherejoe.androidtvboilerplate.ui.search.SearchContentActivity; 9 | 10 | import butterknife.Bind; 11 | import butterknife.ButterKnife; 12 | 13 | public class ContentActivity extends BaseActivity { 14 | 15 | @Bind(R.id.frame_container) FrameLayout mFragmentContainer; 16 | 17 | @Override 18 | public void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_main); 21 | ButterKnife.bind(this); 22 | 23 | getFragmentManager().beginTransaction() 24 | .replace(mFragmentContainer.getId(), ContentFragment.newInstance()).commit(); 25 | } 26 | 27 | @Override 28 | public boolean onSearchRequested() { 29 | startActivity(SearchContentActivity.getStartIntent(this)); 30 | return true; 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/ui/content/ContentFragment.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.content; 2 | 3 | import android.content.Intent; 4 | import android.content.res.Resources; 5 | import android.graphics.Bitmap; 6 | import android.graphics.drawable.ColorDrawable; 7 | import android.graphics.drawable.Drawable; 8 | import android.os.Bundle; 9 | import android.os.Handler; 10 | import android.support.v17.leanback.app.BackgroundManager; 11 | import android.support.v17.leanback.app.BrowseFragment; 12 | import android.support.v17.leanback.widget.ArrayObjectAdapter; 13 | import android.support.v17.leanback.widget.HeaderItem; 14 | import android.support.v17.leanback.widget.ListRow; 15 | import android.support.v17.leanback.widget.ListRowPresenter; 16 | import android.support.v17.leanback.widget.OnItemViewClickedListener; 17 | import android.support.v17.leanback.widget.OnItemViewSelectedListener; 18 | import android.support.v17.leanback.widget.Presenter; 19 | import android.support.v17.leanback.widget.Row; 20 | import android.support.v17.leanback.widget.RowPresenter; 21 | import android.support.v4.content.ContextCompat; 22 | import android.util.DisplayMetrics; 23 | import android.view.View; 24 | import android.widget.Toast; 25 | 26 | import com.bumptech.glide.Glide; 27 | import com.bumptech.glide.request.animation.GlideAnimation; 28 | import com.bumptech.glide.request.target.SimpleTarget; 29 | import com.hitherejoe.androidtvboilerplate.R; 30 | import com.hitherejoe.androidtvboilerplate.data.model.Cat; 31 | import com.hitherejoe.androidtvboilerplate.ui.base.BaseActivity; 32 | import com.hitherejoe.androidtvboilerplate.ui.search.SearchContentActivity; 33 | import com.hitherejoe.androidtvboilerplate.ui.common.CardPresenter; 34 | 35 | import java.net.URI; 36 | import java.util.ArrayList; 37 | import java.util.List; 38 | 39 | import javax.inject.Inject; 40 | 41 | public class ContentFragment extends BrowseFragment implements ContentMvpView { 42 | 43 | @Inject ContentPresenter mContentPresenter; 44 | 45 | private static final int BACKGROUND_UPDATE_DELAY = 300; 46 | 47 | private ArrayObjectAdapter mRowsAdapter; 48 | private BackgroundManager mBackgroundManager; 49 | private DisplayMetrics mMetrics; 50 | private Drawable mDefaultBackground; 51 | private Handler mHandler; 52 | private Runnable mBackgroundRunnable; 53 | 54 | public static ContentFragment newInstance() { 55 | return new ContentFragment(); 56 | } 57 | 58 | @Override 59 | public void onActivityCreated(Bundle savedInstanceState) { 60 | super.onActivityCreated(savedInstanceState); 61 | ((BaseActivity) getActivity()).activityComponent().inject(this); 62 | mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); 63 | mHandler = new Handler(); 64 | mContentPresenter.attachView(this); 65 | 66 | setAdapter(mRowsAdapter); 67 | prepareBackgroundManager(); 68 | setupUIElements(); 69 | setupListeners(); 70 | getCats(); 71 | } 72 | 73 | @Override 74 | public void onDestroy() { 75 | super.onDestroy(); 76 | if (mBackgroundRunnable != null) { 77 | mHandler.removeCallbacks(mBackgroundRunnable); 78 | mBackgroundRunnable = null; 79 | } 80 | mBackgroundManager = null; 81 | mContentPresenter.detachView(); 82 | } 83 | 84 | @Override 85 | public void onStop() { 86 | super.onStop(); 87 | mBackgroundManager.release(); 88 | } 89 | 90 | protected void updateBackground(String uri) { 91 | int width = mMetrics.widthPixels; 92 | int height = mMetrics.heightPixels; 93 | Glide.with(getActivity()) 94 | .load(uri) 95 | .asBitmap() 96 | .centerCrop() 97 | .error(mDefaultBackground) 98 | .into(new SimpleTarget(width, height) { 99 | @Override 100 | public void onResourceReady(Bitmap resource, 101 | GlideAnimation 102 | glideAnimation) { 103 | mBackgroundManager.setBitmap(resource); 104 | } 105 | }); 106 | if (mBackgroundRunnable != null) mHandler.removeCallbacks(mBackgroundRunnable); 107 | } 108 | 109 | private void setupUIElements() { 110 | setBadgeDrawable(ContextCompat.getDrawable(getActivity(), R.drawable.banner_browse)); 111 | setHeadersState(HEADERS_ENABLED); 112 | setHeadersTransitionOnBackEnabled(true); 113 | setBrandColor(ContextCompat.getColor(getActivity(), R.color.primary)); 114 | setSearchAffordanceColor(ContextCompat.getColor(getActivity(), R.color.accent)); 115 | } 116 | 117 | private void setupListeners() { 118 | setOnItemViewClickedListener(mOnItemViewClickedListener); 119 | setOnItemViewSelectedListener(mOnItemViewSelectedListener); 120 | 121 | setOnSearchClickedListener(new View.OnClickListener() { 122 | 123 | @Override 124 | public void onClick(View view) { 125 | startActivity(new Intent(getActivity(), SearchContentActivity.class)); 126 | } 127 | }); 128 | } 129 | 130 | private void prepareBackgroundManager() { 131 | mBackgroundManager = BackgroundManager.getInstance(getActivity()); 132 | mBackgroundManager.attach(getActivity().getWindow()); 133 | mDefaultBackground = 134 | new ColorDrawable(ContextCompat.getColor(getActivity(), R.color.primary_light)); 135 | mBackgroundManager.setColor(ContextCompat.getColor(getActivity(), R.color.primary_light)); 136 | mMetrics = new DisplayMetrics(); 137 | getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics); 138 | } 139 | 140 | private void getCats() { 141 | // Usually we'd load things from an API or database, for example here we just create 142 | // a list of cats from resources and return them back after passing them to the datamanager. 143 | // Obviously we wouldn't usually do this, but this is just for example and allows us 144 | // to still have an example unit test that doesn't require robolectric! 145 | Resources resources = getResources(); 146 | String[] names = resources.getStringArray(R.array.cat_names); 147 | String[] descriptions = resources.getStringArray(R.array.cat_descriptions); 148 | String[] images = resources.getStringArray(R.array.cat_images); 149 | 150 | List cats = new ArrayList<>(); 151 | for (int i = 0; i < names.length; i++) { 152 | cats.add(new Cat(names[i], descriptions[i], images[i])); 153 | } 154 | 155 | mContentPresenter.getCats(cats); 156 | } 157 | 158 | private void startBackgroundTimer(final URI backgroundURI) { 159 | if (mBackgroundRunnable != null) mHandler.removeCallbacks(mBackgroundRunnable); 160 | mBackgroundRunnable = new Runnable() { 161 | @Override 162 | public void run() { 163 | if (backgroundURI != null) updateBackground(backgroundURI.toString()); 164 | } 165 | }; 166 | mHandler.postDelayed(mBackgroundRunnable, BACKGROUND_UPDATE_DELAY); 167 | } 168 | 169 | private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() { 170 | @Override 171 | public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, 172 | RowPresenter.ViewHolder rowViewHolder, Row row) { 173 | // respond to item clicks 174 | } 175 | }; 176 | 177 | private OnItemViewSelectedListener mOnItemViewSelectedListener = 178 | new OnItemViewSelectedListener() { 179 | @Override 180 | public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, 181 | RowPresenter.ViewHolder rowViewHolder, Row row) { 182 | // respond to item selection 183 | if (item instanceof Cat) { 184 | Cat cat = (Cat) item; 185 | String backgroundUrl = cat.imageUrl; 186 | if (backgroundUrl != null) startBackgroundTimer(URI.create(backgroundUrl)); 187 | } 188 | } 189 | }; 190 | 191 | /** 192 | * Method implementations from SearchContentMvpView 193 | */ 194 | 195 | @Override 196 | public void showCats(List cats) { 197 | final ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter()); 198 | listRowAdapter.addAll(0, cats); 199 | HeaderItem header = new HeaderItem(0, getString(R.string.header_title_cats)); 200 | mRowsAdapter.add(new ListRow(header, listRowAdapter)); 201 | } 202 | 203 | @Override 204 | public void showCatsError() { 205 | // show loading error state here 206 | String errorMessage = getString(R.string.error_message_generic); 207 | Toast.makeText(getActivity(), errorMessage, Toast.LENGTH_SHORT).show(); 208 | } 209 | 210 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/ui/content/ContentMvpView.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.content; 2 | 3 | import com.hitherejoe.androidtvboilerplate.data.model.Cat; 4 | import com.hitherejoe.androidtvboilerplate.ui.base.MvpView; 5 | 6 | import java.util.List; 7 | 8 | public interface ContentMvpView extends MvpView { 9 | 10 | void showCats(List cats); 11 | 12 | void showCatsError(); 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/ui/content/ContentPresenter.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.content; 2 | 3 | import com.hitherejoe.androidtvboilerplate.data.DataManager; 4 | import com.hitherejoe.androidtvboilerplate.data.model.Cat; 5 | import com.hitherejoe.androidtvboilerplate.ui.base.BasePresenter; 6 | 7 | import java.util.List; 8 | 9 | import javax.inject.Inject; 10 | 11 | import rx.SingleSubscriber; 12 | import rx.Subscription; 13 | import rx.android.schedulers.AndroidSchedulers; 14 | import rx.schedulers.Schedulers; 15 | import timber.log.Timber; 16 | 17 | public class ContentPresenter extends BasePresenter { 18 | 19 | private Subscription mSubscription; 20 | private final DataManager mDataManager; 21 | 22 | @Inject 23 | public ContentPresenter(DataManager dataManager) { 24 | mDataManager = dataManager; 25 | } 26 | 27 | @Override 28 | public void detachView() { 29 | super.detachView(); 30 | if (mSubscription != null) mSubscription.unsubscribe(); 31 | } 32 | 33 | public void getCats(List cats) { 34 | checkViewAttached(); 35 | 36 | mSubscription = mDataManager.getCats(cats) 37 | .observeOn(AndroidSchedulers.mainThread()) 38 | .subscribeOn(Schedulers.io()) 39 | .subscribe(new SingleSubscriber>() { 40 | @Override 41 | public void onSuccess(List cats) { 42 | getMvpView().showCats(cats); 43 | } 44 | 45 | @Override 46 | public void onError(Throwable error) { 47 | getMvpView().showCatsError(); 48 | Timber.e(error, "There was an error loading the cats!"); 49 | } 50 | }); 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/ui/search/SearchContentActivity.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.search; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | 7 | import com.hitherejoe.androidtvboilerplate.R; 8 | import com.hitherejoe.androidtvboilerplate.ui.base.BaseActivity; 9 | 10 | public class SearchContentActivity extends BaseActivity { 11 | 12 | private SearchContentFragment mSearchContentFragment; 13 | 14 | public static Intent getStartIntent(Context context) { 15 | return new Intent(context, SearchContentActivity.class); 16 | } 17 | 18 | @Override 19 | public void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_search); 22 | 23 | mSearchContentFragment = (SearchContentFragment) getFragmentManager() 24 | .findFragmentById(R.id.search_fragment); 25 | } 26 | 27 | @Override 28 | public boolean onSearchRequested() { 29 | if (mSearchContentFragment.hasResults()) { 30 | startActivity(new Intent(this, SearchContentActivity.class)); 31 | } else { 32 | mSearchContentFragment.startRecognition(); 33 | } 34 | return true; 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/ui/search/SearchContentFragment.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.search; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.content.ActivityNotFoundException; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.pm.PackageManager; 9 | import android.content.res.Resources; 10 | import android.graphics.Bitmap; 11 | import android.graphics.drawable.ColorDrawable; 12 | import android.graphics.drawable.Drawable; 13 | import android.os.Bundle; 14 | import android.os.Handler; 15 | import android.support.v17.leanback.app.BackgroundManager; 16 | import android.support.v17.leanback.app.SearchFragment; 17 | import android.support.v17.leanback.widget.ArrayObjectAdapter; 18 | import android.support.v17.leanback.widget.HeaderItem; 19 | import android.support.v17.leanback.widget.ListRow; 20 | import android.support.v17.leanback.widget.ListRowPresenter; 21 | import android.support.v17.leanback.widget.ObjectAdapter; 22 | import android.support.v17.leanback.widget.OnItemViewClickedListener; 23 | import android.support.v17.leanback.widget.OnItemViewSelectedListener; 24 | import android.support.v17.leanback.widget.Presenter; 25 | import android.support.v17.leanback.widget.Row; 26 | import android.support.v17.leanback.widget.RowPresenter; 27 | import android.support.v17.leanback.widget.SpeechRecognitionCallback; 28 | import android.support.v4.content.ContextCompat; 29 | import android.text.TextUtils; 30 | import android.util.DisplayMetrics; 31 | import android.widget.Toast; 32 | 33 | import com.bumptech.glide.Glide; 34 | import com.bumptech.glide.request.animation.GlideAnimation; 35 | import com.bumptech.glide.request.target.SimpleTarget; 36 | import com.hitherejoe.androidtvboilerplate.R; 37 | import com.hitherejoe.androidtvboilerplate.data.model.Cat; 38 | import com.hitherejoe.androidtvboilerplate.ui.base.BaseActivity; 39 | import com.hitherejoe.androidtvboilerplate.ui.common.CardPresenter; 40 | import com.hitherejoe.androidtvboilerplate.util.NetworkUtil; 41 | import com.hitherejoe.androidtvboilerplate.util.ToastFactory; 42 | 43 | import java.net.URI; 44 | import java.util.ArrayList; 45 | import java.util.List; 46 | 47 | import javax.inject.Inject; 48 | 49 | import timber.log.Timber; 50 | 51 | public class SearchContentFragment extends SearchFragment implements SearchContentMvpView, 52 | SearchFragment.SearchResultProvider { 53 | 54 | @Inject SearchContentPresenter mSearchContentPresenter; 55 | 56 | private static final int BACKGROUND_UPDATE_DELAY = 300; 57 | private static final int REQUEST_SPEECH = 0x00000010; 58 | 59 | private ArrayObjectAdapter mResultsAdapter; 60 | private ArrayObjectAdapter mSearchObjectAdapter; 61 | private BackgroundManager mBackgroundManager; 62 | private Drawable mDefaultBackground; 63 | private DisplayMetrics mMetrics; 64 | private Handler mHandler; 65 | private Runnable mBackgroundRunnable; 66 | 67 | private String mSearchQuery; 68 | 69 | @Override 70 | public void onCreate(Bundle savedInstanceState) { 71 | super.onCreate(savedInstanceState); 72 | ((BaseActivity) getActivity()).activityComponent().inject(this); 73 | mResultsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); 74 | mHandler = new Handler(); 75 | mSearchContentPresenter.attachView(this); 76 | setSearchResultProvider(this); 77 | setupBackgroundManager(); 78 | setListeners(); 79 | } 80 | 81 | public void onDestroy() { 82 | if (mBackgroundRunnable != null) { 83 | mHandler.removeCallbacks(mBackgroundRunnable); 84 | mBackgroundRunnable = null; 85 | } 86 | mBackgroundManager = null; 87 | mSearchContentPresenter.detachView(); 88 | super.onDestroy(); 89 | } 90 | 91 | @Override 92 | public void onStop() { 93 | super.onStop(); 94 | mBackgroundManager.release(); 95 | } 96 | 97 | @Override 98 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 99 | switch (requestCode) { 100 | case REQUEST_SPEECH: 101 | switch (resultCode) { 102 | case Activity.RESULT_OK: 103 | setSearchQuery(data, false); 104 | break; 105 | case Activity.RESULT_CANCELED: 106 | Timber.i("Recognizer canceled"); 107 | break; 108 | } 109 | break; 110 | } 111 | } 112 | 113 | @Override 114 | public ObjectAdapter getResultsAdapter() { 115 | return mResultsAdapter; 116 | } 117 | 118 | @Override 119 | public boolean onQueryTextChange(String newQuery) { 120 | loadQuery(newQuery); 121 | return true; 122 | } 123 | 124 | @Override 125 | public boolean onQueryTextSubmit(String query) { 126 | loadQuery(query); 127 | return true; 128 | } 129 | 130 | public boolean hasResults() { 131 | return mResultsAdapter.size() > 0; 132 | } 133 | 134 | protected void updateBackground(String uri) { 135 | int width = mMetrics.widthPixels; 136 | int height = mMetrics.heightPixels; 137 | Glide.with(getActivity()) 138 | .load(uri) 139 | .asBitmap() 140 | .centerCrop() 141 | .error(mDefaultBackground) 142 | .into(new SimpleTarget(width, height) { 143 | @Override 144 | public void onResourceReady(Bitmap resource, 145 | GlideAnimation 146 | glideAnimation) { 147 | mBackgroundManager.setBitmap(resource); 148 | } 149 | }); 150 | if (mBackgroundRunnable != null) mHandler.removeCallbacks(mBackgroundRunnable); 151 | } 152 | 153 | private void setupBackgroundManager() { 154 | mBackgroundManager = BackgroundManager.getInstance(getActivity()); 155 | mBackgroundManager.attach(getActivity().getWindow()); 156 | mBackgroundManager.setColor(ContextCompat.getColor(getActivity(), R.color.primary_light)); 157 | mDefaultBackground = 158 | new ColorDrawable(ContextCompat.getColor(getActivity(), R.color.primary_light)); 159 | mMetrics = new DisplayMetrics(); 160 | getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics); 161 | } 162 | 163 | private void startBackgroundTimer(final URI backgroundURI) { 164 | if (mBackgroundRunnable != null) mHandler.removeCallbacks(mBackgroundRunnable); 165 | mBackgroundRunnable = new Runnable() { 166 | @Override 167 | public void run() { 168 | if (backgroundURI != null) updateBackground(backgroundURI.toString()); 169 | } 170 | }; 171 | mHandler.postDelayed(mBackgroundRunnable, BACKGROUND_UPDATE_DELAY); 172 | } 173 | 174 | private void setListeners() { 175 | setOnItemViewClickedListener(mOnItemViewClickedListener); 176 | setOnItemViewSelectedListener(mOnItemViewSelectedListener); 177 | if (!hasPermission(Manifest.permission.RECORD_AUDIO)) { 178 | setSpeechRecognitionCallback(new SpeechRecognitionCallback() { 179 | @Override 180 | public void recognizeSpeech() { 181 | try { 182 | startActivityForResult(getRecognizerIntent(), REQUEST_SPEECH); 183 | } catch (ActivityNotFoundException error) { 184 | Timber.e(error, "Cannot find activity for speech recognizer"); 185 | } 186 | } 187 | }); 188 | } 189 | } 190 | 191 | private boolean hasPermission(final String permission) { 192 | final Context context = getActivity(); 193 | return PackageManager.PERMISSION_GRANTED == context.getPackageManager().checkPermission( 194 | permission, context.getPackageName()); 195 | } 196 | 197 | private void loadQuery(String query) { 198 | if ((mSearchQuery != null && !mSearchQuery.equals(query)) && !query.trim().isEmpty() 199 | || (!TextUtils.isEmpty(query) && !query.equals("nil"))) { 200 | if (NetworkUtil.isNetworkConnected(getActivity())) { 201 | mSearchQuery = query; 202 | searchCats(query); 203 | } else { 204 | ToastFactory.createWifiErrorToast(getActivity()).show(); 205 | } 206 | } 207 | } 208 | 209 | private void searchCats(String query) { 210 | mResultsAdapter.clear(); 211 | HeaderItem resultsHeader = new HeaderItem(0, getString(R.string.text_search_results)); 212 | mSearchObjectAdapter = new ArrayObjectAdapter(new CardPresenter()); 213 | ListRow listRow = new ListRow(resultsHeader, mSearchObjectAdapter); 214 | mResultsAdapter.add(listRow); 215 | mSearchObjectAdapter.clear(); 216 | searchCats(); 217 | } 218 | 219 | private void searchCats() { 220 | // Usually we'd load things from an API or database, for example here we just create 221 | // a list of cats from resources and return them back after passing them to the datamanager. 222 | // Obviously we wouldn't usually do this, but this is just for example and allows us 223 | // to still have an example unit test that doesn't require robolectric! 224 | Resources resources = getResources(); 225 | String[] names = resources.getStringArray(R.array.cat_names); 226 | String[] descriptions = resources.getStringArray(R.array.cat_descriptions); 227 | String[] images = resources.getStringArray(R.array.cat_images); 228 | 229 | List cats = new ArrayList<>(); 230 | for (int i = 0; i < names.length; i++) { 231 | cats.add(new Cat(names[i], descriptions[i], images[i])); 232 | } 233 | 234 | mSearchContentPresenter.searchCats(cats); 235 | } 236 | 237 | private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() { 238 | @Override 239 | public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, 240 | RowPresenter.ViewHolder rowViewHolder, Row row) { 241 | // Handle item click 242 | } 243 | }; 244 | 245 | private OnItemViewSelectedListener mOnItemViewSelectedListener = 246 | new OnItemViewSelectedListener() { 247 | @Override 248 | public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, 249 | RowPresenter.ViewHolder rowViewHolder, Row row) { 250 | if (item instanceof Cat) { 251 | String backgroundUrl = ((Cat) item).imageUrl; 252 | if (backgroundUrl != null) startBackgroundTimer(URI.create(backgroundUrl)); 253 | } 254 | } 255 | }; 256 | 257 | @Override 258 | public void showCats(List cats) { 259 | mSearchObjectAdapter.addAll(0, cats); 260 | } 261 | 262 | @Override 263 | public void showCatsError() { 264 | // show loading error state here 265 | String errorMessage = getString(R.string.error_message_generic); 266 | Toast.makeText(getActivity(), errorMessage, Toast.LENGTH_SHORT).show(); 267 | } 268 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/ui/search/SearchContentMvpView.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.search; 2 | 3 | import com.hitherejoe.androidtvboilerplate.data.model.Cat; 4 | import com.hitherejoe.androidtvboilerplate.ui.base.MvpView; 5 | 6 | import java.util.List; 7 | 8 | public interface SearchContentMvpView extends MvpView { 9 | 10 | void showCats(List cats); 11 | 12 | void showCatsError(); 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/ui/search/SearchContentPresenter.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.search; 2 | 3 | import com.hitherejoe.androidtvboilerplate.data.DataManager; 4 | import com.hitherejoe.androidtvboilerplate.data.model.Cat; 5 | import com.hitherejoe.androidtvboilerplate.ui.base.BasePresenter; 6 | 7 | import java.util.List; 8 | 9 | import javax.inject.Inject; 10 | 11 | import rx.SingleSubscriber; 12 | import rx.Subscription; 13 | import rx.android.schedulers.AndroidSchedulers; 14 | import rx.schedulers.Schedulers; 15 | import timber.log.Timber; 16 | 17 | public class SearchContentPresenter extends BasePresenter { 18 | 19 | private Subscription mSubscription; 20 | private final DataManager mDataManager; 21 | 22 | @Inject 23 | public SearchContentPresenter(DataManager dataManager) { 24 | mDataManager = dataManager; 25 | } 26 | 27 | @Override 28 | public void detachView() { 29 | super.detachView(); 30 | if (mSubscription != null) mSubscription.unsubscribe(); 31 | } 32 | 33 | public void searchCats(List cats) { 34 | checkViewAttached(); 35 | 36 | mSubscription = mDataManager.getCats(cats) 37 | .observeOn(AndroidSchedulers.mainThread()) 38 | .subscribeOn(Schedulers.io()) 39 | .subscribe(new SingleSubscriber>() { 40 | @Override 41 | public void onSuccess(List cats) { 42 | getMvpView().showCats(cats); 43 | } 44 | 45 | @Override 46 | public void onError(Throwable error) { 47 | getMvpView().showCatsError(); 48 | Timber.e(error, "There was an error loading the cats!"); 49 | } 50 | }); 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/util/NetworkUtil.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.util; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | import android.net.NetworkInfo; 6 | 7 | public class NetworkUtil { 8 | 9 | public static boolean isNetworkConnected(Context context) { 10 | ConnectivityManager cm = 11 | (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 12 | 13 | NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); 14 | return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/util/ToastFactory.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.util; 2 | 3 | 4 | import android.content.Context; 5 | import android.widget.Toast; 6 | 7 | import com.hitherejoe.androidtvboilerplate.R; 8 | 9 | public class ToastFactory { 10 | 11 | public static Toast createWifiErrorToast(Context context) { 12 | return Toast.makeText( 13 | context, 14 | context.getString(R.string.error_message_network_needed), 15 | Toast.LENGTH_SHORT); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hitherejoe/androidtvboilerplate/util/ViewUtils.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.util; 2 | 3 | import android.content.Context; 4 | import android.util.DisplayMetrics; 5 | 6 | public class ViewUtils { 7 | 8 | public static float convertPixelsToDp(float px, Context context) { 9 | DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 10 | return px / (metrics.densityDpi / 160f); 11 | } 12 | 13 | public static float convertDpToPixel(float dp, Context context) { 14 | DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 15 | return dp * (metrics.densityDpi / 160f); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/AndroidTvBoilerplate/20d473204cd9a2c5f86fb1bc4aa56529435d431d/app/src/main/res/drawable-xhdpi/banner.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/banner_browse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/AndroidTvBoilerplate/20d473204cd9a2c5f86fb1bc4aa56529435d431d/app/src/main/res/drawable-xhdpi/banner_browse.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/card_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/AndroidTvBoilerplate/20d473204cd9a2c5f86fb1bc4aa56529435d431d/app/src/main/res/drawable-xhdpi/card_default.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_search.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/AndroidTvBoilerplate/20d473204cd9a2c5f86fb1bc4aa56529435d431d/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/AndroidTvBoilerplate/20d473204cd9a2c5f86fb1bc4aa56529435d431d/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/AndroidTvBoilerplate/20d473204cd9a2c5f86fb1bc4aa56529435d431d/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/AndroidTvBoilerplate/20d473204cd9a2c5f86fb1bc4aa56529435d431d/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #673AB7 4 | #512DA8 5 | #D1C4E9 6 | #E040FB 7 | #212121 8 | #727272 9 | #FFFFFF 10 | #B6B6B6 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Boilerplate 3 | 4 | 5 | Cats 6 | 7 | 8 | Search results 9 | 10 | 11 | 12 | Oops 13 | Oops, you need a network connection to use Vineyard! 14 | Close 15 | 16 | 17 | Oops, you need a network connection to do that! 18 | Oops, there was an error with the request! 19 | 20 | 21 | 22 | 23 | Cat one 24 | Cat two 25 | Cat three 26 | Cat four 27 | Cat five 28 | Cat six 29 | Cat seven 30 | Cat eight 31 | 32 | 33 | 34 | Description one 35 | Description two 36 | Description three 37 | Description four 38 | Description five 39 | Description six 40 | Description seven 41 | Description eight 42 | 43 | 44 | 45 | https://i.ytimg.com/vi/tntOCGkgt98/maxresdefault.jpg 46 | https://i.ytimg.com/vi/icqDxNab3Do/maxresdefault.jpg 47 | http://cdn.hasinstinct.com/2015/08/14/football-wallpapers-funny-cats-smile-wallpaper-31607.jpg 48 | http://s2.dmcdn.net/Dnepf/1280x720-ET4.jpg 49 | http://cdn.hasinstinct.com/2015/09/22/funny-cute-cat-7035315.jpg 50 | http://www.funny-animalpictures.com/media/content/items/images/funnycats0048_O.jpg 51 | https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcSYz-mKES0-qiefh_xQV1dApGVM2t0UzlSFPB4C8Xm2msyH8tHe 52 | http://www.becauseimacat.com/wp-content/uploads/2015/09/Funny-Cat-Vine-Compilation.jpg 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/test/java/com/hitherejoe/androidtvboilerplate/data/DataManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.data; 2 | 3 | import com.hitherejoe.androidtvboilerplate.data.local.PreferencesHelper; 4 | import com.hitherejoe.androidtvboilerplate.data.model.Cat; 5 | import com.hitherejoe.androidtvboilerplate.data.remote.AndroidTvBoilerplateService; 6 | import com.hitherejoe.androidtvboilerplate.test.common.TestDataFactory; 7 | 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mock; 12 | import org.mockito.runners.MockitoJUnitRunner; 13 | 14 | import java.util.List; 15 | 16 | import rx.observers.TestSubscriber; 17 | 18 | @RunWith(MockitoJUnitRunner.class) 19 | public class DataManagerTest { 20 | 21 | @Mock AndroidTvBoilerplateService mMockAndroidTvBoilerplateService; 22 | @Mock PreferencesHelper mMockPreferencesHelper; 23 | private DataManager mDataManager; 24 | 25 | @Before 26 | public void setUp() { 27 | mDataManager = new DataManager(mMockPreferencesHelper, mMockAndroidTvBoilerplateService); 28 | } 29 | 30 | @Test 31 | public void getCatsCompletesAndEmitsCats() throws Exception { 32 | List mockCats = TestDataFactory.makeCats(10); 33 | 34 | TestSubscriber> result = new TestSubscriber<>(); 35 | mDataManager.getCats(mockCats).subscribe(result); 36 | result.assertNoErrors(); 37 | result.assertValue(mockCats); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /app/src/test/java/com/hitherejoe/androidtvboilerplate/ui/content/ContentPresenterTest.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.content; 2 | 3 | import com.hitherejoe.androidtvboilerplate.data.DataManager; 4 | import com.hitherejoe.androidtvboilerplate.data.model.Cat; 5 | import com.hitherejoe.androidtvboilerplate.test.common.TestDataFactory; 6 | import com.hitherejoe.androidtvboilerplate.util.RxSchedulersOverrideRule; 7 | 8 | import org.junit.After; 9 | import org.junit.Before; 10 | import org.junit.Rule; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mock; 14 | import org.mockito.runners.MockitoJUnitRunner; 15 | 16 | import java.util.List; 17 | 18 | import rx.Single; 19 | 20 | import static org.mockito.Matchers.anyListOf; 21 | import static org.mockito.Mockito.never; 22 | import static org.mockito.Mockito.verify; 23 | import static org.mockito.Mockito.when; 24 | 25 | @RunWith(MockitoJUnitRunner.class) 26 | public class ContentPresenterTest { 27 | 28 | @Mock ContentMvpView mMockContentMvpView; 29 | @Mock DataManager mMockDataManager; 30 | private ContentPresenter mContentPresenter; 31 | 32 | @Rule 33 | public final RxSchedulersOverrideRule mOverrideSchedulersRule = new RxSchedulersOverrideRule(); 34 | 35 | @Before 36 | public void setUp() { 37 | mContentPresenter = new ContentPresenter(mMockDataManager); 38 | mContentPresenter.attachView(mMockContentMvpView); 39 | } 40 | 41 | @After 42 | public void detachView() { 43 | mContentPresenter.detachView(); 44 | } 45 | 46 | @Test 47 | public void getCatsSuccessful() { 48 | List cats = TestDataFactory.makeCats(10); 49 | stubDataManagerGetCats(Single.just(cats)); 50 | 51 | mContentPresenter.getCats(cats); 52 | 53 | verify(mMockContentMvpView).showCats(cats); 54 | verify(mMockContentMvpView, never()).showCatsError(); 55 | } 56 | 57 | @Test 58 | public void getTagsFails() { 59 | List cats = TestDataFactory.makeCats(10); 60 | stubDataManagerGetCats(Single.just(cats)); 61 | stubDataManagerGetCats(Single.>error(new RuntimeException())); 62 | 63 | mContentPresenter.getCats(cats); 64 | 65 | verify(mMockContentMvpView).showCatsError(); 66 | verify(mMockContentMvpView, never()).showCats(anyListOf(Cat.class)); 67 | } 68 | 69 | private void stubDataManagerGetCats(Single> single) { 70 | when(mMockDataManager.getCats(anyListOf(Cat.class))).thenReturn(single); 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /app/src/test/java/com/hitherejoe/androidtvboilerplate/ui/search/SearchContentPresenterTest.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.ui.search; 2 | 3 | import com.hitherejoe.androidtvboilerplate.data.DataManager; 4 | import com.hitherejoe.androidtvboilerplate.data.model.Cat; 5 | import com.hitherejoe.androidtvboilerplate.test.common.TestDataFactory; 6 | import com.hitherejoe.androidtvboilerplate.util.RxSchedulersOverrideRule; 7 | 8 | import org.junit.After; 9 | import org.junit.Before; 10 | import org.junit.Rule; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mock; 14 | import org.mockito.runners.MockitoJUnitRunner; 15 | 16 | import java.util.List; 17 | 18 | import rx.Single; 19 | 20 | import static org.mockito.Matchers.anyListOf; 21 | import static org.mockito.Mockito.never; 22 | import static org.mockito.Mockito.verify; 23 | import static org.mockito.Mockito.when; 24 | 25 | @RunWith(MockitoJUnitRunner.class) 26 | public class SearchContentPresenterTest { 27 | 28 | @Mock SearchContentMvpView mMockSearchContentMvpView; 29 | @Mock DataManager mMockDataManager; 30 | private SearchContentPresenter mSearchContentPresenter; 31 | 32 | @Rule 33 | public final RxSchedulersOverrideRule mOverrideSchedulersRule = new RxSchedulersOverrideRule(); 34 | 35 | @Before 36 | public void setUp() { 37 | mSearchContentPresenter = new SearchContentPresenter(mMockDataManager); 38 | mSearchContentPresenter.attachView(mMockSearchContentMvpView); 39 | } 40 | 41 | @After 42 | public void detachView() { 43 | mSearchContentPresenter.detachView(); 44 | } 45 | 46 | @Test 47 | public void getCatsSuccessful() { 48 | List cats = TestDataFactory.makeCats(10); 49 | stubDataManagerGetCats(Single.just(cats)); 50 | 51 | mSearchContentPresenter.searchCats(cats); 52 | 53 | verify(mMockSearchContentMvpView).showCats(cats); 54 | verify(mMockSearchContentMvpView, never()).showCatsError(); 55 | } 56 | 57 | @Test 58 | public void getTagsFails() { 59 | List cats = TestDataFactory.makeCats(10); 60 | stubDataManagerGetCats(Single.just(cats)); 61 | stubDataManagerGetCats(Single.>error(new RuntimeException())); 62 | 63 | mSearchContentPresenter.searchCats(cats); 64 | 65 | verify(mMockSearchContentMvpView).showCatsError(); 66 | verify(mMockSearchContentMvpView, never()).showCats(anyListOf(Cat.class)); 67 | } 68 | 69 | private void stubDataManagerGetCats(Single> single) { 70 | when(mMockDataManager.getCats(anyListOf(Cat.class))).thenReturn(single); 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /app/src/test/java/com/hitherejoe/androidtvboilerplate/util/DefaultConfig.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.util; 2 | 3 | public class DefaultConfig { 4 | //The api level that Roboelectric will use to run the unit tests 5 | public static final int EMULATE_SDK = 21; 6 | } -------------------------------------------------------------------------------- /app/src/test/java/com/hitherejoe/androidtvboilerplate/util/RxSchedulersOverrideRule.java: -------------------------------------------------------------------------------- 1 | package com.hitherejoe.androidtvboilerplate.util; 2 | 3 | import org.junit.rules.TestRule; 4 | import org.junit.runner.Description; 5 | import org.junit.runners.model.Statement; 6 | 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.lang.reflect.Method; 9 | 10 | import rx.Scheduler; 11 | import rx.android.plugins.RxAndroidPlugins; 12 | import rx.android.plugins.RxAndroidSchedulersHook; 13 | import rx.plugins.RxJavaPlugins; 14 | import rx.plugins.RxJavaSchedulersHook; 15 | import rx.schedulers.Schedulers; 16 | 17 | /** 18 | * This rule registers SchedulerHooks for RxJava and RxAndroid to ensure that subscriptions 19 | * always subscribeOn and observeOn Schedulers.immediate(). 20 | * Warning, this rule will reset RxAndroidPlugins and RxJavaPlugins before and after each test so 21 | * if the application code uses RxJava plugins this may affect the behaviour of the testing method. 22 | */ 23 | public class RxSchedulersOverrideRule implements TestRule { 24 | 25 | private final RxJavaSchedulersHook mRxJavaSchedulersHook = new RxJavaSchedulersHook() { 26 | @Override 27 | public Scheduler getIOScheduler() { 28 | return Schedulers.immediate(); 29 | } 30 | 31 | @Override 32 | public Scheduler getNewThreadScheduler() { 33 | return Schedulers.immediate(); 34 | } 35 | }; 36 | 37 | private final RxAndroidSchedulersHook mRxAndroidSchedulersHook = new RxAndroidSchedulersHook() { 38 | @Override 39 | public Scheduler getMainThreadScheduler() { 40 | return Schedulers.immediate(); 41 | } 42 | }; 43 | 44 | // Hack to get around RxJavaPlugins.reset() not being public 45 | // See https://github.com/ReactiveX/RxJava/issues/2297 46 | // Hopefully the method will be public in new releases of RxAndroid and we can remove the hack. 47 | private void callResetViaReflectionIn(RxJavaPlugins rxJavaPlugins) 48 | throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { 49 | Method method = rxJavaPlugins.getClass().getDeclaredMethod("reset"); 50 | method.setAccessible(true); 51 | method.invoke(rxJavaPlugins); 52 | } 53 | 54 | @Override 55 | public Statement apply(final Statement base, Description description) { 56 | return new Statement() { 57 | @Override 58 | public void evaluate() throws Throwable { 59 | RxAndroidPlugins.getInstance().reset(); 60 | RxAndroidPlugins.getInstance().registerSchedulersHook(mRxAndroidSchedulersHook); 61 | callResetViaReflectionIn(RxJavaPlugins.getInstance()); 62 | RxJavaPlugins.getInstance().registerSchedulersHook(mRxJavaSchedulersHook); 63 | 64 | base.evaluate(); 65 | 66 | RxAndroidPlugins.getInstance().reset(); 67 | callResetViaReflectionIn(RxJavaPlugins.getInstance()); 68 | } 69 | }; 70 | } 71 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | 2 | buildscript { 3 | repositories { 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:1.5.0' 8 | classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | jcenter() 15 | } 16 | } 17 | 18 | task clean(type: Delete) { 19 | delete rootProject.buildDir 20 | } 21 | -------------------------------------------------------------------------------- /config/quality/checkstyle/checkstyle-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 77 | 79 | 81 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 92 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 117 | 118 | 119 | 120 | 121 | 123 | 124 | 125 | 126 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 137 | 138 | 139 | 140 | 141 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 151 | 152 | 153 | 154 | 155 | 157 | 158 | 159 | 160 | 161 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /config/quality/findbugs/android-exclude-filter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /config/quality/pmd/pmd-ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | Custom ruleset for ribot Android application 8 | 9 | .*/R.java 10 | .*/gen/.* 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /config/quality/quality.gradle: -------------------------------------------------------------------------------- 1 | /** 2 | * Set up Checkstyle, Findbugs and PMD to perform extensive code analysis. 3 | * 4 | * Gradle tasks added: 5 | * - checkstyle 6 | * - findbugs 7 | * - pmd 8 | * 9 | * The three tasks above are added as dependencies of the check task so running check will 10 | * run all of them. 11 | */ 12 | 13 | apply plugin: 'checkstyle' 14 | apply plugin: 'findbugs' 15 | apply plugin: 'pmd' 16 | 17 | dependencies { 18 | checkstyle 'com.puppycrawl.tools:checkstyle:6.5' 19 | } 20 | 21 | def qualityConfigDir = "$project.rootDir/config/quality"; 22 | def reportsDir = "$project.buildDir/reports" 23 | 24 | check.dependsOn 'checkstyle', 'findbugs', 'pmd' 25 | 26 | task checkstyle(type: Checkstyle, group: 'Verification', description: 'Runs code style checks') { 27 | configFile file("$qualityConfigDir/checkstyle/checkstyle-config.xml") 28 | source 'src' 29 | include '**/*.java' 30 | 31 | reports { 32 | xml.enabled = true 33 | xml { 34 | destination "$reportsDir/checkstyle/checkstyle.xml" 35 | } 36 | } 37 | 38 | classpath = files( ) 39 | } 40 | 41 | task findbugs(type: FindBugs, 42 | group: 'Verification', 43 | description: 'Inspect java bytecode for bugs', 44 | dependsOn: ['compileDebugSources','compileReleaseSources']) { 45 | 46 | ignoreFailures = false 47 | effort = "max" 48 | reportLevel = "high" 49 | excludeFilter = new File("$qualityConfigDir/findbugs/android-exclude-filter.xml") 50 | classes = files("$project.rootDir/app/build/intermediates/classes") 51 | 52 | source 'src' 53 | include '**/*.java' 54 | exclude '**/gen/**' 55 | 56 | reports { 57 | xml.enabled = true 58 | html.enabled = false 59 | xml { 60 | destination "$reportsDir/findbugs/findbugs.xml" 61 | } 62 | html { 63 | destination "$reportsDir/findbugs/findbugs.html" 64 | } 65 | } 66 | 67 | classpath = files() 68 | } 69 | 70 | 71 | task pmd(type: Pmd, group: 'Verification', description: 'Inspect sourcecode for bugs') { 72 | ruleSetFiles = files("$qualityConfigDir/pmd/pmd-ruleset.xml") 73 | ignoreFailures = false 74 | ruleSets = [] 75 | 76 | source 'src' 77 | include '**/*.java' 78 | exclude '**/gen/**' 79 | 80 | reports { 81 | xml.enabled = true 82 | html.enabled = true 83 | xml { 84 | destination "$reportsDir/pmd/pmd.xml" 85 | } 86 | html { 87 | destination "$reportsDir/pmd/pmd.html" 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/AndroidTvBoilerplate/20d473204cd9a2c5f86fb1bc4aa56529435d431d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Oct 21 11:34:03 PDT 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /images/browse_fragment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/AndroidTvBoilerplate/20d473204cd9a2c5f86fb1bc4aa56529435d431d/images/browse_fragment.png -------------------------------------------------------------------------------- /images/search_fragment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/AndroidTvBoilerplate/20d473204cd9a2c5f86fb1bc4aa56529435d431d/images/search_fragment.png -------------------------------------------------------------------------------- /images/web_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitherejoe/AndroidTvBoilerplate/20d473204cd9a2c5f86fb1bc4aa56529435d431d/images/web_banner.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------