diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000..a3be1b453
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @raccoongang/educationx-app-android-reviewers
diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
new file mode 100644
index 000000000..000cf6472
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -0,0 +1,56 @@
+name: Bug report for the EducationX Android app
+description: Report any issues that you have found with the EducationX Android app. Please [check open issues](https://github.com/raccoongang/educationx-app-android/issues) first, in case it has already been reported.
+labels: [Bug]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report!
+
+ Please report security issues by email to security@raccoongang.com
+ - type: textarea
+ id: reproduction-steps
+ attributes:
+ label: Steps to reproduce
+ description: Please attach screenshots, videos or logs if you can.
+ placeholder: Tell us what you see!
+ value: |
+ 1. Where are you starting? What can you see?
+ 2. What do you click?
+ 3. More steps…
+ validations:
+ required: true
+ - type: textarea
+ id: result
+ attributes:
+ label: Outcome
+ placeholder: Tell us what went wrong
+ value: |
+ #### What did you expect?
+
+ #### What happened instead?
+ validations:
+ required: true
+ - type: input
+ id: device
+ attributes:
+ label: Your phone model
+ placeholder: e.g. Samsung Galaxy S10
+ validations:
+ required: false
+ - type: input
+ id: os
+ attributes:
+ label: Operating system version
+ placeholder: e.g. Tiramisu (OS 13), under "software version"
+ validations:
+ required: false
+ - type: input
+ id: version
+ attributes:
+ label: Application version
+ description: You can find the version information in the Settings of EducationX IOS.
+ placeholder: |
+ e.g. Version: v1.2.2
+ validations:
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml
new file mode 100644
index 000000000..3ba13e0ce
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yaml
@@ -0,0 +1 @@
+blank_issues_enabled: false
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..bbcbbe7d6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
new file mode 100644
index 000000000..33e9421b4
--- /dev/null
+++ b/.github/workflows/unit_tests.yml
@@ -0,0 +1,49 @@
+name: Test
+
+on:
+ workflow_dispatch:
+ pull_request: { }
+ push:
+ branches: [ main, develop ]
+
+# Enrich gradle.properties for CI/CD
+env:
+ GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
+ CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 --warn
+
+jobs:
+ tests:
+ name: Runs unit tests
+ runs-on: ubuntu-latest
+
+ # Allow all jobs on main and develop. Just one per PR.
+ concurrency:
+ group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }}
+ cancel-in-progress: true
+ steps:
+ - name: ⏬ Checkout with LFS
+ uses: nschloe/action-cached-lfs-checkout@v1.2.1
+ with:
+ # Ensure we are building the branch and not the branch after being merged on develop
+ # https://github.com/actions/checkout/issues/881
+ ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
+ - name: ☕️ Use JDK 17
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin' # See 'Supported distributions' for available options
+ java-version: '17'
+ - name: Configure gradle
+ uses: gradle/gradle-build-action@v2.6.1
+ with:
+ cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
+
+ - name: Run unit tests
+ run: ./gradlew testProdReleaseUnitTest $CI_GRADLE_ARG_PROPERTIES
+
+ - name: Upload reports
+ uses: actions/upload-artifact@v3
+ if: failure()
+ with:
+ name: failures
+ path: app/build/reports/
+ retention-days: 5
diff --git a/LICENSE b/LICENSE
index 0ad25db4b..f49a4e16e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,661 +1,201 @@
- GNU AFFERO GENERAL PUBLIC LICENSE
- Version 3, 19 November 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU Affero General Public License is a free, copyleft license for
-software and other kinds of works, specifically designed to ensure
-cooperation with the community in the case of network server software.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-our General Public Licenses are intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- Developers that use our General Public Licenses protect your rights
-with two steps: (1) assert copyright on the software, and (2) offer
-you this License which gives you legal permission to copy, distribute
-and/or modify the software.
-
- A secondary benefit of defending all users' freedom is that
-improvements made in alternate versions of the program, if they
-receive widespread use, become available for other developers to
-incorporate. Many developers of free software are heartened and
-encouraged by the resulting cooperation. However, in the case of
-software used on network servers, this result may fail to come about.
-The GNU General Public License permits making a modified version and
-letting the public access it on a server without ever releasing its
-source code to the public.
-
- The GNU Affero General Public License is designed specifically to
-ensure that, in such cases, the modified source code becomes available
-to the community. It requires the operator of a network server to
-provide the source code of the modified version running there to the
-users of that server. Therefore, public use of a modified version, on
-a publicly accessible server, gives the public access to the source
-code of the modified version.
-
- An older license, called the Affero General Public License and
-published by Affero, was designed to accomplish similar goals. This is
-a different license, not a version of the Affero GPL, but Affero has
-released a new version of the Affero GPL which permits relicensing under
-this license.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU Affero General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Remote Network Interaction; Use with the GNU General Public License.
-
- Notwithstanding any other provision of this License, if you modify the
-Program, your modified version must prominently offer all users
-interacting with it remotely through a computer network (if your version
-supports such interaction) an opportunity to receive the Corresponding
-Source of your version by providing access to the Corresponding Source
-from a network server at no charge, through some standard or customary
-means of facilitating copying of software. This Corresponding Source
-shall include the Corresponding Source for any work covered by version 3
-of the GNU General Public License that is incorporated pursuant to the
-following paragraph.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the work with which it is combined will remain governed by version
-3 of the GNU General Public License.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU Affero General Public License from time to time. Such new versions
-will be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU Affero General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU Affero General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU Affero General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If your software can interact with users remotely through a computer
-network, you should also make sure that it provides a way for users to
-get its source. For example, if your program is a web application, its
-interface could display a "Source" link that leads users to an archive
-of the code. There are many ways you could offer source, and different
-solutions will be better for different programs; see section 13 for the
-specific requirements.
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU AGPL, see
-.
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/README.md b/README.md
index 392e6307e..5c09e9f90 100644
--- a/README.md
+++ b/README.md
@@ -24,23 +24,7 @@ This project uses custom APIs to improve performance and reduce the number of re
You can find the plugin with the API and installation guide [here](https://github.com/raccoongang/mobile-api-extensions).
-## Roadmap
-Please feel welcome to develop any of the suggested features below and submit a pull request.
-
-- ✅ ~~Migrate to the new APIs~~
-- ✅ ~~New Navigation~~
-- ✅ ~~Analytics and Crashlytics~~
-- Recent searches
-- Migrate to the Olive and JWT token
-- UnAuth User mode
-- Prerequisite course
-- Prerequisite sections
-- Scorm XBlocks
-- Native Programs
-- New discovery (catalog)
-- E-Commerce
-
## License
-The code in this repository is licensed under the AGPL v3 license unless otherwise noted.
+The code in this repository is licensed under the Apache-2.0 license unless otherwise noted.
Please see [LICENSE](https://github.com/raccoongang/educationx-app-android/blob/main/LICENSE) file for details.
diff --git a/app/build.gradle b/app/build.gradle
index 303349ad5..831c836b8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -7,12 +7,12 @@ plugins {
}
android {
- compileSdk 33
+ compileSdk 34
defaultConfig {
applicationId "org.openedx.app"
minSdk 24
- targetSdk 33
+ targetSdk 34
versionCode 1
versionName "1.0"
@@ -23,13 +23,16 @@ android {
namespace 'org.openedx.app'
- flavorDimensions "tier"
+ flavorDimensions += "env"
productFlavors {
prod {
+ dimension 'env'
}
develop {
+ dimension 'env'
}
stage {
+ dimension 'env'
}
}
@@ -40,11 +43,11 @@ android {
}
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_11
+ jvmTarget = JavaVersion.VERSION_17
}
buildFeatures {
viewBinding true
@@ -82,8 +85,8 @@ dependencies {
implementation 'androidx.core:core-splashscreen:1.0.1'
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
testImplementation "junit:junit:$junit_version"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 385f39534..086cf5a34 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,7 +27,6 @@
android:name=".AppActivity"
android:exported="true"
android:fitsSystemWindows="true"
- android:label="@string/app_name"
android:screenOrientation="portrait"
android:theme="@style/Theme.App.Starting"
android:windowSoftInputMode="adjustPan">
diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt
index 426a7f4e8..67981f548 100644
--- a/app/src/main/java/org/openedx/app/AppActivity.kt
+++ b/app/src/main/java/org/openedx/app/AppActivity.kt
@@ -13,7 +13,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.window.layout.WindowMetricsCalculator
import org.openedx.auth.presentation.signin.SignInFragment
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.core.extension.requestApplyInsetsWhenAttached
import org.openedx.core.presentation.global.AppData
import org.openedx.core.presentation.global.AppDataHolder
@@ -25,6 +24,7 @@ import org.openedx.profile.presentation.ProfileRouter
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.openedx.app.databinding.ActivityAppBinding
+import org.openedx.core.data.storage.CorePreferences
class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder, AppDataHolder {
@@ -40,7 +40,7 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder, AppDataH
get() = AppData(BuildConfig.VERSION_NAME)
private lateinit var binding: ActivityAppBinding
- private val preferencesManager by inject()
+ private val preferencesManager by inject()
private val viewModel by viewModel()
private val profileRouter by inject()
@@ -57,7 +57,7 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder, AppDataH
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val splashScreen = installSplashScreen()
+ installSplashScreen()
binding = ActivityAppBinding.inflate(layoutInflater)
lifecycle.addObserver(viewModel)
setContentView(binding.root)
diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt
index 9679abaf2..ea24e9d7b 100644
--- a/app/src/main/java/org/openedx/app/AppRouter.kt
+++ b/app/src/main/java/org/openedx/app/AppRouter.kt
@@ -8,7 +8,7 @@ import org.openedx.auth.presentation.restore.RestorePasswordFragment
import org.openedx.auth.presentation.signin.SignInFragment
import org.openedx.auth.presentation.signup.SignUpFragment
import org.openedx.core.FragmentViewType
-import org.openedx.core.domain.model.Account
+import org.openedx.profile.domain.model.Account
import org.openedx.core.domain.model.CoursewareAccess
import org.openedx.core.presentation.course.CourseViewMode
import org.openedx.course.presentation.CourseRouter
diff --git a/app/src/main/java/org/openedx/app/AppViewModel.kt b/app/src/main/java/org/openedx/app/AppViewModel.kt
index ee5a752c3..002e0bd96 100644
--- a/app/src/main/java/org/openedx/app/AppViewModel.kt
+++ b/app/src/main/java/org/openedx/app/AppViewModel.kt
@@ -6,17 +6,17 @@ import androidx.lifecycle.viewModelScope
import androidx.room.RoomDatabase
import org.openedx.core.BaseViewModel
import org.openedx.core.SingleEventLiveData
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.app.system.notifier.AppNotifier
import org.openedx.app.system.notifier.LogoutEvent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.openedx.core.data.storage.CorePreferences
class AppViewModel(
private val notifier: AppNotifier,
private val room: RoomDatabase,
- private val preferencesManager: PreferencesManager,
+ private val preferencesManager: CorePreferences,
private val dispatcher: CoroutineDispatcher,
private val analytics: AppAnalytics
) : BaseViewModel() {
diff --git a/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt b/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt
index ec2622c78..2e18c51eb 100644
--- a/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt
+++ b/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt
@@ -1,11 +1,11 @@
package org.openedx.app.data.networking
import org.openedx.core.ApiConstants
-import org.openedx.core.data.storage.PreferencesManager
import okhttp3.Interceptor
import okhttp3.Response
+import org.openedx.core.data.storage.CorePreferences
-class HeadersInterceptor(private val preferencesManager: PreferencesManager) : Interceptor {
+class HeadersInterceptor(private val preferencesManager: CorePreferences) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response = chain.run {
proceed(
diff --git a/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt b/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt
index f0ad5caa3..777933303 100644
--- a/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt
+++ b/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt
@@ -5,7 +5,6 @@ import com.google.gson.Gson
import org.openedx.auth.data.api.AuthApi
import org.openedx.auth.data.model.AuthResponse
import org.openedx.core.ApiConstants
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.app.system.notifier.AppNotifier
import org.openedx.app.system.notifier.LogoutEvent
import kotlinx.coroutines.runBlocking
@@ -14,13 +13,14 @@ import okhttp3.logging.HttpLoggingInterceptor
import org.json.JSONException
import org.json.JSONObject
import org.openedx.core.BuildConfig
+import org.openedx.core.data.storage.CorePreferences
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.IOException
import java.util.concurrent.TimeUnit
class OauthRefreshTokenAuthenticator(
- private val preferencesManager: PreferencesManager,
+ private val preferencesManager: CorePreferences,
private val appNotifier: AppNotifier,
) : Authenticator {
@@ -35,7 +35,7 @@ class OauthRefreshTokenAuthenticator(
}
}.build()
authApi = Retrofit.Builder()
- .baseUrl(org.openedx.core.BuildConfig.BASE_URL)
+ .baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(Gson()))
.build()
@@ -113,7 +113,7 @@ class OauthRefreshTokenAuthenticator(
private fun refreshAccessToken(refreshToken: String): AuthResponse? {
val response = authApi.refreshAccessToken(
ApiConstants.TOKEN_TYPE_REFRESH,
- org.openedx.core.BuildConfig.CLIENT_ID,
+ BuildConfig.CLIENT_ID,
refreshToken
).execute()
val authResponse = response.body()
diff --git a/core/src/main/java/org/openedx/core/data/storage/PreferencesManager.kt b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt
similarity index 76%
rename from core/src/main/java/org/openedx/core/data/storage/PreferencesManager.kt
rename to app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt
index 3e6766ce0..dd7f652af 100644
--- a/core/src/main/java/org/openedx/core/data/storage/PreferencesManager.kt
+++ b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt
@@ -1,16 +1,16 @@
-package org.openedx.core.data.storage
+package org.openedx.app.data.storage
import android.content.Context
import com.google.gson.Gson
-import org.openedx.core.domain.model.Account
+import org.openedx.core.data.storage.CorePreferences
+import org.openedx.profile.domain.model.Account
import org.openedx.core.domain.model.User
import org.openedx.core.domain.model.VideoSettings
+import org.openedx.profile.data.storage.ProfilePreferences
+class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences {
-class PreferencesManager(private val context: Context) {
-
- private val sharedPreferences =
- context.getSharedPreferences("org.openedx.app", Context.MODE_PRIVATE)
+ private val sharedPreferences = context.getSharedPreferences("org.openedx.app", Context.MODE_PRIVATE)
private fun saveString(key: String, value: String) {
sharedPreferences.edit().apply {
@@ -20,7 +20,7 @@ class PreferencesManager(private val context: Context) {
private fun getString(key: String): String = sharedPreferences.getString(key, "") ?: ""
- fun clear() {
+ override fun clear() {
sharedPreferences.edit().apply {
remove(ACCESS_TOKEN)
remove(REFRESH_TOKEN)
@@ -28,20 +28,19 @@ class PreferencesManager(private val context: Context) {
}.apply()
}
-
- var accessToken: String
+ override var accessToken: String
set(value) {
saveString(ACCESS_TOKEN, value)
}
get() = getString(ACCESS_TOKEN)
- var refreshToken: String
+ override var refreshToken: String
set(value) {
saveString(REFRESH_TOKEN, value)
}
get() = getString(REFRESH_TOKEN)
- var user: User?
+ override var user: User?
set(value) {
val userJson = Gson().toJson(value)
saveString(USER, userJson)
@@ -51,7 +50,7 @@ class PreferencesManager(private val context: Context) {
return Gson().fromJson(userString, User::class.java)
}
- var profile: Account?
+ override var profile: Account?
set(value) {
val accountJson = Gson().toJson(value)
saveString(ACCOUNT, accountJson)
@@ -61,7 +60,7 @@ class PreferencesManager(private val context: Context) {
return Gson().fromJson(accountString, Account::class.java)
}
- var videoSettings: VideoSettings
+ override var videoSettings: VideoSettings
set(value) {
val videoSettingsJson = Gson().toJson(value)
saveString(VIDEO_SETTINGS, videoSettingsJson)
@@ -72,7 +71,6 @@ class PreferencesManager(private val context: Context) {
?: VideoSettings.default
}
-
companion object {
private const val ACCESS_TOKEN = "access_token"
private const val REFRESH_TOKEN = "refresh_token"
diff --git a/app/src/main/java/org/openedx/app/di/AppModule.kt b/app/src/main/java/org/openedx/app/di/AppModule.kt
index 7118f1c08..4b3da913e 100644
--- a/app/src/main/java/org/openedx/app/di/AppModule.kt
+++ b/app/src/main/java/org/openedx/app/di/AppModule.kt
@@ -6,7 +6,7 @@ import com.google.gson.Gson
import com.google.gson.GsonBuilder
import org.openedx.auth.presentation.AuthAnalytics
import org.openedx.auth.presentation.AuthRouter
-import org.openedx.core.data.storage.PreferencesManager
+import org.openedx.app.data.storage.PreferencesManager
import org.openedx.core.module.DownloadWorkerController
import org.openedx.core.module.TranscriptManager
import org.openedx.core.module.download.FileDownloader
@@ -36,10 +36,14 @@ import kotlinx.coroutines.Dispatchers
import org.koin.android.ext.koin.androidApplication
import org.koin.core.qualifier.named
import org.koin.dsl.module
+import org.openedx.core.data.storage.CorePreferences
+import org.openedx.profile.data.storage.ProfilePreferences
val appModule = module {
single { PreferencesManager(get()) }
+ single { get() }
+ single { get() }
single { ResourceManager(get()) }
diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt
index 2b92633f3..3b48775e3 100644
--- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt
+++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt
@@ -6,7 +6,7 @@ import org.openedx.auth.presentation.restore.RestorePasswordViewModel
import org.openedx.auth.presentation.signin.SignInViewModel
import org.openedx.auth.presentation.signup.SignUpViewModel
import org.openedx.core.Validator
-import org.openedx.core.domain.model.Account
+import org.openedx.profile.domain.model.Account
import org.openedx.core.presentation.dialog.SelectDialogViewModel
import org.openedx.course.data.repository.CourseRepository
import org.openedx.course.domain.interactor.CourseInteractor
@@ -82,7 +82,7 @@ val screenModule = module {
viewModel { (courseId: String) -> CourseSectionViewModel(get(), get(), get(), get(), get(), get(), get(), get(), courseId) }
viewModel { (courseId: String) -> CourseUnitContainerViewModel(get(), get(), get(), courseId) }
viewModel { (courseId: String) -> CourseVideoViewModel(courseId, get(), get(), get(), get(), get(), get(), get()) }
- viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get(), get()) }
+ viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get()) }
viewModel { (courseId: String) -> VideoUnitViewModel(courseId, get(), get(), get(), get()) }
viewModel { (courseId:String, handoutsType: String) -> HandoutsViewModel(courseId, handoutsType, get()) }
viewModel { CourseSearchViewModel(get(), get(), get()) }
@@ -92,8 +92,8 @@ val screenModule = module {
factory { DiscussionInteractor(get()) }
viewModel { (courseId: String) -> DiscussionTopicsViewModel(get(), get(), get(), courseId) }
viewModel { (courseId: String, topicId: String, threadType: String) -> DiscussionThreadsViewModel(get(), get(), get(), courseId, topicId, threadType) }
- viewModel { (thread: org.openedx.discussion.domain.model.Thread) -> DiscussionCommentsViewModel(get(), get(), get(), get(), thread) }
- viewModel { (comment: DiscussionComment) -> DiscussionResponsesViewModel(get(), get(), get(), get(), comment) }
+ viewModel { (thread: org.openedx.discussion.domain.model.Thread) -> DiscussionCommentsViewModel(get(), get(), get(), thread) }
+ viewModel { (comment: DiscussionComment) -> DiscussionResponsesViewModel(get(), get(), get(), comment) }
viewModel { (courseId: String) -> DiscussionAddThreadViewModel(get(), get(), get(), courseId) }
viewModel { (courseId: String) -> DiscussionSearchThreadViewModel(get(), get(), get(), courseId) }
}
\ No newline at end of file
diff --git a/app/src/test/java/org/openedx/AppViewModelTest.kt b/app/src/test/java/org/openedx/AppViewModelTest.kt
index fbb8135d0..6b3961864 100644
--- a/app/src/test/java/org/openedx/AppViewModelTest.kt
+++ b/app/src/test/java/org/openedx/AppViewModelTest.kt
@@ -4,7 +4,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
-import org.openedx.core.data.storage.PreferencesManager
+import org.openedx.app.data.storage.PreferencesManager
import org.openedx.core.domain.model.User
import org.openedx.app.room.AppDatabase
import org.openedx.app.system.notifier.AppNotifier
@@ -17,7 +17,7 @@ import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
@@ -34,12 +34,11 @@ class AppViewModelTest {
@get:Rule
val testInstantTaskExecutorRule: TestRule = InstantTaskExecutorRule()
- private val dispatcher = UnconfinedTestDispatcher()
+ private val dispatcher = StandardTestDispatcher()//UnconfinedTestDispatcher()
private val notifier = mockk()
private val room = mockk()
private val preferencesManager = mockk()
- private val dispatcher2 = Dispatchers.IO
private val analytics = mockk()
private val user = User(0, "", "", "")
@@ -59,7 +58,7 @@ class AppViewModelTest {
every { analytics.setUserIdForSession(any()) } returns Unit
every { preferencesManager.user } returns user
every { notifier.notifier } returns flow { }
- val viewModel = AppViewModel(notifier, room, preferencesManager, dispatcher2, analytics)
+ val viewModel = AppViewModel(notifier, room, preferencesManager, dispatcher, analytics)
val mockLifeCycleOwner: LifecycleOwner = mockk()
val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner)
@@ -80,7 +79,7 @@ class AppViewModelTest {
every { preferencesManager.user } returns user
every { room.clearAllTables() } returns Unit
every { analytics.logoutEvent(true) } returns Unit
- val viewModel = AppViewModel(notifier, room, preferencesManager, dispatcher2, analytics)
+ val viewModel = AppViewModel(notifier, room, preferencesManager, dispatcher, analytics)
val mockLifeCycleOwner: LifecycleOwner = mockk()
val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner)
@@ -88,8 +87,8 @@ class AppViewModelTest {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
advanceUntilIdle()
- verify(exactly = 1) { analytics.logoutEvent(true) }
- assert(viewModel.logoutUser.value != null)
+ verify(exactly = 1) { analytics.logoutEvent(true) }
+ assert(viewModel.logoutUser.value != null)
}
@Test
@@ -103,7 +102,7 @@ class AppViewModelTest {
every { preferencesManager.user } returns user
every { room.clearAllTables() } returns Unit
every { analytics.logoutEvent(true) } returns Unit
- val viewModel = AppViewModel(notifier, room, preferencesManager, dispatcher2, analytics)
+ val viewModel = AppViewModel(notifier, room, preferencesManager, dispatcher, analytics)
val mockLifeCycleOwner: LifecycleOwner = mockk()
val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner)
@@ -111,12 +110,12 @@ class AppViewModelTest {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
advanceUntilIdle()
- verify(exactly = 1) { analytics.logoutEvent(true) }
- verify(exactly = 1) { preferencesManager.clear() }
- verify(exactly = 1) { analytics.setUserIdForSession(any()) }
- verify(exactly = 1) { preferencesManager.user }
- verify(exactly = 1) { room.clearAllTables() }
- verify(exactly = 1) { analytics.logoutEvent(true) }
+ verify(exactly = 1) { analytics.logoutEvent(true) }
+ verify(exactly = 1) { preferencesManager.clear() }
+ verify(exactly = 1) { analytics.setUserIdForSession(any()) }
+ verify(exactly = 1) { preferencesManager.user }
+ verify(exactly = 1) { room.clearAllTables() }
+ verify(exactly = 1) { analytics.logoutEvent(true) }
}
}
\ No newline at end of file
diff --git a/auth/build.gradle b/auth/build.gradle
index 0db08a23f..788bb0153 100644
--- a/auth/build.gradle
+++ b/auth/build.gradle
@@ -5,11 +5,11 @@ plugins {
}
android {
- compileSdk 33
+ compileSdk 34
defaultConfig {
minSdk 24
- targetSdk 33
+ targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@@ -17,13 +17,16 @@ android {
namespace 'org.openedx.auth'
- flavorDimensions "tier"
+ flavorDimensions += "env"
productFlavors {
prod {
+ dimension 'env'
}
develop {
+ dimension 'env'
}
stage {
+ dimension 'env'
}
}
@@ -34,11 +37,11 @@ android {
}
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_11
+ jvmTarget = JavaVersion.VERSION_17
}
buildFeatures {
viewBinding true
@@ -52,8 +55,8 @@ android {
dependencies {
implementation project(path: ':core')
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
testImplementation "junit:junit:$junit_version"
diff --git a/auth/src/main/AndroidManifest.xml b/auth/src/main/AndroidManifest.xml
deleted file mode 100644
index a5918e68a..000000000
--- a/auth/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/auth/src/main/java/org/openedx/auth/data/repository/AuthRepository.kt b/auth/src/main/java/org/openedx/auth/data/repository/AuthRepository.kt
index 8da6f661b..d0eab71e4 100644
--- a/auth/src/main/java/org/openedx/auth/data/repository/AuthRepository.kt
+++ b/auth/src/main/java/org/openedx/auth/data/repository/AuthRepository.kt
@@ -3,14 +3,13 @@ package org.openedx.auth.data.repository
import org.openedx.auth.data.api.AuthApi
import org.openedx.auth.data.model.ValidationFields
import org.openedx.core.ApiConstants
-import org.openedx.core.BuildConfig
-import org.openedx.core.data.storage.PreferencesManager
+import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.domain.model.RegistrationField
import org.openedx.core.system.EdxError
class AuthRepository(
private val api: AuthApi,
- private val preferencesManager: PreferencesManager,
+ private val preferencesManager: CorePreferences,
) {
suspend fun login(
diff --git a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt
index 67ad8c294..014b233d8 100644
--- a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt
+++ b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt
@@ -7,6 +7,8 @@ import android.view.ViewGroup
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
@@ -77,9 +79,11 @@ private fun RestorePasswordScreen(
onRestoreButtonClick: (String) -> Unit
) {
val scaffoldState = rememberScaffoldState()
+ val scrollState = rememberScrollState()
var email by rememberSaveable {
mutableStateOf("")
}
+
Scaffold(
scaffoldState = scaffoldState,
modifier = Modifier
@@ -170,13 +174,15 @@ private fun RestorePasswordScreen(
}
Surface(
- modifier = Modifier.fillMaxWidth(),
+ modifier = Modifier
+ .fillMaxWidth(),
color = MaterialTheme.appColors.background,
shape = MaterialTheme.appShapes.screenBackgroundShape
) {
Column(
modifier = Modifier
.fillMaxHeight()
+ .verticalScroll(scrollState)
.background(MaterialTheme.appColors.background),
horizontalAlignment = Alignment.CenterHorizontally
) {
diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt
index 1f82823ac..9ea354c7f 100644
--- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt
+++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt
@@ -8,8 +8,10 @@ import android.view.ViewGroup
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
@@ -99,6 +101,7 @@ private fun LoginScreen(
onForgotPasswordClick: () -> Unit
) {
val scaffoldState = rememberScaffoldState()
+ val scrollState = rememberScrollState()
Scaffold(
scaffoldState = scaffoldState,
@@ -167,12 +170,14 @@ private fun LoginScreen(
Surface(
color = MaterialTheme.appColors.background,
shape = MaterialTheme.appShapes.screenBackgroundShape,
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier
+ .fillMaxSize()
) {
Box(contentAlignment = Alignment.TopCenter) {
Column(
modifier = Modifier
.background(MaterialTheme.appColors.background)
+ .verticalScroll(scrollState)
.then(contentPaddings),
) {
Text(
diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt
index 3d155c0a6..d9ca3bed5 100644
--- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt
+++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt
@@ -10,17 +10,17 @@ import org.openedx.core.BaseViewModel
import org.openedx.core.SingleEventLiveData
import org.openedx.core.UIMessage
import org.openedx.core.Validator
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.core.extension.isInternetError
import org.openedx.core.system.EdxError
import org.openedx.core.system.ResourceManager
import kotlinx.coroutines.launch
+import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.R as CoreRes
class SignInViewModel(
private val interactor: AuthInteractor,
private val resourceManager: ResourceManager,
- private val preferencesManager: PreferencesManager,
+ private val preferencesManager: CorePreferences,
private val validator: Validator,
private val analytics: AuthAnalytics
) : BaseViewModel() {
diff --git a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt
index 89c263f8a..4ec76f151 100644
--- a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt
+++ b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt
@@ -10,7 +10,7 @@ import org.openedx.core.BaseViewModel
import org.openedx.core.R
import org.openedx.core.SingleEventLiveData
import org.openedx.core.UIMessage
-import org.openedx.core.data.storage.PreferencesManager
+import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.domain.model.RegistrationField
import org.openedx.core.extension.isInternetError
import org.openedx.core.system.ResourceManager
@@ -20,7 +20,7 @@ class SignUpViewModel(
private val interactor: AuthInteractor,
private val resourceManager: ResourceManager,
private val analytics: AuthAnalytics,
- private val preferencesManager: PreferencesManager
+ private val preferencesManager: CorePreferences
) : BaseViewModel() {
private val _uiState = MutableLiveData(SignUpUIState.Loading)
diff --git a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt
index 9b2b61a66..97a4689e9 100644
--- a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt
+++ b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt
@@ -6,7 +6,6 @@ import org.openedx.auth.domain.interactor.AuthInteractor
import org.openedx.auth.presentation.AuthAnalytics
import org.openedx.core.UIMessage
import org.openedx.core.Validator
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.core.domain.model.User
import org.openedx.core.system.EdxError
import org.openedx.core.system.ResourceManager
@@ -28,6 +27,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
+import org.openedx.core.data.storage.CorePreferences
import java.net.UnknownHostException
import org.openedx.core.R as CoreRes
@@ -41,7 +41,7 @@ class SignInViewModelTest {
private val validator = mockk()
private val resourceManager = mockk()
- private val preferencesManager = mockk()
+ private val preferencesManager = mockk()
private val interactor = mockk()
private val analytics = mockk()
diff --git a/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt
index fbd2bebed..fbd7f4c4a 100644
--- a/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt
+++ b/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt
@@ -7,7 +7,6 @@ import org.openedx.auth.presentation.AuthAnalytics
import org.openedx.core.ApiConstants
import org.openedx.core.R
import org.openedx.core.UIMessage
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.core.domain.model.RegistrationField
import org.openedx.core.domain.model.RegistrationFieldType
import org.openedx.core.domain.model.User
@@ -30,6 +29,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
+import org.openedx.core.data.storage.CorePreferences
import java.net.UnknownHostException
@@ -41,7 +41,7 @@ class SignUpViewModelTest {
private val dispatcher = StandardTestDispatcher()
private val resourceManager = mockk()
- private val preferencesManager = mockk()
+ private val preferencesManager = mockk()
private val interactor = mockk()
private val analytics = mockk()
diff --git a/build.gradle b/build.gradle
index 047a0e1bd..3eef5969f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,15 +1,15 @@
buildscript {
ext {
- kotlin_version = '1.8.10'
+ kotlin_version = '1.8.21'
coroutines_version = '1.7.1'
- compose_version = '1.4.3'
- compose_compiler_version = '1.4.4'
+ compose_version = '1.5.0'
+ compose_compiler_version = '1.4.7'
}
}
plugins {
- id 'com.android.application' version '7.3.1' apply false
- id 'com.android.library' version '7.3.1' apply false
+ id 'com.android.application' version '8.1.0' apply false
+ id 'com.android.library' version '8.1.0' apply false
id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false
id 'com.google.gms.google-services' version '4.3.15' apply false
id "com.google.firebase.crashlytics" version "2.9.6" apply false
@@ -24,10 +24,10 @@ ext {
appcompat_version = "1.6.1"
material_version = "1.9.0"
lifecycle_version = "2.6.1"
- fragment_version = "1.6.0"
+ fragment_version = "1.6.1"
constraintlayout_version = "2.1.4"
viewpager2_version = "1.0.0"
- exoplayer_version = "2.18.7"
+ media3 = "1.1.1"
youtubeplayer_version = "11.1.0"
firebase_version = "32.1.0"
@@ -41,7 +41,7 @@ ext {
jsoup_version = '1.13.1'
- room_version = '2.5.1'
+ room_version = '2.5.2'
work_version = '2.8.1'
@@ -49,6 +49,6 @@ ext {
//testing
mockk_version = '1.13.3'
- android_arch_version = '2.1.0'
+ android_arch_version = '2.2.0'
junit_version = '4.13.2'
}
\ No newline at end of file
diff --git a/core/build.gradle b/core/build.gradle
index eb022aae2..6bce96edf 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -21,11 +21,11 @@ plugins {
def config = new Yaml().load(new File("config.yaml").newInputStream())
android {
- compileSdk 33
+ compileSdk 34
defaultConfig {
minSdk 24
- targetSdk 33
+ targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@@ -33,9 +33,11 @@ android {
namespace 'org.openedx.core'
- flavorDimensions "tier"
+ flavorDimensions += "env"
productFlavors {
prod {
+ dimension 'env'
+
def envMap = config.environments.find { it.key == "PROD" }
def clientId = envMap.value.OAUTH_CLIENT_ID
def envUrls = envMap.value.URLS
@@ -55,6 +57,8 @@ android {
resValue "string", "feedback_email_address", envUrls.FEEDBACK_EMAIL_ADDRESS
}
develop {
+ dimension 'env'
+
def envMap = config.environments.find { it.key == "DEV" }
def clientId = envMap.value.OAUTH_CLIENT_ID
def envUrls = envMap.value.URLS
@@ -74,6 +78,8 @@ android {
resValue "string", "feedback_email_address", envUrls.FEEDBACK_EMAIL_ADDRESS
}
stage {
+ dimension 'env'
+
def envMap = config.environments.find { it.key == "STAGE" }
def clientId = envMap.value.OAUTH_CLIENT_ID
def envUrls = envMap.value.URLS
@@ -101,11 +107,11 @@ android {
}
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_11
+ jvmTarget = JavaVersion.VERSION_17
}
buildFeatures {
@@ -176,8 +182,8 @@ dependencies {
api "com.google.firebase:firebase-analytics-ktx"
testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
class FirebaseConfig {
diff --git a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt
new file mode 100644
index 000000000..a3eb44d06
--- /dev/null
+++ b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt
@@ -0,0 +1,13 @@
+package org.openedx.core.data.storage
+
+import org.openedx.core.domain.model.User
+import org.openedx.core.domain.model.VideoSettings
+
+interface CorePreferences {
+ var accessToken: String
+ var refreshToken: String
+ var user: User?
+ var videoSettings: VideoSettings
+
+ fun clear()
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt b/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt
index a2471161a..f5c7c8f8a 100644
--- a/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt
+++ b/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt
@@ -4,7 +4,6 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewModelScope
import org.openedx.core.BaseViewModel
import org.openedx.core.BlockType
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.core.domain.model.Block
import org.openedx.core.module.DownloadWorkerController
import org.openedx.core.module.db.DownloadDao
@@ -16,11 +15,12 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
+import org.openedx.core.data.storage.CorePreferences
import java.io.File
abstract class BaseDownloadViewModel(
private val downloadDao: DownloadDao,
- private val preferencesManager: PreferencesManager,
+ private val preferencesManager: CorePreferences,
private val workerController: DownloadWorkerController
) : BaseViewModel() {
diff --git a/course/build.gradle b/course/build.gradle
index 2f5c4e660..f42807f81 100644
--- a/course/build.gradle
+++ b/course/build.gradle
@@ -5,11 +5,11 @@ plugins {
}
android {
- compileSdk 33
+ compileSdk 34
defaultConfig {
minSdk 24
- targetSdk 33
+ targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@@ -24,11 +24,11 @@ android {
}
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_11
+ jvmTarget = JavaVersion.VERSION_17
}
buildFeatures {
@@ -39,13 +39,16 @@ android {
kotlinCompilerExtensionVersion = "$compose_compiler_version"
}
- flavorDimensions "tier"
+ flavorDimensions += "env"
productFlavors {
prod {
+ dimension 'env'
}
develop {
+ dimension 'env'
}
stage {
+ dimension 'env'
}
}
@@ -55,15 +58,14 @@ android {
}
dependencies {
-
implementation project(path: ':core')
implementation project(path: ':discussion')
implementation "com.pierfrancescosoffritti.androidyoutubeplayer:core:$youtubeplayer_version"
- implementation "com.google.android.exoplayer:exoplayer:$exoplayer_version"
-
+ implementation "androidx.media3:media3-exoplayer:$media3"
+ implementation "androidx.media3:media3-ui:$media3"
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
testImplementation "junit:junit:$junit_version"
diff --git a/course/src/main/AndroidManifest.xml b/course/src/main/AndroidManifest.xml
deleted file mode 100644
index a5918e68a..000000000
--- a/course/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt b/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt
index afd82627c..f31d31bdf 100644
--- a/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt
+++ b/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt
@@ -4,7 +4,7 @@ import org.openedx.core.data.api.CourseApi
import org.openedx.core.data.model.BlocksCompletionBody
import org.openedx.core.data.model.EnrollBody
import org.openedx.core.data.model.room.CourseEntity
-import org.openedx.core.data.storage.PreferencesManager
+import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.domain.model.*
import org.openedx.core.exception.NoCachedDataException
import org.openedx.core.module.db.DownloadDao
@@ -16,7 +16,7 @@ class CourseRepository(
private val api: CourseApi,
private val courseDao: CourseDao,
private val downloadDao: DownloadDao,
- private val preferencesManager: PreferencesManager,
+ private val preferencesManager: CorePreferences,
) {
private var courseStructure: CourseStructure? = null
diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt
index 61691d88d..896988dbc 100644
--- a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt
+++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt
@@ -34,7 +34,7 @@ class HandoutsViewModel(
_htmlContent.value = announcementsToHtml(announcements)
}
} catch (e: Exception) {
- e.printStackTrace()
+ //ignore e.printStackTrace()
}
}
}
diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt
index 85f110d1f..dd31e52a1 100644
--- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt
+++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt
@@ -8,7 +8,7 @@ import org.openedx.core.BlockType
import org.openedx.core.R
import org.openedx.core.SingleEventLiveData
import org.openedx.core.UIMessage
-import org.openedx.core.data.storage.PreferencesManager
+import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.domain.model.Block
import org.openedx.core.domain.model.CourseComponentStatus
import org.openedx.core.extension.isInternetError
@@ -30,7 +30,7 @@ class CourseOutlineViewModel(
private val resourceManager: ResourceManager,
private val notifier: CourseNotifier,
private val networkConnection: NetworkConnection,
- private val preferencesManager: PreferencesManager,
+ private val preferencesManager: CorePreferences,
private val analytics: CourseAnalytics,
downloadDao: DownloadDao,
workerController: DownloadWorkerController
diff --git a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt
index 8682caba3..ed68c0d85 100644
--- a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt
+++ b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt
@@ -40,6 +40,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
@@ -347,10 +348,13 @@ private fun CourseSubsectionItem(
if (downloadedState == DownloadedState.DOWNLOADING || downloadedState == DownloadedState.WAITING) {
CircularProgressIndicator(
modifier = Modifier.size(34.dp),
+ backgroundColor = Color.LightGray,
+ strokeWidth = 2.dp,
color = MaterialTheme.appColors.primary
)
}
- IconButton(modifier = iconModifier,
+ IconButton(
+ modifier = iconModifier.padding(top = 2.dp),
onClick = { onDownloadClick(block) }) {
Icon(
imageVector = Icons.Filled.Close,
diff --git a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt
index bbcc5bad1..9faa7a383 100644
--- a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt
+++ b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt
@@ -8,7 +8,7 @@ import org.openedx.core.BlockType
import org.openedx.core.R
import org.openedx.core.SingleEventLiveData
import org.openedx.core.UIMessage
-import org.openedx.core.data.storage.PreferencesManager
+import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.domain.model.Block
import org.openedx.core.extension.isInternetError
import org.openedx.core.module.DownloadWorkerController
@@ -27,7 +27,7 @@ class CourseSectionViewModel(
private val interactor: CourseInteractor,
private val resourceManager: ResourceManager,
private val networkConnection: NetworkConnection,
- private val preferencesManager: PreferencesManager,
+ private val preferencesManager: CorePreferences,
private val notifier: CourseNotifier,
private val analytics: CourseAnalytics,
workerController: DownloadWorkerController,
diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt
index 09b782084..ea5d4fdee 100644
--- a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt
+++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt
@@ -62,7 +62,6 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import org.openedx.core.BlockType
-import org.openedx.core.BuildConfig
import org.openedx.core.domain.model.Block
import org.openedx.core.domain.model.BlockCounts
import org.openedx.core.domain.model.Certificate
@@ -176,10 +175,15 @@ fun CourseSectionCard(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
+ val icon =
+ if (block.completion == 1.0) painterResource(R.drawable.course_ic_task_alt) else painterResource(R.drawable.ic_course_chapter_icon)
+ val iconColor =
+ if (block.completion == 1.0) MaterialTheme.appColors.primary else MaterialTheme.appColors.onSurface
+
Icon(
- painter = painterResource(id = R.drawable.ic_course_chapter_icon),
+ painter = icon,
contentDescription = null,
- tint = MaterialTheme.appColors.textPrimary
+ tint = iconColor
)
Spacer(modifier = Modifier.width(16.dp))
Text(
@@ -215,10 +219,13 @@ fun CourseSectionCard(
if (downloadedState == DownloadedState.DOWNLOADING || downloadedState == DownloadedState.WAITING) {
CircularProgressIndicator(
modifier = Modifier.size(34.dp),
+ backgroundColor = Color.LightGray,
+ strokeWidth = 2.dp,
color = MaterialTheme.appColors.primary
)
}
- IconButton(modifier = iconModifier,
+ IconButton(
+ modifier = iconModifier.padding(top = 2.dp),
onClick = { onDownloadClick(block) }) {
Icon(
imageVector = Icons.Filled.Close,
diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt
index a06c012dd..553c1a1c7 100644
--- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt
+++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt
@@ -69,7 +69,7 @@ class CourseUnitContainerViewModel(
courseName = courseStructure.name
this.blocks.clearAndAddAll(blocks)
} catch (e: Exception) {
- e.printStackTrace()
+ //ignore e.printStackTrace()
}
}
diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt
index 843895fb6..8bbd649d4 100644
--- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt
+++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt
@@ -8,17 +8,17 @@ import android.widget.FrameLayout
import androidx.core.os.bundleOf
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
-import com.google.android.exoplayer2.C
-import com.google.android.exoplayer2.ExoPlayer
-import com.google.android.exoplayer2.MediaItem
-import com.google.android.exoplayer2.Player
+import androidx.media3.common.C
+import androidx.media3.common.MediaItem
+import androidx.media3.common.Player
+import androidx.media3.exoplayer.ExoPlayer
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.parameter.parametersOf
import org.openedx.core.extension.requestApplyInsetsWhenAttached
import org.openedx.core.presentation.global.WindowSizeHolder
import org.openedx.core.presentation.global.viewBinding
import org.openedx.course.R
import org.openedx.course.databinding.FragmentVideoFullScreenBinding
-import org.koin.androidx.viewmodel.ext.android.viewModel
-import org.koin.core.parameter.parametersOf
class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) {
@@ -67,6 +67,7 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) {
initPlayer()
}
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
private fun initPlayer() {
with(binding) {
if (exoPlayer == null) {
@@ -77,7 +78,7 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) {
playerView.setShowNextButton(false)
playerView.setShowPreviousButton(false)
val mediaItem = MediaItem.fromUri(viewModel.videoUrl)
- exoPlayer?.setMediaItem(mediaItem, viewModel.currentVideoTime.toLong())
+ exoPlayer?.setMediaItem(mediaItem, viewModel.currentVideoTime)
exoPlayer?.prepare()
exoPlayer?.playWhenReady = false
diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt
index c675209a1..49e27c79f 100644
--- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt
+++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt
@@ -1,6 +1,5 @@
package org.openedx.course.presentation.unit.video
-import android.graphics.Point
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -18,9 +17,13 @@ import androidx.compose.ui.Modifier
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
-import com.google.android.exoplayer2.ExoPlayer
-import com.google.android.exoplayer2.MediaItem
-import com.google.android.exoplayer2.Player
+import androidx.media3.common.MediaItem
+import androidx.media3.common.Player
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.window.layout.WindowMetricsCalculator
+import org.koin.android.ext.android.inject
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.parameter.parametersOf
import org.openedx.core.extension.computeWindowSizeClasses
import org.openedx.core.extension.dpToPixel
import org.openedx.core.extension.objectToString
@@ -38,9 +41,6 @@ import org.openedx.course.presentation.ui.ConnectionErrorView
import org.openedx.course.presentation.ui.VideoRotateView
import org.openedx.course.presentation.ui.VideoSubtitles
import org.openedx.course.presentation.ui.VideoTitle
-import org.koin.android.ext.android.inject
-import org.koin.androidx.viewmodel.ext.android.viewModel
-import org.koin.core.parameter.parametersOf
import kotlin.math.roundToInt
class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) {
@@ -172,12 +172,11 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) {
}
}
- binding.connectionError.isVisible =
- !viewModel.hasInternetConnection && !viewModel.isDownloaded
- val display = requireActivity().windowManager.defaultDisplay
- val size = Point()
- display.getSize(size)
- val width = size.x - requireContext().dpToPixel(32)
+ binding.connectionError.isVisible = !viewModel.hasInternetConnection && !viewModel.isDownloaded
+
+ val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(requireActivity())
+ val currentBounds = windowMetrics.bounds
+ val width = currentBounds.width() - requireContext().dpToPixel(32)
val minHeight = requireContext().dpToPixel(194).roundToInt()
val height = (width / 16f * 9f).roundToInt()
val layoutParams = binding.playerView.layoutParams as FrameLayout.LayoutParams
@@ -203,7 +202,8 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) {
}
}
- private fun initPlayer() {
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
+ private fun initPlayer() {
with(binding) {
if (exoPlayer == null) {
exoPlayer = ExoPlayer.Builder(requireContext())
diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt
index 6a0b51434..1f9f47f1f 100644
--- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt
+++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt
@@ -119,7 +119,6 @@ class VideoUnitViewModel(
)
} catch (e: Exception) {
isBlockAlreadyCompleted = false
- e.printStackTrace()
}
}
}
diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt
index 01ab0ca57..720013a70 100644
--- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt
+++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt
@@ -1,9 +1,8 @@
package org.openedx.course.presentation.unit.video
import androidx.lifecycle.viewModelScope
-import com.google.android.exoplayer2.C
+import androidx.media3.common.C
import org.openedx.core.BaseViewModel
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.course.data.repository.CourseRepository
import org.openedx.core.system.notifier.CourseNotifier
import org.openedx.core.system.notifier.CourseVideoPositionChanged
@@ -12,7 +11,6 @@ import kotlinx.coroutines.launch
class VideoViewModel(
private val courseId: String,
private val courseRepository: CourseRepository,
- private val preferencesManager: PreferencesManager,
private val notifier: CourseNotifier
) : BaseViewModel() {
@@ -41,7 +39,6 @@ class VideoViewModel(
)
} catch (e: Exception) {
isBlockAlreadyCompleted = false
- e.printStackTrace()
}
}
}
diff --git a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt
index c5086702a..6522a9b32 100644
--- a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt
+++ b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt
@@ -7,7 +7,7 @@ import androidx.lifecycle.viewModelScope
import org.openedx.core.BlockType
import org.openedx.core.SingleEventLiveData
import org.openedx.core.UIMessage
-import org.openedx.core.data.storage.PreferencesManager
+import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.domain.model.Block
import org.openedx.core.module.DownloadWorkerController
import org.openedx.core.module.db.DownloadDao
@@ -25,7 +25,7 @@ class CourseVideoViewModel(
private val interactor: CourseInteractor,
private val resourceManager: ResourceManager,
private val networkConnection: NetworkConnection,
- private val preferencesManager: PreferencesManager,
+ private val preferencesManager: CorePreferences,
private val notifier: CourseNotifier,
downloadDao: DownloadDao,
workerController: DownloadWorkerController
diff --git a/course/src/main/res/layout-w600dp-h480dp/fragment_video_unit.xml b/course/src/main/res/layout-w600dp-h480dp/fragment_video_unit.xml
index b2c2a58b2..eee099310 100644
--- a/course/src/main/res/layout-w600dp-h480dp/fragment_video_unit.xml
+++ b/course/src/main/res/layout-w600dp-h480dp/fragment_video_unit.xml
@@ -34,7 +34,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cv_video_title">
-
-
- ()
private val interactor = mockk()
- private val preferencesManager = mockk()
+ private val preferencesManager = mockk()
private val networkConnection = mockk()
private val notifier = spyk()
private val downloadDao = mockk()
diff --git a/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt
index fdbd34d42..3dba571a6 100644
--- a/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt
+++ b/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt
@@ -7,7 +7,6 @@ import androidx.lifecycle.LifecycleRegistry
import org.openedx.core.BlockType
import org.openedx.core.R
import org.openedx.core.UIMessage
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.core.domain.model.Block
import org.openedx.core.domain.model.BlockCounts
import org.openedx.core.domain.model.CourseStructure
@@ -35,6 +34,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
+import org.openedx.core.data.storage.CorePreferences
import java.net.UnknownHostException
import java.util.*
@@ -51,7 +51,7 @@ class CourseSectionViewModelTest {
private val downloadDao = mockk()
private val workerController = mockk()
private val networkConnection = mockk()
- private val preferencesManager = mockk()
+ private val preferencesManager = mockk()
private val notifier = mockk()
private val analytics = mockk()
diff --git a/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt
index ff0b70871..e517d192a 100644
--- a/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt
+++ b/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt
@@ -1,7 +1,6 @@
package org.openedx.course.presentation.unit.video
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.core.system.notifier.CourseNotifier
import org.openedx.core.system.notifier.CourseVideoPositionChanged
import org.openedx.course.data.repository.CourseRepository
@@ -27,8 +26,6 @@ class VideoViewModelTest {
private val courseRepository = mockk()
private val notifier = mockk()
- private val preferencesManager = mockk()
-
@Before
fun setUp() {
@@ -42,7 +39,7 @@ class VideoViewModelTest {
@Test
fun `sendTime test`() = runTest {
- val viewModel = VideoViewModel("", courseRepository, preferencesManager, notifier)
+ val viewModel = VideoViewModel("", courseRepository, notifier)
coEvery { notifier.send(CourseVideoPositionChanged("", 0)) } returns Unit
viewModel.sendTime()
advanceUntilIdle()
@@ -52,7 +49,7 @@ class VideoViewModelTest {
@Test
fun `markBlockCompleted exception`() = runTest {
- val viewModel = VideoViewModel("", courseRepository, preferencesManager, notifier)
+ val viewModel = VideoViewModel("", courseRepository, notifier)
coEvery {
courseRepository.markBlocksCompletion(
any(),
@@ -72,7 +69,7 @@ class VideoViewModelTest {
@Test
fun `markBlockCompleted success`() = runTest {
- val viewModel = VideoViewModel("", courseRepository, preferencesManager, notifier)
+ val viewModel = VideoViewModel("", courseRepository, notifier)
coEvery {
courseRepository.markBlocksCompletion(
any(),
diff --git a/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt
index 18851bcd2..c0f8b652c 100644
--- a/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt
+++ b/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt
@@ -5,7 +5,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import org.openedx.core.BlockType
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.core.domain.model.Block
import org.openedx.core.domain.model.BlockCounts
import org.openedx.core.domain.model.CourseStructure
@@ -30,6 +29,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
+import org.openedx.core.data.storage.CorePreferences
import java.util.*
@OptIn(ExperimentalCoroutinesApi::class)
@@ -42,7 +42,7 @@ class CourseVideoViewModelTest {
private val resourceManager = mockk()
private val interactor = mockk()
private val notifier = spyk()
- private val preferencesManager = mockk()
+ private val preferencesManager = mockk()
private val networkConnection = mockk()
private val downloadDao = mockk()
private val workerController = mockk()
diff --git a/dashboard/build.gradle b/dashboard/build.gradle
index f78307dd4..c0c3192d0 100644
--- a/dashboard/build.gradle
+++ b/dashboard/build.gradle
@@ -4,11 +4,11 @@ plugins {
}
android {
- compileSdk 33
+ compileSdk 34
defaultConfig {
minSdk 24
- targetSdk 33
+ targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@@ -24,11 +24,11 @@ android {
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_11
+ jvmTarget = JavaVersion.VERSION_17
}
buildFeatures {
@@ -39,13 +39,16 @@ android {
kotlinCompilerExtensionVersion = "$compose_compiler_version"
}
- flavorDimensions "tier"
+ flavorDimensions += "env"
productFlavors {
prod {
+ dimension 'env'
}
develop {
+ dimension 'env'
}
stage {
+ dimension 'env'
}
}
}
@@ -53,8 +56,8 @@ android {
dependencies {
implementation project(path: ':core')
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
testImplementation "junit:junit:$junit_version"
diff --git a/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt b/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt
index c7361b446..cf6c3a845 100644
--- a/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt
+++ b/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt
@@ -9,6 +9,7 @@ import org.openedx.core.ui.WindowSize
import org.openedx.core.ui.WindowType
import org.junit.Rule
import org.junit.Test
+import java.util.Date
class MyCoursesScreenTest {
@@ -17,7 +18,7 @@ class MyCoursesScreenTest {
//region mockEnrolledCourse
private val mockCourseEnrolled = EnrolledCourse(
- auditAccessExpires = "",
+ auditAccessExpires = null,
created = "created",
certificate = Certificate(""),
mode = "mode",
@@ -27,10 +28,10 @@ class MyCoursesScreenTest {
name = "name",
number = "",
org = "Org",
- start = "",
+ start = Date(),
startDisplay = "",
startType = "",
- end = "Ending in 22 November",
+ end = null,
dynamicUpgradeDeadline = "",
subscriptionId = "",
coursewareAccess = CoursewareAccess(
@@ -62,8 +63,12 @@ class MyCoursesScreenTest {
DashboardUIState.Loading,
null,
refreshing = false,
- onItemClick = {},
- onSwipeRefresh = {}
+ canLoadMore = false,
+ hasInternetConnection = true,
+ onReloadClick = {},
+ onSwipeRefresh = {},
+ paginationCallback = {},
+ onItemClick = {}
)
}
@@ -88,8 +93,12 @@ class MyCoursesScreenTest {
DashboardUIState.Courses(listOf(mockCourseEnrolled, mockCourseEnrolled)),
null,
refreshing = false,
- onItemClick = {},
- onSwipeRefresh = {}
+ canLoadMore = false,
+ hasInternetConnection = true,
+ onReloadClick = {},
+ onSwipeRefresh = {},
+ paginationCallback = {},
+ onItemClick = {}
)
}
@@ -107,8 +116,12 @@ class MyCoursesScreenTest {
DashboardUIState.Courses(listOf(mockCourseEnrolled, mockCourseEnrolled)),
null,
refreshing = true,
- onItemClick = {},
- onSwipeRefresh = {}
+ canLoadMore = false,
+ hasInternetConnection = true,
+ onReloadClick = {},
+ onSwipeRefresh = {},
+ paginationCallback = {},
+ onItemClick = {}
)
}
diff --git a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt
index b22fa1671..b43624364 100644
--- a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt
+++ b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt
@@ -1,7 +1,7 @@
package org.openedx.dashboard.data.repository
import org.openedx.core.data.api.CourseApi
-import org.openedx.core.data.storage.PreferencesManager
+import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.domain.model.DashboardCourseList
import org.openedx.core.domain.model.EnrolledCourse
import org.openedx.dashboard.data.DashboardDao
@@ -9,7 +9,7 @@ import org.openedx.dashboard.data.DashboardDao
class DashboardRepository(
private val api: CourseApi,
private val dao: DashboardDao,
- private val preferencesManager: PreferencesManager
+ private val preferencesManager: CorePreferences
) {
suspend fun getEnrolledCourses(page: Int): DashboardCourseList {
diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt
index 1a1073922..714d0c91e 100644
--- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt
+++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt
@@ -91,7 +91,6 @@ class DashboardViewModel(
_uiState.value = DashboardUIState.Courses(ArrayList(coursesList))
}
} catch (e: Exception) {
- e.printStackTrace()
if (e.isInternetError()) {
_uiMessage.value =
UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))
@@ -135,7 +134,6 @@ class DashboardViewModel(
_uiState.value = DashboardUIState.Courses(ArrayList(coursesList))
}
} catch (e: Exception) {
- e.printStackTrace()
if (e.isInternetError()) {
_uiMessage.value =
UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))
diff --git a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt
index 135fe248a..05624f98b 100644
--- a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt
+++ b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt
@@ -41,6 +41,7 @@ class DashboardViewModelTest {
private val interactor = mockk()
private val networkConnection = mockk()
private val notifier = mockk()
+ private val analytics = mockk()
private val noInternet = "Slow or no internet connection"
private val somethingWrong = "Something went wrong"
@@ -64,7 +65,7 @@ class DashboardViewModelTest {
@Test
fun `getCourses no internet connection`() = runTest {
- val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier)
+ val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics)
every { networkConnection.isOnline() } returns true
coEvery { interactor.getEnrolledCourses(any()) } throws UnknownHostException()
@@ -80,7 +81,7 @@ class DashboardViewModelTest {
@Test
fun `getCourses unknown error`() = runTest {
- val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier)
+ val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics)
every { networkConnection.isOnline() } returns true
coEvery { interactor.getEnrolledCourses(any()) } throws Exception()
@@ -96,7 +97,7 @@ class DashboardViewModelTest {
@Test
fun `getCourses from network`() = runTest {
- val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier)
+ val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics)
every { networkConnection.isOnline() } returns true
coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList
coEvery { interactor.getEnrolledCoursesFromCache() } returns listOf(mockk())
@@ -112,7 +113,7 @@ class DashboardViewModelTest {
@Test
fun `getCourses from network with next page`() = runTest {
- val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier)
+ val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics)
every { networkConnection.isOnline() } returns true
coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList.copy(
Pagination(
@@ -138,7 +139,7 @@ class DashboardViewModelTest {
every { networkConnection.isOnline() } returns false
coEvery { interactor.getEnrolledCoursesFromCache() } returns listOf(mockk())
- val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier)
+ val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics)
advanceUntilIdle()
@@ -153,7 +154,7 @@ class DashboardViewModelTest {
fun `updateCourses no internet error`() = runTest {
every { networkConnection.isOnline() } returns true
coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList
- val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier)
+ val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics)
coEvery { interactor.getEnrolledCourses(any()) } throws UnknownHostException()
viewModel.updateCourses()
@@ -173,7 +174,7 @@ class DashboardViewModelTest {
every { networkConnection.isOnline() } returns true
coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList
- val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier)
+ val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics)
coEvery { interactor.getEnrolledCourses(any()) } throws Exception()
viewModel.updateCourses()
@@ -192,7 +193,7 @@ class DashboardViewModelTest {
fun `updateCourses success`() = runTest {
every { networkConnection.isOnline() } returns true
coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList
- val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier)
+ val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics)
viewModel.updateCourses()
advanceUntilIdle()
@@ -209,7 +210,7 @@ class DashboardViewModelTest {
fun `updateCourses success with next page`() = runTest {
every { networkConnection.isOnline() } returns true
coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList.copy(Pagination(10,"2",2,""))
- val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier)
+ val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics)
viewModel.updateCourses()
advanceUntilIdle()
@@ -226,7 +227,7 @@ class DashboardViewModelTest {
fun `CourseDashboardUpdate notifier test`() = runTest {
coEvery { notifier.notifier } returns flow { emit(CourseDashboardUpdate()) }
- val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier)
+ val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics)
val mockLifeCycleOwner: LifecycleOwner = mockk()
val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner)
diff --git a/discovery/build.gradle b/discovery/build.gradle
index f0b329ca1..8e651abda 100644
--- a/discovery/build.gradle
+++ b/discovery/build.gradle
@@ -6,11 +6,11 @@ plugins {
}
android {
- compileSdk 33
+ compileSdk 34
defaultConfig {
minSdk 24
- targetSdk 33
+ targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@@ -26,11 +26,11 @@ android {
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_11
+ jvmTarget = JavaVersion.VERSION_17
}
buildFeatures {
@@ -41,13 +41,16 @@ android {
kotlinCompilerExtensionVersion = "$compose_compiler_version"
}
- flavorDimensions "tier"
+ flavorDimensions += "env"
productFlavors {
prod {
+ dimension 'env'
}
develop {
+ dimension 'env'
}
stage {
+ dimension 'env'
}
}
}
@@ -57,9 +60,8 @@ dependencies {
kapt "androidx.room:room-compiler:$room_version"
-
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
testImplementation "junit:junit:$junit_version"
diff --git a/discovery/src/androidTest/java/org/openedx/discovery/presentation/DiscoveryScreenTest.kt b/discovery/src/androidTest/java/org/openedx/discovery/presentation/DiscoveryScreenTest.kt
index 4ea5c7e43..3975cb6c7 100644
--- a/discovery/src/androidTest/java/org/openedx/discovery/presentation/DiscoveryScreenTest.kt
+++ b/discovery/src/androidTest/java/org/openedx/discovery/presentation/DiscoveryScreenTest.kt
@@ -10,21 +10,21 @@ import org.openedx.core.ui.WindowSize
import org.openedx.core.ui.WindowType
import org.junit.Rule
import org.junit.Test
+import java.util.Date
class DiscoveryScreenTest {
@get:Rule
val composeTestRule = createAndroidComposeRule()
- //region course
-
+ //region mockCourse
private val course = Course(
id = "id",
blocksUrl = "blocksUrl",
courseId = "courseId",
effort = "effort",
- enrollmentStart = "enrollmentStart",
- enrollmentEnd = "enrollmentEnd",
+ enrollmentStart = Date(),
+ enrollmentEnd = null,
hidden = false,
invitationOnly = false,
media = Media(),
@@ -38,9 +38,9 @@ class DiscoveryScreenTest {
end = "end",
startDisplay = "startDisplay",
startType = "startType",
- overview = ""
+ overview = "",
+ isEnrolled = false
)
-
//endregion
@Test
@@ -52,9 +52,12 @@ class DiscoveryScreenTest {
uiMessage = null,
canLoadMore = false,
refreshing = false,
+ hasInternetConnection = true,
+ onSearchClick = {},
onSwipeRefresh = {},
paginationCallback = {},
- onItemClick = {}
+ onItemClick = {},
+ onReloadClick = {}
)
}
@@ -81,9 +84,12 @@ class DiscoveryScreenTest {
uiMessage = null,
canLoadMore = false,
refreshing = false,
+ hasInternetConnection = true,
+ onSearchClick = {},
onSwipeRefresh = {},
paginationCallback = {},
- onItemClick = {}
+ onItemClick = {},
+ onReloadClick = {}
)
}
@@ -101,9 +107,12 @@ class DiscoveryScreenTest {
uiMessage = null,
canLoadMore = true,
refreshing = false,
+ hasInternetConnection = true,
+ onSearchClick = {},
onSwipeRefresh = {},
paginationCallback = {},
- onItemClick = {}
+ onItemClick = {},
+ onReloadClick = {}
)
}
@@ -125,5 +134,4 @@ class DiscoveryScreenTest {
}
}
-
}
\ No newline at end of file
diff --git a/discussion/build.gradle b/discussion/build.gradle
index 0504167e8..77d393d7a 100644
--- a/discussion/build.gradle
+++ b/discussion/build.gradle
@@ -6,11 +6,11 @@ plugins {
android {
namespace 'org.openedx.discussion'
- compileSdk 33
+ compileSdk 34
defaultConfig {
minSdk 24
- targetSdk 33
+ targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@@ -23,11 +23,11 @@ android {
}
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_11
+ jvmTarget = JavaVersion.VERSION_17
}
buildFeatures {
@@ -38,23 +38,25 @@ android {
kotlinCompilerExtensionVersion = "$compose_compiler_version"
}
- flavorDimensions "tier"
+ flavorDimensions += "env"
productFlavors {
prod {
+ dimension 'env'
}
develop {
+ dimension 'env'
}
stage {
+ dimension 'env'
}
}
}
dependencies {
-
implementation project(path: ':core')
- androidTestImplementation 'androidx.test.ext:junit:1.1.4'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
testImplementation "junit:junit:$junit_version"
testImplementation "io.mockk:mockk:$mockk_version"
testImplementation "io.mockk:mockk-android:$mockk_version"
diff --git a/discussion/src/main/AndroidManifest.xml b/discussion/src/main/AndroidManifest.xml
deleted file mode 100644
index a5918e68a..000000000
--- a/discussion/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt b/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt
index e2c18dff1..b95b5447d 100644
--- a/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt
+++ b/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt
@@ -1,7 +1,7 @@
package org.openedx.discussion.data.repository
import org.openedx.core.data.model.BlocksCompletionBody
-import org.openedx.core.data.storage.PreferencesManager
+import org.openedx.core.data.storage.CorePreferences
import org.openedx.discussion.data.api.DiscussionApi
import org.openedx.discussion.data.model.request.*
import org.openedx.discussion.domain.model.CommentsData
@@ -10,7 +10,7 @@ import org.openedx.discussion.domain.model.Topic
class DiscussionRepository(
private val api: DiscussionApi,
- private val preferencesManager: PreferencesManager
+ private val preferencesManager: CorePreferences
) {
private val topics = mutableListOf()
diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModel.kt
index e79b153ed..bcf54a92e 100644
--- a/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModel.kt
+++ b/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModel.kt
@@ -8,7 +8,6 @@ import org.openedx.core.BaseViewModel
import org.openedx.core.R
import org.openedx.core.SingleEventLiveData
import org.openedx.core.UIMessage
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.core.extension.isInternetError
import org.openedx.core.system.ResourceManager
import org.openedx.discussion.domain.interactor.DiscussionInteractor
@@ -23,7 +22,6 @@ import kotlinx.coroutines.launch
class DiscussionCommentsViewModel(
private val interactor: DiscussionInteractor,
private val resourceManager: ResourceManager,
- private val preferencesManager: PreferencesManager,
private val notifier: DiscussionNotifier,
thread: org.openedx.discussion.domain.model.Thread,
) : BaseViewModel() {
diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModel.kt
index 7a13f42f5..3f9b75e60 100644
--- a/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModel.kt
+++ b/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModel.kt
@@ -7,7 +7,6 @@ import org.openedx.core.BaseViewModel
import org.openedx.core.R
import org.openedx.core.SingleEventLiveData
import org.openedx.core.UIMessage
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.core.extension.isInternetError
import org.openedx.core.system.ResourceManager
import org.openedx.discussion.domain.interactor.DiscussionInteractor
@@ -19,7 +18,6 @@ import kotlinx.coroutines.launch
class DiscussionResponsesViewModel(
private val interactor: DiscussionInteractor,
private val resourceManager: ResourceManager,
- private val preferencesManager: PreferencesManager,
private val notifier: DiscussionNotifier,
private var comment: DiscussionComment,
) : BaseViewModel() {
diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/ui/DiscussionUI.kt b/discussion/src/main/java/org/openedx/discussion/presentation/ui/DiscussionUI.kt
index bd285b9d6..0d146692a 100644
--- a/discussion/src/main/java/org/openedx/discussion/presentation/ui/DiscussionUI.kt
+++ b/discussion/src/main/java/org/openedx/discussion/presentation/ui/DiscussionUI.kt
@@ -30,7 +30,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import coil.compose.rememberAsyncImagePainter
+import coil.compose.AsyncImage
+import coil.request.ImageRequest
import org.openedx.core.domain.model.ProfileImage
import org.openedx.core.extension.TextConverter
import org.openedx.core.ui.AutoSizeText
@@ -53,7 +54,11 @@ fun ThreadMainItem(
thread: org.openedx.discussion.domain.model.Thread,
onClick: (String, Boolean) -> Unit,
) {
- val profileImageUrl = thread.users?.get(thread.author)?.image?.imageUrlFull ?: ""
+ val profileImageUrl = if (thread.users?.get(thread.author)?.image?.hasImage == true) {
+ thread.users[thread.author]?.image?.imageUrlFull
+ } else {
+ org.openedx.core.R.drawable.core_ic_default_profile_picture
+ }
val votePainter = if (thread.voted) {
painterResource(id = R.drawable.discussion_ic_like_success)
@@ -82,11 +87,12 @@ fun ThreadMainItem(
modifier = modifier
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- Image(
- painter = rememberAsyncImagePainter(
- model = profileImageUrl,
- error = painterResource(id = org.openedx.core.R.drawable.core_ic_default_profile_picture)
- ),
+ AsyncImage(
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(profileImageUrl)
+ .error(org.openedx.core.R.drawable.core_ic_default_profile_picture)
+ .placeholder(org.openedx.core.R.drawable.core_ic_default_profile_picture)
+ .build(),
contentDescription = null,
modifier = Modifier
.size(48.dp)
@@ -167,8 +173,13 @@ fun CommentItem(
onClick: (String, String, Boolean) -> Unit,
onAddCommentClick: () -> Unit = {},
) {
- val profileImageUrl = comment.profileImage?.imageUrlFull
- ?: comment.users?.get(comment.author)?.image?.imageUrlFull ?: ""
+ val profileImageUrl = if (comment.profileImage?.hasImage == true) {
+ comment.profileImage.imageUrlFull
+ } else if (comment.users?.get(comment.author)?.image?.hasImage == true) {
+ comment.users[comment.author]?.image?.imageUrlFull
+ } else {
+ org.openedx.core.R.drawable.core_ic_default_profile_picture
+ }
val reportText = if (comment.abuseFlagged) {
stringResource(id = R.string.discussion_unreport)
@@ -215,11 +226,12 @@ fun CommentItem(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
- Image(
- painter = rememberAsyncImagePainter(
- model = profileImageUrl,
- error = painterResource(id = org.openedx.core.R.drawable.core_ic_default_profile_picture)
- ),
+ AsyncImage(
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(profileImageUrl)
+ .error(org.openedx.core.R.drawable.core_ic_default_profile_picture)
+ .placeholder(org.openedx.core.R.drawable.core_ic_default_profile_picture)
+ .build(),
contentDescription = null,
modifier = Modifier
.size(32.dp)
@@ -312,8 +324,13 @@ fun CommentMainItem(
comment: DiscussionComment,
onClick: (String, String, Boolean) -> Unit,
) {
- val profileImageUrl = comment.profileImage?.imageUrlFull
- ?: comment.users?.get(comment.author)?.image?.imageUrlFull ?: ""
+ val profileImageUrl = if (comment.profileImage?.hasImage == true) {
+ comment.profileImage.imageUrlFull
+ } else if (comment.users?.get(comment.author)?.image?.hasImage == true) {
+ comment.users[comment.author]?.image?.imageUrlFull
+ } else {
+ org.openedx.core.R.drawable.core_ic_default_profile_picture
+ }
val reportText = if (comment.abuseFlagged) {
stringResource(id = R.string.discussion_unreport)
@@ -352,11 +369,12 @@ fun CommentMainItem(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
- Image(
- painter = rememberAsyncImagePainter(
- model = profileImageUrl,
- error = painterResource(id = org.openedx.core.R.drawable.core_ic_default_profile_picture)
- ),
+ AsyncImage(
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(profileImageUrl)
+ .error(org.openedx.core.R.drawable.core_ic_default_profile_picture)
+ .placeholder(org.openedx.core.R.drawable.core_ic_default_profile_picture)
+ .build(),
contentDescription = null,
modifier = Modifier
.size(32.dp)
diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt
index 62a569a3e..8e55f7cd2 100644
--- a/discussion/src/test/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt
+++ b/discussion/src/test/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt
@@ -6,7 +6,6 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import org.openedx.core.R
import org.openedx.core.UIMessage
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.core.domain.model.Pagination
import org.openedx.core.extension.TextConverter
import org.openedx.core.system.ResourceManager
@@ -23,6 +22,7 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.*
import org.junit.*
import org.junit.rules.TestRule
+import org.openedx.core.data.storage.CorePreferences
import java.net.UnknownHostException
@OptIn(ExperimentalCoroutinesApi::class)
@@ -35,7 +35,7 @@ class DiscussionCommentsViewModelTest {
private val resourceManager = mockk()
private val interactor = mockk()
- private val preferencesManager = mockk()
+ private val preferencesManager = mockk()
private val notifier = mockk(relaxed = true)
private val noInternet = "Slow or no internet connection"
@@ -138,7 +138,6 @@ class DiscussionCommentsViewModelTest {
DiscussionCommentsViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockThread
)
@@ -162,7 +161,6 @@ class DiscussionCommentsViewModelTest {
DiscussionCommentsViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockThread.copy(type = DiscussionType.QUESTION)
)
@@ -193,7 +191,6 @@ class DiscussionCommentsViewModelTest {
DiscussionCommentsViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockThread.copy(type = DiscussionType.QUESTION)
)
@@ -223,7 +220,6 @@ class DiscussionCommentsViewModelTest {
DiscussionCommentsViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockThread
)
@@ -257,7 +253,6 @@ class DiscussionCommentsViewModelTest {
DiscussionCommentsViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockThread
)
@@ -286,17 +281,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
- coEvery { interactor.setThreadRead(any()) } throws UnknownHostException()
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.getThreadQuestionComments(any(), any(), any()) } returns CommentsData(
@@ -323,16 +316,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "2", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.getThreadComments(any(), eq(2)) } returns CommentsData(
comments,
@@ -357,16 +349,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.setThreadVoted(any(), any()) } throws UnknownHostException()
@@ -387,13 +378,13 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
val viewModel =
DiscussionCommentsViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockThread
)
@@ -416,13 +407,13 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
val viewModel =
DiscussionCommentsViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockThread
)
@@ -444,13 +435,13 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
val viewModel =
DiscussionCommentsViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockThread
)
@@ -474,13 +465,13 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
val viewModel =
DiscussionCommentsViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockThread
)
@@ -504,16 +495,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.setCommentFlagged(any(), any()) } returns mockComment.copy(id = "0")
@@ -533,16 +523,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.setCommentVoted(any(), any()) } throws UnknownHostException()
@@ -562,16 +551,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.setCommentVoted(any(), any()) } throws Exception()
@@ -591,16 +579,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.setCommentVoted(any(), any()) } returns mockComment.copy(id = "0")
@@ -619,16 +606,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.setThreadFlagged(any(), any()) } throws UnknownHostException()
@@ -648,16 +634,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.setThreadFlagged(any(), any()) } throws Exception()
@@ -677,16 +662,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.setThreadFlagged(any(), any()) } returns mockThread
@@ -708,17 +692,16 @@ class DiscussionCommentsViewModelTest {
)
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
-
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
coEvery { interactor.setThreadFollowed(any(), any()) } throws UnknownHostException()
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
+
viewModel.setThreadFollowed(true)
advanceUntilIdle()
@@ -735,16 +718,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.setThreadFollowed(any(), any()) } throws Exception()
@@ -765,16 +747,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.setThreadFollowed(any(), any()) } returns mockThread
@@ -793,16 +774,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { notifier.notifier } returns flow {
delay(100)
@@ -829,16 +809,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "2", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { notifier.notifier } returns flow {
delay(100)
@@ -865,16 +844,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "2", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { notifier.notifier } returns flow {
delay(100)
@@ -899,16 +877,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "2", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.createComment(any(), any(), any()) } throws UnknownHostException()
viewModel.createComment("")
@@ -927,16 +904,15 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "2", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.createComment(any(), any(), any()) } throws Exception()
viewModel.createComment("")
@@ -962,7 +938,6 @@ class DiscussionCommentsViewModelTest {
DiscussionCommentsViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockThread
)
@@ -983,18 +958,17 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "2", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.createComment(any(), any(), any()) } returns mockComment
- every { preferencesManager.profile?.username } returns ""
+ every { preferencesManager.user?.username } returns ""
viewModel.createComment("")
advanceUntilIdle()
@@ -1007,18 +981,17 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "2", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.createComment(any(), any(), any()) } returns mockComment
- every { preferencesManager.profile?.username } returns ""
+ every { preferencesManager.user?.username } returns ""
viewModel.createComment("")
advanceUntilIdle()
@@ -1032,24 +1005,22 @@ class DiscussionCommentsViewModelTest {
comments,
Pagination(10, "", 4, "1")
)
+ coEvery { interactor.setThreadRead(any()) } returns mockThread
every { resourceManager.getString(eq(mockThread.type.resId)) } returns ""
- val viewModel =
- DiscussionCommentsViewModel(
- interactor,
- resourceManager,
- preferencesManager,
- notifier,
- mockThread
- )
+ val viewModel = DiscussionCommentsViewModel(
+ interactor,
+ resourceManager,
+ notifier,
+ mockThread
+ )
coEvery { interactor.createComment(any(), any(), any()) } returns mockComment
- every { preferencesManager.profile?.username } returns ""
+ every { preferencesManager.user?.username } returns ""
viewModel.createComment("")
advanceUntilIdle()
assert(viewModel.uiState.value is DiscussionCommentsUIState.Success)
-
}
}
\ No newline at end of file
diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt
index 039c34e62..61fa44df7 100644
--- a/discussion/src/test/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt
+++ b/discussion/src/test/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt
@@ -3,7 +3,6 @@ package org.openedx.discussion.presentation.responses
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import org.openedx.core.R
import org.openedx.core.UIMessage
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.core.domain.model.Pagination
import org.openedx.core.extension.LinkedImageText
import org.openedx.core.system.ResourceManager
@@ -19,6 +18,7 @@ import kotlinx.coroutines.test.*
import org.junit.*
import org.junit.Assert.*
import org.junit.rules.TestRule
+import org.openedx.core.data.storage.CorePreferences
import java.net.UnknownHostException
@OptIn(ExperimentalCoroutinesApi::class)
@@ -31,7 +31,7 @@ class DiscussionResponsesViewModelTest {
private val resourceManager = mockk()
private val interactor = mockk()
- private val preferencesManager = mockk()
+ private val preferencesManager = mockk()
private val notifier = mockk(relaxed = true)
private val noInternet = "Slow or no internet connection"
@@ -133,7 +133,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -154,7 +153,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -178,7 +176,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -202,7 +199,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -225,7 +221,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -249,7 +244,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -277,7 +271,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -300,7 +293,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -323,7 +315,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -347,7 +338,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -371,7 +361,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -394,7 +383,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -417,7 +405,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -440,7 +427,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -465,7 +451,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -490,7 +475,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -515,7 +499,6 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
@@ -540,12 +523,11 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
coEvery { interactor.createComment(any(), any(), any()) } returns mockComment
- every { preferencesManager.profile?.username } returns ""
+ every { preferencesManager.user?.username } returns ""
viewModel.createComment("")
advanceUntilIdle()
@@ -562,12 +544,11 @@ class DiscussionResponsesViewModelTest {
val viewModel = DiscussionResponsesViewModel(
interactor,
resourceManager,
- preferencesManager,
notifier,
mockComment.copy(id = "0")
)
coEvery { interactor.createComment(any(), any(), any()) } returns mockComment
- every { preferencesManager.profile?.username } returns ""
+ every { preferencesManager.user?.username } returns ""
viewModel.createComment("")
advanceUntilIdle()
diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt
index f8441d28d..6944e33c4 100644
--- a/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt
+++ b/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt
@@ -3,7 +3,6 @@ package org.openedx.discussion.presentation.threads
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import org.openedx.core.R
import org.openedx.core.UIMessage
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.core.domain.model.ProfileImage
import org.openedx.core.extension.TextConverter
import org.openedx.core.system.ResourceManager
@@ -23,6 +22,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
+import org.openedx.core.data.storage.CorePreferences
import java.net.UnknownHostException
@OptIn(ExperimentalCoroutinesApi::class)
@@ -35,7 +35,7 @@ class DiscussionAddThreadViewModelTest {
private val resourceManager = mockk()
private val interactor = mockk()
- private val preferencesManager = mockk()
+ private val preferencesManager = mockk()
private val notifier = mockk(relaxed = true)
private val noInternet = "Slow or no internet connection"
diff --git a/gradle.properties b/gradle.properties
index cd0519bb2..022338b78 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -20,4 +20,6 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
-android.nonTransitiveRClass=true
\ No newline at end of file
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=true
+android.nonFinalResIds=false
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index bf7e3a01a..c90152329 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Mon Sep 12 17:38:01 EEST 2022
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
diff --git a/profile/build.gradle b/profile/build.gradle
index 6f290cc71..1c3c6f301 100644
--- a/profile/build.gradle
+++ b/profile/build.gradle
@@ -6,11 +6,11 @@ plugins {
android {
namespace 'org.openedx.profile'
- compileSdk 33
+ compileSdk 34
defaultConfig {
minSdk 24
- targetSdk 33
+ targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@@ -24,11 +24,11 @@ android {
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_11
+ jvmTarget = JavaVersion.VERSION_17
}
buildFeatures {
@@ -39,24 +39,25 @@ android {
kotlinCompilerExtensionVersion = "$compose_compiler_version"
}
- flavorDimensions "tier"
+ flavorDimensions += "env"
productFlavors {
prod {
+ dimension 'env'
}
develop {
+ dimension 'env'
}
stage {
+ dimension 'env'
}
}
}
dependencies {
-
implementation project(path: ":core")
-
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
testImplementation "junit:junit:$junit_version"
testImplementation "io.mockk:mockk:$mockk_version"
testImplementation "io.mockk:mockk-android:$mockk_version"
diff --git a/profile/src/main/AndroidManifest.xml b/profile/src/main/AndroidManifest.xml
deleted file mode 100644
index e81cbf6cf..000000000
--- a/profile/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/profile/src/main/java/org/openedx/profile/data/model/Account.kt b/profile/src/main/java/org/openedx/profile/data/model/Account.kt
index e1fe4a52b..c3fe9304e 100644
--- a/profile/src/main/java/org/openedx/profile/data/model/Account.kt
+++ b/profile/src/main/java/org/openedx/profile/data/model/Account.kt
@@ -2,8 +2,9 @@ package org.openedx.profile.data.model
import com.google.gson.annotations.SerializedName
import org.openedx.core.data.model.ProfileImage
+import org.openedx.profile.domain.model.Account
import java.util.*
-import org.openedx.core.domain.model.Account as DomainAccount
+import org.openedx.profile.domain.model.Account as DomainAccount
data class Account(
@SerializedName("username")
@@ -47,8 +48,8 @@ data class Account(
ALL_USERS
}
- fun mapToDomain(): org.openedx.core.domain.model.Account {
- return org.openedx.core.domain.model.Account(
+ fun mapToDomain(): Account {
+ return Account(
username = username ?: "",
bio = bio?:"",
requiresParentalConsent = requiresParentalConsent ?: false,
diff --git a/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt b/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt
index a1381a5e5..7e64c98cc 100644
--- a/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt
+++ b/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt
@@ -2,24 +2,24 @@ package org.openedx.profile.data.repository
import androidx.room.RoomDatabase
import org.openedx.core.ApiConstants
-import org.openedx.core.BuildConfig
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.profile.data.api.ProfileApi
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.asRequestBody
+import org.openedx.core.data.storage.CorePreferences
+import org.openedx.profile.domain.model.Account
import java.io.File
class ProfileRepository(
private val api: ProfileApi,
private val room: RoomDatabase,
- private val preferencesManager: PreferencesManager
+ private val preferencesManager: CorePreferences
) {
- suspend fun getAccount(): org.openedx.core.domain.model.Account {
+ suspend fun getAccount(): Account {
return api.getAccount(preferencesManager.user?.username!!).mapToDomain()
}
- suspend fun updateAccount(fields: Map): org.openedx.core.domain.model.Account {
+ suspend fun updateAccount(fields: Map): Account {
return api.updateAccount(preferencesManager.user?.username!!, fields).mapToDomain()
}
diff --git a/profile/src/main/java/org/openedx/profile/data/storage/ProfilePreferences.kt b/profile/src/main/java/org/openedx/profile/data/storage/ProfilePreferences.kt
new file mode 100644
index 000000000..857c1d769
--- /dev/null
+++ b/profile/src/main/java/org/openedx/profile/data/storage/ProfilePreferences.kt
@@ -0,0 +1,7 @@
+package org.openedx.profile.data.storage
+
+import org.openedx.profile.domain.model.Account
+
+interface ProfilePreferences {
+ var profile: Account?
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/openedx/core/domain/model/Account.kt b/profile/src/main/java/org/openedx/profile/domain/model/Account.kt
similarity index 91%
rename from core/src/main/java/org/openedx/core/domain/model/Account.kt
rename to profile/src/main/java/org/openedx/profile/domain/model/Account.kt
index 947b0c9d2..a195f6dbe 100644
--- a/core/src/main/java/org/openedx/core/domain/model/Account.kt
+++ b/profile/src/main/java/org/openedx/profile/domain/model/Account.kt
@@ -1,9 +1,11 @@
-package org.openedx.core.domain.model
+package org.openedx.profile.domain.model
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import org.openedx.core.AppDataConstants.USER_MIN_YEAR
import kotlinx.parcelize.Parcelize
+import org.openedx.core.domain.model.LanguageProficiency
+import org.openedx.core.domain.model.ProfileImage
import java.util.*
@Parcelize
diff --git a/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt b/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt
index 59fc683e1..457b02035 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt
@@ -1,7 +1,7 @@
package org.openedx.profile.presentation
import androidx.fragment.app.FragmentManager
-import org.openedx.core.domain.model.Account
+import org.openedx.profile.domain.model.Account
interface ProfileRouter {
diff --git a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt
index 9c9b6f568..43515f98a 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt
@@ -18,21 +18,57 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.foundation.*
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material.*
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Divider
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.ModalBottomSheetLayout
+import androidx.compose.material.ModalBottomSheetValue
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.outlined.Report
-import androidx.compose.runtime.*
+import androidx.compose.material.rememberModalBottomSheetState
+import androidx.compose.material.rememberScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.mutableStateMapOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@@ -40,7 +76,12 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.*
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@@ -57,24 +98,41 @@ import androidx.compose.ui.window.DialogWindowProvider
import androidx.core.content.FileProvider
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
-import coil.compose.rememberAsyncImagePainter
+import coil.compose.AsyncImage
+import coil.request.ImageRequest
+import kotlinx.coroutines.launch
+import org.koin.android.ext.android.inject
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.parameter.parametersOf
import org.openedx.core.AppDataConstants.DEFAULT_MIME_TYPE
import org.openedx.core.R
import org.openedx.core.UIMessage
-import org.openedx.core.domain.model.Account
+import org.openedx.profile.domain.model.Account
import org.openedx.core.domain.model.LanguageProficiency
import org.openedx.core.domain.model.ProfileImage
import org.openedx.core.domain.model.RegistrationField
import org.openedx.core.extension.getFileName
import org.openedx.core.extension.parcelable
-import org.openedx.core.ui.*
-import org.openedx.core.ui.theme.*
+import org.openedx.core.ui.BackBtn
+import org.openedx.core.ui.HandleUIMessage
+import org.openedx.core.ui.IconText
+import org.openedx.core.ui.OpenEdXButton
+import org.openedx.core.ui.OpenEdXOutlinedButton
+import org.openedx.core.ui.SheetContent
+import org.openedx.core.ui.WindowSize
+import org.openedx.core.ui.WindowType
+import org.openedx.core.ui.isImeVisibleState
+import org.openedx.core.ui.noRippleClickable
+import org.openedx.core.ui.rememberSaveableMap
+import org.openedx.core.ui.rememberWindowSize
+import org.openedx.core.ui.statusBarsInset
+import org.openedx.core.ui.theme.OpenEdXTheme
+import org.openedx.core.ui.theme.appColors
+import org.openedx.core.ui.theme.appShapes
+import org.openedx.core.ui.theme.appTypography
+import org.openedx.core.ui.windowSizeValue
import org.openedx.core.utils.LocaleUtils
import org.openedx.profile.presentation.ProfileRouter
-import kotlinx.coroutines.launch
-import org.koin.android.ext.android.inject
-import org.koin.androidx.viewmodel.ext.android.viewModel
-import org.koin.core.parameter.parametersOf
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
@@ -336,7 +394,13 @@ private fun EditProfileScreen(
}
val imageRes: Any = if (!isImageDeleted) {
- selectedImageUri?.toString() ?: uiState.account.profileImage.imageUrlFull
+ if (selectedImageUri != null) {
+ selectedImageUri.toString()
+ } else if (uiState.account.profileImage.hasImage) {
+ uiState.account.profileImage.imageUrlFull
+ } else {
+ R.drawable.core_ic_default_profile_picture
+ }
} else {
R.drawable.core_ic_default_profile_picture
}
@@ -534,12 +598,12 @@ private fun EditProfileScreen(
)
Spacer(modifier = Modifier.height(32.dp))
Box(contentAlignment = Alignment.BottomEnd) {
- Image(
- painter = rememberAsyncImagePainter(
- model = imageRes,
- placeholder = painterResource(id = R.drawable.core_ic_default_profile_picture),
- error = painterResource(id = R.drawable.core_ic_default_profile_picture)
- ),
+ AsyncImage(
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(imageRes)
+ .error(R.drawable.core_ic_default_profile_picture)
+ .placeholder(R.drawable.core_ic_default_profile_picture)
+ .build(),
contentScale = ContentScale.Crop,
contentDescription = null,
modifier = Modifier
@@ -831,7 +895,10 @@ private fun ProfileFields(
name = stringResource(id = profileR.string.profile_spoken_language),
initialValue = lang,
onClick = {
- onFieldClick(LANGUAGE, context.getString(profileR.string.profile_spoken_language))
+ onFieldClick(
+ LANGUAGE,
+ context.getString(profileR.string.profile_spoken_language)
+ )
}
)
InputEditField(
diff --git a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileUIState.kt
index 7329207f7..5654800ca 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileUIState.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileUIState.kt
@@ -1,6 +1,6 @@
package org.openedx.profile.presentation.edit
-import org.openedx.core.domain.model.Account
+import org.openedx.profile.domain.model.Account
data class EditProfileUIState(
val account: Account,
diff --git a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt
index f114e9ab4..1aec603a2 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt
@@ -7,7 +7,7 @@ import androidx.lifecycle.viewModelScope
import org.openedx.core.BaseViewModel
import org.openedx.core.R
import org.openedx.core.UIMessage
-import org.openedx.core.domain.model.Account
+import org.openedx.profile.domain.model.Account
import org.openedx.core.extension.isInternetError
import org.openedx.core.system.ResourceManager
import org.openedx.profile.domain.interactor.ProfileInteractor
diff --git a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileFragment.kt
index dabf67849..96ee13059 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileFragment.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileFragment.kt
@@ -37,10 +37,13 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.fragment.app.Fragment
-import coil.compose.rememberAsyncImagePainter
+import coil.compose.AsyncImage
+import coil.request.ImageRequest
+import org.koin.android.ext.android.inject
+import org.koin.androidx.viewmodel.ext.android.viewModel
import org.openedx.core.R
import org.openedx.core.UIMessage
-import org.openedx.core.domain.model.Account
+import org.openedx.profile.domain.model.Account
import org.openedx.core.domain.model.ProfileImage
import org.openedx.core.presentation.global.AppData
import org.openedx.core.presentation.global.AppDataHolder
@@ -51,8 +54,6 @@ import org.openedx.core.ui.theme.appShapes
import org.openedx.core.ui.theme.appTypography
import org.openedx.core.utils.EmailUtil
import org.openedx.profile.presentation.ProfileRouter
-import org.koin.android.ext.android.inject
-import org.koin.androidx.viewmodel.ext.android.viewModel
class ProfileFragment : Fragment() {
@@ -253,12 +254,17 @@ private fun ProfileScreen(
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
- Image(
- painter = rememberAsyncImagePainter(
- model = uiState.account.profileImage.imageUrlFull,
- placeholder = painterResource(id = R.drawable.core_ic_default_profile_picture),
- error = painterResource(id = R.drawable.core_ic_default_profile_picture)
- ),
+ val profileImage = if (uiState.account.profileImage.hasImage) {
+ uiState.account.profileImage.imageUrlFull
+ } else {
+ R.drawable.core_ic_default_profile_picture
+ }
+ AsyncImage(
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(profileImage)
+ .error(R.drawable.core_ic_default_profile_picture)
+ .placeholder(R.drawable.core_ic_default_profile_picture)
+ .build(),
contentDescription = null,
modifier = Modifier
.border(
diff --git a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileUIState.kt
index 3bb0b9646..c33975a5f 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileUIState.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileUIState.kt
@@ -1,6 +1,6 @@
package org.openedx.profile.presentation.profile
-import org.openedx.core.domain.model.Account
+import org.openedx.profile.domain.model.Account
sealed class ProfileUIState {
data class Data(val account: Account) : ProfileUIState()
diff --git a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt
index 15dc9b46a..081b041f8 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt
@@ -7,7 +7,6 @@ import androidx.lifecycle.viewModelScope
import org.openedx.core.BaseViewModel
import org.openedx.core.R
import org.openedx.core.UIMessage
-import org.openedx.core.data.storage.PreferencesManager
import org.openedx.core.extension.isInternetError
import org.openedx.core.module.DownloadWorkerController
import org.openedx.core.system.AppCookieManager
@@ -20,10 +19,11 @@ import org.openedx.profile.system.notifier.ProfileNotifier
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.openedx.profile.data.storage.ProfilePreferences
class ProfileViewModel(
private val interactor: ProfileInteractor,
- private val preferencesManager: PreferencesManager,
+ private val preferencesManager: ProfilePreferences,
private val resourceManager: ResourceManager,
private val notifier: ProfileNotifier,
private val dispatcher: CoroutineDispatcher,
@@ -69,6 +69,12 @@ class ProfileViewModel(
_uiState.value = ProfileUIState.Loading
viewModelScope.launch {
try {
+ val cachedAccount = preferencesManager.profile
+ if (cachedAccount == null) {
+ _uiState.value = ProfileUIState.Loading
+ } else {
+ _uiState.value = ProfileUIState.Data(cachedAccount)
+ }
val account = interactor.getAccount()
_uiState.value = ProfileUIState.Data(account)
preferencesManager.profile = account
@@ -102,7 +108,6 @@ class ProfileViewModel(
analytics.logoutEvent(false)
_successLogout.value = true
} catch (e: Exception) {
- e.printStackTrace()
if (e.isInternetError()) {
_uiMessage.value =
UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))
diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoQualityViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoQualityViewModel.kt
index b8fff185b..06c9bd6e6 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoQualityViewModel.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoQualityViewModel.kt
@@ -4,14 +4,14 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import org.openedx.core.BaseViewModel
-import org.openedx.core.data.storage.PreferencesManager
+import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.domain.model.VideoQuality
import org.openedx.profile.system.notifier.ProfileNotifier
import org.openedx.profile.system.notifier.VideoQualityChanged
import kotlinx.coroutines.launch
class VideoQualityViewModel(
- private val preferencesManager: PreferencesManager,
+ private val preferencesManager: CorePreferences,
private val notifier: ProfileNotifier
) : BaseViewModel() {
diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsViewModel.kt
index 4ad67259f..743986b09 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsViewModel.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsViewModel.kt
@@ -5,7 +5,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import org.openedx.core.BaseViewModel
-import org.openedx.core.data.storage.PreferencesManager
+import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.domain.model.VideoSettings
import org.openedx.profile.system.notifier.ProfileNotifier
import org.openedx.profile.system.notifier.VideoQualityChanged
@@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class VideoSettingsViewModel(
- private val preferencesManager: PreferencesManager,
+ private val preferencesManager: CorePreferences,
private val notifier: ProfileNotifier
) : BaseViewModel() {
diff --git a/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt b/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt
index 0cef5b03a..ecfb1fadf 100644
--- a/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt
+++ b/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt
@@ -3,7 +3,7 @@ package org.openedx.profile.presentation.edit
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import org.openedx.core.R
import org.openedx.core.UIMessage
-import org.openedx.core.domain.model.Account
+import org.openedx.profile.domain.model.Account
import org.openedx.core.domain.model.ProfileImage
import org.openedx.core.system.ResourceManager
import org.openedx.profile.domain.interactor.ProfileInteractor
diff --git a/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt b/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt
index d27e95851..fa763d41f 100644
--- a/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt
+++ b/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt
@@ -6,8 +6,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import org.openedx.core.R
import org.openedx.core.UIMessage
-import org.openedx.core.data.storage.PreferencesManager
-import org.openedx.core.domain.model.Account
+import org.openedx.profile.domain.model.Account
import org.openedx.core.module.DownloadWorkerController
import org.openedx.core.system.AppCookieManager
import org.openedx.core.system.ResourceManager
@@ -30,6 +29,8 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
+import org.openedx.core.data.storage.CorePreferences
+import org.openedx.profile.data.storage.ProfilePreferences
import java.net.UnknownHostException
@OptIn(ExperimentalCoroutinesApi::class)
@@ -42,7 +43,7 @@ class ProfileViewModelTest {
private val dispatcherIO = UnconfinedTestDispatcher()
private val resourceManager = mockk()
- private val preferencesManager = mockk()
+ private val preferencesManager = mockk()
private val interactor = mockk()
private val notifier = mockk()
private val cookieManager = mockk()
@@ -67,7 +68,7 @@ class ProfileViewModelTest {
}
@Test
- fun `getAccount no internetConnection`() = runTest {
+ fun `getAccount no internetConnection and cache is null`() = runTest {
val viewModel =
ProfileViewModel(
interactor,
@@ -79,6 +80,7 @@ class ProfileViewModelTest {
workerController,
analytics
)
+ coEvery { preferencesManager.profile } returns null
coEvery { interactor.getAccount() } throws UnknownHostException()
advanceUntilIdle()
@@ -89,6 +91,30 @@ class ProfileViewModelTest {
assertEquals(noInternet, message?.message)
}
+ @Test
+ fun `getAccount no internetConnection and cache is not null`() = runTest {
+ val viewModel =
+ ProfileViewModel(
+ interactor,
+ preferencesManager,
+ resourceManager,
+ notifier,
+ dispatcher,
+ cookieManager,
+ workerController,
+ analytics
+ )
+ coEvery { preferencesManager.profile } returns account
+ coEvery { interactor.getAccount() } throws UnknownHostException()
+ advanceUntilIdle()
+
+ coVerify(exactly = 1) { interactor.getAccount() }
+
+ val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage
+ assert(viewModel.uiState.value is ProfileUIState.Data)
+ assertEquals(noInternet, message?.message)
+ }
+
@Test
fun `getAccount unknown exception`() = runTest {
val viewModel =
@@ -102,6 +128,7 @@ class ProfileViewModelTest {
workerController,
analytics
)
+ coEvery { preferencesManager.profile } returns null
coEvery { interactor.getAccount() } throws Exception()
advanceUntilIdle()
@@ -125,6 +152,7 @@ class ProfileViewModelTest {
workerController,
analytics
)
+ coEvery { preferencesManager.profile } returns null
coEvery { interactor.getAccount() } returns account
every { preferencesManager.profile = any() } returns Unit
advanceUntilIdle()
@@ -198,6 +226,7 @@ class ProfileViewModelTest {
workerController,
analytics
)
+ coEvery { preferencesManager.profile } returns mockk()
coEvery { interactor.getAccount() } returns mockk()
every { analytics.logoutEvent(false) } returns Unit
every { preferencesManager.profile = any() } returns Unit
@@ -211,7 +240,6 @@ class ProfileViewModelTest {
assert(viewModel.uiMessage.value == null)
assert(viewModel.successLogout.value == true)
-
}
@Test
@@ -226,6 +254,7 @@ class ProfileViewModelTest {
workerController,
analytics
)
+ coEvery { preferencesManager.profile } returns null
every { notifier.notifier } returns flow { emit(AccountUpdated()) }
val mockLifeCycleOwner: LifecycleOwner = mockk()
val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner)