diff --git a/.aiexclude b/.aiexclude
new file mode 100644
index 000000000000..05433f9baf75
--- /dev/null
+++ b/.aiexclude
@@ -0,0 +1,39 @@
+# OS X generated file
+.DS_Store
+
+# Build-related files
+fastlane/
+
+# Key-related files
+.jks
+.keystore
+
+# Backup files
+.bak
+
+# Generated files
+bin/
+gen/
+build/
+build.log
+
+# Built application files
+.apk
+.ap_
+.aab
+
+# Dex VM files
+.dex
+
+# Configuration files
+.configure
+.configure-files/
+google-services.json
+google-upload-credentials.json
+firebase.secrets.json
+sentry.properties
+
+# Gradle files
+gradle.properties
+local.properties
+local-builds.gradle
diff --git a/.buildkite/beta-builds.yml b/.buildkite/beta-builds.yml
index 9d77c5e399b9..f6fb82ade867 100644
--- a/.buildkite/beta-builds.yml
+++ b/.buildkite/beta-builds.yml
@@ -1,11 +1,11 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json
+---
+
# This pipeline is meant to be run via the Buildkite API, and is
# only used for beta builds
-# Nodes with values to reuse in the pipeline.
-common_params:
- # Common plugin settings to use with the `plugins` key.
- - &common_plugins
- - automattic/a8c-ci-toolkit#2.17.0
+agents:
+ queue: "android"
steps:
#################
@@ -14,7 +14,7 @@ steps:
- label: "Gradle Wrapper Validation"
command: |
validate_gradle_wrapper
- plugins: *common_plugins
+ plugins: [$CI_TOOLKIT]
# Wait for Gradle Wrapper to be validated before running any other jobs
- wait
@@ -47,7 +47,7 @@ steps:
key: wpbuild
command: ".buildkite/commands/beta-build.sh wordpress"
depends_on: wplint
- plugins: *common_plugins
+ plugins: [$CI_TOOLKIT]
notify:
- slack: "#build-and-ship"
@@ -55,7 +55,7 @@ steps:
key: jpbuild
command: ".buildkite/commands/beta-build.sh jetpack"
depends_on: jplint
- plugins: *common_plugins
+ plugins: [$CI_TOOLKIT]
notify:
- slack: "#build-and-ship"
@@ -67,4 +67,4 @@ steps:
- wpbuild
- jpbuild
command: ".buildkite/commands/create-github-release.sh"
- plugins: *common_plugins
+ plugins: [$CI_TOOLKIT]
diff --git a/.buildkite/code-freeze.yml b/.buildkite/code-freeze.yml
index 2f63df2ba25b..0c8e12918ffd 100644
--- a/.buildkite/code-freeze.yml
+++ b/.buildkite/code-freeze.yml
@@ -1,15 +1,18 @@
-# Nodes with values to reuse in the pipeline.
-common_params:
- # Common plugin settings to use with the `plugins` key.
- - &common_plugins
- - automattic/a8c-ci-toolkit#2.17.0
+# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json
+---
+
steps:
- label: "Code Freeze"
- plugins: *common_plugins
+ plugins: [$CI_TOOLKIT]
command: |
- .buildkite/commands/configure-git-for-release-management.sh
+ echo '--- :robot_face: Use bot for git operations'
+ source use-bot-for-git
+ echo '--- :ruby: Setup Ruby Tools'
install_gems
+ echo '--- :snowflake: Start Code Freeze'
bundle exec fastlane code_freeze skip_confirm:true
+ agents:
+ queue: "tumblr-metal"
diff --git a/.buildkite/commands/configure-git-for-release-management.sh b/.buildkite/commands/configure-git-for-release-management.sh
deleted file mode 100755
index eb39be490f66..000000000000
--- a/.buildkite/commands/configure-git-for-release-management.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash -eu
-
-# Git command line client is not configured in Buildkite. Temporarily, we configure it in each step.
-# Later on, we should be able to configure the agent instead.
-curl -L https://api.github.com/meta | jq -r '.ssh_keys | .[]' | sed -e 's/^/github.com /' >> ~/.ssh/known_hosts
-git config --global user.email "mobile+wpmobilebot@automattic.com"
-git config --global user.name "Automattic Release Bot"
-
-# Buildkite is currently using the https url to checkout. We need to override it to be able to use the deploy key.
-git remote set-url origin git@github.com:wordpress-mobile/WordPress-Android.git
diff --git a/.buildkite/complete-code-freeze.yml b/.buildkite/complete-code-freeze.yml
index 5f1b22ee87b2..5898aa2e9285 100644
--- a/.buildkite/complete-code-freeze.yml
+++ b/.buildkite/complete-code-freeze.yml
@@ -1,16 +1,21 @@
-# Nodes with values to reuse in the pipeline.
-common_params:
- # Common plugin settings to use with the `plugins` key.
- - &common_plugins
- - automattic/a8c-ci-toolkit#2.17.0
+# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json
+---
+
steps:
- label: "Complete Code Freeze"
- plugins: *common_plugins
+ plugins: [$CI_TOOLKIT]
command: |
- .buildkite/commands/configure-git-for-release-management.sh
+ echo '--- :robot_face: Use bot for git operations'
+ source use-bot-for-git
+
+ echo '--- :git: Checkout Release Branch'
.buildkite/commands/checkout-release-branch.sh
+ echo '--- :ruby: Setup Ruby Tools'
install_gems
+ echo '--- :snowflake: Complete Code Freeze'
bundle exec fastlane complete_code_freeze skip_confirm:true
+ agents:
+ queue: "tumblr-metal"
diff --git a/.buildkite/finalize-release.yml b/.buildkite/finalize-release.yml
index 988b6f9e6905..0c91008c11f7 100644
--- a/.buildkite/finalize-release.yml
+++ b/.buildkite/finalize-release.yml
@@ -1,18 +1,23 @@
-# Nodes with values to reuse in the pipeline.
-common_params:
- # Common plugin settings to use with the `plugins` key.
- - &common_plugins
- - automattic/a8c-ci-toolkit#2.17.0
+# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json
+---
+
steps:
- label: "Finalize release"
- plugins: *common_plugins
+ plugins: [$CI_TOOLKIT]
command: |
- .buildkite/commands/configure-git-for-release-management.sh
+ echo '--- :robot_face: Use bot for git operations'
+ source use-bot-for-git
+
+ echo '--- :git: Checkout Release Branch'
.buildkite/commands/checkout-release-branch.sh
+ echo '--- :ruby: Setup Ruby Tools'
install_gems
cp gradle.properties-example gradle.properties
+ echo '--- :shipit: Finalize Release'
bundle exec fastlane finalize_release skip_confirm:true
+ agents:
+ queue: "tumblr-metal"
diff --git a/.buildkite/new-beta-release.yml b/.buildkite/new-beta-release.yml
index 3ce0d1503059..db233146ca9a 100644
--- a/.buildkite/new-beta-release.yml
+++ b/.buildkite/new-beta-release.yml
@@ -1,17 +1,20 @@
-# Nodes with values to reuse in the pipeline.
-common_params:
- # Common plugin settings to use with the `plugins` key.
- - &common_plugins
- - automattic/a8c-ci-toolkit#2.17.0
+# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json
+---
+
steps:
- label: "New Beta Release"
- plugins: *common_plugins
+ plugins: [$CI_TOOLKIT]
command: |
- .buildkite/commands/configure-git-for-release-management.sh
+ echo '--- :robot_face: Use bot for git operations'
+ source use-bot-for-git
+ echo '--- :ruby: Setup Ruby Tools'
install_gems
cp gradle.properties-example gradle.properties
+ echo '--- :shipit: New Beta Release'
bundle exec fastlane new_beta_release skip_confirm:true
+ agents:
+ queue: "tumblr-metal"
diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml
index 87d96c8032e3..8dd0c3b34ed1 100644
--- a/.buildkite/pipeline.yml
+++ b/.buildkite/pipeline.yml
@@ -1,10 +1,8 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json
+---
+
# Nodes with values to reuse in the pipeline.
common_params:
- # Common plugin settings to use with the `plugins` key.
- - &ci_toolkit
- automattic/a8c-ci-toolkit#2.18.2
- - &test_collector
- test-collector#v1.8.0
- &test_collector_common_params
files: "buildkite-test-analytics/*.xml"
format: "junit"
@@ -19,7 +17,7 @@ steps:
- label: "Gradle Wrapper Validation"
command: |
validate_gradle_wrapper
- plugins: [*ci_toolkit]
+ plugins: [$CI_TOOLKIT]
# Wait for Gradle Wrapper to be validated before running any other jobs
- wait
@@ -29,11 +27,21 @@ steps:
#################
- group: "šµļøāāļø Linters"
steps:
+ - label: "ā¢ļø Danger - PR Check"
+ command: danger
+ key: danger
+ if: "build.pull_request.id != null"
+ retry:
+ manual:
+ permit_on_passed: true
+ agents:
+ queue: "linter"
+
- label: "šµļø checkstyle"
command: |
cp gradle.properties-example gradle.properties
./gradlew checkstyle
- plugins: [*ci_toolkit]
+ plugins: [$CI_TOOLKIT]
artifact_paths:
- "**/build/reports/checkstyle/checkstyle.*"
@@ -41,7 +49,7 @@ steps:
command: |
cp gradle.properties-example gradle.properties
./gradlew detekt
- plugins: [*ci_toolkit]
+ plugins: [$CI_TOOLKIT]
artifact_paths:
- "**/build/reports/detekt/detekt.html"
@@ -60,7 +68,7 @@ steps:
cp gradle.properties-example gradle.properties
.buildkite/commands/dependency-tree-diff.sh
if: build.pull_request.id != null
- plugins: [*ci_toolkit]
+ plugins: [$CI_TOOLKIT]
#################
# Unit Tests
@@ -70,8 +78,8 @@ steps:
- label: "š¬ Unit Test WordPress"
command: ".buildkite/commands/run-unit-tests.sh wordpress"
plugins:
- - *ci_toolkit
- - *test_collector :
+ - $CI_TOOLKIT
+ - $TEST_COLLECTOR :
<<: *test_collector_common_params
api-token-env-name: "BUILDKITE_ANALYTICS_TOKEN_UNIT_TESTS_WORDPRESS"
artifact_paths:
@@ -80,8 +88,8 @@ steps:
- label: "š¬ Unit Test Processors"
command: ".buildkite/commands/run-unit-tests.sh processors"
plugins:
- - *ci_toolkit
- - *test_collector :
+ - $CI_TOOLKIT
+ - $TEST_COLLECTOR :
<<: *test_collector_common_params
api-token-env-name: "BUILDKITE_ANALYTICS_TOKEN_UNIT_TESTS_PROCESSORS"
artifact_paths:
@@ -90,8 +98,8 @@ steps:
- label: "š¬ Unit Test Image Editor"
command: ".buildkite/commands/run-unit-tests.sh image-editor"
plugins:
- - *ci_toolkit
- - *test_collector :
+ - $CI_TOOLKIT
+ - $TEST_COLLECTOR :
<<: *test_collector_common_params
api-token-env-name: "BUILDKITE_ANALYTICS_TOKEN_UNIT_TESTS_IMAGE_EDITOR"
artifact_paths:
@@ -105,8 +113,8 @@ steps:
- label: ":wordpress: š¬ Instrumented tests"
command: ".buildkite/commands/run-instrumented-tests.sh wordpress"
plugins:
- - *ci_toolkit
- - *test_collector :
+ - $CI_TOOLKIT
+ - $TEST_COLLECTOR :
<<: *test_collector_common_params
api-token-env-name: "BUILDKITE_ANALYTICS_TOKEN_INSTRUMENTED_TESTS_WORDPRESS"
artifact_paths:
@@ -115,8 +123,8 @@ steps:
- label: ":jetpack: š¬ Instrumented tests"
command: ".buildkite/commands/run-instrumented-tests.sh jetpack"
plugins:
- - *ci_toolkit
- - *test_collector :
+ - $CI_TOOLKIT
+ - $TEST_COLLECTOR :
<<: *test_collector_common_params
api-token-env-name: "BUILDKITE_ANALYTICS_TOKEN_INSTRUMENTED_TESTS_JETPACK"
artifact_paths:
@@ -130,9 +138,9 @@ steps:
- label: ":wordpress: :android: Prototype Build"
command: ".buildkite/commands/prototype-build.sh wordpress"
if: build.pull_request.id != null
- plugins: [*ci_toolkit]
+ plugins: [$CI_TOOLKIT]
- label: ":jetpack: :android: Prototype Build"
command: ".buildkite/commands/prototype-build.sh jetpack"
if: build.pull_request.id != null
- plugins: [*ci_toolkit]
+ plugins: [$CI_TOOLKIT]
diff --git a/.buildkite/release-builds.yml b/.buildkite/release-builds.yml
index c15541c25f66..77b20b4647ac 100644
--- a/.buildkite/release-builds.yml
+++ b/.buildkite/release-builds.yml
@@ -1,11 +1,11 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json
+---
+
# This pipeline is meant to be run via the Buildkite API, and is
# only used for release builds
-# Nodes with values to reuse in the pipeline.
-common_params:
- # Common plugin settings to use with the `plugins` key.
- - &common_plugins
- - automattic/a8c-ci-toolkit#2.17.0
+agents:
+ queue: "android"
steps:
#################
@@ -15,7 +15,7 @@ steps:
command: |
validate_gradle_wrapper
priority: 1
- plugins: *common_plugins
+ plugins: [$CI_TOOLKIT]
# Wait for Gradle Wrapper to be validated before running any other jobs
- wait
@@ -51,7 +51,7 @@ steps:
command: ".buildkite/commands/release-build.sh wordpress"
priority: 1
depends_on: wplint
- plugins: *common_plugins
+ plugins: [$CI_TOOLKIT]
notify:
- slack: "#build-and-ship"
@@ -60,7 +60,7 @@ steps:
command: ".buildkite/commands/release-build.sh jetpack"
priority: 1
depends_on: jplint
- plugins: *common_plugins
+ plugins: [$CI_TOOLKIT]
notify:
- slack: "#build-and-ship"
@@ -73,4 +73,4 @@ steps:
- jpbuild
command: ".buildkite/commands/create-github-release.sh"
priority: 1
- plugins: *common_plugins
+ plugins: [$CI_TOOLKIT]
diff --git a/.buildkite/schedules/dependency-analysis.yml b/.buildkite/schedules/dependency-analysis.yml
new file mode 100644
index 000000000000..6ecad5fb00ea
--- /dev/null
+++ b/.buildkite/schedules/dependency-analysis.yml
@@ -0,0 +1,18 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json
+---
+
+agents:
+ queue: "android"
+
+steps:
+ - label: "dependency analysis"
+ command: |
+ echo "--- š Analyzing"
+ cp gradle.properties-example gradle.properties
+ ./gradlew buildHealth
+ plugins: [$CI_TOOLKIT]
+ artifact_paths:
+ - "build/reports/dependency-analysis/build-health-report.*"
+ notify:
+ - slack: "#android-core-notifs"
+ if: build.state == "failed"
diff --git a/.buildkite/shared-pipeline-vars b/.buildkite/shared-pipeline-vars
new file mode 100644
index 000000000000..2dbbc02e7912
--- /dev/null
+++ b/.buildkite/shared-pipeline-vars
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+ # This file is `source`'d before calling `buildkite-agent pipeline upload`, and can be used
+ # to set up some variables that will be interpolated in the `.yml` pipeline before uploading it.
+
+ export CI_TOOLKIT="automattic/a8c-ci-toolkit#3.4.2"
+ export TEST_COLLECTOR="test-collector#v1.10.1"
diff --git a/.buildkite/update-release-notes.yml b/.buildkite/update-release-notes.yml
index 67748b4711aa..1b2ee0a53ae0 100644
--- a/.buildkite/update-release-notes.yml
+++ b/.buildkite/update-release-notes.yml
@@ -1,16 +1,21 @@
-# Nodes with values to reuse in the pipeline.
-common_params:
- # Common plugin settings to use with the `plugins` key.
- - &common_plugins
- - automattic/a8c-ci-toolkit#2.17.0
+# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json
+---
+
steps:
- label: "Update release notes"
- plugins: *common_plugins
+ plugins: [$CI_TOOLKIT]
command: |
- .buildkite/commands/configure-git-for-release-management.sh
+ echo '--- :robot_face: Use bot for git operations'
+ source use-bot-for-git
+
+ echo '--- :git: Checkout Editorial Branch'
.buildkite/commands/checkout-editorial-branch.sh
+ echo '--- :ruby: Setup Ruby Tools'
install_gems
+ echo '--- :memo: Update Release Notes'
bundle exec fastlane update_appstore_strings version:${RELEASE_VERSION}
+ agents:
+ queue: "tumblr-metal"
diff --git a/.configure b/.configure
index d6e9f159bcec..9abd2a6ba5c6 100644
--- a/.configure
+++ b/.configure
@@ -1,7 +1,7 @@
{
"project_name": "WordPress-Android",
"branch": "trunk",
- "pinned_hash": "715a5a119a334ec1ef16b5a6bd77c52094144813",
+ "pinned_hash": "4b536af182cc61c263b07262e41c141f06fd5de6",
"files_to_copy": [
{
"file": "android/WPAndroid/gradle.properties",
diff --git a/.configure-files/gradle.properties.enc b/.configure-files/gradle.properties.enc
index c2c83c9d1071..cfe19b47de6b 100644
Binary files a/.configure-files/gradle.properties.enc and b/.configure-files/gradle.properties.enc differ
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index ec310f03d8f8..12fb366b7d8a 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -32,7 +32,7 @@ Fixes #
-----
-## Testing Checklist:
+## Testing Checklist (strike-out the not-applying and unnecessary ones):
- [ ] WordPress.com sites and self-hosted Jetpack sites.
- [ ] Portrait and landscape orientations.
diff --git a/.github/workflows/run-danger.yml b/.github/workflows/run-danger.yml
index 856ab8cea46d..ec845c47c73a 100644
--- a/.github/workflows/run-danger.yml
+++ b/.github/workflows/run-danger.yml
@@ -1,13 +1,17 @@
-name: ā¢ļø Danger
+name: ā¢ļø Trigger Danger On Buildkite
on:
pull_request:
- types: [opened, reopened, ready_for_review, synchronize, edited, labeled, unlabeled, milestoned, demilestoned]
+ types: [labeled, unlabeled, milestoned, demilestoned]
jobs:
dangermattic:
- # runs on draft PRs only for opened / synchronize events
- if: ${{ (github.event.pull_request.draft == false) || (github.event.pull_request.draft == true && contains(fromJSON('["opened", "synchronize"]'), github.event.action)) }}
- uses: Automattic/dangermattic/.github/workflows/reusable-run-danger.yml@v1.0.0
+ if: ${{ (github.event.pull_request.draft == false) }}
+ uses: Automattic/dangermattic/.github/workflows/reusable-retry-buildkite-step-on-events.yml@v1.1.2
+ with:
+ org-slug: "automattic"
+ pipeline-slug: "wordpress-android"
+ retry-step-key: "danger"
+ build-commit-sha: "${{ github.event.pull_request.head.sha }}"
secrets:
- github-token: ${{ secrets.DANGERMATTIC_GITHUB_TOKEN }}
+ buildkite-api-token: ${{ secrets.TRIGGER_BK_BUILD_TOKEN }}
diff --git a/.github/workflows/submit-gradle-dependencies.yml b/.github/workflows/submit-gradle-dependencies.yml
index c1d2a5bfe5fe..1315fd2f085d 100644
--- a/.github/workflows/submit-gradle-dependencies.yml
+++ b/.github/workflows/submit-gradle-dependencies.yml
@@ -17,8 +17,4 @@ jobs:
java-version: '17'
- run: cp gradle.properties-example gradle.properties
- name: Setup Gradle to generate and submit dependency graphs
- uses: gradle/gradle-build-action@v2
- with:
- dependency-graph: generate-and-submit
- - name: Generate the dependency graph which will be submitted post-job
- run: ./gradlew :WordPress:dependencies --no-configure-on-demand
+ uses: gradle/actions/dependency-submission@v3
diff --git a/.github/workflows/validate-issues.yml b/.github/workflows/validate-issues.yml
index c9899d62603d..01b05ac2ffe2 100644
--- a/.github/workflows/validate-issues.yml
+++ b/.github/workflows/validate-issues.yml
@@ -6,7 +6,7 @@ on:
jobs:
check-labels-on-issues:
- uses: Automattic/dangermattic/.github/workflows/reusable-check-labels-on-issues.yml@v1.0.0
+ uses: Automattic/dangermattic/.github/workflows/reusable-check-labels-on-issues.yml@v1.1.2
with:
label-format-list: '[
"^\[.+\]",
diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml
index 7f921dc3495d..b084c99aaa78 100644
--- a/.idea/checkstyle-idea.xml
+++ b/.idea/checkstyle-idea.xml
@@ -1,18 +1,18 @@
-
-
-
+
+
+
diff --git a/Dangerfile b/Dangerfile
index d439496653f9..ef55a1240632 100644
--- a/Dangerfile
+++ b/Dangerfile
@@ -3,7 +3,8 @@
github.dismiss_out_of_range_messages
# `files: []` forces rubocop to scan all files, not just the ones modified in the PR
-rubocop.lint(files: [], force_exclusion: true, inline_comment: true, fail_on_inline_comment: true, include_cop_names: true)
+# Added a custom `rubocop_cmd` to prevent RuboCop from running using `bundle exec`, which we don't want on the linter agent
+rubocop.lint(files: [], force_exclusion: true, inline_comment: true, fail_on_inline_comment: true, include_cop_names: true, rubocop_cmd: ': | rubocop')
manifest_pr_checker.check_gemfile_lock_updated
diff --git a/Gemfile b/Gemfile
index 995ee1e3ebbb..734bb6e959ab 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,7 +8,8 @@ gem 'nokogiri'
### Fastlane Plugins
-gem 'fastlane-plugin-wpmreleasetoolkit', '~> 9.2'
+gem 'fastlane-plugin-sentry'
+gem 'fastlane-plugin-wpmreleasetoolkit', '~> 11.0'
# gem 'fastlane-plugin-wpmreleasetoolkit', path: '../../release-toolkit'
# gem 'fastlane-plugin-wpmreleasetoolkit', git: 'https://github.com/wordpress-mobile/release-toolkit', branch: ''
diff --git a/Gemfile.lock b/Gemfile.lock
index 651960877cce..9f729ce44ab1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -5,7 +5,7 @@ GEM
base64
nkf
rexml
- activesupport (7.1.3)
+ activesupport (7.1.3.4)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -17,30 +17,29 @@ GEM
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
- artifactory (3.0.15)
+ artifactory (3.0.17)
ast (2.4.2)
atomos (0.1.3)
aws-eventstream (1.3.0)
- aws-partitions (1.894.0)
- aws-sdk-core (3.191.2)
+ aws-partitions (1.944.0)
+ aws-sdk-core (3.197.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
- base64
jmespath (~> 1, >= 1.6.1)
- aws-sdk-kms (1.77.0)
- aws-sdk-core (~> 3, >= 3.191.0)
+ aws-sdk-kms (1.84.0)
+ aws-sdk-core (~> 3, >= 3.197.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.143.0)
- aws-sdk-core (~> 3, >= 3.191.0)
+ aws-sdk-s3 (1.152.3)
+ aws-sdk-core (~> 3, >= 3.197.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
- bigdecimal (3.1.6)
- buildkit (1.5.0)
+ bigdecimal (3.1.8)
+ buildkit (1.6.0)
sawyer (>= 0.6)
chroma (0.2.0)
claide (1.1.0)
@@ -52,7 +51,7 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
- concurrent-ruby (1.2.3)
+ concurrent-ruby (1.3.3)
connection_pool (2.4.1)
cork (0.3.0)
colored2 (~> 3.1)
@@ -69,39 +68,25 @@ GEM
no_proxy_fix
octokit (>= 4.0)
terminal-table (>= 1, < 4)
- danger-dangermattic (1.0.0)
+ danger-dangermattic (1.1.1)
danger (~> 9.4)
- danger-junit (~> 1.0)
danger-plugin-api (~> 1.0)
- danger-rubocop (~> 0.12)
- danger-swiftlint (~> 0.35)
- danger-xcode_summary (~> 1.0)
- rubocop (~> 1.60)
- danger-junit (1.0.2)
- danger (> 2.0)
- ox (~> 2.0)
+ danger-rubocop (~> 0.13)
+ rubocop (~> 1.63)
danger-plugin-api (1.0.0)
danger (> 2.0)
- danger-rubocop (0.12.0)
+ danger-rubocop (0.13.0)
danger
rubocop (~> 1.0)
- danger-swiftlint (0.35.0)
- danger
- rake (> 10)
- thor (~> 1.0.0)
- danger-xcode_summary (1.3.0)
- danger-plugin-api (~> 1.0)
- xcresult (~> 0.2)
declarative (0.0.20)
diffy (3.4.2)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
- drb (2.2.0)
- ruby2_keywords
+ drb (2.2.1)
emoji_regex (3.2.3)
- excon (0.109.0)
+ excon (0.110.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
@@ -132,15 +117,15 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
- fastimage (2.3.0)
- fastlane (2.219.0)
+ fastimage (2.3.1)
+ fastlane (2.220.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
- colored
+ colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
@@ -161,10 +146,10 @@ GEM
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
- optparse (>= 0.1.1)
+ optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
- security (= 0.1.3)
+ security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
@@ -173,8 +158,10 @@ GEM
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
- xcpretty-travis-formatter (>= 0.0.3)
- fastlane-plugin-wpmreleasetoolkit (9.4.0)
+ xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
+ fastlane-plugin-sentry (1.19.0)
+ os (~> 1.1, >= 1.1.4)
+ fastlane-plugin-wpmreleasetoolkit (11.0.2)
activesupport (>= 6.1.7.1)
buildkit (~> 1.5)
chroma (= 0.2.0)
@@ -183,7 +170,7 @@ GEM
git (~> 1.3)
google-cloud-storage (~> 1.31)
java-properties (~> 0.3.0)
- nokogiri (~> 1.11, < 1.16)
+ nokogiri (~> 1.11, < 1.17)
octokit (~> 6.1)
parallel (~> 1.14)
plist (~> 3.1)
@@ -211,12 +198,12 @@ GEM
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
- google-cloud-core (1.6.1)
+ google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
- google-cloud-errors (1.3.1)
+ google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
@@ -232,34 +219,34 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
- http-cookie (1.0.5)
+ http-cookie (1.0.6)
domain_name (~> 0.5)
httpclient (2.8.3)
- i18n (1.14.1)
+ i18n (1.14.5)
concurrent-ruby (~> 1.0)
java-properties (0.3.0)
jmespath (1.6.2)
- json (2.7.1)
- jwt (2.8.0)
+ json (2.7.2)
+ jwt (2.8.1)
base64
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
language_server-protocol (3.17.0.3)
- mini_magick (4.12.0)
+ mini_magick (4.13.0)
mini_mime (1.1.5)
- mini_portile2 (2.8.5)
- minitest (5.22.2)
+ mini_portile2 (2.8.7)
+ minitest (5.23.1)
multi_json (1.15.0)
- multipart-post (2.4.0)
+ multipart-post (2.4.1)
mutex_m (0.2.0)
nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.1)
nkf (0.2.0)
no_proxy_fix (0.1.2)
- nokogiri (1.15.5)
+ nokogiri (1.16.6)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
octokit (6.1.1)
@@ -267,34 +254,34 @@ GEM
sawyer (~> 0.9)
open4 (1.3.4)
options (2.3.2)
- optparse (0.4.0)
+ optparse (0.5.0)
os (1.1.4)
- ox (2.14.17)
- parallel (1.24.0)
- parser (3.3.0.5)
+ parallel (1.25.1)
+ parser (3.3.2.0)
ast (~> 2.4.1)
racc
plist (3.7.1)
- progress_bar (1.3.3)
- highline (>= 1.6, < 3)
+ progress_bar (1.3.4)
+ highline (>= 1.6)
options (~> 2.3.0)
- public_suffix (5.0.4)
- racc (1.7.3)
+ public_suffix (5.0.5)
+ racc (1.8.0)
rainbow (3.1.1)
- rake (13.1.0)
+ rake (13.2.1)
rake-compiler (1.2.7)
rake
rchardet (1.8.0)
- regexp_parser (2.9.0)
+ regexp_parser (2.9.2)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
- rexml (3.2.6)
+ rexml (3.2.9)
+ strscan
rmagick (4.3.0)
rouge (2.0.7)
- rubocop (1.60.2)
+ rubocop (1.64.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
@@ -302,18 +289,18 @@ GEM
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
- rubocop-ast (>= 1.30.0, < 2.0)
+ rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
- rubocop-ast (1.30.0)
- parser (>= 3.2.1.0)
+ rubocop-ast (1.31.3)
+ parser (>= 3.3.1.0)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
sawyer (0.9.2)
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
- security (0.1.3)
+ security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
@@ -322,10 +309,10 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
+ strscan (3.1.0)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
- thor (1.0.1)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
@@ -347,7 +334,6 @@ GEM
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
- xcresult (0.2.1)
PLATFORMS
ruby
@@ -355,7 +341,8 @@ PLATFORMS
DEPENDENCIES
danger-dangermattic (~> 1.0)
fastlane (~> 2)
- fastlane-plugin-wpmreleasetoolkit (~> 9.2)
+ fastlane-plugin-sentry
+ fastlane-plugin-wpmreleasetoolkit (~> 11.0)
nokogiri
rmagick (~> 4.1)
diff --git a/README.md b/README.md
index 6de18b6906d5..b75475e50ee5 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,8 @@ If you're a developer wanting to contribute, read on.
## Build Instructions ##
-1. Make sure you've installed [Android Studio](https://developer.android.com/studio/index.html).
-1. Install npm using [Node Version Manager](https://github.com/nvm-sh/nvm)(nvm), as described in step one from the [Block Editor Quickstart guide](https://developer.wordpress.org/block-editor/tutorials/devenv/#quickstart)
+1. Make sure you've installed [Android Studio](https://developer.android.com/studio).
+1. Install npm using [Node Version Manager](https://github.com/nvm-sh/nvm)(nvm), as described in step one from the [Block Editor Quickstart guide](https://developer.wordpress.org/block-editor/getting-started/devenv/#quickstart)
1. `cd WordPress-Android` to enter the working directory.
1. `cp gradle.properties-example gradle.properties` to set up the sample app credentials file.
1. In Android Studio, open the project from the local repository. This will auto-generate `local.properties` with the SDK location.
@@ -16,40 +16,7 @@ If you're a developer wanting to contribute, read on.
Notes:
-* To use WordPress.com features (login to WordPress.com, access Reader and Stats, etc) you need a WordPress.com OAuth2 ID and secret. Please read the [OAuth2 Authentication](#oauth2-authentication) section.
-* While loading/building the app in Android Studio ignore the prompt to update the gradle plugin version as that will probably introduce build errors. On the other hand, feel free to update if you are planning to work on ensuring the compatibility of the newer version.
-
-
-## OAuth2 Authentication ##
-
-In order to use WordPress.com functions you will need a client ID and
-a client secret key. These details will be used to authenticate your
-application and verify that the API calls being made are valid. You can
-create an application or view details for your existing applications with
-our [WordPress.com applications manager][5].
-
-When creating your application, you should select "Native client" for the application type.
-The "**Website URL**", "**Redirect URLs**", and "**Javascript Origins**" fields are required but not used for
-the mobile apps. Just use "**[https://localhost](https://localhost)**".
-
-Once you've created your application in the [applications manager][5], you'll
-need to edit the `./gradle.properties` file and change the
-`wp.oauth.app_id` and `wp.oauth.app_secret` fields. Then you can compile and
-run the app on a device or an emulator and try to login with a WordPress.com
-account. Note that authenticating to WordPress.com via Google is not supported
-in development builds of the app, only in the official release.
-
-Note that credentials created with our [WordPress.com applications manager][5]
-allow login only and not signup. New accounts must be created using the [official app][1]
-or [on the web](https://wordpress.com/start). Login is restricted to the WordPress.com
-account with which the credentials were created. In other words, if the credentials
-were created with foo@email.com, you will only be able to login with foo@email.com.
-Using another account like bar@email.com will cause the `Client cannot use "password" grant_type` error.
-
-For security reasons, some account-related actions aren't supported for development
-builds when using a WordPress.com account with 2-factor authentication enabled.
-
-Read more about [OAuth2][6] and the [WordPress.com REST endpoint][7].
+* While loading/building the app in Android Studio, ignore the prompt to update the Gradle plugin version, as that will probably introduce build errors. On the other hand, feel free to update if you are planning to work on ensuring the compatibility of the newer version.
## Build and Test ##
@@ -61,6 +28,13 @@ To build, install, and test the project from the command line:
$ ./gradlew :WordPress:testWordPressVanillaDebugUnitTest # assemble, install and run unit tests
$ ./gradlew :WordPress:connectedWordPressVanillaDebugAndroidTest # assemble, install and run Android tests
+## Running the app ##
+
+You can use your own WordPress site for developing and testing the app. If you don't have one, you can create a temporary test site for free at https://jurassic.ninja/.
+On the app start up screen, choose "Enter your existing site address" and enter the URL of your site and your credentials.
+
+Note: Access to WordPress.com features is temporarily disabled in the development environment.
+
## Directory structure ##
.
āāā libs # dependencies used to build debug variants
@@ -118,7 +92,7 @@ in the `libs/` directory comes from external libraries, which might
be covered by a different license compatible with the GPLv2.
[1]: https://play.google.com/store/apps/details?id=org.wordpress.android
-[3]: http://developer.android.com/sdk/installing/studio.html
+[3]: https://developer.android.com/studio
[4]: https://make.wordpress.org/chat/
[5]: https://developer.wordpress.com/apps/
[6]: https://developer.wordpress.com/docs/oauth2/
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 77db9de8a38a..aad8816a97b6 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -1,13 +1,70 @@
*** PLEASE FOLLOW THIS FORMAT: [] []
-24.4
+25.2
+-----
+* [*] Fix ability to update a published post's date [https://github.com/wordpress-mobile/WordPress-Android/pull/21036]
+
+
+25.1
+-----
+* [*] [internal] Block editor: Add onContentUpdate bridge functionality [https://github.com/wordpress-mobile/gutenberg-mobile/pull/20852]
+
+25.0
+-----
+* [*] Fixed a rare crash on Posts List screen [https://github.com/wordpress-mobile/WordPress-Android/pull/20813]
+* [*] Fixed a rare crash on the Login screen [https://github.com/wordpress-mobile/WordPress-Android/pull/20821]
+* [*] Fix a crash that occurs when remove a user [https://github.com/wordpress-mobile/WordPress-Android/pull/20837]
+* [*] Fixed a rare crash on the featured image confirmation dialog [https://github.com/wordpress-mobile/WordPress-Android/pull/20836]
+* [*] Fixed an ANR issue on the Post List screen [https://github.com/wordpress-mobile/WordPress-Android/pull/20833]
+* [*] Fixed a crash that occurs with Blogging Reminders [https://github.com/wordpress-mobile/WordPress-Android/pull/20845]
+* [*] [internal] Block Editor: Upgrade target sdk version to Android API 34 [https://github.com/wordpress-mobile/WordPress-Android/pull/20841]
+* [*] [internal] In-app updates feature [https://github.com/wordpress-mobile/WordPress-Android/pull/20822]
+* [**] [Jetpack-only] Reader: Add a new feed dedicated to tags [https://github.com/wordpress-mobile/WordPress-Android/pull/20812]
+
+24.9
+-----
+* [*] [Jetpack-only] Removed Social section from the detail screen of Total Followers card [https://github.com/wordpress-mobile/WordPress-Android/pull/20763]
+* [***] [Jetpack-only] Reorganized Stats to include updated Traffic and Insights tabs, along with a newly added Subscribers tab to improve subscriber metrics analysis [https://github.com/wordpress-mobile/WordPress-Android/pull/20756]
+* [*] Site picker: Fixed the UI alignment issue in RTL [https://github.com/wordpress-mobile/WordPress-Android/pull/20804]
+
+24.8
+-----
+* [*] [Jetpack-only] Fixed percentage numbers direction on Stats for RTL languages [https://github.com/wordpress-mobile/WordPress-Android/pull/20656]
+
+24.7
+-----
+* [**] Site picker: Added the feature to pin sites, redesigned the UI, and deprecated the Show/Hide sites functionality. [https://github.com/wordpress-mobile/WordPress-Android/pull/20521]
+* [*] [internal] Updates the way the app retrieves the User-Agent request header [https://github.com/wordpress-mobile/WordPress-Android/pull/20603]
+
+24.6
-----
+* [**] Block editor: Highlight text fixes [https://github.com/WordPress/gutenberg/pull/57650]
+* [*] Block editor: Fixes an Aztec editor crash occurring on some occasions when the keyboard suggestions are used [https://github.com/wordpress-mobile/WordPress-Android/pull/20518]
+* [*] [Jetpack-only] Change "ā" symbol with "100%" on stats [https://github.com/wordpress-mobile/WordPress-Android/pull/20564]
+* [**] [internal][Jetpack-only] Reader: introduce "reading preferences", an experimental feature that allows users to customize their Reader post content screen with the color, font, and size that they like the most. [https://github.com/wordpress-mobile/WordPress-Android/pull/20567]
+24.5
+-----
+* [*] [internal] Block editor: Remove code associated to Story block [https://github.com/wordpress-mobile/WordPress-Android/pull/20400]
+* [*] [Jetpack-only] Fixes broken links on some notifications [https://github.com/wordpress-mobile/WordPress-Android/pull/20417]
+* [**] [internal] Block editor: Upgrade React Native to version 0.73.3 [#20167]
+
+24.4
+-----
+* [***] [Jetpack-only] Improved Notifications experience with richer UI elements and interactions [https://github.com/wordpress-mobile/WordPress-Android/pull/20072]
+* [**] [Jetpack-only] Block editor: Introduce VideoPress v5 support, to fix issues using video block with dotcom and Jetpack sites [https://github.com/wordpress-mobile/gutenberg-mobile/pull/6634]
+* [**] [internal] Removed the Stories from the codebase [https://github.com/wordpress-mobile/WordPress-Android/pull/20016]
+* [***] [Jetpack-only] Stats: Introducing Traffic tab, delivering improved graphs, and combining Days/Weeks/Months/Years tabs into one, behind a feature flag. [https://github.com/wordpress-mobile/WordPress-Android/pull/19942]
+* [*] Block editor: Prevent crash when autoscrolling to blocks [https://github.com/WordPress/gutenberg/pull/59110]
+* [*] Block editor: Remove opacity change when images are being uploaded [https://github.com/WordPress/gutenberg/pull/59264]
+* [*] Block editor: Media & Text blocks correctly show an error message when the attached video upload fails [https://github.com/WordPress/gutenberg/pull/59288]
+* [*] [Jetpack-only] Themes: Filters themes that are not available due to tier to prevent activation errors. [https://github.com/wordpress-mobile/WordPress-Android/pull/20425]
24.3
-----
* [**] Added support to use third-party passkey providers and other devices passkeys as a WordPress.com login option [https://github.com/wordpress-mobile/WordPress-Android/pull/20174]
* [*] [Jetpack-only] Fix the visibility issue with the menu button on the stats [https://github.com/wordpress-mobile/WordPress-Android/pull/20175]
+* [*] [internal][WordPress-only] Updates Jetpack banners and badges copy for consistency [https://github.com/wordpress-mobile/WordPress-Android/pull/20123]
24.2
-----
diff --git a/WordPress/build.gradle b/WordPress/build.gradle
index 1a7c673ac696..8ebe0ab44ae4 100644
--- a/WordPress/build.gradle
+++ b/WordPress/build.gradle
@@ -1,10 +1,10 @@
+import io.sentry.android.gradle.extensions.InstrumentationFeature
import se.bjurr.violations.comments.github.plugin.gradle.ViolationCommentsToGitHubTask
import se.bjurr.violations.lib.model.SEVERITY
plugins {
id "com.android.application"
id "org.jetbrains.kotlin.android"
- id "org.jetbrains.kotlin.kapt"
id "org.jetbrains.kotlin.plugin.parcelize"
id "org.jetbrains.kotlin.plugin.allopen"
id "io.sentry.android.gradle"
@@ -12,19 +12,24 @@ plugins {
id "com.google.gms.google-services"
id "com.google.dagger.hilt.android"
id "org.jetbrains.kotlinx.kover"
+ id "com.google.devtools.ksp"
}
sentry {
tracingInstrumentation {
enabled = true
- features = [io.sentry.android.gradle.extensions.InstrumentationFeature.DATABASE]
- logcat {
- enabled = false
- }
- }
- autoInstallation {
- enabled = false
- }
+ features = [InstrumentationFeature.DATABASE]
+ logcat.enabled = false
+ }
+ autoInstallation.enabled = false
+ includeSourceContext = true
+ autoUploadSourceContext = true
+ includeDependenciesReport = false
+ /* Sentry won't send source context or add performance instrumentations for debug builds
+ so we can save build times. Sending events will still work in debug builds
+ (if enabled in WPCrashLoggingDataProvider).
+ */
+ ignoredBuildTypes = ["debug"]
}
repositories {
@@ -38,8 +43,8 @@ repositories {
includeGroup "org.wordpress.gutenberg-mobile"
includeGroupByRegex "org.wordpress.react-native-libraries.*"
includeGroup "com.automattic"
- includeGroup "com.automattic.stories"
includeGroup "com.automattic.tracks"
+ includeGroup "com.gravatar"
}
}
maven {
@@ -113,7 +118,6 @@ android {
buildConfigField "boolean", "READER_COMMENTS_MODERATION", "false"
buildConfigField "boolean", "SITE_INTENT_QUESTION", "true"
buildConfigField "boolean", "SITE_NAME", "false"
- buildConfigField "boolean", "LANDING_SCREEN_REVAMP", "true"
buildConfigField "boolean", "LAND_ON_THE_EDITOR", "false"
buildConfigField "boolean", "QRCODE_AUTH_FLOW", "false"
buildConfigField "boolean", "BETA_SITE_DESIGNS", "false"
@@ -142,9 +146,15 @@ android {
buildConfigField "boolean", "PLANS_IN_SITE_CREATION", "false"
buildConfigField "boolean", "READER_IMPROVEMENTS", "false"
buildConfigField "boolean", "BLOGANUARY_DASHBOARD_NUDGE", "false"
- buildConfigField "boolean", "IN_APP_REVIEWS", "false"
buildConfigField "boolean", "DYNAMIC_DASHBOARD_CARDS", "false"
- buildConfigField "boolean", "STATS_TRAFFIC_TAB", "false"
+ buildConfigField "boolean", "STATS_TRAFFIC_SUBSCRIBERS_TABS", "false"
+ buildConfigField "boolean", "READER_DISCOVER_NEW_ENDPOINT", "false"
+ buildConfigField "boolean", "READER_READING_PREFERENCES", "false"
+ buildConfigField "boolean", "READER_READING_PREFERENCES_FEEDBACK", "false"
+ buildConfigField "boolean", "READER_TAGS_FEED", "false"
+ buildConfigField "boolean", "READER_ANNOUNCEMENT_CARD", "false"
+ buildConfigField "boolean", "VOICE_TO_CONTENT", "false"
+ buildConfigField "boolean", "READER_FLOATING_BUTTON", "false"
// Override these constants in jetpack product flavor to enable/ disable features
buildConfigField "boolean", "ENABLE_SITE_CREATION", "true"
@@ -159,6 +169,8 @@ android {
buildConfigField "boolean", "BLAZE_MANAGE_CAMPAIGNS", "false"
buildConfigField "boolean", "DASHBOARD_PERSONALIZATION", "false"
buildConfigField "boolean", "ENABLE_SITE_MONITORING", "false"
+ buildConfigField "boolean", "SYNC_PUBLISHING", "false"
+ buildConfigField "boolean", "ENABLE_IN_APP_UPDATES", "false"
manifestPlaceholders = [magicLinkScheme:"wordpress"]
}
@@ -336,16 +348,12 @@ static def addBuildConfigFieldsFromPrefixedProperties(variant, properties, prefi
variant.buildConfigField "String", it.key.toUpperCase(), "\"${it.value}\""
}
}
-kapt {
- // Enable to infer error types in stubs (see: https://kotlinlang.org/docs/kapt.html#non-existent-type-correction)
- correctErrorTypes true
-}
dependencies {
- implementation 'androidx.webkit:webkit:1.10.0'
+ implementation 'androidx.webkit:webkit:1.11.0'
implementation "androidx.navigation:navigation-compose:$androidxComposeNavigationVersion"
compileOnly project(path: ':libs:annotations')
- kapt project(':libs:processors')
+ ksp project(':libs:processors')
implementation (project(path:':libs:networking')) {
exclude group: "com.android.volley"
exclude group: 'org.wordpress', module: 'utils'
@@ -357,7 +365,7 @@ dependencies {
implementation (project(path:':libs:editor')) {
exclude group: 'org.wordpress', module: 'utils'
}
- implementation("$gradle.ext.fluxCBinaryPath") {
+ implementation("$gradle.ext.fluxCBinaryPath:$wordPressFluxCVersion") {
version {
strictly wordPressFluxCVersion
}
@@ -365,7 +373,7 @@ dependencies {
exclude group: 'org.wordpress', module: 'utils'
exclude group: 'com.android.support', module: 'support-annotations'
}
- implementation ("$gradle.ext.wputilsBinaryPath") {
+ implementation ("$gradle.ext.wputilsBinaryPath:$wordPressUtilsVersion") {
version {
strictly wordPressUtilsVersion
}
@@ -375,13 +383,8 @@ dependencies {
exclude group: 'org.wordpress', module: 'utils'
}
implementation "$gradle.ext.aboutAutomatticBinaryPath:$automatticAboutVersion"
- implementation ("$gradle.ext.storiesAndroidPath:$automatticStoriesVersion") {
- exclude group: 'androidx.navigation', module: 'navigation-fragment-ktx'
- exclude group: 'androidx.navigation', module: 'navigation-ui-ktx'
- }
- implementation "$gradle.ext.storiesAndroidMp4ComposePath:$automatticStoriesVersion"
- implementation("$gradle.ext.tracksBinaryPath") {
+ implementation("$gradle.ext.tracksBinaryPath:$automatticTracksVersion") {
version {
strictly automatticTracksVersion
}
@@ -391,6 +394,10 @@ dependencies {
exclude group: 'com.mcxiaoke.volley'
}
implementation "org.wordpress:persistentedittext:$wordPressPersistentEditTextVersion"
+ implementation "$gradle.ext.gravatarBinaryPath:$gravatarVersion"
+
+ implementation "com.google.android.play:app-update:$googlePlayInAppUpdateVersion"
+ implementation "com.google.android.play:app-update-ktx:$googlePlayInAppUpdateVersion"
implementation "androidx.arch.core:core-common:$androidxArchCoreVersion"
implementation "androidx.arch.core:core-runtime:$androidxArchCoreVersion"
@@ -441,7 +448,6 @@ dependencies {
implementation "com.github.chrisbanes:PhotoView:$chrisbanesPhotoviewVersion"
implementation "org.greenrobot:eventbus:$eventBusVersion"
implementation "org.greenrobot:eventbus-java:$eventBusVersion"
- implementation "com.squareup.okio:okio:$squareupOkioVersion"
implementation "com.squareup.retrofit2:retrofit:$squareupRetrofitVersion"
implementation "org.apache.commons:commons-text:$apacheCommonsTextVersion"
implementation "com.airbnb.android:lottie:$lottieVersion"
@@ -452,7 +458,7 @@ dependencies {
exclude group: 'androidx.appcompat', module: 'appcompat'
}
implementation "com.github.bumptech.glide:glide:$glideVersion"
- kapt "com.github.bumptech.glide:compiler:$glideVersion"
+ ksp "com.github.bumptech.glide:ksp:$glideVersion"
implementation "com.github.bumptech.glide:volley-integration:$glideVersion"
implementation "com.github.indexos.media-for-mobile:domain:$indexosMediaForMobileVersion"
implementation "com.github.indexos.media-for-mobile:android:$indexosMediaForMobileVersion"
@@ -466,11 +472,9 @@ dependencies {
exclude group: 'com.android.support', module: 'support-annotations'
}
implementation "com.google.dagger:dagger-android-support:$gradle.ext.daggerVersion"
- kapt "com.google.dagger:dagger-android-processor:$gradle.ext.daggerVersion"
+ ksp "com.google.dagger:dagger-android-processor:$gradle.ext.daggerVersion"
implementation "com.google.dagger:hilt-android:$gradle.ext.daggerVersion"
- kapt "com.google.dagger:hilt-compiler:$gradle.ext.daggerVersion"
-
- testImplementation "$gradle.ext.storiesAndroidPhotoEditorPath:$automatticStoriesVersion"
+ ksp "com.google.dagger:hilt-compiler:$gradle.ext.daggerVersion"
testImplementation("androidx.arch.core:core-testing:$androidxArchCoreVersion", {
exclude group: 'com.android.support', module: 'support-compat'
@@ -478,7 +482,6 @@ dependencies {
exclude group: 'com.android.support', module: 'support-core-utils'
})
testImplementation "junit:junit:$junitVersion"
- testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$gradle.ext.kotlinVersion"
testImplementation "org.assertj:assertj-core:$assertjVersion"
@@ -486,7 +489,7 @@ dependencies {
androidTestImplementation project(path:':libs:mocks')
- androidTestImplementation "org.mockito:mockito-android:$mockitoVersion"
+ androidTestImplementation "org.mockito:mockito-android:$mockitoAndroidVersion"
androidTestImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
androidTestImplementation "com.squareup.okhttp3:mockwebserver:$squareupMockWebServerVersion"
androidTestImplementation "androidx.test.uiautomator:uiautomator:$androidxTestUiAutomatorVersion"
@@ -525,7 +528,7 @@ dependencies {
androidTestImplementation (name:'cloudtestingscreenshotter_lib', ext:'aar') // Screenshots on Firebase Cloud Testing
androidTestImplementation "androidx.work:work-testing:$androidxWorkManagerVersion"
androidTestImplementation "com.google.dagger:hilt-android-testing:$gradle.ext.daggerVersion"
- kaptAndroidTest "com.google.dagger:hilt-android-compiler:$gradle.ext.daggerVersion"
+ kspAndroidTest "com.google.dagger:hilt-android-compiler:$gradle.ext.daggerVersion"
// Enables Java 8+ API desugaring support
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$androidDesugarVersion"
lintChecks "org.wordpress:lint:$wordPressLintVersion"
@@ -569,6 +572,8 @@ dependencies {
// Cascade - Compose nested menu
implementation "me.saket.cascade:cascade-compose:2.3.0"
+ implementation "com.automattic.tracks:crashlogging:$automatticTracksVersion"
+
// - Flipper
debugImplementation ("com.facebook.flipper:flipper:$flipperVersion") {
exclude group:'org.jetbrains.kotlinx', module:'kotlinx-serialization-json-jvm'
@@ -724,3 +729,26 @@ if (project.hasProperty("debugStoreFile")) {
}
}
}
+
+// Copy React Native JavaScript bundle and source map so they can be upload it to the Crash logging
+// service during the build process.
+android {
+ applicationVariants.configureEach { variant ->
+ def variantAssets = variant.mergeAssetsProvider.get().outputDir.get()
+
+ tasks.register("delete${variant.name.capitalize()}ReactNativeBundleSourceMap", Delete) {
+ delete(fileTree(dir: variantAssets, includes: ['**/*.bundle.map']))
+ }
+
+ tasks.register("copy${variant.name.capitalize()}ReactNativeBundleSourceMap", Copy) {
+ from(variantAssets)
+ into("${buildDir}/react-native-bundle-source-map")
+ include("*.bundle", "*.bundle.map")
+ finalizedBy("delete${variant.name.capitalize()}ReactNativeBundleSourceMap")
+ }
+
+ variant.mergeAssetsProvider.configure {
+ finalizedBy("copy${variant.name.capitalize()}ReactNativeBundleSourceMap")
+ }
+ }
+}
diff --git a/WordPress/jetpack_metadata/PlayStoreStrings.po b/WordPress/jetpack_metadata/PlayStoreStrings.po
index 46c0d8b9741a..4fe12f2066fa 100644
--- a/WordPress/jetpack_metadata/PlayStoreStrings.po
+++ b/WordPress/jetpack_metadata/PlayStoreStrings.po
@@ -10,20 +10,23 @@ msgstr ""
"X-Generator: VsCode\n"
"Project-Id-Version: Jetpack - Apps - Android - Release Notes\n"
-msgctxt "release_note_243"
+msgctxt "release_note_251"
msgid ""
-"24.3:\n"
-"Log in to your site with Google credentials, physical devices, and third-party passkeys. Donāt worry, the siteās still locked down tighter than the Colonelās secret recipe.\n"
-"We hid the menu icon on Stats screens so itās only visible from the Insights tab. Ninja mode: activated.\n"
+"25.1:\n"
+"We have no updates in a house.\n"
+"We have no updates with a mouse.\n"
+"We have no updates here or there.\n"
+"We have no updates anywhere.\n"
+"We have no updates to report.\n"
+"If you need help, ask tech support.\n"
msgstr ""
-msgctxt "release_note_242"
+msgctxt "release_note_250"
msgid ""
-"24.2:\n"
-"- We added a new look and feel for content navigation and filtering.\n"
-"- Images and other media wonāt āblinkā during upload.\n"
-"- The editor wonāt crash when youāre working on a large post.\n"
-"- We added new Site Monitoring menu items like metrics, PHP logs, and web server logs.\n"
+"25.0:\n"
+"The Tags feed is live! You can now see content with specific tags, all in one place. Tag, youāre it.\n"
+"\n"
+"We fixed assorted crashes on the login and Posts List screens, as well as actions associated with blogging reminders, feature images, and user removal. Less crashing? How smashing.\n"
msgstr ""
#. translators: Release notes for this version to be displayed in the Play Store. Limit to 500 characters including spaces and commas!
diff --git a/WordPress/jetpack_metadata/release_notes.txt b/WordPress/jetpack_metadata/release_notes.txt
index 17d689ccd4c5..49c9b48be074 100644
--- a/WordPress/jetpack_metadata/release_notes.txt
+++ b/WordPress/jetpack_metadata/release_notes.txt
@@ -1,2 +1,6 @@
-Log in to your site with Google credentials, physical devices, and third-party passkeys. Donāt worry, the siteās still locked down tighter than the Colonelās secret recipe.
-We hid the menu icon on Stats screens so itās only visible from the Insights tab. Ninja mode: activated.
+We have no updates in a house.
+We have no updates with a mouse.
+We have no updates here or there.
+We have no updates anywhere.
+We have no updates to report.
+If you need help, ask tech support.
diff --git a/WordPress/metadata/PlayStoreStrings.po b/WordPress/metadata/PlayStoreStrings.po
index 9e2282892dec..812e2a4a80df 100644
--- a/WordPress/metadata/PlayStoreStrings.po
+++ b/WordPress/metadata/PlayStoreStrings.po
@@ -10,18 +10,21 @@ msgstr ""
"X-Generator: VsCode\n"
"Project-Id-Version: Release Notes & Play Store Descriptions\n"
-msgctxt "release_note_243"
+msgctxt "release_note_251"
msgid ""
-"24.3:\n"
-"You can log in to your WordPress website using Google credentials, physical devices, and other third-party passkeys. Donāt worry, your site is still locked down tighter than the Colonelās secret recipe.\n"
+"25.1:\n"
+"We have no updates in a house.\n"
+"We have no updates with a mouse.\n"
+"We have no updates here or there.\n"
+"We have no updates anywhere.\n"
+"We have no updates to report.\n"
+"If you need help, ask tech support.\n"
msgstr ""
-msgctxt "release_note_242"
+msgctxt "release_note_250"
msgid ""
-"24.2:\n"
-"We fixed an issue that made images and other media blink away while being uploaded. Presto, no more disappearing act.\n"
-"\n"
-"The editor wonāt crash anymore when youāre working on large posts. Thatās right, weāve saved your drafts and your sanity.\n"
+"25.0:\n"
+"We fixed assorted crashes on the login and Posts List screens, as well as actions associated with blogging reminders, feature images, and user removal. Less crashing? How smashing.\n"
msgstr ""
#. translators: Release notes for this version to be displayed in the Play Store. Limit to 500 characters including spaces and commas!
diff --git a/WordPress/metadata/release_notes.txt b/WordPress/metadata/release_notes.txt
index 89c2c07ee6ac..49c9b48be074 100644
--- a/WordPress/metadata/release_notes.txt
+++ b/WordPress/metadata/release_notes.txt
@@ -1 +1,6 @@
-You can log in to your WordPress website using Google credentials, physical devices, and other third-party passkeys. Donāt worry, your site is still locked down tighter than the Colonelās secret recipe.
+We have no updates in a house.
+We have no updates with a mouse.
+We have no updates here or there.
+We have no updates anywhere.
+We have no updates to report.
+If you need help, ask tech support.
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/UserAgentTest.java b/WordPress/src/androidTest/java/org/wordpress/android/UserAgentTest.java
index 8e93118c2d07..79ec7c5bec52 100644
--- a/WordPress/src/androidTest/java/org/wordpress/android/UserAgentTest.java
+++ b/WordPress/src/androidTest/java/org/wordpress/android/UserAgentTest.java
@@ -1,7 +1,13 @@
package org.wordpress.android;
+import android.webkit.WebSettings;
+
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.wordpress.android.fluxc.network.UserAgent;
+
+import javax.inject.Inject;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNotNull;
@@ -24,14 +30,21 @@ public class UserAgentTest {
@Rule(order = 1)
public InitializationRule initRule = new InitializationRule();
+ @Inject UserAgent mUserAgent;
+
+ @Before
+ public void setUp() {
+ hiltRule.inject();
+ }
+
@Test
public void testGetUserAgentAndGetDefaultUserAgent() {
- String userAgent = WordPress.getUserAgent();
+ String userAgent = mUserAgent.toString();
assertNotNull("User-Agent must be set", userAgent);
assertTrue("User-Agent must not be an empty string", userAgent.length() > 0);
assertTrue("User-Agent must contain app name substring", userAgent.contains(USER_AGENT_APPNAME));
- String defaultUserAgent = WordPress.getDefaultUserAgent();
+ String defaultUserAgent = WebSettings.getDefaultUserAgent(AppInitializer.Companion.getContext());
assertNotNull("Default User-Agent must be set", defaultUserAgent);
assertTrue("Default User-Agent must not be an empty string", defaultUserAgent.length() > 0);
assertFalse("Default User-Agent must not contain app name", defaultUserAgent.contains(USER_AGENT_APPNAME));
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/StatsGranularTabsTest.kt b/WordPress/src/androidTest/java/org/wordpress/android/e2e/StatsGranularTabsTest.kt
new file mode 100644
index 000000000000..ada96b9c90c6
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/StatsGranularTabsTest.kt
@@ -0,0 +1,62 @@
+package org.wordpress.android.e2e
+
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.matcher.ViewMatchers
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.wordpress.android.BuildConfig
+import org.wordpress.android.R
+import org.wordpress.android.e2e.pages.MySitesPage
+import org.wordpress.android.support.BaseTest
+import org.wordpress.android.support.ComposeEspressoLink
+import org.wordpress.android.support.WPSupportUtils
+import org.wordpress.android.util.StatsKeyValueData
+import org.wordpress.android.util.StatsMocksReader
+import org.wordpress.android.util.StatsVisitsData
+
+@HiltAndroidTest
+class StatsGranularTabsTest : BaseTest() {
+ @Before
+ fun setUp() {
+ assumeTrue(BuildConfig.IS_JETPACK_APP)
+ ComposeEspressoLink().unregister()
+ logoutIfNecessary()
+ wpLogin()
+ }
+
+ @After
+ fun tearDown() {
+ // "tabLayout" is a Tab switcher for stats.
+ // We need to leave stats at the end of test.
+ if (WPSupportUtils.isElementDisplayed(Espresso.onView(ViewMatchers.withId(R.id.tabLayout)))) {
+ Espresso.pressBack()
+ }
+ }
+
+ @Test
+ fun e2eAllDayStatsLoad() {
+ val todayVisits = StatsVisitsData("97", "28", "14", "11")
+ val postsList: List = StatsMocksReader().readDayTopPostsToList()
+ val referrersList: List = StatsMocksReader().readDayTopReferrersToList()
+ val clicksList: List = StatsMocksReader().readDayClicksToList()
+ val authorsList: List = StatsMocksReader().readDayAuthorsToList()
+ val countriesList: List = StatsMocksReader().readDayCountriesToList()
+ val videosList: List = StatsMocksReader().readDayVideoPlaysToList()
+ val downloadsList: List = StatsMocksReader().readDayFileDownloadsToList()
+ MySitesPage()
+ .go()
+ .goToStats()
+ .openDayStats()
+ .assertVisits(todayVisits)
+ .scrollToPosts().assertPosts(postsList)
+ .scrollToReferrers().assertReferrers(referrersList)
+ .scrollToClicks().assertClicks(clicksList)
+ .scrollToAuthors().assertAuthors(authorsList)
+ .scrollToCountries().assertCountries(countriesList)
+ .scrollToVideos().assertVideos(videosList)
+ .scrollToFileDownloads().assertDownloads(downloadsList)
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/StatsTests.kt b/WordPress/src/androidTest/java/org/wordpress/android/e2e/StatsTests.kt
index 9cb69659d814..77a26a4175b3 100644
--- a/WordPress/src/androidTest/java/org/wordpress/android/e2e/StatsTests.kt
+++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/StatsTests.kt
@@ -4,9 +4,8 @@ import androidx.test.espresso.Espresso
import androidx.test.espresso.matcher.ViewMatchers
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
-import org.junit.Assume.assumeTrue
+import org.junit.Assume
import org.junit.Before
-import org.junit.Ignore
import org.junit.Test
import org.wordpress.android.BuildConfig
import org.wordpress.android.R
@@ -14,17 +13,14 @@ import org.wordpress.android.e2e.pages.MySitesPage
import org.wordpress.android.support.BaseTest
import org.wordpress.android.support.ComposeEspressoLink
import org.wordpress.android.support.WPSupportUtils
-import org.wordpress.android.util.StatsKeyValueData
-import org.wordpress.android.util.StatsMocksReader
-import org.wordpress.android.util.StatsVisitsData
+import org.wordpress.android.wiremock.WireMockStub
+import org.wordpress.android.wiremock.WireMockUrlPath
@HiltAndroidTest
-class StatsTests : BaseTest() {
+class StatsTests : BaseTest(listOf(WireMockStub(urlPath = WireMockUrlPath.FEATURE_RESPONSE, fileName = "new-stats-feature-response.json"))) {
@Before
fun setUp() {
- // We're not running Stats tests for JP.
- // See https://github.com/wordpress-mobile/WordPress-Android/issues/18065
- assumeTrue(!BuildConfig.IS_JETPACK_APP)
+ Assume.assumeTrue(BuildConfig.IS_JETPACK_APP)
ComposeEspressoLink().unregister()
logoutIfNecessary()
wpLogin()
@@ -39,28 +35,11 @@ class StatsTests : BaseTest() {
}
}
- @Ignore("Will be taken care of in a future PR - scrollToPosts is not working")
@Test
fun e2eAllDayStatsLoad() {
- val todayVisits = StatsVisitsData("97", "28", "14", "11")
- val postsList: List = StatsMocksReader().readDayTopPostsToList()
- val referrersList: List = StatsMocksReader().readDayTopReferrersToList()
- val clicksList: List = StatsMocksReader().readDayClicksToList()
- val authorsList: List = StatsMocksReader().readDayAuthorsToList()
- val countriesList: List = StatsMocksReader().readDayCountriesToList()
- val videosList: List = StatsMocksReader().readDayVideoPlaysToList()
- val downloadsList: List = StatsMocksReader().readDayFileDownloadsToList()
MySitesPage()
.go()
.goToStats()
- .openDayStats()
- .assertVisits(todayVisits)
- .scrollToPosts().assertPosts(postsList)
- .scrollToReferrers().assertReferrers(referrersList)
- .scrollToClicks().assertClicks(clicksList)
- .scrollToAuthors().assertAuthors(authorsList)
- .scrollToCountries().assertCountries(countriesList)
- .scrollToVideos().assertVideos(videosList)
- .scrollToFileDownloads().assertDownloads(downloadsList)
+ .hasNewStatTabs()
}
}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/LandingPage.kt b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/LandingPage.kt
index 6cacf61c1e1b..b19f16ea543d 100644
--- a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/LandingPage.kt
+++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/LandingPage.kt
@@ -4,41 +4,27 @@ import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
-import org.wordpress.android.BuildConfig
import org.wordpress.android.R
import org.wordpress.android.support.ComposeEspressoLink
-import org.wordpress.android.support.WPSupportUtils.clickOn
import org.wordpress.android.support.WPSupportUtils.getTranslatedString
import org.wordpress.android.ui.compose.TestTags
object LandingPage {
- private const val isNewUiEnabled = BuildConfig.IS_JETPACK_APP || BuildConfig.LANDING_SCREEN_REVAMP
-
@JvmStatic
fun tapContinueWithWpCom(composeTestRule: ComposeTestRule) {
- if (isNewUiEnabled) {
- // New UI - See LoginPrologueRevampedFragment
- composeTestRule
- .onNodeWithTag(TestTags.BUTTON_WPCOM_AUTH)
- .performClick()
- } else {
- // Old UI - See LoginPrologueFragment
- clickOn(R.id.continue_with_wpcom_button)
- }
+ // New UI - See LoginPrologueRevampedFragment
+ composeTestRule
+ .onNodeWithTag(TestTags.BUTTON_WPCOM_AUTH)
+ .performClick()
ComposeEspressoLink().unregister()
}
@JvmStatic
fun tapEnterYourSiteAddress(composeTestRule: ComposeTestRule) {
- if (isNewUiEnabled) {
- // New UI - See LoginPrologueRevampedFragment
- composeTestRule
- .onNodeWithText(getTranslatedString(R.string.enter_your_site_address))
- .performClick()
- } else {
- // Old UI - See LoginPrologueFragment
- clickOn(R.id.enter_your_site_address_button)
- }
+ // New UI - See LoginPrologueRevampedFragment
+ composeTestRule
+ .onNodeWithText(getTranslatedString(R.string.enter_your_site_address))
+ .performClick()
}
}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/MySitesPage.kt b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/MySitesPage.kt
index 8cce56e7b95e..ba549724ea11 100644
--- a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/MySitesPage.kt
+++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/MySitesPage.kt
@@ -63,15 +63,7 @@ class MySitesPage {
fun startNewSite() {
switchSite()
- // If the device has a narrower display, the menu_add is hidden in the overflow
- if (WPSupportUtils.isElementDisplayed(R.id.menu_add)) {
- WPSupportUtils.clickOn(R.id.menu_add)
- } else {
- // open the overflow and then click on the item with text
- Espresso.openActionBarOverflowOrOptionsMenu(ApplicationProvider.getApplicationContext())
- Espresso.onView(ViewMatchers.withText(WPSupportUtils.getTranslatedString(R.string.site_picker_add_site)))
- .perform(ViewActions.click())
- }
+ WPSupportUtils.clickOn(R.id.button_add_site)
}
fun goToSettings() {
@@ -149,7 +141,7 @@ class MySitesPage {
val statsButton = Espresso.onView(
Matchers.allOf(
ViewMatchers.withText(R.string.stats),
- ViewMatchers.withId(R.id.my_site_item_primary_text)
+ ViewMatchers.withId(R.id.quick_link_item)
)
)
WPSupportUtils.clickOn(statsButton)
@@ -158,7 +150,7 @@ class MySitesPage {
WPSupportUtils.waitForElementToBeDisplayedWithoutFailure(R.id.tabLayout)
// Wait for the stats to load
- WPSupportUtils.idleFor(8000)
+ WPSupportUtils.idleFor(4000)
return StatsPage()
}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/StatsPage.kt b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/StatsPage.kt
index 0f515bfdc26a..6096099be7ee 100644
--- a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/StatsPage.kt
+++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/StatsPage.kt
@@ -1,16 +1,37 @@
package org.wordpress.android.e2e.pages
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
import org.hamcrest.Matchers
import org.wordpress.android.R
import org.wordpress.android.support.WPSupportUtils
+import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel
import org.wordpress.android.util.StatsKeyValueData
import org.wordpress.android.util.StatsVisitsData
class StatsPage {
+ /**
+ * Matcher to check that the right tabs exist.
+ */
+ fun hasNewStatTabs(): StatsPage {
+ Espresso.onView(
+ Matchers.allOf(
+ ViewMatchers.isDescendantOfA(ViewMatchers.withId(R.id.tabLayout)),
+ ViewMatchers.withText("Traffic")
+ )
+ ).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
+ Espresso.onView(
+ Matchers.allOf(
+ ViewMatchers.isDescendantOfA(ViewMatchers.withId(R.id.tabLayout)),
+ ViewMatchers.withText("Insights")
+ )
+ ).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
+ return this
+ }
fun openDayStats(): StatsPage {
val daysStatsTab = Espresso.onView(
Matchers.allOf(
@@ -31,37 +52,37 @@ class StatsPage {
}
fun scrollToPosts(): StatsPage {
- scrollToCard("Posts and Pages")
+ scrollToCard(1, StatsListViewModel.StatsSection.DAYS)
return this
}
fun scrollToReferrers(): StatsPage {
- scrollToCard("Referrers")
+ scrollToCard(2, StatsListViewModel.StatsSection.DAYS)
return this
}
fun scrollToClicks(): StatsPage {
- scrollToCard("Clicks")
+ scrollToCard(3, StatsListViewModel.StatsSection.DAYS)
return this
}
fun scrollToAuthors(): StatsPage {
- scrollToCard("Authors")
+ scrollToCard(4, StatsListViewModel.StatsSection.DAYS)
return this
}
fun scrollToCountries(): StatsPage {
- scrollToCard("Countries")
+ scrollToCard(5, StatsListViewModel.StatsSection.DAYS)
return this
}
fun scrollToVideos(): StatsPage {
- scrollToCard("Videos")
+ scrollToCard(7, StatsListViewModel.StatsSection.DAYS)
return this
}
fun scrollToFileDownloads(): StatsPage {
- scrollToCard("File downloads")
+ scrollToCard(8, StatsListViewModel.StatsSection.DAYS)
return this
}
@@ -96,7 +117,7 @@ class StatsPage {
)
)
)
- cardStructure.check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
+ cardStructure.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
return this
}
@@ -121,7 +142,7 @@ class StatsPage {
)
)
)
- cardStructure.check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
+ cardStructure.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
}
}
@@ -160,15 +181,14 @@ class StatsPage {
return this
}
- private fun scrollToCard(cardHeader: String) {
- val card = Espresso.onView(
- Matchers.allOf(
- ViewMatchers.isDescendantOfA(visibleCoordinatorLayout),
- ViewMatchers.withId(R.id.stats_block_list),
- ViewMatchers.hasDescendant(ViewMatchers.withText(cardHeader))
- )
+ private fun scrollToCard(viewholderPosition: Int, section: StatsListViewModel.StatsSection) {
+ WPSupportUtils.idleFor(2000)
+ Espresso.onView(Matchers.allOf(
+ ViewMatchers.withTagValue(Matchers.`is`(section.name))
+ )).perform(
+ RecyclerViewActions.scrollToPosition(viewholderPosition)
)
- WPSupportUtils.scrollIntoView(R.id.statsPager, card, 0.5.toFloat())
+ WPSupportUtils.idleFor(2000)
}
companion object {
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/networking/GravatarApiTest.java b/WordPress/src/androidTest/java/org/wordpress/android/networking/GravatarApiTest.java
deleted file mode 100644
index 762b808321f9..000000000000
--- a/WordPress/src/androidTest/java/org/wordpress/android/networking/GravatarApiTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package org.wordpress.android.networking;
-
-import android.content.Context;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-
-import javax.inject.Inject;
-
-import static junit.framework.TestCase.assertEquals;
-import static junit.framework.TestCase.assertTrue;
-
-import dagger.hilt.android.testing.HiltAndroidRule;
-import dagger.hilt.android.testing.HiltAndroidTest;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okio.Buffer;
-
-@HiltAndroidTest
-public class GravatarApiTest {
- @Rule
- public HiltAndroidRule hiltRule = new HiltAndroidRule(this);
-
- @Inject Context mContext;
-
- @Before
- public void setUp() {
- hiltRule.inject();
- }
-
- @Test
- public void testGravatarUploadRequest() throws IOException {
- final String fileContent = "abcdefg";
-
- File tempFile = new File(mContext.getCacheDir(), "tempFile.jpg");
- FileOutputStream fos = new FileOutputStream(tempFile);
- fos.write(fileContent.getBytes());
- fos.flush();
- fos.close();
-
- final String email = "a@b.com";
- Request uploadRequest = GravatarApi.prepareGravatarUpload(email, tempFile);
-
- assertEquals("POST", uploadRequest.method());
-
- RequestBody requestBody = uploadRequest.body();
- assertTrue(requestBody.contentType().toString().startsWith("multipart/form-data"));
-
- final Buffer buffer = new Buffer();
- requestBody.writeTo(buffer);
- final String body = buffer.readUtf8();
-
- assertTrue(body.contains("Content-Disposition: form-data; name=\"account\""));
- assertTrue(body.contains("Content-Length: " + email.length()));
- assertTrue(body.contains(email));
-
- assertTrue(body.contains(
- "Content-Disposition: form-data; name=\"filedata\"; filename=\"" + tempFile.getName() + "\""));
- assertTrue(body.contains("Content-Type: multipart/form-data"));
- assertTrue(body.contains(fileContent));
- }
-}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/rules/Retry.kt b/WordPress/src/androidTest/java/org/wordpress/android/rules/Retry.kt
new file mode 100644
index 000000000000..7c9cf7c0b287
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/rules/Retry.kt
@@ -0,0 +1,10 @@
+package org.wordpress.android.rules
+
+/**
+ * Annotation used to denote you want to retry a UI test function.
+ *
+ * @property numberOfTimes the number of times you want to retry the function, with a default of 1.
+ */
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.FUNCTION)
+annotation class Retry(val numberOfTimes: Int = 1)
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/rules/RetryTestRule.kt b/WordPress/src/androidTest/java/org/wordpress/android/rules/RetryTestRule.kt
new file mode 100644
index 000000000000..22eff0fad137
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/rules/RetryTestRule.kt
@@ -0,0 +1,40 @@
+package org.wordpress.android.rules
+
+import android.util.Log
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+const val TAG = "RetryTestRule"
+
+/**
+ * Custom rule used to retry running a test if a problem occurs.
+ */
+class RetryTestRule : TestRule {
+ override fun apply(base: Statement?, description: Description?): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ // We only retry functions that are annotated with @Retry.
+ val retry = description?.getAnnotation(Retry::class.java)
+ if (retry != null) {
+ var lastThrown: Throwable? = null
+ for (i in 0..retry.numberOfTimes) {
+ try {
+ base?.evaluate()
+ return
+ } catch (t: Throwable) {
+ Log.e(TAG, "Test failed to run due to problem on run $i", t)
+ lastThrown = t
+ }
+ }
+ Log.e(TAG, "Could not pass test.")
+ if (lastThrown != null) {
+ throw lastThrown
+ }
+ }
+ // If test function does not have @Retry, run as normal.
+ base?.evaluate()
+ }
+ }
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/support/BaseTest.java b/WordPress/src/androidTest/java/org/wordpress/android/support/BaseTest.java
index e1c967f4029b..7d95f55c8bb2 100644
--- a/WordPress/src/androidTest/java/org/wordpress/android/support/BaseTest.java
+++ b/WordPress/src/androidTest/java/org/wordpress/android/support/BaseTest.java
@@ -1,7 +1,9 @@
package org.wordpress.android.support;
import android.app.Instrumentation;
+import android.util.Log;
+import androidx.annotation.Nullable;
import androidx.compose.ui.test.junit4.ComposeTestRule;
import androidx.test.espresso.accessibility.AccessibilityChecks;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
@@ -10,6 +12,7 @@
import com.fasterxml.jackson.databind.util.ISO8601Utils;
import com.github.jknack.handlebars.Helper;
import com.github.jknack.handlebars.Options;
+import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer;
import com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.DateOffset;
import com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.HandlebarsHelper;
@@ -28,14 +31,18 @@
import org.wordpress.android.e2e.flows.LoginFlow;
import org.wordpress.android.e2e.pages.MePage;
import org.wordpress.android.e2e.pages.MySitesPage;
+import org.wordpress.android.editor.Utils;
import org.wordpress.android.mocks.AndroidNotifier;
import org.wordpress.android.mocks.AssetFileSource;
+import org.wordpress.android.rules.RetryTestRule;
import org.wordpress.android.ui.WPLaunchActivity;
+import org.wordpress.android.wiremock.WireMockStub;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
@@ -52,6 +59,7 @@
import dagger.hilt.android.testing.HiltAndroidRule;
public class BaseTest {
+ static final String TAG = BaseTest.class.getSimpleName();
public static final int WIREMOCK_PORT = 8080;
@Rule(order = 0)
@@ -70,18 +78,48 @@ public class BaseTest {
@Rule(order = 4)
public WireMockRule wireMockRule;
- {
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ @Rule(order = 5)
+ public RetryTestRule retryTestRule = new RetryTestRule();
+
+ public BaseTest() {
+ this(null);
+ }
+ /**
+ * Constructor
+ *
+ * @param wireMockStubs the wiremock stubs to use for this specific test.
+ */
+ public BaseTest(@Nullable final List wireMockStubs) {
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
wireMockRule = new WireMockRule(
options().port(WIREMOCK_PORT)
- .fileSource(new AssetFileSource(instrumentation.getContext().getAssets()))
+ .fileSource(
+ new AssetFileSource(instrumentation.getContext().getAssets())
+ )
+
.extensions(new ResponseTemplateTransformer(true, new HashMap() {
{
put("fnow", new UnlocalizedDateHelper());
}
}))
.notifier(new AndroidNotifier()));
+ if (wireMockStubs != null && !wireMockStubs.isEmpty()) {
+ for (WireMockStub wireMockStub : wireMockStubs) {
+ try {
+ final String result = Utils.getStringFromInputStream(
+ instrumentation.getContext().getClassLoader().getResourceAsStream(
+ wireMockStub.getFileName()
+ )
+ );
+ // This is where we can stub out
+ wireMockRule.stubFor(WireMock.get(WireMock.urlPathMatching(wireMockStub.getUrlPath().getPath()))
+ .willReturn(WireMock.aResponse().withBody(result)));
+ } catch (final Exception exception) {
+ Log.e(TAG, "Problem stubbing endpoint", exception);
+ }
+ }
+ }
}
@Before
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.java b/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.java
deleted file mode 100644
index d52f9ebab469..000000000000
--- a/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.wordpress.android.ui.notifications;
-
-import android.text.SpannableStringBuilder;
-
-import org.junit.Test;
-import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
-
-import static junit.framework.TestCase.assertFalse;
-import static junit.framework.TestCase.assertTrue;
-
-import dagger.hilt.android.testing.HiltAndroidTest;
-
-@HiltAndroidTest
-public class NotificationsUtilsTest {
- @Test
- public void testSpannableHasCharacterAtIndex() {
- SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder("This is only a test.");
-
- assertTrue(NotificationsUtils.spannableHasCharacterAtIndex(spannableStringBuilder, 's', 3));
- assertFalse(NotificationsUtils.spannableHasCharacterAtIndex(spannableStringBuilder, 's', 4));
-
- // Test with bogus params
- assertFalse(NotificationsUtils.spannableHasCharacterAtIndex(null, 'b', -1));
- }
-}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.kt b/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.kt
new file mode 100644
index 000000000000..5dcc36f90040
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.kt
@@ -0,0 +1,118 @@
+package org.wordpress.android.ui.notifications
+
+import android.text.SpannableStringBuilder
+import android.text.style.ClickableSpan
+import android.widget.TextView
+import androidx.test.platform.app.InstrumentationRegistry
+import dagger.hilt.android.testing.HiltAndroidTest
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertNotNull
+import junit.framework.TestCase.assertTrue
+import org.junit.Test
+import org.wordpress.android.fluxc.tools.FormattableContent
+import org.wordpress.android.fluxc.tools.FormattableRange
+import org.wordpress.android.ui.notifications.blocks.NoteBlockClickableSpan
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils
+
+@HiltAndroidTest
+class NotificationsUtilsTest {
+ @Test
+ fun testSpannableHasCharacterAtIndex() {
+ val spannableStringBuilder = SpannableStringBuilder("This is only a test.")
+
+ assertTrue(NotificationsUtils.spannableHasCharacterAtIndex(spannableStringBuilder, 's', 3))
+ assertFalse(NotificationsUtils.spannableHasCharacterAtIndex(spannableStringBuilder, 's', 4))
+
+ // Test with bogus params
+ assertFalse(NotificationsUtils.spannableHasCharacterAtIndex(null, 'b', -1))
+ }
+
+ @Test
+ fun testGetSpannableContentForRangesAndSkipInvalidUrls() {
+ // Create a FormattableContent object
+ val range1 = FormattableRange(indices = listOf(10, 14), url = "https://example.com", type = "a")
+ val range2 = FormattableRange(indices = listOf(5, 20), url = "", type = "a") // invalid url to skip
+ val formattableContent = FormattableContent(
+ text = "This is a test content with a link",
+ ranges = listOf(range1, range2)
+ )
+
+ // Create a TextView object
+ val textView = TextView(InstrumentationRegistry.getInstrumentation().context)
+
+ // Call the method with the created objects
+ val result = NotificationsUtils.getSpannableContentForRanges(formattableContent, textView, false) {}
+
+ // Check the result
+ assertNotNull(result)
+ assertEquals("This is a test content with a link", result.toString())
+
+ // Check if the link is correctly set
+ val spans = result.getSpans(10, 14, ClickableSpan::class.java)
+ assertTrue(spans.size == 1)
+ assertEquals("https://example.com", (spans[0] as NoteBlockClickableSpan).formattableRange.url)
+ }
+
+ @Test
+ fun testGetSpannableContentForRangesWithNoRanges() {
+ // Create a FormattableContent object with no ranges
+ val formattableContent = FormattableContent(text = "This is a test content with no link")
+
+ // Create a TextView object
+ val textView = TextView(InstrumentationRegistry.getInstrumentation().context)
+
+ // Call the method with the created objects
+ val result = NotificationsUtils.getSpannableContentForRanges(formattableContent, textView, false) {}
+
+ // Check the result
+ assertNotNull(result)
+ assertEquals("This is a test content with no link", result.toString())
+
+ // Check if no ClickableSpan is set
+ val spans = result.getSpans(0, result.length, ClickableSpan::class.java)
+ assertTrue(spans.isEmpty())
+ }
+
+ @Test
+ fun testGetSpannableContentForRangesWithInvalidIndex() {
+ // Create a FormattableContent object with a range with an invalid index
+ val range = FormattableRange(indices = listOf(50, 54), url = "https://example.com", type = "a")
+ val formattableContent = FormattableContent(text = "This is a test content", ranges = listOf(range))
+
+ // Create a TextView object
+ val textView = TextView(InstrumentationRegistry.getInstrumentation().context)
+
+ // Call the method with the created objects
+ val result = NotificationsUtils.getSpannableContentForRanges(formattableContent, textView, false) {}
+
+ // Check the result
+ assertNotNull(result)
+ assertEquals("This is a test content", result.toString())
+
+ // Check if no ClickableSpan is set
+ val spans = result.getSpans(0, result.length, ClickableSpan::class.java)
+ assertTrue(spans.isEmpty())
+ }
+
+ @Test
+ fun testGetSpannableContentForRangesWithNullUrl() {
+ // Create a FormattableContent object with a range with a null URL
+ val range = FormattableRange(indices = listOf(10, 14), url = null, type = "a")
+ val formattableContent = FormattableContent(text = "This is a test content with a link", ranges = listOf(range))
+
+ // Create a TextView object
+ val textView = TextView(InstrumentationRegistry.getInstrumentation().context)
+
+ // Call the method with the created objects
+ val result = NotificationsUtils.getSpannableContentForRanges(formattableContent, textView, false) {}
+
+ // Check the result
+ assertNotNull(result)
+ assertEquals("This is a test content with a link", result.toString())
+
+ // Check if no ClickableSpan is set for the range with the null URL
+ val spans = result.getSpans(10, 14, ClickableSpan::class.java)
+ assertTrue(spans.isEmpty())
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/util/WPAvatarUtilsTest.kt b/WordPress/src/androidTest/java/org/wordpress/android/util/WPAvatarUtilsTest.kt
new file mode 100644
index 000000000000..7687059d23e6
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/util/WPAvatarUtilsTest.kt
@@ -0,0 +1,54 @@
+package org.wordpress.android.util
+
+import com.gravatar.DefaultAvatarOption
+import dagger.hilt.android.testing.HiltAndroidTest
+import junit.framework.TestCase.assertEquals
+import org.junit.Test
+
+@HiltAndroidTest
+class WPAvatarUtilsTest {
+ @Test
+ fun rewriteAvatarUrlReplaceNonGravatarUrlToPhotonUrl() {
+ assertEquals(
+ "https://i0.wp.com/example.com/image.jpg?strip=info&quality=65&resize=100,100&ssl=1",
+ WPAvatarUtils.rewriteAvatarUrl("https://example.com/image.jpg", 100)
+ )
+ }
+
+ @Test
+ fun rewriteAvatarUrlDropQueryParamsFromGravatarUrlAndAddDefaults() {
+ assertEquals(
+ "https://www.gravatar.com/avatar/" +
+ "31c5543c1734d25c7206f5fd591525d0295bec6fe84ff82f946a34fe970a1e66?d=mp&s=100",
+ WPAvatarUtils.rewriteAvatarUrl(
+ "https://www.gravatar.com/avatar/" +
+ "31c5543c1734d25c7206f5fd591525d0295bec6fe84ff82f946a34fe970a1e66?d=wavatar&s=1000",
+ 100
+ )
+ )
+ }
+
+ @Test
+ fun rewriteAvatarUrlAddDefaultsToGravatarUrl() {
+ assertEquals(
+ "https://www.gravatar.com/avatar/" +
+ "31c5543c1734d25c7206f5fd591525d0295bec6fe84ff82f946a34fe970a1e66?d=404&s=200",
+ WPAvatarUtils.rewriteAvatarUrl(
+ "https://www.gravatar.com/avatar/" +
+ "31c5543c1734d25c7206f5fd591525d0295bec6fe84ff82f946a34fe970a1e66",
+ 200, DefaultAvatarOption.Status404
+ )
+ )
+ }
+
+ @Test
+ fun rewriteAvatarUrlInvalidGravatarUrl() {
+ assertEquals(
+ "",
+ WPAvatarUtils.rewriteAvatarUrl(
+ "https://www.gravatar.com/avatar/?d=404&s=200",
+ 200, DefaultAvatarOption.Status404
+ )
+ )
+ }
+}
diff --git a/WordPress/src/androidTest/java/org/wordpress/android/wiremock/WireMockStub.kt b/WordPress/src/androidTest/java/org/wordpress/android/wiremock/WireMockStub.kt
new file mode 100644
index 000000000000..516d915f0ef5
--- /dev/null
+++ b/WordPress/src/androidTest/java/org/wordpress/android/wiremock/WireMockStub.kt
@@ -0,0 +1,18 @@
+package org.wordpress.android.wiremock
+
+/**
+ * Represents a WireMock#stubFor call on a WireMockServer.
+ * @property apiPathUrl The API path we are matching against.
+ * @property fileName The filename used to return as the body stored in the androidTest resources sub directory.
+ */
+data class WireMockStub(
+ val urlPath: WireMockUrlPath,
+ val fileName: String,
+)
+
+/**
+ * Enum used to represent supported URLs we can stub.
+ */
+enum class WireMockUrlPath(val path: String) {
+ FEATURE_RESPONSE("/wpcom/v2/mobile/feature-flags/")
+}
diff --git a/WordPress/src/androidTest/resources/new-stats-feature-response.json b/WordPress/src/androidTest/resources/new-stats-feature-response.json
new file mode 100644
index 000000000000..de0234285090
--- /dev/null
+++ b/WordPress/src/androidTest/resources/new-stats-feature-response.json
@@ -0,0 +1,3 @@
+{
+ "stats_traffic_subscribers_tabs": true
+}
diff --git a/WordPress/src/debug/AndroidManifest.xml b/WordPress/src/debug/AndroidManifest.xml
index f8c9e84b0392..08c726b25fcd 100644
--- a/WordPress/src/debug/AndroidManifest.xml
+++ b/WordPress/src/debug/AndroidManifest.xml
@@ -26,6 +26,9 @@
android:name="android.permission.DUMP"
tools:ignore="ProtectedPermissions" />
+
+
+
Additional Libraries
TagSoup: Copyright 2002-2008, John Cowan
uCrop: Copyright 2017, Yalantis
Simple Tool Tip: Copyright 2016, Xizhi Zhu
- okio/okhttp: Copyright 2013 Square, Inc.
EventBus: Copyright 2012-2016 Markus Junginger, greenrobot
Mobile 4 Media: Copyright 2016, INDExOS
Tenor Android Core: Copyright 2017, Tenor Inc
diff --git a/WordPress/src/jetpack/assets/reader_text_events.js b/WordPress/src/jetpack/assets/reader_text_events.js
new file mode 100644
index 000000000000..68cc132b5eeb
--- /dev/null
+++ b/WordPress/src/jetpack/assets/reader_text_events.js
@@ -0,0 +1,26 @@
+/*
+ * wvHandler is the name of the message handler in the webview, added via createJsObject inside
+ * ReaderPostRenderer. It handles the `postMessage` calls from the WebView.
+ */
+
+function debounce(fn, timeout) {
+ let timer;
+ return () => {
+ clearTimeout(timer);
+ timer = setTimeout(fn, timeout);
+ }
+}
+
+const textHighlighted = debounce(
+ () => wvHandler.postMessage("articleTextHighlighted"),
+ 1000
+);
+
+document.addEventListener('selectionchange', function(event) {
+ const selection = document.getSelection().toString();
+ if (selection.length > 0) {
+ textHighlighted();
+ }
+});
+
+document.addEventListener('copy', event => wvHandler.postMessage("articleTextCopied"));
diff --git a/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueFragment.kt b/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueFragment.kt
deleted file mode 100644
index 85001ff89efc..000000000000
--- a/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueFragment.kt
+++ /dev/null
@@ -1,106 +0,0 @@
-package org.wordpress.android.ui.accounts.login
-
-import android.content.Context
-import android.os.Bundle
-import android.view.View
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.viewModels
-import com.google.android.material.button.MaterialButton
-import dagger.hilt.android.AndroidEntryPoint
-import org.wordpress.android.R
-import org.wordpress.android.databinding.JetpackLoginPrologueScreenBinding
-import org.wordpress.android.ui.accounts.LoginNavigationEvents.ShowEmailLoginScreen
-import org.wordpress.android.ui.accounts.LoginNavigationEvents.ShowLoginViaSiteAddressScreen
-import org.wordpress.android.util.WPActivityUtils
-
-@AndroidEntryPoint
-class LoginPrologueFragment : Fragment(R.layout.jetpack_login_prologue_screen) {
- private val viewModel: LoginPrologueViewModel by viewModels()
- private lateinit var loginPrologueListener: LoginPrologueListener
-
- @Suppress("TooGenericExceptionThrown")
- override fun onAttach(context: Context) {
- super.onAttach(context)
- if (context !is LoginPrologueListener) {
- throw RuntimeException("$context must implement LoginPrologueListener")
- }
- loginPrologueListener = context
- }
-
- @Suppress("DEPRECATION")
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- // setting up a full screen flags for the decor view of this fragment,
- // that will work with transparent status bar
- WPActivityUtils.showFullScreen(view)
-
- updateSystemBars(showDarkStatusAndNavBarInLightMode = true)
-
- with(JetpackLoginPrologueScreenBinding.bind(view)) { initViewModel() }
- }
-
- private fun JetpackLoginPrologueScreenBinding.initViewModel() {
- initObservers()
- viewModel.start()
- }
-
- private fun JetpackLoginPrologueScreenBinding.initObservers() {
- viewModel.navigationEvents.observe(viewLifecycleOwner, { events ->
- events.getContentIfNotHandled()?.let {
- when (it) {
- is ShowEmailLoginScreen -> loginPrologueListener.showEmailLoginScreen()
- is ShowLoginViaSiteAddressScreen -> loginPrologueListener.loginViaSiteAddress()
- else -> Unit // Do nothing
- }
- }
- })
-
- viewModel.uiState.observe(viewLifecycleOwner, { uiState ->
- updateButtonUiState(
- bottomButtonsContainer.continueWithWpcomButton,
- uiState.continueWithWpcomButtonState.title,
- uiState.continueWithWpcomButtonState.onClick
- )
- updateButtonUiState(
- bottomButtonsContainer.enterYourSiteAddressButton,
- uiState.enterYourSiteAddressButtonState.title,
- uiState.enterYourSiteAddressButtonState.onClick
- )
- })
- }
-
- private fun updateButtonUiState(button: MaterialButton, title: Int, onClick: () -> Unit) {
- button.setText(title)
- button.setOnClickListener { onClick.invoke() }
- }
-
- override fun onResume() {
- super.onResume()
- viewModel.onFragmentResume()
- }
-
- @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
- // important for accessibility - talkback
- activity?.setTitle(R.string.login_prologue_screen_title)
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- updateSystemBars(showDarkStatusAndNavBarInLightMode = false)
- }
-
- @Suppress("DEPRECATION")
- private fun updateSystemBars(showDarkStatusAndNavBarInLightMode: Boolean) {
- activity?.let {
- WPActivityUtils.setLightStatusBar(it.window, !showDarkStatusAndNavBarInLightMode)
- WPActivityUtils.setLightNavigationBar(it.window, !showDarkStatusAndNavBarInLightMode)
- }
- }
-
- companion object {
- const val TAG = "jetpack_login_prologue_fragment_tag"
- }
-}
diff --git a/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueRevampedFragment.kt b/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueRevampedFragment.kt
index 525f505b4113..5f0f26b0967d 100644
--- a/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueRevampedFragment.kt
+++ b/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueRevampedFragment.kt
@@ -6,7 +6,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
diff --git a/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueRevampedViewModel.kt b/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueRevampedViewModel.kt
index 82ce069e4ab7..73fba28b164c 100644
--- a/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueRevampedViewModel.kt
+++ b/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueRevampedViewModel.kt
@@ -1,6 +1,5 @@
package org.wordpress.android.ui.accounts.login
-import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
@@ -9,7 +8,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
-import dagger.hilt.android.qualifiers.ApplicationContext
import org.wordpress.android.analytics.AnalyticsTracker.Stat.LOGIN_PROLOGUE_VIEWED
import org.wordpress.android.ui.accounts.UnifiedLoginTracker
import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Click
@@ -41,7 +39,7 @@ private const val DEFAULT_PITCH = (-30 * PI / 180).toFloat()
class LoginPrologueRevampedViewModel @Inject constructor(
private val unifiedLoginTracker: UnifiedLoginTracker,
analyticsTrackerWrapper: AnalyticsTrackerWrapper,
- @ApplicationContext appContext: Context,
+ sensorManager: SensorManager,
) : ViewModel() {
private val accelerometerData = FloatArray(3)
private val magnetometerData = FloatArray(3)
@@ -88,9 +86,6 @@ class LoginPrologueRevampedViewModel @Inject constructor(
* velocity and position for each frame.
*/
private val _positionData = object : MutableLiveData(), SensorEventListener {
- private val sensorManager
- get() = appContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager
-
override fun onActive() {
super.onActive()
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.also {
diff --git a/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueViewModel.kt b/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueViewModel.kt
deleted file mode 100644
index fc53c15b941a..000000000000
--- a/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/LoginPrologueViewModel.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-package org.wordpress.android.ui.accounts.login
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MediatorLiveData
-import androidx.lifecycle.MutableLiveData
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.CoroutineDispatcher
-import org.wordpress.android.R
-import org.wordpress.android.analytics.AnalyticsTracker.Stat
-import org.wordpress.android.modules.UI_THREAD
-import org.wordpress.android.ui.accounts.LoginNavigationEvents
-import org.wordpress.android.ui.accounts.LoginNavigationEvents.ShowEmailLoginScreen
-import org.wordpress.android.ui.accounts.LoginNavigationEvents.ShowLoginViaSiteAddressScreen
-import org.wordpress.android.ui.accounts.UnifiedLoginTracker
-import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Click.CONTINUE_WITH_WORDPRESS_COM
-import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Click.LOGIN_WITH_SITE_ADDRESS
-import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Flow
-import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Step.PROLOGUE
-import org.wordpress.android.ui.accounts.login.LoginPrologueViewModel.ButtonUiState.ContinueWithWpcomButtonState
-import org.wordpress.android.ui.accounts.login.LoginPrologueViewModel.ButtonUiState.EnterYourSiteAddressButtonState
-import org.wordpress.android.util.BuildConfigWrapper
-import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
-import org.wordpress.android.viewmodel.Event
-import org.wordpress.android.viewmodel.ScopedViewModel
-import javax.inject.Inject
-import javax.inject.Named
-
-@HiltViewModel
-class LoginPrologueViewModel @Inject constructor(
- private val unifiedLoginTracker: UnifiedLoginTracker,
- private val analyticsTrackerWrapper: AnalyticsTrackerWrapper,
- private val buildConfigWrapper: BuildConfigWrapper,
- @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher
-) : ScopedViewModel(mainDispatcher) {
- private val _navigationEvents = MediatorLiveData>()
- val navigationEvents: LiveData> = _navigationEvents
-
- private val _uiState: MutableLiveData = MutableLiveData()
- val uiState: LiveData = _uiState
-
- private var isStarted = false
- fun start() {
- if (isStarted) return
- isStarted = true
-
- analyticsTrackerWrapper.track(stat = Stat.LOGIN_PROLOGUE_VIEWED)
- unifiedLoginTracker.track(flow = Flow.PROLOGUE, step = PROLOGUE)
-
- _uiState.value = UiState(
- enterYourSiteAddressButtonState = EnterYourSiteAddressButtonState(::onEnterYourSiteAddressButtonClick),
- continueWithWpcomButtonState = ContinueWithWpcomButtonState(
- title = if (buildConfigWrapper.isSignupEnabled) {
- R.string.continue_with_wpcom
- } else {
- R.string.continue_with_wpcom_no_signup
- },
- onClick = ::onContinueWithWpcomButtonClick
- )
- )
- }
-
- fun onFragmentResume() {
- unifiedLoginTracker.setFlowAndStep(Flow.PROLOGUE, PROLOGUE)
- }
-
- private fun onContinueWithWpcomButtonClick() {
- unifiedLoginTracker.trackClick(CONTINUE_WITH_WORDPRESS_COM)
- _navigationEvents.postValue(Event(ShowEmailLoginScreen))
- }
-
- private fun onEnterYourSiteAddressButtonClick() {
- unifiedLoginTracker.trackClick(LOGIN_WITH_SITE_ADDRESS)
- _navigationEvents.postValue(Event(ShowLoginViaSiteAddressScreen))
- }
-
- data class UiState(
- val enterYourSiteAddressButtonState: EnterYourSiteAddressButtonState,
- val continueWithWpcomButtonState: ContinueWithWpcomButtonState
- )
-
- sealed class ButtonUiState {
- abstract val title: Int
- abstract val onClick: (() -> Unit)
-
- data class ContinueWithWpcomButtonState(
- override val title: Int,
- override val onClick: () -> Unit
- ) : ButtonUiState()
-
- data class EnterYourSiteAddressButtonState(override val onClick: () -> Unit) : ButtonUiState() {
- override val title = R.string.enter_your_site_address
- }
- }
-}
diff --git a/WordPress/src/jetpack/java/org/wordpress/android/util/config/InAppUpdateBlockingVersionConfigConstants.kt b/WordPress/src/jetpack/java/org/wordpress/android/util/config/InAppUpdateBlockingVersionConfigConstants.kt
new file mode 100644
index 000000000000..3d539360338c
--- /dev/null
+++ b/WordPress/src/jetpack/java/org/wordpress/android/util/config/InAppUpdateBlockingVersionConfigConstants.kt
@@ -0,0 +1,5 @@
+package org.wordpress.android.util.config
+
+const val IN_APP_UPDATE_BLOCKING_VERSION_REMOTE_FIELD = "jp_in_app_update_blocking_version_android"
+
+
diff --git a/WordPress/src/jetpack/res/drawable/bg_note_avatar_badge.xml b/WordPress/src/jetpack/res/drawable/bg_note_avatar_badge.xml
deleted file mode 100644
index 6b7cf25d33c3..000000000000
--- a/WordPress/src/jetpack/res/drawable/bg_note_avatar_badge.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
diff --git a/WordPress/src/jetpack/res/drawable/bg_rectangle_blue_90_gradient.xml b/WordPress/src/jetpack/res/drawable/bg_rectangle_blue_90_gradient.xml
deleted file mode 100644
index e58af124b74c..000000000000
--- a/WordPress/src/jetpack/res/drawable/bg_rectangle_blue_90_gradient.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/WordPress/src/jetpack/res/drawable/bg_stars_background_large.xml b/WordPress/src/jetpack/res/drawable/bg_stars_background_large.xml
deleted file mode 100644
index 6f0c24e77887..000000000000
--- a/WordPress/src/jetpack/res/drawable/bg_stars_background_large.xml
+++ /dev/null
@@ -1,2438 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/WordPress/src/jetpack/res/drawable/img_jetpack_horizontal_white.xml b/WordPress/src/jetpack/res/drawable/img_jetpack_horizontal_white.xml
deleted file mode 100644
index 7b1ceed7ca0e..000000000000
--- a/WordPress/src/jetpack/res/drawable/img_jetpack_horizontal_white.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/WordPress/src/jetpack/res/layout/jetpack_login_prologue_screen.xml b/WordPress/src/jetpack/res/layout/jetpack_login_prologue_screen.xml
deleted file mode 100644
index 0aa7f0d2a641..000000000000
--- a/WordPress/src/jetpack/res/layout/jetpack_login_prologue_screen.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/WordPress/src/jetpack/res/values/dimens.xml b/WordPress/src/jetpack/res/values/dimens.xml
index ccfeb04b541e..a7b8a5647733 100644
--- a/WordPress/src/jetpack/res/values/dimens.xml
+++ b/WordPress/src/jetpack/res/values/dimens.xml
@@ -1,5 +1,4 @@
- 40dp
20dp
diff --git a/WordPress/src/jetpack/res/values/styles_login.xml b/WordPress/src/jetpack/res/values/styles_login.xml
index d024766081b6..de1807300aa4 100644
--- a/WordPress/src/jetpack/res/values/styles_login.xml
+++ b/WordPress/src/jetpack/res/values/styles_login.xml
@@ -23,16 +23,4 @@
- @color/white
-
-
-
-
diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml
index 28599bd22e30..715c240b83c5 100644
--- a/WordPress/src/main/AndroidManifest.xml
+++ b/WordPress/src/main/AndroidManifest.xml
@@ -17,6 +17,7 @@
+
+
@@ -249,15 +251,6 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.posts.PostsListActivity" />
-
-
-
-
-
@@ -851,105 +844,88 @@
+ android:label="Reader Update Service" />
+ android:label="Reader Update JobService" />
+ android:label="Reader Discover Service" />
+ android:label="Reader Discover JobService" />
+ android:label="Reader Post Service" />
+ android:label="Reader Post JobService" />
+ android:label="Reader Search Service" />
+ android:label="Reader Search Job Service" />
+ android:label="Reader Comment Service" />
+ android:label="Suggestion Service" />
+ android:label="Notifications Quick Actions processing Service" />
+ android:label="Notifications Update Service" />
+ android:label="Notifications Update Job Service" />
+ android:label="Installation Referrer Service" />
+ android:label="Installation Referrer Service" />
+ android:label="Login to WPCOM Service" />
+ android:label="Site Creation Service" />
+ android:permission="android.permission.BIND_REMOTEVIEWS" />
+ android:exported="false" >
@@ -1157,8 +1131,6 @@
android:label="@string/site_monitoring"
android:theme="@style/WordPress.NoActionBar"/>
-
-
diff --git a/WordPress/src/main/assets/jetpack-ai-transcription-test-audio-file.m4a b/WordPress/src/main/assets/jetpack-ai-transcription-test-audio-file.m4a
new file mode 100644
index 000000000000..3c94cbb32f9b
Binary files /dev/null and b/WordPress/src/main/assets/jetpack-ai-transcription-test-audio-file.m4a differ
diff --git a/WordPress/src/main/assets/licenses.html b/WordPress/src/main/assets/licenses.html
index f774c12d914c..ef5943951f07 100644
--- a/WordPress/src/main/assets/licenses.html
+++ b/WordPress/src/main/assets/licenses.html
@@ -31,7 +31,6 @@ Additional Libraries
TagSoup: Copyright 2002-2008, John Cowan
uCrop: Copyright 2017, Yalantis
Simple Tool Tip: Copyright 2016, Xizhi Zhu
- okio/okhttp: Copyright 2013 Square, Inc.
EventBus: Copyright 2012-2016 Markus Junginger, greenrobot
Mobile 4 Media: Copyright 2016, INDExOS
Tenor Android Core: Copyright 2017, Tenor Inc
diff --git a/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt b/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt
index 02eb23ed98bc..51a8fb144837 100644
--- a/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt
+++ b/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt
@@ -22,9 +22,7 @@ import android.os.Build.VERSION_CODES
import android.os.Bundle
import android.os.SystemClock
import android.text.TextUtils
-import android.util.AndroidRuntimeException
import android.util.Log
-import android.webkit.WebSettings
import android.webkit.WebView
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
@@ -39,12 +37,6 @@ import com.google.android.gms.auth.api.Auth
import com.google.android.gms.common.api.GoogleApiClient
import com.google.firebase.iid.FirebaseInstanceId
import com.wordpress.rest.RestClient
-import com.wordpress.stories.compose.NotificationTrackerProvider
-import com.wordpress.stories.compose.frame.StoryNotificationType
-import com.wordpress.stories.compose.frame.StoryNotificationType.STORY_FRAME_SAVE_ERROR
-import com.wordpress.stories.compose.frame.StoryNotificationType.STORY_FRAME_SAVE_SUCCESS
-import com.wordpress.stories.compose.frame.StoryNotificationType.STORY_SAVE_ERROR
-import com.wordpress.stories.compose.frame.StoryNotificationType.STORY_SAVE_SUCCESS
import kotlinx.coroutines.CoroutineScope
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
@@ -61,6 +53,7 @@ import org.wordpress.android.fluxc.generated.ListActionBuilder
import org.wordpress.android.fluxc.generated.PostActionBuilder
import org.wordpress.android.fluxc.generated.SiteActionBuilder
import org.wordpress.android.fluxc.generated.ThemeActionBuilder
+import org.wordpress.android.fluxc.network.UserAgent
import org.wordpress.android.fluxc.network.rest.wpcom.site.PrivateAtomicCookie
import org.wordpress.android.fluxc.store.AccountStore
import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged
@@ -76,7 +69,6 @@ import org.wordpress.android.networking.ConnectionChangeReceiver
import org.wordpress.android.networking.OAuthAuthenticator
import org.wordpress.android.networking.RestClientUtils
import org.wordpress.android.push.GCMRegistrationScheduler
-import org.wordpress.android.push.NotificationType
import org.wordpress.android.support.ZendeskHelper
import org.wordpress.android.ui.ActivityId
import org.wordpress.android.ui.debug.cookies.DebugCookieManager
@@ -93,7 +85,6 @@ import org.wordpress.android.ui.posts.editor.ImageEditorTracker
import org.wordpress.android.ui.prefs.AppPrefs
import org.wordpress.android.ui.reader.tracker.ReaderTracker
import org.wordpress.android.ui.stats.refresh.lists.widget.WidgetUpdater.StatsWidgetUpdaters
-import org.wordpress.android.ui.stories.media.StoryMediaSaveUploadBridge
import org.wordpress.android.ui.uploads.UploadService
import org.wordpress.android.ui.uploads.UploadStarter
import org.wordpress.android.util.AppLog
@@ -101,6 +92,7 @@ import org.wordpress.android.util.AppLog.T
import org.wordpress.android.util.AppLog.T.MAIN
import org.wordpress.android.util.AppThemeUtils
import org.wordpress.android.util.BitmapLruCache
+import org.wordpress.android.util.BuildConfigWrapper
import org.wordpress.android.util.DateTimeUtils
import org.wordpress.android.util.EncryptedLogging
import org.wordpress.android.util.FluxCUtils
@@ -118,10 +110,12 @@ import org.wordpress.android.util.config.OpenWebLinksWithJetpackFlowFeatureConfi
import org.wordpress.android.util.enqueuePeriodicUploadWorkRequestForAllSites
import org.wordpress.android.util.experiments.ExPlat
import org.wordpress.android.util.image.ImageManager
-import org.wordpress.android.widgets.AppRatingDialog
+import org.wordpress.android.widgets.AppReviewManager
import org.wordpress.android.workers.WordPressWorkersFactory
import java.io.File
import java.io.IOException
+import java.lang.Exception
+import java.net.CookieManager
import java.util.Date
import javax.inject.Inject
import javax.inject.Named
@@ -132,6 +126,9 @@ class AppInitializer @Inject constructor(
wellSqlInitializer: WellSqlInitializer,
private val application: Application
) : DefaultLifecycleObserver {
+ @Inject
+ lateinit var userAgent: UserAgent
+
@Inject
lateinit var dispatcher: Dispatcher
@@ -171,9 +168,6 @@ class AppInitializer @Inject constructor(
@Inject
lateinit var imageEditorTracker: ImageEditorTracker
- @Inject
- lateinit var storyMediaSaveUploadBridge: StoryMediaSaveUploadBridge
-
@Inject
lateinit var crashLogging: CrashLogging
@@ -196,7 +190,12 @@ class AppInitializer @Inject constructor(
lateinit var gcmRegistrationScheduler: GCMRegistrationScheduler
@Inject
- lateinit var debugCookieManager: DebugCookieManager
+ lateinit var cookieManager: CookieManager
+
+ @Inject
+ lateinit var buildConfig: BuildConfigWrapper
+
+ private lateinit var debugCookieManager: DebugCookieManager
@Inject
@Named(APPLICATION_SCOPE)
@@ -233,8 +232,6 @@ class AppInitializer @Inject constructor(
lateinit var jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper
private lateinit var applicationLifecycleMonitor: ApplicationLifecycleMonitor
- lateinit var storyNotificationTrackerProvider: StoryNotificationTrackerProvider
- private set
@Suppress("DEPRECATION")
private lateinit var credentialsClient: GoogleApiClient
@@ -286,9 +283,9 @@ class AppInitializer @Inject constructor(
}
fun init() {
+ crashLogging.initialize()
dispatcher.register(this)
appConfig.init(appScope)
-
// Upload any encrypted logs that were queued but not yet uploaded
encryptedLogging.start()
@@ -306,7 +303,7 @@ class AppInitializer @Inject constructor(
initWpDb()
context?.let { enableHttpResponseCache(it) }
- AppRatingDialog.init(application)
+ AppReviewManager.init(application)
if (!initialized) {
// EventBus setup
@@ -318,7 +315,7 @@ class AppInitializer @Inject constructor(
.installDefaultEventBus()
}
- RestClientUtils.setUserAgent(userAgent)
+ RestClientUtils.setUserAgent(userAgent.toString())
if (!initialized) {
zendeskHelper.setupZendesk(
@@ -368,21 +365,26 @@ class AppInitializer @Inject constructor(
systemNotificationsTracker.checkSystemNotificationsState()
ImageEditorInitializer.init(imageManager, imageEditorTracker, imageEditorFileUtils, appScope)
- storyNotificationTrackerProvider = StoryNotificationTrackerProvider()
- storyMediaSaveUploadBridge.init(application)
- ProcessLifecycleOwner.get().lifecycle.addObserver(storyMediaSaveUploadBridge)
-
exPlat.forceRefresh()
- debugCookieManager.sync()
+ initDebugCookieManager()
if (!initialized && BuildConfig.DEBUG && Build.VERSION.SDK_INT >= VERSION_CODES.R) {
initAppOpsManager()
}
+ AppLog.i(T.UTILS, "AppInitializer.userAgentString: $userAgent")
+
initialized = true
}
+ private fun initDebugCookieManager() {
+ if (buildConfig.isDebugSettingsEnabled()) {
+ debugCookieManager = DebugCookieManager(application, cookieManager, buildConfig)
+ debugCookieManager.sync()
+ }
+ }
+
/**
* Data access auditing
* @link https://developer.android.com/guide/topics/data/audit-access
@@ -421,19 +423,14 @@ class AppInitializer @Inject constructor(
WorkManager.initialize(application, configBuilder.build())
}
+ @Suppress("TooGenericExceptionCaught")
private fun enableLogRecording() {
AppLog.enableRecording(true)
- AppLog.enableLogFilePersistence(application.baseContext, MAX_LOG_COUNT)
- AppLog.addListener { tag, logLevel, message ->
- val sb = StringBuffer()
- sb.append(logLevel.toString())
- .append("/")
- .append(AppLog.TAG)
- .append("-")
- .append(tag.toString())
- .append(": ")
- .append(message)
- crashLogging.recordEvent(sb.toString(), null)
+ try {
+ AppLog.enableLogFilePersistence(application.baseContext, MAX_LOG_COUNT)
+ } catch (e: Exception) {
+ AppLog.enableRecording(false)
+ AppLog.e(T.UTILS, "Error enabling log file persistence", e)
}
}
@@ -969,29 +966,6 @@ class AppInitializer @Inject constructor(
}
}
- inner class StoryNotificationTrackerProvider : NotificationTrackerProvider {
- private fun translateNotificationTypes(storyNotificationType: StoryNotificationType): NotificationType {
- return when (storyNotificationType) {
- STORY_SAVE_SUCCESS -> NotificationType.STORY_SAVE_SUCCESS
- STORY_SAVE_ERROR -> NotificationType.STORY_SAVE_ERROR
- STORY_FRAME_SAVE_SUCCESS -> NotificationType.STORY_FRAME_SAVE_SUCCESS
- STORY_FRAME_SAVE_ERROR -> NotificationType.STORY_FRAME_SAVE_ERROR
- }
- }
-
- override fun trackShownNotification(storyNotificationType: StoryNotificationType) {
- systemNotificationsTracker.trackShownNotification(translateNotificationTypes(storyNotificationType))
- }
-
- override fun trackTappedNotification(storyNotificationType: StoryNotificationType) {
- systemNotificationsTracker.trackTappedNotification(translateNotificationTypes(storyNotificationType))
- }
-
- override fun trackDismissedNotification(storyNotificationType: StoryNotificationType) {
- systemNotificationsTracker.trackDismissedNotification(translateNotificationTypes(storyNotificationType))
- }
- }
-
private fun updateNotificationSettings() {
if (!jetpackFeatureRemovalPhaseHelper.shouldShowNotifications()) {
NotificationsUtils.cancelAllNotifications(application)
@@ -1092,55 +1066,6 @@ class AppInitializer @Inject constructor(
)
}
- /**
- * Device's default User-Agent string.
- * E.g.:
- * "Mozilla/5.0 (Linux; Android 6.0; Android SDK built for x86_64 Build/MASTER; wv)
- * AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/44.0.2403.119 Mobile
- * Safari/537.36"
- */
- @Suppress("SwallowedException")
- val defaultUserAgent: String by lazy {
- try {
- WebSettings.getDefaultUserAgent(context)
- } catch (e: AndroidRuntimeException) {
- // Catch AndroidRuntimeException that could be raised by the WebView() constructor.
- // See https://github.com/wordpress-mobile/WordPress-Android/issues/3594
-
- // initialize with the empty string, it's a rare issue
- ""
- } catch (expected: NullPointerException) {
- // Catch NullPointerException that could be raised by WebSettings.getDefaultUserAgent()
- // See https://github.com/wordpress-mobile/WordPress-Android/issues/3838
-
- // initialize with the empty string, it's a rare issue
- ""
- } catch (e: IllegalArgumentException) {
- // Catch IllegalArgumentException that could be raised by WebSettings.getDefaultUserAgent()
- // See https://github.com/wordpress-mobile/WordPress-Android/issues/9015
-
- // initialize with the empty string, it's a rare issue
- ""
- }
- }
-
- /**
- * User-Agent string when making HTTP connections, for both API traffic and WebViews. Appends
- * "wp-android/version" to WebView's default User-Agent string for the webservers to get the full feature list
- * of the browser and serve content accordingly, e.g.:
- * "Mozilla/5.0 (Linux; Android 6.0; Android SDK built for x86_64 Build/MASTER; wv)
- * AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/44.0.2403.119 Mobile
- * Safari/537.36 wp-android/4.7"
- * Note that app versions prior to 2.7 simply used "wp-android" as the user agent
- **/
- val userAgent: String by lazy {
- if (TextUtils.isEmpty(defaultUserAgent)) {
- WordPress.USER_AGENT_APPNAME + "/" + PackageUtils.getVersionName(context)
- } else {
- (defaultUserAgent + " " + WordPress.USER_AGENT_APPNAME + "/" + PackageUtils.getVersionName(context))
- }
- }
-
fun getBitmapCache(): BitmapLruCache {
if (bitmapCache == null) {
// The cache size will be measured in kilobytes rather than number of items.
diff --git a/WordPress/src/debug/java/org/wordpress/android/WellSqlInitializer.kt b/WordPress/src/main/java/org/wordpress/android/WellSqlInitializer.kt
similarity index 100%
rename from WordPress/src/debug/java/org/wordpress/android/WellSqlInitializer.kt
rename to WordPress/src/main/java/org/wordpress/android/WellSqlInitializer.kt
diff --git a/WordPress/src/main/java/org/wordpress/android/WordPress.kt b/WordPress/src/main/java/org/wordpress/android/WordPress.kt
index 6918015aae05..1ed866d9a713 100644
--- a/WordPress/src/main/java/org/wordpress/android/WordPress.kt
+++ b/WordPress/src/main/java/org/wordpress/android/WordPress.kt
@@ -4,7 +4,6 @@ import android.app.Application
import android.content.Context
import com.android.volley.RequestQueue
import dagger.hilt.EntryPoints
-import org.wordpress.android.AppInitializer.StoryNotificationTrackerProvider
import org.wordpress.android.fluxc.tools.FluxCImageLoader
import org.wordpress.android.modules.AppComponent
@@ -13,9 +12,6 @@ import org.wordpress.android.modules.AppComponent
* application. Containing public static variables and methods to be accessed by other classes.
*/
abstract class WordPress : Application() {
- val storyNotificationTrackerProvider: StoryNotificationTrackerProvider
- get() = initializer().storyNotificationTrackerProvider
-
abstract fun initializer(): AppInitializer
fun component(): AppComponent = EntryPoints.get(this, AppComponent::class.java)
@@ -74,11 +70,5 @@ abstract class WordPress : Application() {
fun getRestClientUtilsV2() = AppInitializer.restClientUtilsV2
fun getRestClientUtilsV0() = AppInitializer.restClientUtilsV0
-
- @JvmStatic
- fun getDefaultUserAgent() = AppInitializer.defaultUserAgent
-
- @JvmStatic
- fun getUserAgent() = AppInitializer.userAgent
}
}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/AsyncTaskExecutor.kt b/WordPress/src/main/java/org/wordpress/android/datasets/AsyncTaskExecutor.kt
new file mode 100644
index 000000000000..5b63e65e5b8e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/AsyncTaskExecutor.kt
@@ -0,0 +1,51 @@
+package org.wordpress.android.datasets
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Helper class to handle asynchronous I/O tasks using coroutines
+ * @see Introduction
+ */
+object AsyncTaskExecutor {
+ /**
+ * Execute a data loading task in the IO thread and handle the result on the main thread
+ */
+ @JvmStatic
+ fun executeIo(scope: CoroutineScope, backgroundTask: () -> T, callback: AsyncTaskCallback) {
+ execute(scope, Dispatchers.IO, backgroundTask, callback)
+ }
+
+ /**
+ * Execute a data loading task in the default thread and handle the result on the main thread
+ */
+ @JvmStatic
+ fun executeDefault(scope: CoroutineScope, backgroundTask: () -> T, callback: AsyncTaskCallback) {
+ execute(scope, Dispatchers.Default, backgroundTask, callback)
+ }
+
+ private fun execute(
+ scope: CoroutineScope,
+ dispatcher: CoroutineDispatcher,
+ backgroundTask: () -> T,
+ callback: AsyncTaskCallback
+ ) {
+ scope.launch(dispatcher) {
+ // handle the background task
+ val result = backgroundTask()
+
+ withContext(Dispatchers.Main) {
+ // handle the result on the main thread
+ callback.onTaskFinished(result)
+ }
+ }
+ }
+
+ interface AsyncTaskCallback {
+ fun onTaskFinished(result: T)
+ }
+}
+
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/NotificationsTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/NotificationsTable.java
index 4918c8d671a3..890ca77e99c6 100644
--- a/WordPress/src/main/java/org/wordpress/android/datasets/NotificationsTable.java
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/NotificationsTable.java
@@ -6,6 +6,7 @@
import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
@@ -77,10 +78,10 @@ public static ArrayList getLatestNotes(int limit) {
}
private static boolean putNote(Note note, boolean checkBeforeInsert) {
- String rawNote = prepareNote(note.getId(), note.getJSON().toString());
+ String rawNote = prepareNote(note.getId(), note.getJson().toString());
ContentValues values = new ContentValues();
- values.put("type", note.getType());
+ values.put("type", note.getRawType());
values.put("timestamp", note.getTimestamp());
values.put("raw_note_data", rawNote);
@@ -124,7 +125,7 @@ private static String prepareNote(String noteId, String noteSrc) {
return noteSrc;
}
- public static void saveNotes(List notes, boolean clearBeforeSaving) {
+ public static void saveNotes(@NonNull List notes, boolean clearBeforeSaving) {
getDb().beginTransaction();
try {
if (clearBeforeSaving) {
@@ -142,7 +143,7 @@ public static void saveNotes(List notes, boolean clearBeforeSaving) {
}
}
- public static boolean saveNote(Note note) {
+ public static boolean saveNote(@NonNull Note note) {
getDb().beginTransaction();
boolean saved = false;
try {
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/PublicizeTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/PublicizeTable.java
index 71f44134363c..52968d43263e 100644
--- a/WordPress/src/main/java/org/wordpress/android/datasets/PublicizeTable.java
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/PublicizeTable.java
@@ -157,7 +157,7 @@ private static PublicizeService getServiceFromCursor(Cursor c) {
}
public static boolean onlyExternalConnections(String serviceId) {
- if (serviceId == null && serviceId.isEmpty()) {
+ if (serviceId == null || serviceId.isEmpty()) {
return false;
}
@@ -233,7 +233,7 @@ public static void setConnectionsForSite(long siteId, PublicizeConnectionList co
db.delete(CONNECTIONS_TABLE, "site_id=?", new String[]{Long.toString(siteId)});
stmt = db.compileStatement(
- "INSERT INTO " + CONNECTIONS_TABLE
+ "INSERT OR REPLACE INTO " + CONNECTIONS_TABLE
+ " (id," // 1
+ " site_id," // 2
+ " user_id," // 3
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderLikeTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderLikeTable.java
index da721afea783..973a79244d70 100644
--- a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderLikeTable.java
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderLikeTable.java
@@ -77,14 +77,18 @@ public static void setCurrentUserLikesPost(ReaderPost post, boolean isLiked, lon
if (post == null) {
return;
}
+ setCurrentUserLikesPost(post.postId, post.blogId, isLiked, wpComUserId);
+ }
+
+ public static void setCurrentUserLikesPost(long postId, long blogId, boolean isLiked, long wpComUserId) {
if (isLiked) {
ContentValues values = new ContentValues();
- values.put("blog_id", post.blogId);
- values.put("post_id", post.postId);
+ values.put("blog_id", blogId);
+ values.put("post_id", postId);
values.put("user_id", wpComUserId);
ReaderDatabase.getWritableDb().insert("tbl_post_likes", null, values);
} else {
- String[] args = {Long.toString(post.blogId), Long.toString(post.postId), Long.toString(wpComUserId)};
+ String[] args = {Long.toString(blogId), Long.toString(postId), Long.toString(wpComUserId)};
ReaderDatabase.getWritableDb().delete("tbl_post_likes", "blog_id=? AND post_id=? AND user_id=?", args);
}
}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderUserTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderUserTable.java
index 51451930d5e2..e8222d43127b 100644
--- a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderUserTable.java
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderUserTable.java
@@ -7,7 +7,7 @@
import org.wordpress.android.models.ReaderUser;
import org.wordpress.android.models.ReaderUserIdList;
import org.wordpress.android.models.ReaderUserList;
-import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.WPAvatarUtils;
import org.wordpress.android.util.SqlUtils;
import java.util.ArrayList;
@@ -108,7 +108,7 @@ public static ArrayList getAvatarUrls(ReaderUserIdList userIds, int max,
if (c.moveToFirst()) {
do {
long userId = c.getLong(0);
- String url = GravatarUtils.fixGravatarUrl(c.getString(1), avatarSz);
+ String url = WPAvatarUtils.rewriteAvatarUrl(c.getString(1), avatarSz);
// add current user to the top
if (userId == wpComUserId) {
avatars.add(0, url);
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/wrappers/NotificationsTableWrapper.kt b/WordPress/src/main/java/org/wordpress/android/datasets/wrappers/NotificationsTableWrapper.kt
new file mode 100644
index 000000000000..8c667439ad2c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/wrappers/NotificationsTableWrapper.kt
@@ -0,0 +1,15 @@
+package org.wordpress.android.datasets.wrappers
+
+import dagger.Reusable
+import org.wordpress.android.datasets.NotificationsTable
+import org.wordpress.android.models.Note
+import javax.inject.Inject
+
+@Reusable
+class NotificationsTableWrapper @Inject constructor() {
+ fun saveNote(note: Note): Boolean = NotificationsTable.saveNote(note)
+
+ fun saveNotes(notes: List, clearBeforeSaving: Boolean) {
+ NotificationsTable.saveNotes(notes, clearBeforeSaving)
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/wrappers/ReaderPostTableWrapper.kt b/WordPress/src/main/java/org/wordpress/android/datasets/wrappers/ReaderPostTableWrapper.kt
index eeabbd6a0e0f..8b532688a530 100644
--- a/WordPress/src/main/java/org/wordpress/android/datasets/wrappers/ReaderPostTableWrapper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/wrappers/ReaderPostTableWrapper.kt
@@ -5,6 +5,8 @@ import org.wordpress.android.datasets.ReaderPostTable
import org.wordpress.android.models.ReaderPost
import org.wordpress.android.models.ReaderPostList
import org.wordpress.android.models.ReaderTag
+import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResult
+import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId
import javax.inject.Inject
@Reusable
@@ -30,6 +32,23 @@ class ReaderPostTableWrapper @Inject constructor() {
fun getNumPostsWithTag(readerTag: ReaderTag): Int = ReaderPostTable.getNumPostsWithTag(readerTag)
- fun addOrUpdatePosts(readerTag: ReaderTag, posts: ReaderPostList) =
+ fun addOrUpdatePosts(readerTag: ReaderTag?, posts: ReaderPostList) =
ReaderPostTable.addOrUpdatePosts(readerTag, posts)
+
+ fun deletePostsWithTag(tag: ReaderTag) = ReaderPostTable.deletePostsWithTag(tag)
+
+ fun comparePosts(posts: ReaderPostList): UpdateResult = ReaderPostTable.comparePosts(posts)
+
+ fun updateBookmarkedPostPseudoId(posts: ReaderPostList) = ReaderPostTable.updateBookmarkedPostPseudoId(posts)
+
+ fun setGapMarkerForTag(blogId: Long, postId: Long, tag: ReaderTag) =
+ ReaderPostTable.setGapMarkerForTag(blogId, postId, tag)
+
+ fun removeGapMarkerForTag(tag: ReaderTag) = ReaderPostTable.removeGapMarkerForTag(tag)
+
+ fun deletePostsBeforeGapMarkerForTag(tag: ReaderTag) = ReaderPostTable.deletePostsBeforeGapMarkerForTag(tag)
+
+ fun hasOverlap(posts: ReaderPostList?, tag: ReaderTag): Boolean = ReaderPostTable.hasOverlap(posts, tag)
+
+ fun getGapMarkerIdsForTag(tag: ReaderTag): ReaderBlogIdPostId? = ReaderPostTable.getGapMarkerIdsForTag(tag)
}
diff --git a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemAppColor.kt b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemAppColor.kt
index fe3cf85b052c..0998292b157a 100644
--- a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemAppColor.kt
+++ b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemAppColor.kt
@@ -18,31 +18,34 @@ object DesignSystemAppColor {
// Grays
@Stable
- val Gray = Color(0xFFF2F2F7)
+ val Gray = Color(0x66C2C2C6)
@Stable
- val Gray10 = Color(0xFFC2C2C6)
+ val Gray22 = Color(0x38FFFFFF)
@Stable
- val Gray20 = Color(0x99EBEBF5)
+ val Gray30 = Color(0x4DFFFFFF)
@Stable
- val Gray30 = Color(0xFF9B9B9E)
+ val Gray40 = Color(0x66FFFFFF)
@Stable
- val Gray40 = Color(0x993C3C43)
+ val Gray60 = Color(0x99EBEBF5)
@Stable
- val Gray50 = Color(0x4D3C3C43)
+ val DarkGray8 = Color(0x14000000)
@Stable
- val Gray60 = Color(0xFF4E4E4F)
+ val DarkGray15 = Color(0x26000000)
@Stable
- val Gray70 = Color(0xFF3A3A3C)
+ val DarkGray30 = Color(0x4D000000)
@Stable
- val Gray80 = Color(0xFF2C2C2E)
+ val DarkGray40 = Color(0x66000000)
+
+ @Stable
+ val DarkGray55 = Color(0x8C000000)
// Blues
@Stable
diff --git a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemAppTypography.kt b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemAppTypography.kt
new file mode 100644
index 000000000000..beafc2be9d5f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemAppTypography.kt
@@ -0,0 +1,67 @@
+package org.wordpress.android.designsystem
+
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+/**
+ * Object containing static common typography used throughout the project.
+ */
+object DesignSystemAppTypography {
+ @Stable
+ val heading1 = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 32.sp,
+ lineHeight = 40.sp,
+ letterSpacing = 0.sp
+ )
+ val heading2 = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 28.sp,
+ lineHeight = 36.sp,
+ letterSpacing = 0.sp
+ )
+ val heading3 = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 24.sp,
+ lineHeight = 32.sp,
+ letterSpacing = 0.sp
+ )
+ val heading4 = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ )
+ val bodyLargeEmphasized = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.25.sp
+ )
+ val bodyMediumEmphasized = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.25.sp
+ )
+ val bodySmallEmphasized = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.25.sp
+ )
+ val footnote = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ val footnoteEmphasized = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemColorsScreen.kt b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemColorsScreen.kt
new file mode 100644
index 000000000000..1541bccd1d9b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemColorsScreen.kt
@@ -0,0 +1,118 @@
+package org.wordpress.android.designsystem
+
+import android.content.res.Configuration
+import androidx.compose.foundation.background
+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.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Divider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import org.wordpress.android.R
+
+@Composable
+fun DesignSystemColorsScreen(
+ modifier: Modifier = Modifier
+) {
+ LazyColumn(
+ modifier = modifier,
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.spacedBy(
+ dimensionResource(id = R.dimen.reader_follow_sheet_button_margin_top)
+ )
+ ) {
+ item {
+ ColorTitle("Foreground")
+ val listForeground: List = listOf(
+ ColorOption("Primary", MaterialTheme.colorScheme.primary),
+ ColorOption("Secondary", MaterialTheme.colorScheme.secondary),
+ ColorOption("Tertiary", MaterialTheme.colorScheme.tertiary),
+ ColorOption("Brand", MaterialTheme.colorScheme.brand),
+ ColorOption("Error", MaterialTheme.colorScheme.error),
+ ColorOption("Warning", MaterialTheme.colorScheme.warning),
+ ColorOption("WP", MaterialTheme.colorScheme.wp),
+ )
+ ColorCardList(listForeground)
+
+ ColorTitle("Background")
+ val listBackground: List = listOf(
+ ColorOption("Primary", MaterialTheme.colorScheme.primaryContainer),
+ ColorOption("Secondary", MaterialTheme.colorScheme.secondaryContainer),
+ ColorOption("Tertiary", MaterialTheme.colorScheme.tertiaryContainer),
+ ColorOption("Quaternary", MaterialTheme.colorScheme.quaternaryContainer),
+ ColorOption("Brand", MaterialTheme.colorScheme.brandContainer),
+ ColorOption("WP", MaterialTheme.colorScheme.wpContainer),
+ )
+ ColorCardList(listBackground)
+ }
+ }
+}
+@OptIn(ExperimentalStdlibApi::class)
+@Composable
+fun ColorCard (colorName: String, color: Color) {
+ Row (modifier = Modifier.padding(10.dp, 3.dp).fillMaxWidth()) {
+ Column {
+ Box(
+ modifier = Modifier
+ .size(45.dp)
+ .clip(shape = RoundedCornerShape(5.dp, 5.dp, 5.dp, 5.dp))
+ .background(color)
+ )
+ }
+ Column {
+ Text(
+ modifier = Modifier.padding(start = 25.dp, end = 40.dp),
+ text = colorName,
+ color = MaterialTheme.colorScheme.primary,
+ )
+ Text(
+ modifier = Modifier.padding(start = 25.dp, end = 40.dp),
+ text = "#" + color.value.toHexString().uppercase().substring(0,8),
+ color = MaterialTheme.colorScheme.secondary
+ )
+ }
+ }
+ Divider(modifier = Modifier.padding(start = 10.dp, end = 10.dp))
+}
+@Composable
+fun ColorCardList(colorOptions: List) {
+ colorOptions.forEach { colorOption ->
+ ColorCard(colorOption.title, colorOption.color)
+ }
+}
+class ColorOption(var title: String, var color: Color)
+@Composable
+fun ColorTitle(title: String) {
+ Text(
+ modifier = Modifier.padding(start = 10.dp, top = 10.dp, bottom = 10.dp),
+ text = title,
+ style = MaterialTheme.typography.titleMedium.copy(color = MaterialTheme.colorScheme.primary),
+ )
+}
+@Preview(name = "Light Mode")
+@Preview(
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+ name = "Dark Mode"
+)
+@Composable
+fun DesignSystemColorsScreenPreview() {
+ DesignSystemTheme {
+ DesignSystemColorsScreen()
+ }
+}
+
diff --git a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemComponentsScreen.kt b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemComponentsScreen.kt
index 9ad12beb4467..31cdff2649ff 100644
--- a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemComponentsScreen.kt
+++ b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemComponentsScreen.kt
@@ -1,5 +1,6 @@
package org.wordpress.android.designsystem
+import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -33,13 +34,20 @@ fun DesignSystemComponentsScreen(
}
}
-@Preview
+@Preview(name = "Light Mode")
+@Preview(
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+ name = "Dark Mode"
+)
@Composable
fun StartDesignSystemComponentsScreenPreview(){
- DesignSystemComponentsScreen(
- modifier = Modifier
- .fillMaxSize()
- .padding(dimensionResource(R.dimen.button_container_shadow_height))
- )
+ DesignSystemTheme {
+ DesignSystemComponentsScreen(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(dimensionResource(R.dimen.button_container_shadow_height))
+ )
+ }
}
diff --git a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemDataSource.kt b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemDataSource.kt
index dba23937a37e..40594ce2cfb5 100644
--- a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemDataSource.kt
+++ b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemDataSource.kt
@@ -8,9 +8,8 @@ object DesignSystemDataSource {
Pair(R.string.design_system_components, DesignSystemScreen.Components.name),
)
val foundationScreenButtonOptions = listOf(
- R.string.design_system_foundation_colors,
- R.string.design_system_foundation_fonts,
- R.string.design_system_foundation_lengths
+ Pair(R.string.design_system_foundation_colors, DesignSystemScreen.Colors.name),
+ Pair(R.string.design_system_foundation_fonts, DesignSystemScreen.Fonts.name)
)
val componentsScreenButtonOptions = listOf(
R.string.design_system_components_dsbutton
diff --git a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemFontsScreen.kt b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemFontsScreen.kt
new file mode 100644
index 000000000000..aacdc40e3f56
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemFontsScreen.kt
@@ -0,0 +1,93 @@
+package org.wordpress.android.designsystem
+
+import android.content.res.Configuration
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.Divider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import org.wordpress.android.R
+
+@Composable
+fun DesignSystemFontsScreen(
+ modifier: Modifier = Modifier
+) {
+ LazyColumn(
+ modifier = modifier,
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.spacedBy(
+ dimensionResource(id = R.dimen.reader_follow_sheet_button_margin_top)
+ )
+ ) {
+ item {
+ FontsTitle("Heading")
+ FontCard(text = "Heading1", font = MaterialTheme.typography.heading1)
+ FontCard(text = "Heading2", font = MaterialTheme.typography.heading2)
+ FontCard(text = "Heading3", font = MaterialTheme.typography.heading3)
+ FontCard(text = "Heading4", font = MaterialTheme.typography.heading4)
+
+ FontsTitle("Body")
+ FontCard(text = "Body Small", font = MaterialTheme.typography.bodySmall)
+ FontCard(text = "Body Medium", font = MaterialTheme.typography.bodyMedium)
+ FontCard(text = "Body Large", font = MaterialTheme.typography.bodyLarge)
+ FontCard(text = "Body Small Emphasized", font = MaterialTheme.typography.bodySmallEmphasized)
+ FontCard(text = "Body Medium Emphasized", font = MaterialTheme.typography.bodyMediumEmphasized)
+ FontCard(text = "Body Large Emphasized", font = MaterialTheme.typography.bodyLargeEmphasized)
+
+ FontsTitle("Miscellaneous")
+ FontCard(text = "Footnote", font = MaterialTheme.typography.footnote)
+ FontCard(text = "Footnote Emphasized", font = MaterialTheme.typography.footnoteEmphasized)
+ }
+ }
+}
+@Composable
+fun FontCard (text: String, font: TextStyle) {
+ Row (modifier = Modifier
+ .padding(10.dp, 3.dp)
+ .defaultMinSize(minHeight = 34.dp)
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically) {
+ Column {
+ Text(
+ modifier = Modifier.padding(start = 25.dp, end = 40.dp),
+ style = font,
+ text = text,
+ color = MaterialTheme.colorScheme.primary,
+ )
+ }
+ }
+ Divider(modifier = Modifier.padding(start = 10.dp, end = 10.dp))
+}
+@Composable
+fun FontsTitle(title: String) {
+ Text(
+ modifier = Modifier.padding(start = 10.dp, top = 10.dp, bottom = 10.dp),
+ text = title,
+ style = MaterialTheme.typography.titleMedium.copy(color = MaterialTheme.colorScheme.primary),
+ )
+}
+@Preview(name = "Light Mode")
+@Preview(
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+ name = "Dark Mode"
+)
+@Composable
+fun DesignSystemFontsScreenPreview() {
+ DesignSystemTheme {
+ DesignSystemFontsScreen()
+ }
+}
+
diff --git a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemFoundationScreen.kt b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemFoundationScreen.kt
index 6564580c0606..5f31209a37b2 100644
--- a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemFoundationScreen.kt
+++ b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemFoundationScreen.kt
@@ -13,6 +13,7 @@ import org.wordpress.android.R
@Composable
fun DesignSystemFoundationScreen(
+ onButtonClicked: (String) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn (
@@ -25,8 +26,8 @@ fun DesignSystemFoundationScreen(
item {
DesignSystemDataSource.foundationScreenButtonOptions.forEach { item ->
SelectOptionButton(
- labelResourceId = item,
- onClick = {}
+ labelResourceId = item.first,
+ onClick = { onButtonClicked(item.second) }
)
}
}
@@ -36,10 +37,13 @@ fun DesignSystemFoundationScreen(
@Preview
@Composable
fun StartDesignSystemFoundationScreenPreview(){
- DesignSystemFoundationScreen(
- modifier = Modifier
- .fillMaxSize()
- .padding(dimensionResource(R.dimen.button_container_shadow_height))
- )
+ DesignSystemTheme {
+ DesignSystemFoundationScreen(
+ onButtonClicked = {},
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(dimensionResource(R.dimen.button_container_shadow_height))
+ )
+ }
}
diff --git a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemScreen.kt b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemScreen.kt
index bbe2753a09ac..fabf9b96099a 100644
--- a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemScreen.kt
+++ b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemScreen.kt
@@ -12,6 +12,11 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -20,14 +25,15 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
-import org.wordpress.android.R
import org.wordpress.android.ui.compose.components.MainTopAppBar
import org.wordpress.android.ui.compose.components.NavigationIcons
enum class DesignSystemScreen {
Start,
Foundation,
- Components
+ Components,
+ Colors,
+ Fonts
}
@Composable
@@ -55,16 +61,32 @@ fun StartDesignSystemPreview(){
DesignSystem {}
}
}
+private fun getTitleForRoute(route: String): String {
+ return when (route) {
+ "Start" -> "Design System"
+ "Foundation" -> "Foundation"
+ "Components" -> "Components"
+ "Colors" -> "Colors"
+ "Fonts" -> "Fonts"
+ else -> ""
+ }
+}
@Composable
fun DesignSystem(
onBackTapped: () -> Unit
) {
val navController: NavHostController = rememberNavController()
+ var actionBarTitle by remember { mutableStateOf("") }
+ LaunchedEffect(navController) {
+ navController.currentBackStackEntryFlow.collect { backStackEntry ->
+ actionBarTitle = getTitleForRoute(backStackEntry.destination.route.toString())
+ }
+ }
Scaffold(
topBar = {
MainTopAppBar(
- title = stringResource(R.string.preference_design_system),
+ title = actionBarTitle,
navigationIcon = NavigationIcons.BackIcon,
onNavigationIconClick = { onBackTapped() },
backgroundColor = MaterialTheme.colorScheme.primaryContainer,
@@ -89,6 +111,9 @@ fun DesignSystem(
}
composable(route = DesignSystemScreen.Foundation.name) {
DesignSystemFoundationScreen(
+ onButtonClicked = {
+ navController.navigate(it)
+ },
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
@@ -101,6 +126,20 @@ fun DesignSystem(
.padding(innerPadding)
)
}
+ composable(route = DesignSystemScreen.Colors.name) {
+ DesignSystemColorsScreen(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ )
+ }
+ composable(route = DesignSystemScreen.Fonts.name) {
+ DesignSystemFontsScreen(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ )
+ }
}
}
}
diff --git a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemTheme.kt b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemTheme.kt
index 42e76196ac5e..f83d71e88a43 100644
--- a/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemTheme.kt
+++ b/WordPress/src/main/java/org/wordpress/android/designsystem/DesignSystemTheme.kt
@@ -5,6 +5,7 @@ import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Surface
+import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
@@ -12,8 +13,12 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
private val localColors = staticCompositionLocalOf { extraPaletteLight }
+internal val localTypography = staticCompositionLocalOf { extraTypography }
@Composable
fun DesignSystemTheme(
@@ -35,36 +40,38 @@ fun DesignSystemThemeWithoutBackground(
} else {
extraPaletteLight
}
-
- CompositionLocalProvider (localColors provides extraColors) {
+ val extraTypography = extraTypography
+ CompositionLocalProvider (localColors provides extraColors, localTypography provides extraTypography) {
MaterialTheme(
colorScheme = if (isDarkTheme) paletteDarkScheme else paletteLightScheme,
+ typography = typography,
content = content
)
}
}
+
private val paletteLightScheme = lightColorScheme(
primary = DesignSystemAppColor.Black,
primaryContainer = DesignSystemAppColor.White,
- secondary = DesignSystemAppColor.Gray40,
- secondaryContainer = DesignSystemAppColor.Gray,
- tertiary = DesignSystemAppColor.Gray50,
- tertiaryContainer = DesignSystemAppColor.Gray10,
+ secondary = DesignSystemAppColor.DarkGray55,
+ secondaryContainer = DesignSystemAppColor.DarkGray8,
+ tertiary = DesignSystemAppColor.DarkGray30,
+ tertiaryContainer = DesignSystemAppColor.DarkGray15,
error = DesignSystemAppColor.Red,
)
private val paletteDarkScheme = darkColorScheme(
primary = DesignSystemAppColor.White,
primaryContainer = DesignSystemAppColor.Black,
- secondary = DesignSystemAppColor.Gray20,
- secondaryContainer = DesignSystemAppColor.Gray70,
- tertiary = DesignSystemAppColor.Gray10,
- tertiaryContainer = DesignSystemAppColor.Gray80,
+ secondary = DesignSystemAppColor.Gray60,
+ secondaryContainer = DesignSystemAppColor.Gray22,
+ tertiary = DesignSystemAppColor.Gray,
+ tertiaryContainer = DesignSystemAppColor.Gray30,
error = DesignSystemAppColor.Red10,
)
private val extraPaletteLight = ExtraColors(
- quartenaryContainer = DesignSystemAppColor.Gray30,
+ quaternaryContainer = DesignSystemAppColor.DarkGray40,
brand = DesignSystemAppColor.Green,
brandContainer = DesignSystemAppColor.Green,
warning = DesignSystemAppColor.Orange,
@@ -73,7 +80,7 @@ private val extraPaletteLight = ExtraColors(
)
private val extraPaletteDark = ExtraColors(
- quartenaryContainer = DesignSystemAppColor.Gray60,
+ quaternaryContainer = DesignSystemAppColor.Gray40,
brand = DesignSystemAppColor.Green10,
brandContainer = DesignSystemAppColor.Green20,
warning = DesignSystemAppColor.Orange10,
@@ -82,7 +89,7 @@ private val extraPaletteDark = ExtraColors(
)
data class ExtraColors(
- val quartenaryContainer: Color,
+ val quaternaryContainer: Color,
val brand: Color,
val brandContainer: Color,
val warning: Color,
@@ -90,10 +97,10 @@ data class ExtraColors(
val wpContainer: Color,
)
@Suppress("UnusedReceiverParameter")
-val ColorScheme.quartenary
+val ColorScheme.quaternaryContainer
@Composable
@ReadOnlyComposable
- get() = localColors.current.quartenaryContainer
+ get() = localColors.current.quaternaryContainer
@Suppress("UnusedReceiverParameter")
val ColorScheme.brand
@@ -125,6 +132,98 @@ val ColorScheme.wpContainer
@ReadOnlyComposable
get() = localColors.current.wpContainer
+private val extraTypography = ExtraTypography(
+ heading1 = DesignSystemAppTypography.heading1,
+ heading2 = DesignSystemAppTypography.heading2,
+ heading3 = DesignSystemAppTypography.heading3,
+ heading4 = DesignSystemAppTypography.heading4,
+ bodyLargeEmphasized = DesignSystemAppTypography.bodyLargeEmphasized,
+ bodyMediumEmphasized = DesignSystemAppTypography.bodyMediumEmphasized,
+ bodySmallEmphasized = DesignSystemAppTypography.bodySmallEmphasized,
+ footnote = DesignSystemAppTypography.footnote,
+ footnoteEmphasized = DesignSystemAppTypography.footnoteEmphasized,
+ )
+
+data class ExtraTypography(
+ val heading1: TextStyle,
+ val heading2: TextStyle,
+ val heading3: TextStyle,
+ val heading4: TextStyle,
+ val bodyLargeEmphasized: TextStyle,
+ val bodyMediumEmphasized: TextStyle,
+ val bodySmallEmphasized: TextStyle,
+ val footnote: TextStyle,
+ val footnoteEmphasized: TextStyle,
+ )
+@Suppress("UnusedReceiverParameter")
+val Typography.heading1
+ @Composable
+ @ReadOnlyComposable
+ get() = localTypography.current.heading1
+
+@Suppress("UnusedReceiverParameter")
+val Typography.heading2
+ @Composable
+ @ReadOnlyComposable
+ get() = localTypography.current.heading2
+
+@Suppress("UnusedReceiverParameter")
+val Typography.heading3
+ @Composable
+ @ReadOnlyComposable
+ get() = localTypography.current.heading3
+
+val Typography.heading4
+ @Composable
+ @ReadOnlyComposable
+ get() = localTypography.current.heading4
+
+val Typography.bodyLargeEmphasized
+ @Composable
+ @ReadOnlyComposable
+ get() = localTypography.current.bodyLargeEmphasized
+
+val Typography.bodyMediumEmphasized
+ @Composable
+ @ReadOnlyComposable
+ get() = localTypography.current.bodyMediumEmphasized
+
+val Typography.bodySmallEmphasized
+ @Composable
+ @ReadOnlyComposable
+ get() = localTypography.current.bodySmallEmphasized
+
+val Typography.footnote
+ @Composable
+ @ReadOnlyComposable
+ get() = localTypography.current.footnote
+
+val Typography.footnoteEmphasized
+ @Composable
+ @ReadOnlyComposable
+ get() = localTypography.current.footnoteEmphasized
+
+val typography = Typography(
+ bodyLarge = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.25.sp
+ ),
+ bodyMedium = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.25.sp
+ ),
+ bodySmall = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.25.sp
+ ),
+)
+
@Composable
private fun ContentInSurface(
content: @Composable () -> Unit
diff --git a/WordPress/src/main/java/org/wordpress/android/inappupdate/IInAppUpdateManager.kt b/WordPress/src/main/java/org/wordpress/android/inappupdate/IInAppUpdateManager.kt
new file mode 100644
index 000000000000..fc1135f274c8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/inappupdate/IInAppUpdateManager.kt
@@ -0,0 +1,15 @@
+package org.wordpress.android.inappupdate
+
+import android.app.Activity
+
+interface IInAppUpdateManager {
+ fun checkForAppUpdate(activity: Activity, listener: InAppUpdateListener)
+ fun completeAppUpdate()
+ fun cancelAppUpdate(updateType: Int)
+ fun onUserAcceptedAppUpdate(updateType: Int)
+
+ companion object {
+ const val APP_UPDATE_IMMEDIATE_REQUEST_CODE = 1001
+ const val APP_UPDATE_FLEXIBLE_REQUEST_CODE = 1002
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTracker.kt b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTracker.kt
new file mode 100644
index 000000000000..6bbc8cefe071
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTracker.kt
@@ -0,0 +1,40 @@
+package org.wordpress.android.inappupdate
+
+import com.google.android.play.core.install.model.AppUpdateType
+import org.wordpress.android.analytics.AnalyticsTracker
+import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
+import javax.inject.Inject
+
+class InAppUpdateAnalyticsTracker @Inject constructor(
+ private val tracker: AnalyticsTrackerWrapper
+) {
+ fun trackUpdateShown(updateType: Int) {
+ tracker.track(AnalyticsTracker.Stat.IN_APP_UPDATE_SHOWN, createPropertyMap(updateType))
+ }
+
+ fun trackUpdateAccepted(updateType: Int) {
+ tracker.track(AnalyticsTracker.Stat.IN_APP_UPDATE_ACCEPTED, createPropertyMap(updateType))
+ }
+
+ fun trackUpdateDismissed(updateType: Int) {
+ tracker.track(AnalyticsTracker.Stat.IN_APP_UPDATE_DISMISSED, createPropertyMap(updateType))
+ }
+
+ fun trackAppRestartToCompleteUpdate() {
+ tracker.track(AnalyticsTracker.Stat.IN_APP_UPDATE_COMPLETED_WITH_APP_RESTART_BY_USER)
+ }
+
+ private fun createPropertyMap(updateType: Int): Map {
+ return when (updateType) {
+ AppUpdateType.FLEXIBLE -> mapOf(PROPERTY_UPDATE_TYPE to UPDATE_TYPE_FLEXIBLE)
+ AppUpdateType.IMMEDIATE -> mapOf(PROPERTY_UPDATE_TYPE to UPDATE_TYPE_BLOCKING)
+ else -> emptyMap()
+ }
+ }
+
+ companion object {
+ const val PROPERTY_UPDATE_TYPE = "type"
+ const val UPDATE_TYPE_FLEXIBLE = "flexible"
+ const val UPDATE_TYPE_BLOCKING = "blocking"
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateListener.kt b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateListener.kt
new file mode 100644
index 000000000000..e002395f3cd9
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateListener.kt
@@ -0,0 +1,34 @@
+package org.wordpress.android.inappupdate
+
+/**
+ * Abstract class for handling callbacks related to in-app update events.
+ *
+ * Each method provides a default implementation that does nothing, allowing
+ * implementers to only override the necessary methods without implementing
+ * all callback methods.
+ */
+abstract class InAppUpdateListener {
+ open fun onAppUpdateStarted(type: Int) {
+ // Default empty implementation
+ }
+
+ open fun onAppUpdateDownloaded() {
+ // Default empty implementation
+ }
+
+ open fun onAppUpdateInstalled() {
+ // Default empty implementation
+ }
+
+ open fun onAppUpdateFailed() {
+ // Default empty implementation
+ }
+
+ open fun onAppUpdateCancelled() {
+ // Default empty implementation
+ }
+
+ open fun onAppUpdatePending() {
+ // Default empty implementation
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerImpl.kt b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerImpl.kt
new file mode 100644
index 000000000000..23809e8796ef
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerImpl.kt
@@ -0,0 +1,244 @@
+package org.wordpress.android.inappupdate
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.util.Log
+import com.google.android.play.core.appupdate.AppUpdateInfo
+import com.google.android.play.core.appupdate.AppUpdateManager
+import com.google.android.play.core.appupdate.AppUpdateOptions
+import com.google.android.play.core.install.InstallState
+import com.google.android.play.core.install.InstallStateUpdatedListener
+import com.google.android.play.core.install.model.AppUpdateType
+import com.google.android.play.core.install.model.InstallStatus
+import com.google.android.play.core.install.model.InstallStatus.CANCELED
+import com.google.android.play.core.install.model.InstallStatus.DOWNLOADED
+import com.google.android.play.core.install.model.InstallStatus.DOWNLOADING
+import com.google.android.play.core.install.model.InstallStatus.FAILED
+import com.google.android.play.core.install.model.InstallStatus.INSTALLED
+import com.google.android.play.core.install.model.InstallStatus.INSTALLING
+import com.google.android.play.core.install.model.InstallStatus.PENDING
+import com.google.android.play.core.install.model.UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS
+import com.google.android.play.core.install.model.UpdateAvailability.UPDATE_AVAILABLE
+import com.google.android.play.core.install.model.UpdateAvailability.UPDATE_NOT_AVAILABLE
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.wordpress.android.inappupdate.IInAppUpdateManager.Companion.APP_UPDATE_FLEXIBLE_REQUEST_CODE
+import org.wordpress.android.inappupdate.IInAppUpdateManager.Companion.APP_UPDATE_IMMEDIATE_REQUEST_CODE
+
+import org.wordpress.android.util.BuildConfigWrapper
+import org.wordpress.android.util.config.RemoteConfigWrapper
+import javax.inject.Singleton
+
+@Singleton
+@Suppress("TooManyFunctions")
+class InAppUpdateManagerImpl(
+ @ApplicationContext private val applicationContext: Context,
+ private val coroutineScope: CoroutineScope,
+ private val appUpdateManager: AppUpdateManager,
+ private val remoteConfigWrapper: RemoteConfigWrapper,
+ private val buildConfigWrapper: BuildConfigWrapper,
+ private val inAppUpdateAnalyticsTracker: InAppUpdateAnalyticsTracker,
+ private val currentTimeProvider: () -> Long = {System.currentTimeMillis()}
+): IInAppUpdateManager {
+ private var updateListener: InAppUpdateListener? = null
+
+ override fun checkForAppUpdate(activity: Activity, listener: InAppUpdateListener) {
+ updateListener = listener
+ appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
+ handleUpdateInfoSuccess(appUpdateInfo, activity)
+ }.addOnFailureListener { exception ->
+ Log.e(TAG, "Failed to check for update: ${exception.message}")
+ }
+ }
+
+ override fun completeAppUpdate() {
+ coroutineScope.launch(Dispatchers.Main) {
+ // Track the app restart to complete update
+ inAppUpdateAnalyticsTracker.trackAppRestartToCompleteUpdate()
+
+ // Delay so the event above can be logged
+ delay(RESTART_DELAY_IN_MILLIS)
+
+ // Complete the update
+ appUpdateManager.completeUpdate()
+ }
+ }
+
+ override fun cancelAppUpdate(updateType: Int) {
+ appUpdateManager.unregisterListener(installStateListener)
+ inAppUpdateAnalyticsTracker.trackUpdateDismissed(updateType)
+ }
+
+ override fun onUserAcceptedAppUpdate(updateType: Int) {
+ inAppUpdateAnalyticsTracker.trackUpdateAccepted(updateType)
+ }
+
+ private fun handleUpdateInfoSuccess(appUpdateInfo: AppUpdateInfo, activity: Activity) {
+ when (appUpdateInfo.updateAvailability()) {
+ UPDATE_NOT_AVAILABLE -> {
+ /* do nothing */
+ }
+ UPDATE_AVAILABLE -> {
+ handleUpdateAvailable(appUpdateInfo, activity)
+ }
+ DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS -> {
+ handleUpdateInProgress(appUpdateInfo, activity)
+ }
+ else -> { /* do nothing */ }
+ }
+ }
+
+ private fun handleUpdateAvailable(appUpdateInfo: AppUpdateInfo, activity: Activity) {
+ if (appUpdateInfo.installStatus() == DOWNLOADED) {
+ updateListener?.onAppUpdateDownloaded()
+ return
+ }
+
+ val updateVersion = getAvailableUpdateAppVersion(appUpdateInfo)
+ if (updateVersion != getLastUpdateRequestedVersion()) {
+ resetLastUpdateRequestInfo()
+ }
+
+ if (isImmediateUpdateNecessary()) {
+ if (shouldRequestImmediateUpdate()) {
+ requestImmediateUpdate(appUpdateInfo, activity)
+ }
+ } else if (shouldRequestFlexibleUpdate()) {
+ requestFlexibleUpdate(appUpdateInfo, activity)
+ }
+ }
+
+ private fun handleUpdateInProgress(appUpdateInfo: AppUpdateInfo, activity: Activity) {
+ if (isImmediateUpdateInProgress(appUpdateInfo)) {
+ requestImmediateUpdate(appUpdateInfo, activity)
+ } else {
+ requestFlexibleUpdate(appUpdateInfo, activity)
+ }
+ }
+
+ private fun requestImmediateUpdate(appUpdateInfo: AppUpdateInfo, activity: Activity) {
+ updateListener?.onAppUpdateStarted(AppUpdateType.IMMEDIATE)
+ requestUpdate(AppUpdateType.IMMEDIATE, appUpdateInfo, activity)
+ }
+
+ private fun requestFlexibleUpdate(appUpdateInfo: AppUpdateInfo, activity: Activity) {
+ appUpdateManager.registerListener(installStateListener)
+ updateListener?.onAppUpdateStarted(AppUpdateType.FLEXIBLE)
+ requestUpdate(AppUpdateType.FLEXIBLE, appUpdateInfo, activity)
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ private fun requestUpdate(updateType: Int, appUpdateInfo: AppUpdateInfo, activity: Activity) {
+ saveLastUpdateRequestInfo(appUpdateInfo)
+ val requestCode = if (updateType == AppUpdateType.IMMEDIATE) {
+ APP_UPDATE_IMMEDIATE_REQUEST_CODE
+ } else {
+ APP_UPDATE_FLEXIBLE_REQUEST_CODE
+ }
+ try {
+ appUpdateManager.startUpdateFlowForResult(
+ appUpdateInfo,
+ activity,
+ AppUpdateOptions.newBuilder(updateType).build(),
+ requestCode
+ )
+ inAppUpdateAnalyticsTracker.trackUpdateShown(updateType)
+ } catch (e: Exception) {
+ Log.e(TAG, "requestUpdate for type: $updateType, exception occurred")
+ Log.e(TAG, e.message.toString())
+ appUpdateManager.unregisterListener(installStateListener)
+ }
+ }
+
+ private val installStateListener = object : InstallStateUpdatedListener {
+ @SuppressLint("SwitchIntDef")
+ override fun onStateUpdate(state: InstallState) {
+ when (state.installStatus()) {
+ DOWNLOADED -> {
+ updateListener?.onAppUpdateDownloaded()
+ }
+ INSTALLED -> {
+ updateListener?.onAppUpdateInstalled()
+ appUpdateManager.unregisterListener(this) // 'this' refers to the listener object
+ }
+ CANCELED -> {
+ updateListener?.onAppUpdateCancelled()
+ appUpdateManager.unregisterListener(this)
+ }
+ FAILED -> {
+ updateListener?.onAppUpdateFailed()
+ appUpdateManager.unregisterListener(this)
+ }
+ PENDING -> {
+ updateListener?.onAppUpdatePending()
+ }
+ DOWNLOADING, INSTALLING, InstallStatus.UNKNOWN -> {
+ /* do nothing */
+ }
+ }
+ }
+ }
+
+ private fun isImmediateUpdateInProgress(appUpdateInfo: AppUpdateInfo) =
+ appUpdateInfo.updateAvailability() == DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS
+ && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
+ && isImmediateUpdateNecessary()
+
+ private fun saveLastUpdateRequestInfo(appUpdateInfo: AppUpdateInfo) {
+ val sharedPref = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
+ sharedPref.edit().apply {
+ putInt(KEY_LAST_APP_UPDATE_CHECK_VERSION, getAvailableUpdateAppVersion(appUpdateInfo))
+ putLong(KEY_LAST_APP_UPDATE_CHECK_TIME, currentTimeProvider.invoke())
+ apply()
+ }
+ }
+
+ private fun resetLastUpdateRequestInfo() {
+ val sharedPref = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
+ sharedPref.edit().apply {
+ putInt(KEY_LAST_APP_UPDATE_CHECK_VERSION, -1)
+ putLong(KEY_LAST_APP_UPDATE_CHECK_TIME, -1L)
+ apply()
+ }
+ }
+
+ private fun getLastUpdateRequestedVersion() =
+ applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
+ .getInt(KEY_LAST_APP_UPDATE_CHECK_VERSION, -1)
+
+ private fun getLastUpdateRequestedTime() =
+ applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
+ .getLong(KEY_LAST_APP_UPDATE_CHECK_TIME, -1L)
+
+ private fun shouldRequestFlexibleUpdate() =
+ currentTimeProvider.invoke() - getLastUpdateRequestedTime() >= getFlexibleUpdateIntervalInMillis()
+
+ private fun shouldRequestImmediateUpdate() =
+ currentTimeProvider.invoke() - getLastUpdateRequestedTime() >= IMMEDIATE_UPDATE_INTERVAL_IN_MILLIS
+
+ @Suppress("MagicNumber")
+ private fun getFlexibleUpdateIntervalInMillis(): Long =
+ 1000 * 60 * 60 * 24 * remoteConfigWrapper.getInAppUpdateFlexibleIntervalInDays().toLong()
+
+ private fun getCurrentAppVersion() = buildConfigWrapper.getAppVersionCode()
+
+ private fun getLastBlockingAppVersion(): Int = remoteConfigWrapper.getInAppUpdateBlockingVersion()
+
+ private fun getAvailableUpdateAppVersion(appUpdateInfo: AppUpdateInfo) = appUpdateInfo.availableVersionCode()
+
+ private fun isImmediateUpdateNecessary() = getCurrentAppVersion() < getLastBlockingAppVersion()
+
+ companion object {
+ const val IMMEDIATE_UPDATE_INTERVAL_IN_MILLIS = 1000 * 60 * 5 // 5 minutes
+ const val KEY_LAST_APP_UPDATE_CHECK_TIME = "last_app_update_check_time"
+
+ private const val TAG = "AppUpdateChecker"
+ private const val PREF_NAME = "in_app_update_prefs"
+ private const val KEY_LAST_APP_UPDATE_CHECK_VERSION = "last_app_update_check_version"
+ private const val RESTART_DELAY_IN_MILLIS = 500L
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerNoop.kt b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerNoop.kt
new file mode 100644
index 000000000000..d732dc62d7e9
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerNoop.kt
@@ -0,0 +1,21 @@
+package org.wordpress.android.inappupdate
+
+import android.app.Activity
+
+class InAppUpdateManagerNoop: IInAppUpdateManager {
+ override fun checkForAppUpdate(activity: Activity, listener: InAppUpdateListener) {
+ /* Empty implementation */
+ }
+
+ override fun completeAppUpdate() {
+ /* Empty implementation */
+ }
+
+ override fun cancelAppUpdate(updateType: Int) {
+ /* Empty implementation */
+ }
+
+ override fun onUserAcceptedAppUpdate(updateType: Int) {
+ /* Empty implementation */
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/localcontentmigration/UserFlagsProviderHelper.kt b/WordPress/src/main/java/org/wordpress/android/localcontentmigration/UserFlagsProviderHelper.kt
index 839474dc2f14..5d8a75087824 100644
--- a/WordPress/src/main/java/org/wordpress/android/localcontentmigration/UserFlagsProviderHelper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/localcontentmigration/UserFlagsProviderHelper.kt
@@ -68,7 +68,6 @@ class UserFlagsProviderHelper @Inject constructor(
UndeletablePrefKey.BOOKMARKS_SAVED_LOCALLY_DIALOG_SHOWN.name,
UndeletablePrefKey.SWIPE_TO_NAVIGATE_NOTIFICATIONS.name,
UndeletablePrefKey.SWIPE_TO_NAVIGATE_READER.name,
- UndeletablePrefKey.SHOULD_SHOW_STORIES_INTRO.name,
UndeletablePrefKey.SHOULD_SHOW_STORAGE_WARNING.name,
UndeletablePrefKey.LAST_USED_USER_ID.name,
contextProvider.getContext().getString(R.string.pref_key_app_theme),
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Note.java b/WordPress/src/main/java/org/wordpress/android/models/Note.java
deleted file mode 100644
index 25421c90d7d4..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/models/Note.java
+++ /dev/null
@@ -1,559 +0,0 @@
-/**
- * Note represents a single WordPress.com notification
- */
-package org.wordpress.android.models;
-
-import android.text.Spannable;
-import android.text.TextUtils;
-import android.util.Base64;
-
-import androidx.annotation.NonNull;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.wordpress.android.fluxc.model.CommentModel;
-import org.wordpress.android.fluxc.model.CommentStatus;
-import org.wordpress.android.ui.notifications.utils.NotificationsUtilsWrapper;
-import org.wordpress.android.util.AppLog;
-import org.wordpress.android.util.DateTimeUtils;
-import org.wordpress.android.util.DateUtils;
-import org.wordpress.android.util.JSONUtils;
-import org.wordpress.android.util.StringUtils;
-
-import java.io.UnsupportedEncodingException;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.EnumSet;
-import java.util.zip.DataFormatException;
-import java.util.zip.Inflater;
-
-public class Note {
- private static final String TAG = "NoteModel";
-
- // Maximum character length for a comment preview
- private static final int MAX_COMMENT_PREVIEW_LENGTH = 200;
-
- // Note types
- public static final String NOTE_FOLLOW_TYPE = "follow";
- public static final String NOTE_LIKE_TYPE = "like";
- public static final String NOTE_COMMENT_TYPE = "comment";
- public static final String NOTE_MATCHER_TYPE = "automattcher";
- public static final String NOTE_COMMENT_LIKE_TYPE = "comment_like";
- public static final String NOTE_REBLOG_TYPE = "reblog";
- public static final String NOTE_NEW_POST_TYPE = "new_post";
- public static final String NOTE_VIEW_MILESTONE = "view_milestone";
- public static final String NOTE_UNKNOWN_TYPE = "unknown";
-
- // JSON action keys
- private static final String ACTION_KEY_REPLY = "replyto-comment";
- private static final String ACTION_KEY_APPROVE = "approve-comment";
- private static final String ACTION_KEY_SPAM = "spam-comment";
- private static final String ACTION_KEY_LIKE = "like-comment";
-
- private JSONObject mActions;
- private JSONObject mNoteJSON;
- private final String mKey;
-
- private final Object mSyncLock = new Object();
- private String mLocalStatus;
-
- public enum EnabledActions {
- ACTION_REPLY,
- ACTION_APPROVE,
- ACTION_UNAPPROVE,
- ACTION_SPAM,
- ACTION_LIKE
- }
-
- public enum NoteTimeGroup {
- GROUP_TODAY,
- GROUP_YESTERDAY,
- GROUP_OLDER_TWO_DAYS,
- GROUP_OLDER_WEEK,
- GROUP_OLDER_MONTH
- }
-
- public Note(String key, JSONObject noteJSON) {
- mKey = key;
- mNoteJSON = noteJSON;
- }
-
- public Note(JSONObject noteJSON) {
- mNoteJSON = noteJSON;
- mKey = mNoteJSON.optString("id", "");
- }
-
- public JSONObject getJSON() {
- return mNoteJSON != null ? mNoteJSON : new JSONObject();
- }
-
- public String getId() {
- return mKey;
- }
-
- public String getType() {
- return queryJSON("type", NOTE_UNKNOWN_TYPE);
- }
-
- private Boolean isType(String type) {
- return getType().equals(type);
- }
-
- public Boolean isCommentType() {
- synchronized (mSyncLock) {
- return (isAutomattcherType() && JSONUtils.queryJSON(mNoteJSON, "meta.ids.comment", -1) != -1)
- || isType(NOTE_COMMENT_TYPE);
- }
- }
-
- public Boolean isAutomattcherType() {
- return isType(NOTE_MATCHER_TYPE);
- }
-
- public Boolean isNewPostType() {
- return isType(NOTE_NEW_POST_TYPE);
- }
-
- public Boolean isFollowType() {
- return isType(NOTE_FOLLOW_TYPE);
- }
-
- public Boolean isLikeType() {
- return isPostLikeType() || isCommentLikeType();
- }
-
- public Boolean isPostLikeType() {
- return isType(NOTE_LIKE_TYPE);
- }
-
- public Boolean isCommentLikeType() {
- return isType(NOTE_COMMENT_LIKE_TYPE);
- }
-
- public Boolean isReblogType() {
- return isType(NOTE_REBLOG_TYPE);
- }
-
- public Boolean isViewMilestoneType() {
- return isType(NOTE_VIEW_MILESTONE);
- }
-
- public Boolean isCommentReplyType() {
- return isCommentType() && getParentCommentId() > 0;
- }
-
- // Returns true if the user has replied to this comment note
- public Boolean isCommentWithUserReply() {
- return isCommentType() && !TextUtils.isEmpty(getCommentSubjectNoticon());
- }
-
- public Boolean isUserList() {
- return isLikeType() || isFollowType() || isReblogType();
- }
-
- /*
- * does user have permission to moderate/reply/spam this comment?
- */
- public boolean canModerate() {
- EnumSet enabledActions = getEnabledActions();
- return enabledActions != null && (enabledActions.contains(EnabledActions.ACTION_APPROVE) || enabledActions
- .contains(EnabledActions.ACTION_UNAPPROVE));
- }
-
- public boolean canMarkAsSpam() {
- EnumSet enabledActions = getEnabledActions();
- return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_SPAM));
- }
-
- public boolean canReply() {
- EnumSet enabledActions = getEnabledActions();
- return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_REPLY));
- }
-
- public boolean canTrash() {
- return canModerate();
- }
-
- public boolean canLike() {
- EnumSet enabledActions = getEnabledActions();
- return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_LIKE));
- }
-
- public String getLocalStatus() {
- return StringUtils.notNullStr(mLocalStatus);
- }
-
- public void setLocalStatus(String localStatus) {
- mLocalStatus = localStatus;
- }
-
- public JSONObject getSubject() {
- try {
- synchronized (mSyncLock) {
- JSONArray subjectArray = mNoteJSON.getJSONArray("subject");
- if (subjectArray.length() > 0) {
- return subjectArray.getJSONObject(0);
- }
- }
- } catch (JSONException e) {
- return null;
- }
-
- return null;
- }
-
- public Spannable getFormattedSubject(NotificationsUtilsWrapper notificationsUtilsWrapper) {
- return notificationsUtilsWrapper.getSpannableContentForRanges(getSubject());
- }
-
- public String getTitle() {
- return queryJSON("title", "");
- }
-
- public String getIconURL() {
- return queryJSON("icon", "");
- }
-
- public String getCommentSubject() {
- synchronized (mSyncLock) {
- JSONArray subjectArray = mNoteJSON.optJSONArray("subject");
- if (subjectArray != null) {
- String commentSubject = JSONUtils.queryJSON(subjectArray, "subject[1].text", "");
-
- // Trim down the comment preview if the comment text is too large.
- if (commentSubject != null && commentSubject.length() > MAX_COMMENT_PREVIEW_LENGTH) {
- commentSubject = commentSubject.substring(0, MAX_COMMENT_PREVIEW_LENGTH - 1);
- }
-
- return commentSubject;
- }
- }
-
- return "";
- }
-
- public String getCommentSubjectNoticon() {
- JSONArray subjectRanges = queryJSON("subject[0].ranges", new JSONArray());
- if (subjectRanges != null) {
- for (int i = 0; i < subjectRanges.length(); i++) {
- try {
- JSONObject rangeItem = subjectRanges.getJSONObject(i);
- if (rangeItem.has("type") && rangeItem.optString("type").equals("noticon")) {
- return rangeItem.optString("value", "");
- }
- } catch (JSONException e) {
- return "";
- }
- }
- }
-
- return "";
- }
-
- public long getCommentReplyId() {
- return queryJSON("meta.ids.reply_comment", 0);
- }
-
- /**
- * Compare note timestamp to now and return a time grouping
- */
- public static NoteTimeGroup getTimeGroupForTimestamp(long timestamp) {
- Date today = new Date();
- Date then = new Date(timestamp * 1000);
-
- if (then.compareTo(DateUtils.addMonths(today, -1)) < 0) {
- return NoteTimeGroup.GROUP_OLDER_MONTH;
- } else if (then.compareTo(DateUtils.addWeeks(today, -1)) < 0) {
- return NoteTimeGroup.GROUP_OLDER_WEEK;
- } else if (then.compareTo(DateUtils.addDays(today, -2)) < 0
- || DateUtils.isSameDay(DateUtils.addDays(today, -2), then)) {
- return NoteTimeGroup.GROUP_OLDER_TWO_DAYS;
- } else if (DateUtils.isSameDay(DateUtils.addDays(today, -1), then)) {
- return NoteTimeGroup.GROUP_YESTERDAY;
- } else {
- return NoteTimeGroup.GROUP_TODAY;
- }
- }
-
- public static class TimeStampComparator implements Comparator {
- @Override
- public int compare(Note a, Note b) {
- return b.getTimestampString().compareTo(a.getTimestampString());
- }
- }
-
- /**
- * The inverse of isRead
- */
- public Boolean isUnread() {
- return !isRead();
- }
-
- private Boolean isRead() {
- return queryJSON("read", 0) == 1;
- }
-
- public void setRead() {
- try {
- mNoteJSON.putOpt("read", 1);
- } catch (JSONException e) {
- AppLog.e(AppLog.T.NOTIFS, "Failed to set 'read' property", e);
- }
- }
-
- /**
- * Get the timestamp provided by the API for the note
- */
- public long getTimestamp() {
- return DateTimeUtils.timestampFromIso8601(getTimestampString());
- }
-
- public String getTimestampString() {
- return queryJSON("timestamp", "");
- }
-
- public JSONArray getBody() {
- try {
- synchronized (mSyncLock) {
- return mNoteJSON.getJSONArray("body");
- }
- } catch (JSONException e) {
- return new JSONArray();
- }
- }
-
- // returns character code for notification font
- public String getNoticonCharacter() {
- return queryJSON("noticon", "");
- }
-
- private JSONObject getCommentActions() {
- if (mActions == null) {
- // Find comment block that matches the root note comment id
- long commentId = getCommentId();
- JSONArray bodyArray = getBody();
- for (int i = 0; i < bodyArray.length(); i++) {
- try {
- JSONObject bodyItem = bodyArray.getJSONObject(i);
- if (bodyItem.has("type") && bodyItem.optString("type").equals("comment")
- && commentId == JSONUtils.queryJSON(bodyItem, "meta.ids.comment", 0)) {
- mActions = JSONUtils.queryJSON(bodyItem, "actions", new JSONObject());
- break;
- }
- } catch (JSONException e) {
- break;
- }
- }
-
- if (mActions == null) {
- mActions = new JSONObject();
- }
- }
-
- return mActions;
- }
-
- /*
- * returns the actions allowed on this note, assumes it's a comment notification
- */
- public EnumSet getEnabledActions() {
- EnumSet actions = EnumSet.noneOf(EnabledActions.class);
- JSONObject jsonActions = getCommentActions();
- if (jsonActions == null || jsonActions.length() == 0) {
- return actions;
- }
-
- if (jsonActions.has(ACTION_KEY_REPLY)) {
- actions.add(EnabledActions.ACTION_REPLY);
- }
- if (jsonActions.has(ACTION_KEY_APPROVE) && jsonActions.optBoolean(ACTION_KEY_APPROVE, false)) {
- actions.add(EnabledActions.ACTION_UNAPPROVE);
- }
- if (jsonActions.has(ACTION_KEY_APPROVE) && !jsonActions.optBoolean(ACTION_KEY_APPROVE, false)) {
- actions.add(EnabledActions.ACTION_APPROVE);
- }
- if (jsonActions.has(ACTION_KEY_SPAM)) {
- actions.add(EnabledActions.ACTION_SPAM);
- }
- if (jsonActions.has(ACTION_KEY_LIKE)) {
- actions.add(EnabledActions.ACTION_LIKE);
- }
-
- return actions;
- }
-
- public int getSiteId() {
- return queryJSON("meta.ids.site", 0);
- }
-
- public int getPostId() {
- return queryJSON("meta.ids.post", 0);
- }
-
- public long getCommentId() {
- return queryJSON("meta.ids.comment", 0);
- }
-
- public long getParentCommentId() {
- return queryJSON("meta.ids.parent_comment", 0);
- }
-
- /**
- * Rudimentary system for pulling an item out of a JSON object hierarchy
- */
- private U queryJSON(String query, U defaultObject) {
- synchronized (mSyncLock) {
- if (mNoteJSON == null) {
- return defaultObject;
- }
- return JSONUtils.queryJSON(mNoteJSON, query, defaultObject);
- }
- }
-
- /**
- * Constructs a new Comment object based off of data in a Note
- */
- @NonNull
- public CommentModel buildComment() {
- CommentModel comment = new CommentModel();
- comment.setRemotePostId(getPostId());
- comment.setRemoteCommentId(getCommentId());
- comment.setAuthorName(getCommentAuthorName());
- comment.setDatePublished(DateTimeUtils.iso8601FromTimestamp(getTimestamp()));
- comment.setContent(getCommentText());
- comment.setStatus(getCommentStatus().toString());
- comment.setAuthorUrl(getCommentAuthorUrl());
- comment.setPostTitle(getTitle()); // unavailable in note model
- comment.setAuthorEmail(""); // unavailable in note model
- comment.setAuthorProfileImageUrl(getIconURL());
- comment.setILike(hasLikedComment());
- return comment;
- }
-
- public String getCommentAuthorName() {
- JSONArray bodyArray = getBody();
-
- for (int i = 0; i < bodyArray.length(); i++) {
- try {
- JSONObject bodyItem = bodyArray.getJSONObject(i);
- if (bodyItem.has("type") && bodyItem.optString("type").equals("user")) {
- return bodyItem.optString("text");
- }
- } catch (JSONException e) {
- return "";
- }
- }
-
- return "";
- }
-
- private String getCommentText() {
- return queryJSON("body[last].text", "");
- }
-
- private String getCommentAuthorUrl() {
- JSONArray bodyArray = getBody();
-
- for (int i = 0; i < bodyArray.length(); i++) {
- try {
- JSONObject bodyItem = bodyArray.getJSONObject(i);
- if (bodyItem.has("type") && bodyItem.optString("type").equals("user")) {
- return JSONUtils.queryJSON(bodyItem, "meta.links.home", "");
- }
- } catch (JSONException e) {
- return "";
- }
- }
-
- return "";
- }
-
- public CommentStatus getCommentStatus() {
- EnumSet enabledActions = getEnabledActions();
-
- if (enabledActions.contains(EnabledActions.ACTION_UNAPPROVE)) {
- return CommentStatus.APPROVED;
- } else if (enabledActions.contains(EnabledActions.ACTION_APPROVE)) {
- return CommentStatus.UNAPPROVED;
- }
-
- return CommentStatus.ALL;
- }
-
- public boolean hasLikedComment() {
- JSONObject jsonActions = getCommentActions();
- return !(jsonActions == null || jsonActions.length() == 0) && jsonActions.optBoolean(ACTION_KEY_LIKE);
- }
-
- public String getUrl() {
- return queryJSON("url", "");
- }
-
- public JSONArray getHeader() {
- synchronized (mSyncLock) {
- return mNoteJSON.optJSONArray("header");
- }
- }
-
- // this method is used to compare two Notes: as it's potentially a very processing intensive operation,
- // we're only comparing the note id, timestamp, and raw JSON length, which is accurate enough for
- // the purpose of checking if the local Note is any different from a remote note.
- public boolean equalsTimeAndLength(Note note) {
- if (note == null) {
- return false;
- }
-
- if (this.getTimestampString().equalsIgnoreCase(note.getTimestampString())
- && this.getJSON().length() == note.getJSON().length()) {
- return true;
- }
- return false;
- }
-
- public static synchronized Note buildFromBase64EncodedData(String noteId, String base64FullNoteData) {
- Note note = null;
-
- if (base64FullNoteData == null) {
- return null;
- }
-
- byte[] b64DecodedPayload = Base64.decode(base64FullNoteData, Base64.DEFAULT);
-
- // Decompress the payload
- Inflater decompresser = new Inflater();
- decompresser.setInput(b64DecodedPayload, 0, b64DecodedPayload.length);
- byte[] result = new byte[4096]; // max length an Android PN payload can have
- int resultLength = 0;
- try {
- resultLength = decompresser.inflate(result);
- decompresser.end();
- } catch (DataFormatException e) {
- AppLog.e(AppLog.T.NOTIFS, "Can't decompress the PN BlockListPayload. It could be > 4K", e);
- }
-
- String out = null;
- try {
- out = new String(result, 0, resultLength, "UTF8");
- } catch (UnsupportedEncodingException e) {
- AppLog.e(AppLog.T.NOTIFS, "Notification data contains non UTF8 characters.", e);
- }
-
- if (out != null) {
- try {
- JSONObject jsonObject = new JSONObject(out);
- if (jsonObject.has("notes")) {
- JSONArray jsonArray = jsonObject.getJSONArray("notes");
- if (jsonArray != null && jsonArray.length() == 1) {
- jsonObject = jsonArray.getJSONObject(0);
- }
- }
- note = new Note(noteId, jsonObject);
- } catch (JSONException e) {
- AppLog.e(AppLog.T.NOTIFS, "Can't parse the Note JSON received in the PN", e);
- }
- }
-
- return note;
- }
-}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Note.kt b/WordPress/src/main/java/org/wordpress/android/models/Note.kt
new file mode 100644
index 000000000000..7dfe4cdb1543
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Note.kt
@@ -0,0 +1,442 @@
+/**
+ * Note represents a single WordPress.com notification
+ */
+package org.wordpress.android.models
+
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.TextUtils
+import android.util.Base64
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import org.wordpress.android.fluxc.model.CommentModel
+import org.wordpress.android.fluxc.model.CommentStatus
+import org.wordpress.android.ui.notifications.utils.NotificationsUtilsWrapper
+import org.wordpress.android.util.AppLog
+import org.wordpress.android.util.DateTimeUtils
+import org.wordpress.android.util.DateUtils.addDays
+import org.wordpress.android.util.DateUtils.addMonths
+import org.wordpress.android.util.DateUtils.addWeeks
+import org.wordpress.android.util.DateUtils.isSameDay
+import org.wordpress.android.util.JSONUtils
+import org.wordpress.android.util.StringUtils
+import java.io.UnsupportedEncodingException
+import java.util.Date
+import java.util.EnumSet
+import java.util.zip.DataFormatException
+import java.util.zip.Inflater
+
+class Note {
+ val id: String
+ var localStatus: String? = null
+ get() = StringUtils.notNullStr(field)
+
+ private var mNoteJSON: JSONObject? = null
+
+ constructor(key: String, noteJSON: JSONObject?) {
+ id = key
+ mNoteJSON = noteJSON
+ }
+
+ constructor(noteJSON: JSONObject?) {
+ mNoteJSON = noteJSON
+ id = mNoteJSON?.optString("id", "") ?: ""
+ }
+
+ enum class EnabledActions {
+ ACTION_REPLY,
+ ACTION_APPROVE,
+ ACTION_UNAPPROVE,
+ ACTION_SPAM,
+ ACTION_LIKE_COMMENT,
+ ACTION_LIKE_POST
+ }
+
+ enum class NoteTimeGroup {
+ GROUP_TODAY,
+ GROUP_YESTERDAY,
+ GROUP_OLDER_TWO_DAYS,
+ GROUP_OLDER_WEEK,
+ GROUP_OLDER_MONTH
+ }
+
+ /**
+ * Immutable lazily initialised properties from the note JSON
+ */
+
+ val siteId: Int by lazy { queryJSON("meta.ids.site", 0) }
+ val postId: Int by lazy { queryJSON("meta.ids.post", 0) }
+ val rawType: String by lazy { queryJSON("type", NOTE_UNKNOWN_TYPE) }
+ val commentId: Long by lazy { queryJSON("meta.ids.comment", 0).toLong() }
+ val parentCommentId: Long by lazy { queryJSON("meta.ids.parent_comment", 0).toLong() }
+ val url: String by lazy { queryJSON("url", "") }
+ val header: JSONArray? by lazy { mNoteJSON?.optJSONArray("header") }
+ val commentReplyId: Long by lazy { queryJSON("meta.ids.reply_comment", 0).toLong() }
+ val title: String by lazy { queryJSON("title", "") }
+ val iconURL: String by lazy { queryJSON("icon", "") }
+ val enabledCommentActions: EnumSet by lazy { getEnabledActions(commentActions) }
+ private val enabledPostActions: EnumSet by lazy { getEnabledActions(postActions) }
+ private val timestampString: String by lazy { queryJSON("timestamp", "") }
+ private val commentText: String by lazy { queryJSON("body[last].text", "") }
+ private val commentActions: JSONObject by lazy { getActions(commentId, "comment") }
+ private val postActions: JSONObject by lazy { getActions(postId.toLong(), "post") }
+
+ val body: JSONArray by lazy {
+ runCatching {
+ mNoteJSON?.getJSONArray("body") ?: JSONArray()
+ }.getOrElse {
+ JSONArray()
+ }
+ }
+
+ val subject: JSONObject? by lazy {
+ runCatching {
+ val subjectArray = mNoteJSON?.getJSONArray("subject")
+ if (subjectArray != null && subjectArray.length() > 0) {
+ subjectArray.getJSONObject(0)
+ } else null
+ }.getOrElse {
+ null
+ }
+ }
+
+ val iconURLs: List? by lazy {
+ val bodyArray = mNoteJSON?.optJSONArray("body")
+ if (bodyArray != null && bodyArray.length() > 0) {
+ val iconUrls = ArrayList()
+ for (i in 0 until bodyArray.length()) {
+ val iconUrl = JSONUtils.queryJSON(bodyArray, "body[$i].media[0].url", "")
+ if (iconUrl != null && iconUrl.isNotEmpty()) {
+ iconUrls.add(iconUrl)
+ }
+ }
+ return@lazy iconUrls
+ }
+ null
+ }
+
+ val commentSubject: String? by lazy {
+ val subjectArray = mNoteJSON?.optJSONArray("subject")
+ if (subjectArray != null) {
+ var commentSubject = JSONUtils.queryJSON(subjectArray, "subject[1].text", "")
+
+ // Trim down the comment preview if the comment text is too large.
+ if (commentSubject != null && commentSubject.length > MAX_COMMENT_PREVIEW_LENGTH) {
+ commentSubject = commentSubject.substring(0, MAX_COMMENT_PREVIEW_LENGTH - 1)
+ }
+ return@lazy commentSubject
+ }
+ ""
+ }
+
+ val commentSubjectNoticon: String by lazy {
+ with(queryJSON("subject[0].ranges", JSONArray())) {
+ for (i in 0 until length()) {
+ runCatching {
+ val rangeItem = getJSONObject(i)
+ if (rangeItem.has("type") && rangeItem.optString("type") == "noticon") {
+ return@lazy rangeItem.optString("value", "")
+ }
+ }.getOrElse {
+ return@lazy ""
+ }
+ }
+ }
+ ""
+ }
+
+ val commentAuthorName: String by lazy {
+ val bodyArray = body
+ for (i in 0 until bodyArray.length()) {
+ runCatching {
+ val bodyItem = bodyArray.getJSONObject(i)
+ if (bodyItem.has("type") && bodyItem.optString("type") == "user") {
+ return@lazy bodyItem.optString("text")
+ }
+ }.getOrElse {
+ return@lazy ""
+ }
+ }
+ ""
+ }
+
+ val isCommentType: Boolean by lazy {
+ isTypeRaw(NOTE_COMMENT_TYPE) ||
+ isAutomattcherType && JSONUtils.queryJSON(mNoteJSON, "meta.ids.comment", -1) != -1
+ }
+
+ private val commentAuthorUrl: String by lazy {
+ val bodyArray = body
+ for (i in 0 until bodyArray.length()) {
+ runCatching {
+ val bodyItem = bodyArray.getJSONObject(i)
+ if (bodyItem.has("type") && bodyItem.optString("type") == "user") {
+ return@lazy JSONUtils.queryJSON(bodyItem, "meta.links.home", "")
+ }
+ }.getOrElse {
+ return@lazy ""
+ }
+ }
+ ""
+ }
+
+ /**
+ * Computed properties
+ */
+ val json: JSONObject
+ get() = mNoteJSON ?: JSONObject()
+ val isAutomattcherType: Boolean
+ get() = isTypeRaw(NOTE_MATCHER_TYPE)
+ val isNewPostType: Boolean
+ get() = isTypeRaw(NOTE_NEW_POST_TYPE)
+ val isFollowType: Boolean
+ get() = isTypeRaw(NOTE_FOLLOW_TYPE)
+ val isLikeType: Boolean
+ get() = isPostLikeType || isCommentLikeType
+ val isPostLikeType: Boolean
+ get() = isTypeRaw(NOTE_LIKE_TYPE)
+ val isCommentLikeType: Boolean
+ get() = isTypeRaw(NOTE_COMMENT_LIKE_TYPE)
+ val isViewMilestoneType: Boolean
+ get() = isTypeRaw(NOTE_VIEW_MILESTONE)
+ val isCommentReplyType: Boolean
+ get() = isCommentType && parentCommentId > 0
+ val isCommentWithUserReply: Boolean
+ // Returns true if the user has replied to this comment note
+ get() = isCommentType && !TextUtils.isEmpty(commentSubjectNoticon)
+ val isUserList: Boolean
+ get() = isLikeType || isFollowType
+ val isUnread: Boolean // Parsing every time since it may change
+ get() = queryJSON("read", 0) != 1
+ val timestamp: Long
+ get() = DateTimeUtils.timestampFromIso8601(timestampString)
+ val commentStatus: CommentStatus
+ get() = if (enabledCommentActions.contains(EnabledActions.ACTION_UNAPPROVE)) {
+ CommentStatus.APPROVED
+ } else if (enabledCommentActions.contains(EnabledActions.ACTION_APPROVE)) {
+ CommentStatus.UNAPPROVED
+ } else {
+ CommentStatus.ALL
+ }
+
+ /**
+ * Setters
+ */
+
+ fun setRead() = updateReadState(1)
+
+ fun setUnread() = updateReadState(0)
+
+ private fun updateReadState(read: Int) {
+ try {
+ mNoteJSON?.putOpt("read", read)
+ } catch (e: JSONException) {
+ AppLog.e(AppLog.T.NOTIFS, "Failed to set 'read' property", e)
+ }
+ }
+
+ fun setLikedComment(liked: Boolean) {
+ try {
+ commentActions.put(ACTION_KEY_LIKE_COMMENT, liked)
+ } catch (e: JSONException) {
+ AppLog.e(AppLog.T.NOTIFS, "Failed to set 'like' property for the note", e)
+ }
+ }
+
+ fun setLikedPost(liked: Boolean) {
+ try {
+ postActions.put(ACTION_KEY_LIKE_POST, liked)
+ } catch (e: JSONException) {
+ AppLog.e(AppLog.T.NOTIFS, "Failed to set 'like' property for the note", e)
+ }
+ }
+
+ /**
+ * Helper methods
+ */
+
+ fun canModerate() = enabledCommentActions.contains(EnabledActions.ACTION_APPROVE) ||
+ enabledCommentActions.contains(EnabledActions.ACTION_UNAPPROVE)
+
+ fun canReply() = enabledCommentActions.contains(EnabledActions.ACTION_REPLY)
+
+ fun canLikeComment() = enabledCommentActions.contains(EnabledActions.ACTION_LIKE_COMMENT)
+
+ fun canLikePost() = enabledPostActions.contains(EnabledActions.ACTION_LIKE_POST)
+
+ fun getFormattedSubject(notificationsUtilsWrapper: NotificationsUtilsWrapper): Spannable {
+ return subject?.let { notificationsUtilsWrapper.getSpannableContentForRanges(it) } ?: SpannableString("")
+ }
+
+ fun hasLikedComment() = commentActions.length() > 0 && commentActions.optBoolean(ACTION_KEY_LIKE_COMMENT)
+
+ fun hasLikedPost() = postActions.length() > 0 && postActions.optBoolean(ACTION_KEY_LIKE_POST)
+
+ /**
+ * Compares two notes to see if they are the same: as it's potentially a very processing intensive operation,
+ * we're only comparing the note id, timestamp, and raw JSON length, which is accurate enough for the purpose of
+ * checking if the local Note is any different from a remote note.
+ */
+ fun equalsTimeAndLength(note: Note?) = note != null &&
+ (timestampString.equals(note.timestampString, ignoreCase = true) &&
+ json.length() == note.json.length())
+
+ /**
+ * Constructs a new Comment object based off of data in a Note
+ */
+ fun buildComment(): CommentModel {
+ val comment = CommentModel()
+ comment.remotePostId = postId.toLong()
+ comment.remoteCommentId = commentId
+ comment.authorName = commentAuthorName
+ comment.datePublished = DateTimeUtils.iso8601FromTimestamp(timestamp)
+ comment.content = commentText
+ comment.status = commentStatus.toString()
+ comment.authorUrl = commentAuthorUrl
+ comment.postTitle = title // unavailable in note model
+ comment.authorEmail = "" // unavailable in note model
+ comment.authorProfileImageUrl = iconURL
+ comment.iLike = hasLikedComment()
+ return comment
+ }
+
+ private fun isTypeRaw(rawType: String) = this.rawType == rawType
+
+ /**
+ * Rudimentary system for pulling an item out of a JSON object hierarchy
+ */
+ private fun queryJSON(query: String?, defaultObject: U): U =
+ if (mNoteJSON == null) defaultObject
+ else JSONUtils.queryJSON(mNoteJSON, query, defaultObject)
+
+ /**
+ * Get the actions for a given comment or post
+ * @param itemId The comment or post id
+ * @param type The type of the item: `post` or `comment`
+ */
+ private fun getActions(itemId: Long, type: String): JSONObject {
+ var actions: JSONObject? = null
+ var foundOrError = false
+ var i = 0
+ while (!foundOrError && i < body.length()) {
+ val bodyItem = runCatching { body.getJSONObject(i) }.getOrNull()
+ if (bodyItem?.has("type") == true && bodyItem.optString("type") == type &&
+ itemId == JSONUtils.queryJSON(bodyItem, "meta.ids.$type", 0).toLong()) {
+ actions = JSONUtils.queryJSON(bodyItem, "actions", JSONObject())
+ foundOrError = true
+ }
+ i++
+ }
+ return actions ?: JSONObject()
+ }
+
+ private fun getEnabledActions(jsonActions: JSONObject): EnumSet {
+ val actions = EnumSet.noneOf(EnabledActions::class.java)
+ if (jsonActions.length() == 0) return actions
+
+ if (jsonActions.has(ACTION_KEY_REPLY)) {
+ actions.add(EnabledActions.ACTION_REPLY)
+ }
+ if (jsonActions.has(ACTION_KEY_APPROVE) && jsonActions.optBoolean(ACTION_KEY_APPROVE, false)) {
+ actions.add(EnabledActions.ACTION_UNAPPROVE)
+ }
+ if (jsonActions.has(ACTION_KEY_APPROVE) && !jsonActions.optBoolean(ACTION_KEY_APPROVE, false)) {
+ actions.add(EnabledActions.ACTION_APPROVE)
+ }
+ if (jsonActions.has(ACTION_KEY_SPAM)) {
+ actions.add(EnabledActions.ACTION_SPAM)
+ }
+ if (jsonActions.has(ACTION_KEY_LIKE_COMMENT)) {
+ actions.add(EnabledActions.ACTION_LIKE_COMMENT)
+ }
+ if (jsonActions.has(ACTION_KEY_LIKE_POST)) {
+ actions.add(EnabledActions.ACTION_LIKE_POST)
+ }
+ return actions
+ }
+
+ companion object {
+ private const val MAX_COMMENT_PREVIEW_LENGTH = 200 // maximum character length for a comment preview
+ private const val MAX_PN_LENGTH = 4096 // max length an Android PN payload can have
+
+ // Note types
+ const val NOTE_FOLLOW_TYPE = "follow"
+ const val NOTE_LIKE_TYPE = "like"
+ const val NOTE_COMMENT_TYPE = "comment"
+ const val NOTE_MATCHER_TYPE = "automattcher"
+ const val NOTE_COMMENT_LIKE_TYPE = "comment_like"
+ const val NOTE_NEW_POST_TYPE = "new_post"
+ const val NOTE_VIEW_MILESTONE = "view_milestone"
+ const val NOTE_UNKNOWN_TYPE = "unknown"
+
+ // JSON action keys
+ private const val ACTION_KEY_REPLY = "replyto-comment"
+ private const val ACTION_KEY_APPROVE = "approve-comment"
+ private const val ACTION_KEY_SPAM = "spam-comment"
+ private const val ACTION_KEY_LIKE_COMMENT = "like-comment"
+ private const val ACTION_KEY_LIKE_POST = "like-post"
+
+ // Time constants
+ private const val LAST_MONTH = -1
+ private const val LAST_WEEK = -1
+ private const val LAST_TWO_DAYS = -2
+ private const val SINCE_YESTERDAY = -1
+ private const val MILLISECOND = 1000
+
+ /**
+ * Compare note timestamp to now and return a time grouping
+ */
+ fun getTimeGroupForTimestamp(timestamp: Long): NoteTimeGroup {
+ val today = Date()
+ val then = Date(timestamp * MILLISECOND)
+ return when {
+ then < addMonths(today, LAST_MONTH) -> NoteTimeGroup.GROUP_OLDER_MONTH
+ then < addWeeks(today, LAST_WEEK) -> NoteTimeGroup.GROUP_OLDER_WEEK
+ then < addDays(today, LAST_TWO_DAYS) ||
+ isSameDay(addDays(today, LAST_TWO_DAYS), then) -> NoteTimeGroup.GROUP_OLDER_TWO_DAYS
+ isSameDay(addDays(today, SINCE_YESTERDAY), then) -> NoteTimeGroup.GROUP_YESTERDAY
+ else -> NoteTimeGroup.GROUP_TODAY
+ }
+ }
+
+ @JvmStatic
+ @Synchronized
+ fun buildFromBase64EncodedData(noteId: String, base64FullNoteData: String?): Note? {
+ if (base64FullNoteData == null) return null
+ val b64DecodedPayload = Base64.decode(base64FullNoteData, Base64.DEFAULT)
+
+ // Decompress the payload
+ val decompresser = Inflater()
+ decompresser.setInput(b64DecodedPayload, 0, b64DecodedPayload.size)
+ val result = ByteArray(MAX_PN_LENGTH)
+ val resultLength = try {
+ val length = decompresser.inflate(result)
+ decompresser.end()
+ length
+ } catch (e: DataFormatException) {
+ AppLog.e(AppLog.T.NOTIFS, "Can't decompress the PN BlockListPayload. It could be > 4K", e)
+ 0
+ }
+ val out: String? = try {
+ String(result, 0, resultLength, charset("UTF8"))
+ } catch (e: UnsupportedEncodingException) {
+ AppLog.e(AppLog.T.NOTIFS, "Notification data contains non UTF8 characters.", e)
+ null
+ }
+ return out?.runCatching {
+ var jsonObject = JSONObject(out)
+ if (jsonObject.has("notes")) {
+ val jsonArray: JSONArray? = jsonObject.getJSONArray("notes")
+ if (jsonArray != null && jsonArray.length() == 1) {
+ jsonObject = jsonArray.getJSONObject(0)
+ }
+ }
+ Note(noteId, jsonObject)
+ }?.getOrElse {
+ AppLog.e(AppLog.T.NOTIFS, "Can't parse the Note JSON received in the PN")
+ null
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/NoteExtensions.kt b/WordPress/src/main/java/org/wordpress/android/models/NoteExtensions.kt
new file mode 100644
index 000000000000..28b0d9b949ac
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/NoteExtensions.kt
@@ -0,0 +1,36 @@
+package org.wordpress.android.models
+
+
+val Note.type
+ get() = NoteType.from(rawType)
+
+sealed class Notification {
+ data class PostLike(val url: String, val title: String): Notification()
+ data object NewPost: Notification()
+ data object Comment: Notification()
+ data object Unknown: Notification()
+
+ companion object {
+ fun from(rawNote: Note) = when(rawNote.type) {
+ NoteType.PostLike -> PostLike(url = rawNote.url, title = rawNote.title)
+ NoteType.NewPost -> NewPost
+ NoteType.Comment -> Comment
+ else -> Unknown
+ }
+ }
+}
+enum class NoteType(val rawType: String) {
+ Follow(Note.NOTE_FOLLOW_TYPE),
+ PostLike(Note.NOTE_LIKE_TYPE),
+ Comment(Note.NOTE_COMMENT_TYPE),
+ Matcher(Note.NOTE_MATCHER_TYPE),
+ CommentLike(Note.NOTE_COMMENT_LIKE_TYPE),
+ NewPost(Note.NOTE_NEW_POST_TYPE),
+ ViewMilestone(Note.NOTE_VIEW_MILESTONE),
+ Unknown(Note.NOTE_UNKNOWN_TYPE);
+
+ companion object {
+ private val map = entries.associateBy(NoteType::rawType)
+ fun from(rawType: String) = map[rawType] ?: Unknown
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/NoticonUtils.kt b/WordPress/src/main/java/org/wordpress/android/models/NoticonUtils.kt
deleted file mode 100644
index ce02662dc261..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/models/NoticonUtils.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package org.wordpress.android.models
-
-import org.wordpress.android.R
-import javax.inject.Inject
-
-class NoticonUtils
-@Inject constructor() {
- fun noticonToGridicon(noticon: String): Int {
- // Transformation based on Calypso: https://git.io/JqUEC
- return when (noticon) {
- "\uf814" -> R.drawable.ic_mention_white_24dp
- "\uf300" -> R.drawable.ic_comment_white_24dp
- "\uf801" -> R.drawable.ic_add_white_24dp
- "\uf455" -> R.drawable.ic_info_white_24dp
- "\uf470" -> R.drawable.ic_lock_white_24dp
- "\uf806" -> R.drawable.ic_stats_alt_white_24dp
- "\uf805" -> R.drawable.ic_reblog_white_24dp
- "\uf408" -> R.drawable.ic_star_white_24dp
- "\uf804" -> R.drawable.ic_trophy_white_24dp
- "\uf467" -> R.drawable.ic_reply_white_24dp
- "\uf414" -> R.drawable.ic_notice_white_24dp
- "\uf418" -> R.drawable.ic_checkmark_white_24dp
- "\uf447" -> R.drawable.ic_cart_white_24dp
- else -> R.drawable.ic_info_white_24dp
- }
- }
-}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/PeopleListFilter.java b/WordPress/src/main/java/org/wordpress/android/models/PeopleListFilter.java
index 33213b8a937b..76daccbaf751 100644
--- a/WordPress/src/main/java/org/wordpress/android/models/PeopleListFilter.java
+++ b/WordPress/src/main/java/org/wordpress/android/models/PeopleListFilter.java
@@ -7,8 +7,8 @@
public enum PeopleListFilter implements FilterCriteria {
TEAM(R.string.people_dropdown_item_team),
- FOLLOWERS(R.string.people_dropdown_item_followers),
- EMAIL_FOLLOWERS(R.string.people_dropdown_item_email_followers),
+ SUBSCRIBERS(R.string.people_dropdown_item_subscribers),
+ EMAIL_SUBSCRIBERS(R.string.people_dropdown_item_email_subscribers),
VIEWERS(R.string.people_dropdown_item_viewers);
private final int mLabelResId;
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java
index cbe82cda4cf3..8f8f10a92f5d 100644
--- a/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java
@@ -185,9 +185,11 @@ public static ReaderPost fromJson(JSONObject json) {
// if there's no featured image, check if featured media has been set to an image
if (!post.hasFeaturedImage() && json.has("featured_media")) {
JSONObject jsonMedia = json.optJSONObject("featured_media");
- String type = JSONUtils.getString(jsonMedia, "type");
- if (type.equals("image")) {
- post.mFeaturedImage = JSONUtils.getString(jsonMedia, "uri");
+ if (jsonMedia != null) {
+ String type = JSONUtils.getString(jsonMedia, "type");
+ if (type.equals("image")) {
+ post.mFeaturedImage = JSONUtils.getString(jsonMedia, "uri");
+ }
}
}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java
index a8e8018e9814..4676fb991291 100644
--- a/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java
@@ -184,6 +184,10 @@ public boolean isBookmarked() {
return tagType == ReaderTagType.BOOKMARKED;
}
+ public boolean isTags() {
+ return tagType == ReaderTagType.TAGS;
+ }
+
public boolean isDiscover() {
return tagType == ReaderTagType.DEFAULT && getEndpoint().endsWith(DISCOVER_PATH);
}
@@ -204,7 +208,7 @@ public boolean isA8C() {
}
public boolean isFilterable() {
- return this.isFollowedSites() || this.isA8C() || this.isP2();
+ return this.isFollowedSites() || this.isA8C() || this.isP2() || this.isTags();
}
public boolean isListTopic() {
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java
index d7d84f191168..cb3486efc523 100644
--- a/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java
@@ -7,7 +7,8 @@ public enum ReaderTagType {
CUSTOM_LIST,
SEARCH,
INTERESTS,
- DISCOVER_POST_CARDS;
+ DISCOVER_POST_CARDS,
+ TAGS;
private static final int INT_DEFAULT = 0;
private static final int INT_FOLLOWED = 1;
diff --git a/WordPress/src/main/java/org/wordpress/android/models/RoleUtils.java b/WordPress/src/main/java/org/wordpress/android/models/RoleUtils.java
index 6df388644a8b..d0e557928bf5 100644
--- a/WordPress/src/main/java/org/wordpress/android/models/RoleUtils.java
+++ b/WordPress/src/main/java/org/wordpress/android/models/RoleUtils.java
@@ -29,7 +29,7 @@ public static List getInviteRoles(SiteStore siteStore, SiteModel site
RoleModel viewerOrFollowerRole = new RoleModel();
// the remote expects "follower" as the role parameter even if the role is "viewer"
viewerOrFollowerRole.setName("follower");
- int displayNameRes = siteModel.isPrivate() ? R.string.role_viewer : R.string.role_follower;
+ int displayNameRes = siteModel.isPrivate() ? R.string.role_viewer : R.string.role_subscriber;
viewerOrFollowerRole.setDisplayName(context.getString(displayNameRes));
inviteRoles.add(viewerOrFollowerRole);
return inviteRoles;
diff --git a/WordPress/src/main/java/org/wordpress/android/models/recommend/RecommendApiCallsProvider.kt b/WordPress/src/main/java/org/wordpress/android/models/recommend/RecommendApiCallsProvider.kt
index d59d2c1f6119..e324f64e5679 100644
--- a/WordPress/src/main/java/org/wordpress/android/models/recommend/RecommendApiCallsProvider.kt
+++ b/WordPress/src/main/java/org/wordpress/android/models/recommend/RecommendApiCallsProvider.kt
@@ -51,7 +51,7 @@ class RecommendApiCallsProvider @Inject constructor(
cont.resume(Failure(errorMessage))
}
- restClientProvider.getRestClientUtilsV2().get(
+ restClientProvider.getRestClientUtilsV2().getWithLocale(
endPointPath,
listener,
errorListener
diff --git a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java
index 993703e791c2..f3fe37d426d6 100644
--- a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java
+++ b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java
@@ -41,6 +41,7 @@
import org.wordpress.android.ui.layoutpicker.LayoutPreviewFragment;
import org.wordpress.android.ui.layoutpicker.LayoutsAdapter;
import org.wordpress.android.ui.main.AddContentAdapter;
+import org.wordpress.android.ui.main.ChooseSiteViewHolder;
import org.wordpress.android.ui.main.MainBottomSheetFragment;
import org.wordpress.android.ui.main.MeFragment;
import org.wordpress.android.ui.main.SitePickerAdapter;
@@ -61,6 +62,7 @@
import org.wordpress.android.ui.notifications.NotificationsDetailListFragment;
import org.wordpress.android.ui.notifications.NotificationsListFragmentPage;
import org.wordpress.android.ui.notifications.adapters.NotesAdapter;
+import org.wordpress.android.ui.notifications.adapters.NoteViewHolder;
import org.wordpress.android.ui.notifications.receivers.NotificationsPendingDraftsReceiver;
import org.wordpress.android.ui.pages.PageListFragment;
import org.wordpress.android.ui.pages.PageParentFragment;
@@ -91,6 +93,7 @@
import org.wordpress.android.ui.posts.PostDatePickerDialogFragment;
import org.wordpress.android.ui.posts.PostListFragment;
import org.wordpress.android.ui.posts.PostNotificationScheduleTimeDialogFragment;
+import org.wordpress.android.ui.posts.PostResolutionOverlayFragment;
import org.wordpress.android.ui.posts.PostSettingsListDialogFragment;
import org.wordpress.android.ui.posts.PostSettingsTagsFragment;
import org.wordpress.android.ui.posts.PostTimePickerDialogFragment;
@@ -158,6 +161,7 @@
import org.wordpress.android.ui.reader.views.ReaderWebView;
import org.wordpress.android.ui.sitecreation.theme.DesignPreviewFragment;
import org.wordpress.android.ui.stats.StatsConnectJetpackActivity;
+import org.wordpress.android.ui.stats.refresh.lists.widget.WidgetBlockListProvider;
import org.wordpress.android.ui.stats.refresh.lists.widget.alltime.AllTimeWidgetBlockListProviderFactory;
import org.wordpress.android.ui.stats.refresh.lists.widget.alltime.AllTimeWidgetListProvider;
import org.wordpress.android.ui.stats.refresh.lists.widget.alltime.StatsAllTimeWidget;
@@ -171,8 +175,6 @@
import org.wordpress.android.ui.stats.refresh.lists.widget.weeks.WeekViewsWidgetListProvider;
import org.wordpress.android.ui.stats.refresh.lists.widget.weeks.WeekWidgetBlockListProviderFactory;
import org.wordpress.android.ui.stockmedia.StockMediaPickerActivity;
-import org.wordpress.android.ui.stories.StoryComposerActivity;
-import org.wordpress.android.ui.stories.intro.StoriesIntroDialogFragment;
import org.wordpress.android.ui.suggestion.SuggestionActivity;
import org.wordpress.android.ui.suggestion.adapters.SuggestionAdapter;
import org.wordpress.android.ui.themes.ThemeBrowserFragment;
@@ -221,6 +223,8 @@ public interface AppComponent {
void inject(SitePickerAdapter object);
+ void inject(ChooseSiteViewHolder object);
+
void inject(SiteSettingsFragment object);
void inject(SiteSettingsInterface object);
@@ -343,6 +347,8 @@ public interface AppComponent {
void inject(NotesAdapter object);
+ void inject(NoteViewHolder object);
+
void inject(ThemeBrowserFragment object);
void inject(SelectCategoriesActivity object);
@@ -454,11 +460,6 @@ public interface AppComponent {
void inject(FeatureAnnouncementDialogFragment object);
void inject(FeatureAnnouncementListAdapter object);
-
- void inject(StoryComposerActivity object);
-
- void inject(StoriesIntroDialogFragment object);
-
void inject(ReaderDiscoverFragment object);
void inject(ReaderSearchActivity object);
@@ -553,7 +554,11 @@ public interface AppComponent {
void inject(WeekViewsWidgetListProvider object);
+ void inject(WidgetBlockListProvider object);
+
void inject(WeekWidgetBlockListProviderFactory object);
void inject(WPMainNavigationView object);
+
+ void inject(PostResolutionOverlayFragment object);
}
diff --git a/WordPress/src/main/java/org/wordpress/android/modules/AppConfigModule.java b/WordPress/src/main/java/org/wordpress/android/modules/AppConfigModule.java
index 86ec20e8b317..8845f7dbe586 100644
--- a/WordPress/src/main/java/org/wordpress/android/modules/AppConfigModule.java
+++ b/WordPress/src/main/java/org/wordpress/android/modules/AppConfigModule.java
@@ -11,6 +11,8 @@
import org.wordpress.android.fluxc.network.UserAgent;
import org.wordpress.android.fluxc.network.rest.wpcom.auth.AppSecrets;
+import javax.inject.Singleton;
+
import dagger.Module;
import dagger.Provides;
import dagger.hilt.InstallIn;
@@ -25,6 +27,7 @@ public AppSecrets provideAppSecrets() {
return new AppSecrets(BuildConfig.OAUTH_APP_ID, BuildConfig.OAUTH_APP_SECRET);
}
+ @Singleton
@Provides
public UserAgent provideUserAgent(@ApplicationContext Context appContext) {
return new UserAgent(appContext, WordPress.USER_AGENT_APPNAME);
diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java
index fdc80b43fe03..a2d0f248d15b 100644
--- a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java
+++ b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java
@@ -3,15 +3,22 @@
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
+import android.hardware.SensorManager;
import androidx.lifecycle.LiveData;
import androidx.preference.PreferenceManager;
+import com.google.android.play.core.appupdate.AppUpdateManager;
+import com.google.android.play.core.appupdate.AppUpdateManagerFactory;
import com.tenor.android.core.network.ApiClient;
import com.tenor.android.core.network.ApiService;
import com.tenor.android.core.network.IApiClient;
import org.wordpress.android.BuildConfig;
+import org.wordpress.android.inappupdate.IInAppUpdateManager;
+import org.wordpress.android.inappupdate.InAppUpdateAnalyticsTracker;
+import org.wordpress.android.inappupdate.InAppUpdateManagerImpl;
+import org.wordpress.android.inappupdate.InAppUpdateManagerNoop;
import org.wordpress.android.ui.ActivityNavigator;
import org.wordpress.android.ui.jetpack.backup.download.BackupDownloadStep;
import org.wordpress.android.ui.jetpack.backup.download.BackupDownloadStepsProvider;
@@ -20,10 +27,20 @@
import org.wordpress.android.ui.mediapicker.loader.TenorGifClient;
import org.wordpress.android.ui.sitecreation.SiteCreationStep;
import org.wordpress.android.ui.sitecreation.SiteCreationStepsProvider;
+import org.wordpress.android.util.BuildConfigWrapper;
+import org.wordpress.android.util.audio.AudioRecorder;
+import org.wordpress.android.util.audio.IAudioRecorder;
+import org.wordpress.android.util.audio.RecordingStrategy;
+import org.wordpress.android.util.audio.RecordingStrategy.VoiceToContentRecordingStrategy;
+import org.wordpress.android.util.audio.VoiceToContentStrategy;
+import org.wordpress.android.util.config.InAppUpdatesFeatureConfig;
+import org.wordpress.android.util.config.RemoteConfigWrapper;
import org.wordpress.android.util.wizard.WizardManager;
import org.wordpress.android.viewmodel.helpers.ConnectionStatus;
import org.wordpress.android.viewmodel.helpers.ConnectionStatusLiveData;
+import javax.inject.Named;
+
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
@@ -31,6 +48,8 @@
import dagger.hilt.InstallIn;
import dagger.hilt.android.qualifiers.ApplicationContext;
import dagger.hilt.components.SingletonComponent;
+import kotlinx.coroutines.CoroutineScope;
+import static org.wordpress.android.modules.ThreadModuleKt.APPLICATION_SCOPE;
@InstallIn(SingletonComponent.class)
@Module(includes = AndroidInjectionModule.class)
@@ -75,8 +94,57 @@ public static WizardManager provideRestoreWizardManager(
return new WizardManager<>(stepsProvider.getSteps());
}
+ @Provides
+ public static AppUpdateManager provideAppUpdateManager(@ApplicationContext Context context) {
+ return AppUpdateManagerFactory.create(context);
+ }
+
+ @Provides
+ public static IInAppUpdateManager provideInAppUpdateManager(
+ @ApplicationContext Context context,
+ @Named(APPLICATION_SCOPE) CoroutineScope appScope,
+ AppUpdateManager appUpdateManager,
+ RemoteConfigWrapper remoteConfigWrapper,
+ BuildConfigWrapper buildConfigWrapper,
+ InAppUpdatesFeatureConfig inAppUpdatesFeatureConfig,
+ InAppUpdateAnalyticsTracker inAppUpdateAnalyticsTracker
+ ) {
+ // Check if in-app updates feature is enabled
+ return inAppUpdatesFeatureConfig.isEnabled()
+ ? new InAppUpdateManagerImpl(
+ context,
+ appScope,
+ appUpdateManager,
+ remoteConfigWrapper,
+ buildConfigWrapper,
+ inAppUpdateAnalyticsTracker,
+ System::currentTimeMillis
+ )
+ : new InAppUpdateManagerNoop();
+ }
+
@Provides
public static ActivityNavigator provideActivityNavigator(@ApplicationContext Context context) {
return new ActivityNavigator();
}
+
+ @Provides
+ public static SensorManager provideSensorManager(@ApplicationContext Context context) {
+ return (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+ }
+
+ @VoiceToContentStrategy
+ @Provides
+ public static IAudioRecorder provideAudioRecorder(
+ @ApplicationContext Context context,
+ @VoiceToContentStrategy RecordingStrategy recordingStrategy
+ ) {
+ return new AudioRecorder(context, recordingStrategy);
+ }
+
+ @VoiceToContentStrategy
+ @Provides
+ public static RecordingStrategy provideVoiceToContentRecordingStrategy() {
+ return new VoiceToContentRecordingStrategy();
+ }
}
diff --git a/WordPress/src/main/java/org/wordpress/android/modules/GravatarModule.kt b/WordPress/src/main/java/org/wordpress/android/modules/GravatarModule.kt
new file mode 100644
index 000000000000..771be2044923
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/modules/GravatarModule.kt
@@ -0,0 +1,17 @@
+package org.wordpress.android.modules
+
+import com.gravatar.services.AvatarService
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@InstallIn(SingletonComponent::class)
+@Module
+class GravatarModule {
+ @Singleton
+ @Provides
+ fun provideGravatarApi(
+ ): AvatarService = AvatarService()
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/modules/PostModule.kt b/WordPress/src/main/java/org/wordpress/android/modules/PostModule.kt
new file mode 100644
index 000000000000..14d034b274b5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/modules/PostModule.kt
@@ -0,0 +1,17 @@
+package org.wordpress.android.modules
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import org.wordpress.android.ui.posts.IPostFreshnessChecker
+import org.wordpress.android.ui.posts.PostFreshnessCheckerImpl
+import javax.inject.Singleton
+
+@InstallIn(SingletonComponent::class)
+@Module
+class PostModule {
+ @Singleton
+ @Provides
+ fun providePostFreshnessChecker(): IPostFreshnessChecker = PostFreshnessCheckerImpl()
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java
index 1f8bb88b6ac3..9989e37ba302 100644
--- a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java
+++ b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java
@@ -33,6 +33,7 @@
import org.wordpress.android.ui.posts.EditorBloggingPromptsViewModel;
import org.wordpress.android.ui.posts.EditorJetpackSocialViewModel;
import org.wordpress.android.ui.posts.PostListMainViewModel;
+import org.wordpress.android.ui.posts.PostResolutionOverlayViewModel;
import org.wordpress.android.ui.posts.editor.StorePostViewModel;
import org.wordpress.android.ui.posts.prepublishing.PrepublishingViewModel;
import org.wordpress.android.ui.posts.prepublishing.categories.PrepublishingCategoriesViewModel;
@@ -53,11 +54,11 @@
import org.wordpress.android.ui.reader.viewmodels.ReaderPostListViewModel;
import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel;
import org.wordpress.android.ui.reader.viewmodels.SubfilterPageViewModel;
-import org.wordpress.android.ui.review.ReviewViewModel;
import org.wordpress.android.ui.stats.refresh.lists.DaysListViewModel;
import org.wordpress.android.ui.stats.refresh.lists.InsightsDetailListViewModel;
import org.wordpress.android.ui.stats.refresh.lists.InsightsListViewModel;
import org.wordpress.android.ui.stats.refresh.lists.MonthsListViewModel;
+import org.wordpress.android.ui.stats.refresh.lists.SubscribersListViewModel;
import org.wordpress.android.ui.stats.refresh.lists.TotalCommentsDetailListViewModel;
import org.wordpress.android.ui.stats.refresh.lists.TotalFollowersDetailListViewModel;
import org.wordpress.android.ui.stats.refresh.lists.TotalLikesDetailListViewModel;
@@ -72,8 +73,6 @@
import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsSiteSelectionViewModel;
import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetConfigureViewModel;
import org.wordpress.android.ui.stats.refresh.lists.widget.minified.StatsMinifiedWidgetConfigureViewModel;
-import org.wordpress.android.ui.stories.StoryComposerViewModel;
-import org.wordpress.android.ui.stories.intro.StoriesIntroViewModel;
import org.wordpress.android.ui.suggestion.SuggestionViewModel;
import org.wordpress.android.ui.whatsnew.FeatureAnnouncementViewModel;
import org.wordpress.android.viewmodel.ViewModelFactory;
@@ -81,7 +80,6 @@
import org.wordpress.android.viewmodel.accounts.PostSignupInterstitialViewModel;
import org.wordpress.android.viewmodel.activitylog.ActivityLogViewModel;
import org.wordpress.android.viewmodel.history.HistoryViewModel;
-import org.wordpress.android.viewmodel.main.SitePickerViewModel;
import org.wordpress.android.viewmodel.main.WPMainActivityViewModel;
import org.wordpress.android.viewmodel.mlp.ModalLayoutPickerViewModel;
import org.wordpress.android.viewmodel.pages.PageListViewModel;
@@ -159,6 +157,11 @@ abstract class ViewModelModule {
@ViewModelKey(InsightsListViewModel.class)
abstract ViewModel insightsTabViewModel(InsightsListViewModel viewModel);
+ @Binds
+ @IntoMap
+ @ViewModelKey(SubscribersListViewModel.class)
+ abstract ViewModel subscribersTabViewModel(SubscribersListViewModel viewModel);
+
@Binds
@IntoMap
@ViewModelKey(TrafficListViewModel.class)
@@ -314,11 +317,6 @@ abstract class ViewModelModule {
@ViewModelKey(FeatureAnnouncementViewModel.class)
abstract ViewModel featureAnnouncementViewModel(FeatureAnnouncementViewModel viewModel);
- @Binds
- @IntoMap
- @ViewModelKey(SitePickerViewModel.class)
- abstract ViewModel sitePickerViewModel(SitePickerViewModel viewModel);
-
@Binds
@IntoMap
@ViewModelKey(ReaderViewModel.class)
@@ -359,16 +357,6 @@ abstract class ViewModelModule {
@ViewModelKey(PrepublishingPublishSettingsViewModel.class)
abstract ViewModel prepublishingPublishSettingsViewModel(PrepublishingPublishSettingsViewModel viewModel);
- @Binds
- @IntoMap
- @ViewModelKey(StoryComposerViewModel.class)
- abstract ViewModel storyComposerViewModel(StoryComposerViewModel viewModel);
-
- @Binds
- @IntoMap
- @ViewModelKey(StoriesIntroViewModel.class)
- abstract ViewModel storiesIntroViewModel(StoriesIntroViewModel viewModel);
-
@Binds
@IntoMap
@ViewModelKey(PhotoPickerViewModel.class)
@@ -472,11 +460,6 @@ abstract class ViewModelModule {
@ViewModelKey(UnifiedCommentListViewModel.class)
abstract ViewModel unifiedCommentListViewModel(UnifiedCommentListViewModel viewModel);
- @Binds
- @IntoMap
- @ViewModelKey(ReviewViewModel.class)
- abstract ViewModel reviewViewModel(ReviewViewModel viewModel);
-
@Binds
@IntoMap
@ViewModelKey(BloggingRemindersViewModel.class)
@@ -551,4 +534,9 @@ abstract class ViewModelModule {
@IntoMap
@ViewModelKey(EditorJetpackSocialViewModel.class)
abstract ViewModel editorJetpackSocialViewModel(EditorJetpackSocialViewModel viewModel);
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(PostResolutionOverlayViewModel.class)
+ abstract ViewModel postResolutionOverlayViewModel(PostResolutionOverlayViewModel viewModel);
}
diff --git a/WordPress/src/main/java/org/wordpress/android/modules/WordPressGlideModule.kt b/WordPress/src/main/java/org/wordpress/android/modules/WordPressGlideModule.kt
index 23d1e923a843..610583368701 100644
--- a/WordPress/src/main/java/org/wordpress/android/modules/WordPressGlideModule.kt
+++ b/WordPress/src/main/java/org/wordpress/android/modules/WordPressGlideModule.kt
@@ -24,7 +24,7 @@ import javax.inject.Named
@GlideModule
class WordPressGlideModule : AppGlideModule() {
@Inject
- @Named("custom-ssl")
+ @Named("custom-ssl-custom-redirects")
lateinit var requestQueue: RequestQueue
@Inject
diff --git a/WordPress/src/main/java/org/wordpress/android/networking/GravatarApi.java b/WordPress/src/main/java/org/wordpress/android/networking/GravatarApi.java
deleted file mode 100644
index 3c6f8d82eb82..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/networking/GravatarApi.java
+++ /dev/null
@@ -1,139 +0,0 @@
-package org.wordpress.android.networking;
-
-import android.os.Handler;
-import android.os.Looper;
-
-import org.wordpress.android.analytics.AnalyticsTracker;
-import org.wordpress.android.util.AppLog;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-
-import okhttp3.Call;
-import okhttp3.Callback;
-import okhttp3.ConnectionPool;
-import okhttp3.Interceptor;
-import okhttp3.MultipartBody;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-
-public class GravatarApi {
- public static final String API_BASE_URL = "https://api.gravatar.com/v1/";
- private static final int DEFAULT_TIMEOUT = 15000;
-
- public interface GravatarUploadListener {
- void onSuccess();
-
- void onError();
- }
-
- private static OkHttpClient createClient(final String accessToken) {
- OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
- // This should help with recovery from the SocketTimeoutException
- // https://github.com/square/okhttp/issues/3146#issuecomment-311158567
- httpClientBuilder.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
- .retryOnConnectionFailure(true)
- .readTimeout(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
- .connectionPool(
- new ConnectionPool(0, 1, TimeUnit.NANOSECONDS)
- );
- // // uncomment the following line to add logcat logging
- // httpClientBuilder.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY));
-
- // add oAuth token usage
- httpClientBuilder.addInterceptor(new Interceptor() {
- @Override
- public Response intercept(Interceptor.Chain chain) throws IOException {
- Request original = chain.request();
- Request.Builder requestBuilder = original.newBuilder()
- .header("Authorization", "Bearer " + accessToken)
- .method(original.method(), original.body());
- Request request = requestBuilder.build();
- return chain.proceed(request);
- }
- });
-
- return httpClientBuilder.build();
- }
-
- public static Request prepareGravatarUpload(String email, File file) {
- return new Request.Builder()
- .url(API_BASE_URL + "upload-image")
- .post(new MultipartBody.Builder()
- .setType(MultipartBody.FORM)
- .addFormDataPart("account", email)
- .addFormDataPart("filedata", file.getName(), new StreamingRequest(file))
- .build())
- .build();
- }
-
- public static void uploadGravatar(final File file, final String email, final String accessToken,
- final GravatarUploadListener gravatarUploadListener) {
- Request request = prepareGravatarUpload(email, file);
-
- createClient(accessToken).newCall(request).enqueue(
- new Callback() {
- @Override
- public void onResponse(Call call, final Response response) throws IOException {
- if (!response.isSuccessful()) {
- Map properties = new HashMap<>();
- properties.put("network_response_code", response.code());
-
- // response's body can only be read once so, keep it in a local variable
- String responseBody;
-
- try {
- responseBody = response.body().string();
- } catch (IOException e) {
- responseBody = "null";
- }
- properties.put("network_response_body", responseBody);
-
- AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_UPLOAD_UNSUCCESSFUL,
- properties);
- AppLog.w(AppLog.T.API, "Network call unsuccessful trying to upload Gravatar: "
- + responseBody);
- }
-
- new Handler(Looper.getMainLooper()).post(new Runnable() {
- @Override
- public void run() {
- if (response.isSuccessful()) {
- gravatarUploadListener.onSuccess();
- } else {
- gravatarUploadListener.onError();
- }
- }
- });
- }
-
- @Override
- public void onFailure(okhttp3.Call call, final IOException e) {
- String exceptionClass = e != null ? e.getClass().getCanonicalName() : "null";
- String exceptionMessage = e != null ? e.getMessage() : "null";
-
- AppLog.w(AppLog.T.API, "Network call failure trying to upload Gravatar!"
- + exceptionMessage);
-
- // Don't track exceptions caused by poor internet connectivity
- if (!(e instanceof java.net.UnknownHostException)) {
- Map properties = new HashMap<>();
- properties.put("network_exception_class", exceptionClass);
- properties.put("network_exception_message", exceptionMessage);
- AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_UPLOAD_EXCEPTION, properties);
- }
-
- new Handler(Looper.getMainLooper()).post(new Runnable() {
- @Override
- public void run() {
- gravatarUploadListener.onError();
- }
- });
- }
- });
- }
-}
diff --git a/WordPress/src/main/java/org/wordpress/android/networking/StreamingRequest.java b/WordPress/src/main/java/org/wordpress/android/networking/StreamingRequest.java
deleted file mode 100644
index 60c6880feb27..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/networking/StreamingRequest.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.wordpress.android.networking;
-
-import java.io.File;
-import java.io.IOException;
-
-import okhttp3.MediaType;
-import okhttp3.RequestBody;
-import okhttp3.internal.Util;
-import okio.BufferedSink;
-import okio.Okio;
-import okio.Source;
-
-public class StreamingRequest extends RequestBody {
- public static final int CHUNK_SIZE = 2048;
-
- private final File mFile;
-
- public StreamingRequest(File file) {
- mFile = file;
- }
-
- @Override
- public MediaType contentType() {
- return MediaType.parse("multipart/form-data");
- }
-
- @Override
- public void writeTo(BufferedSink sink) throws IOException {
- Source source = null;
- try {
- source = Okio.source(mFile);
-
- while (source.read(sink.buffer(), CHUNK_SIZE) != -1) {
- sink.flush();
- }
- } finally {
- Util.closeQuietly(source);
- }
- }
-};
-
diff --git a/WordPress/src/main/java/org/wordpress/android/push/GCMMessageHandler.java b/WordPress/src/main/java/org/wordpress/android/push/GCMMessageHandler.java
index 5495ae79f441..e2a732f6a284 100644
--- a/WordPress/src/main/java/org/wordpress/android/push/GCMMessageHandler.java
+++ b/WordPress/src/main/java/org/wordpress/android/push/GCMMessageHandler.java
@@ -32,6 +32,7 @@
import org.wordpress.android.ui.notifications.SystemNotificationsTracker;
import org.wordpress.android.ui.notifications.utils.NotificationsActions;
import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtilsWrapper;
import org.wordpress.android.ui.prefs.AppPrefs;
import org.wordpress.android.util.AppLog;
import org.wordpress.android.util.AppLog.T;
@@ -74,15 +75,14 @@ public class GCMMessageHandler {
private static final String PUSH_ARG_TYPE = "type";
private static final String PUSH_ARG_USER = "user";
- private static final String PUSH_ARG_TITLE = "title";
- private static final String PUSH_ARG_MSG = "msg";
+ protected static final String PUSH_ARG_TITLE = "title";
+ protected static final String PUSH_ARG_MSG = "msg";
- private static final String PUSH_TYPE_COMMENT = "c";
+ protected static final String PUSH_TYPE_COMMENT = "c";
private static final String PUSH_TYPE_LIKE = "like";
private static final String PUSH_TYPE_COMMENT_LIKE = "comment_like";
private static final String PUSH_TYPE_AUTOMATTCHER = "automattcher";
private static final String PUSH_TYPE_FOLLOW = "follow";
- private static final String PUSH_TYPE_REBLOG = "reblog";
private static final String PUSH_TYPE_PUSH_AUTH = "push_auth";
private static final String PUSH_TYPE_BADGE_RESET = "badge-reset";
private static final String PUSH_TYPE_NOTE_DELETE = "note-delete";
@@ -100,9 +100,10 @@ public class GCMMessageHandler {
private final ArrayMap mActiveNotificationsMap;
private final NotificationHelper mNotificationHelper;
- @Inject GCMMessageHandler(SystemNotificationsTracker systemNotificationsTracker) {
+ @Inject GCMMessageHandler(SystemNotificationsTracker systemNotificationsTracker,
+ NotificationsUtilsWrapper notificationsUtilsWrapper) {
mActiveNotificationsMap = new ArrayMap<>();
- mNotificationHelper = new NotificationHelper(this, systemNotificationsTracker);
+ mNotificationHelper = new NotificationHelper(this, systemNotificationsTracker, notificationsUtilsWrapper);
}
synchronized void rebuildAndUpdateNotificationsOnSystemBarForThisNote(Context context,
@@ -120,13 +121,6 @@ synchronized void rebuildAndUpdateNotificationsOnSystemBarForThisNote(Context co
}
}
- public synchronized void rebuildAndUpdateNotifsOnSystemBarForRemainingNote(Context context) {
- if (mActiveNotificationsMap.size() > 0) {
- Bundle remainingNote = mActiveNotificationsMap.values().iterator().next();
- mNotificationHelper.rebuildAndUpdateNotificationsOnSystemBar(context, remainingNote);
- }
- }
-
private synchronized Bundle getCurrentNoteBundleForNoteId(String noteId) {
if (mActiveNotificationsMap.size() > 0) {
// get the corresponding bundle for this noteId
@@ -278,10 +272,14 @@ public static class NotificationHelper {
private GCMMessageHandler mGCMMessageHandler;
private SystemNotificationsTracker mSystemNotificationsTracker;
+ private NotificationsUtilsWrapper mNotificationsUtilsWrapper;
+
NotificationHelper(GCMMessageHandler gCMMessageHandler,
- SystemNotificationsTracker systemNotificationsTracker) {
+ SystemNotificationsTracker systemNotificationsTracker,
+ NotificationsUtilsWrapper notificationsUtilsWrapper) {
mGCMMessageHandler = gCMMessageHandler;
mSystemNotificationsTracker = systemNotificationsTracker;
+ mNotificationsUtilsWrapper = notificationsUtilsWrapper;
}
void handleDefaultPush(Context context, @NonNull Bundle data, long wpcomUserId) {
@@ -354,11 +352,8 @@ private void buildAndShowNotificationFromNoteData(Context context, Bundle data)
String noteType = StringUtils.notNullStr(data.getString(PUSH_ARG_TYPE));
- String title = StringEscapeUtils.unescapeHtml4(data.getString(PUSH_ARG_TITLE));
- if (title == null) {
- title = context.getString(R.string.app_name);
- }
- String message = StringEscapeUtils.unescapeHtml4(data.getString(PUSH_ARG_MSG));
+ String title = getNotificationTitle(data, noteType, context.getString(R.string.app_name));
+ String message = getNotificationMessage(data, noteType);
/*
* if this has the same note_id as the previous notification, and the previous notification
@@ -421,6 +416,45 @@ private void buildAndShowNotificationFromNoteData(Context context, Bundle data)
);
}
+ @NonNull
+ protected String getNotificationTitle(@NonNull Bundle data,
+ @NonNull String noteType,
+ @NonNull String defaultTitle) {
+ String title;
+ if (noteType.equals(PUSH_TYPE_COMMENT)) {
+ title = StringEscapeUtils.unescapeHtml4(data.getString(PUSH_ARG_MSG));
+ } else {
+ title = StringEscapeUtils.unescapeHtml4(data.getString(PUSH_ARG_TITLE));
+ }
+ if (title == null) {
+ return defaultTitle;
+ }
+ return title;
+ }
+
+ @NonNull
+ protected String getNotificationMessage(@NonNull Bundle data, @NonNull String noteType) {
+ if (noteType.equals(PUSH_TYPE_COMMENT)) {
+ String noteId = data.getString(PUSH_ARG_NOTE_ID);
+ if (noteId != null) {
+ Note note = mNotificationsUtilsWrapper.getNoteById(noteId);
+ if (note != null) {
+ String summary = note.getCommentSubject();
+ if (!TextUtils.isEmpty(summary)) {
+ return summary;
+ }
+ }
+ }
+ }
+
+ // Not a comment or the comment content was not retrieved
+ String message = StringEscapeUtils.unescapeHtml4(data.getString(PUSH_ARG_MSG));
+ if (message == null) {
+ return "";
+ }
+ return message;
+ }
+
private void showNotificationForNoteData(Context context, Bundle noteData, NotificationCompat.Builder builder) {
String noteType = StringUtils.notNullStr(noteData.getString(PUSH_ARG_TYPE));
String wpcomNoteID = noteData.getString(PUSH_ARG_NOTE_ID, "");
@@ -481,7 +515,7 @@ private void addActionsForCommentNotification(Context context, NotificationCompa
} else {
// else offer REPLY / LIKE actions
// LIKE can only be enabled for wp.com sites, so if this is a Jetpack site don't enable LIKEs
- if (note.canLike()) {
+ if (note.canLikeComment()) {
addCommentLikeActionForCommentNotification(context, builder, noteId);
}
}
@@ -714,8 +748,6 @@ private NotificationType fromNoteType(String noteType) {
return NotificationType.AUTOMATTCHER;
case PUSH_TYPE_FOLLOW:
return NotificationType.FOLLOW;
- case PUSH_TYPE_REBLOG:
- return NotificationType.REBLOG;
case PUSH_TYPE_PUSH_AUTH:
return NotificationType.AUTHENTICATION;
case PUSH_TYPE_BADGE_RESET:
@@ -1069,7 +1101,6 @@ private boolean shouldCircularizeNoteIcon(String noteType) {
case PUSH_TYPE_COMMENT_LIKE:
case PUSH_TYPE_AUTOMATTCHER:
case PUSH_TYPE_FOLLOW:
- case PUSH_TYPE_REBLOG:
return true;
default:
return false;
diff --git a/WordPress/src/main/java/org/wordpress/android/push/NotificationType.kt b/WordPress/src/main/java/org/wordpress/android/push/NotificationType.kt
index 21cb74ab6878..f4c6b971bbcc 100644
--- a/WordPress/src/main/java/org/wordpress/android/push/NotificationType.kt
+++ b/WordPress/src/main/java/org/wordpress/android/push/NotificationType.kt
@@ -6,7 +6,6 @@ enum class NotificationType {
COMMENT_LIKE,
AUTOMATTCHER,
FOLLOW,
- REBLOG,
BADGE_RESET,
NOTE_DELETE,
TEST_NOTE,
@@ -23,10 +22,6 @@ enum class NotificationType {
MEDIA_UPLOAD_SUCCESS,
MEDIA_UPLOAD_ERROR,
POST_PUBLISHED,
- STORY_SAVE_SUCCESS,
- STORY_SAVE_ERROR,
- STORY_FRAME_SAVE_SUCCESS,
- STORY_FRAME_SAVE_ERROR,
BLOGGING_REMINDERS,
CREATE_SITE,
WEEKLY_ROUNDUP,
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java
index c747aa323831..1c16c014490a 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java
@@ -16,16 +16,12 @@
import androidx.core.app.TaskStackBuilder;
import androidx.fragment.app.Fragment;
-import com.wordpress.stories.compose.frame.FrameSaveNotifier;
-import com.wordpress.stories.compose.frame.StorySaveEvents.StorySaveResult;
-
import org.wordpress.android.BuildConfig;
import org.wordpress.android.R;
import org.wordpress.android.WordPress;
import org.wordpress.android.analytics.AnalyticsTracker;
import org.wordpress.android.analytics.AnalyticsTracker.Stat;
import org.wordpress.android.datasets.ReaderPostTable;
-import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId;
import org.wordpress.android.fluxc.model.PostImmutableModel;
import org.wordpress.android.fluxc.model.PostModel;
import org.wordpress.android.fluxc.model.SiteModel;
@@ -75,9 +71,9 @@
import org.wordpress.android.ui.jetpack.scan.history.ScanHistoryActivity;
import org.wordpress.android.ui.jetpackoverlay.JetpackStaticPosterActivity;
import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallActivity;
+import org.wordpress.android.ui.main.ChooseSiteActivity;
import org.wordpress.android.ui.main.MeActivity;
-import org.wordpress.android.ui.main.SitePickerActivity;
-import org.wordpress.android.ui.main.SitePickerAdapter.SitePickerMode;
+import org.wordpress.android.ui.main.SitePickerMode;
import org.wordpress.android.ui.main.WPMainActivity;
import org.wordpress.android.ui.main.jetpack.migration.JetpackMigrationActivity;
import org.wordpress.android.ui.media.MediaBrowserActivity;
@@ -85,13 +81,12 @@
import org.wordpress.android.ui.pages.PageParentActivity;
import org.wordpress.android.ui.pages.PagesActivity;
import org.wordpress.android.ui.people.PeopleManagementActivity;
-import org.wordpress.android.ui.photopicker.MediaPickerConstants;
-import org.wordpress.android.ui.photopicker.PhotoPickerActivity;
import org.wordpress.android.ui.plans.PlansActivity;
import org.wordpress.android.ui.plugins.PluginBrowserActivity;
import org.wordpress.android.ui.plugins.PluginDetailActivity;
import org.wordpress.android.ui.plugins.PluginUtils;
import org.wordpress.android.ui.posts.EditPostActivity;
+import org.wordpress.android.ui.posts.EditPostActivityConstants;
import org.wordpress.android.ui.posts.JetpackSecuritySettingsActivity;
import org.wordpress.android.ui.posts.PostListType;
import org.wordpress.android.ui.posts.PostUtils;
@@ -123,7 +118,6 @@
import org.wordpress.android.ui.stats.refresh.lists.sections.insights.management.InsightsManagementActivity;
import org.wordpress.android.ui.stats.refresh.utils.StatsLaunchedFrom;
import org.wordpress.android.ui.stockmedia.StockMediaPickerActivity;
-import org.wordpress.android.ui.stories.StoryComposerActivity;
import org.wordpress.android.ui.suggestion.SuggestionActivity;
import org.wordpress.android.ui.suggestion.SuggestionType;
import org.wordpress.android.ui.themes.ThemeBrowserActivity;
@@ -144,14 +138,11 @@
import java.util.List;
import java.util.Map;
-import static com.wordpress.stories.util.BundleUtilsKt.KEY_STORY_INDEX;
-import static com.wordpress.stories.util.BundleUtilsKt.KEY_STORY_SAVE_RESULT;
import static org.wordpress.android.analytics.AnalyticsTracker.ACTIVITY_LOG_ACTIVITY_ID_KEY;
import static org.wordpress.android.analytics.AnalyticsTracker.Stat.POST_LIST_ACCESS_ERROR;
import static org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_ARTICLE_DETAIL_REBLOGGED;
import static org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_ARTICLE_REBLOGGED;
import static org.wordpress.android.analytics.AnalyticsTracker.Stat.STATS_ACCESS_ERROR;
-import static org.wordpress.android.editor.gutenberg.GutenbergEditorFragment.ARG_STORY_BLOCK_ID;
import static org.wordpress.android.imageeditor.preview.PreviewImageFragment.ARG_EDIT_IMAGE_DATA;
import static org.wordpress.android.login.LoginMode.JETPACK_LOGIN_ONLY;
import static org.wordpress.android.login.LoginMode.WPCOM_LOGIN_ONLY;
@@ -167,9 +158,6 @@
import static org.wordpress.android.ui.media.MediaBrowserActivity.ARG_BROWSER_TYPE;
import static org.wordpress.android.ui.pages.PagesActivityKt.EXTRA_PAGE_LIST_TYPE_KEY;
import static org.wordpress.android.ui.pages.PagesActivityKt.EXTRA_PAGE_REMOTE_ID_KEY;
-import static org.wordpress.android.ui.stories.StoryComposerActivity.KEY_ALL_UNFLATTENED_LOADED_SLIDES;
-import static org.wordpress.android.ui.stories.StoryComposerActivity.KEY_LAUNCHED_FROM_GUTENBERG;
-import static org.wordpress.android.ui.stories.StoryComposerActivity.KEY_POST_LOCAL_ID;
import static org.wordpress.android.viewmodel.activitylog.ActivityLogDetailViewModelKt.ACTIVITY_LOG_ARE_BUTTONS_VISIBLE_KEY;
import static org.wordpress.android.viewmodel.activitylog.ActivityLogDetailViewModelKt.ACTIVITY_LOG_ID_KEY;
import static org.wordpress.android.viewmodel.activitylog.ActivityLogDetailViewModelKt.ACTIVITY_LOG_IS_DASHBOARD_CARD_ENTRY_KEY;
@@ -221,7 +209,7 @@ public static void showMainActivityAndSignupEpilogue(Activity activity, String n
* @param site the preselected site
*/
public static void showSitePickerForResult(Activity activity, SiteModel site) {
- Intent intent = createSitePickerIntent(activity, site, SitePickerMode.DEFAULT_MODE);
+ Intent intent = createSitePickerIntent(activity, site, SitePickerMode.DEFAULT);
activity.startActivityForResult(intent, RequestCodes.SITE_PICKER);
}
@@ -246,36 +234,9 @@ public static void showSitePickerForResult(Fragment fragment, SiteModel site, Si
* @return the site picker intent
*/
private static Intent createSitePickerIntent(Context context, SiteModel site, SitePickerMode mode) {
- Intent intent = new Intent(context, SitePickerActivity.class);
- intent.putExtra(SitePickerActivity.KEY_SITE_LOCAL_ID, site.getId());
- intent.putExtra(SitePickerActivity.KEY_SITE_PICKER_MODE, mode);
- return intent;
- }
-
- /**
- * Use {@link org.wordpress.android.ui.photopicker.MediaPickerLauncher::showPhotoPickerForResult} instead
- */
- @Deprecated
- public static void showPhotoPickerForResult(Activity activity,
- @NonNull MediaBrowserType browserType,
- @Nullable SiteModel site,
- @Nullable Integer localPostId) {
- Intent intent = createShowPhotoPickerIntent(activity, browserType, site, localPostId);
- activity.startActivityForResult(intent, RequestCodes.PHOTO_PICKER);
- }
-
- private static Intent createShowPhotoPickerIntent(Context context,
- @NonNull MediaBrowserType browserType,
- @Nullable SiteModel site,
- @Nullable Integer localPostId) {
- Intent intent = new Intent(context, PhotoPickerActivity.class);
- intent.putExtra(ARG_BROWSER_TYPE, browserType);
- if (site != null) {
- intent.putExtra(WordPress.SITE, site);
- }
- if (localPostId != null) {
- intent.putExtra(MediaPickerConstants.LOCAL_POST_ID, localPostId.intValue());
- }
+ Intent intent = new Intent(context, ChooseSiteActivity.class);
+ intent.putExtra(ChooseSiteActivity.KEY_SITE_LOCAL_ID, site.getId());
+ intent.putExtra(ChooseSiteActivity.KEY_SITE_PICKER_MODE, mode.name());
return intent;
}
@@ -438,7 +399,7 @@ public static void openEditorForSiteInNewStack(Context context, @NonNull SiteMod
Intent editorIntent = new Intent(context, EditPostActivity.class);
editorIntent.putExtra(WordPress.SITE, site);
- editorIntent.putExtra(EditPostActivity.EXTRA_IS_PAGE, false);
+ editorIntent.putExtra(EditPostActivityConstants.EXTRA_IS_PAGE, false);
taskStackBuilder.addNextIntent(mainActivityIntent);
taskStackBuilder.addNextIntent(editorIntent);
@@ -452,8 +413,8 @@ public static void openEditorForPostInNewStack(Context context, @NonNull SiteMod
Intent editorIntent = new Intent(context, EditPostActivity.class);
editorIntent.putExtra(WordPress.SITE, site);
- editorIntent.putExtra(EditPostActivity.EXTRA_POST_LOCAL_ID, localPostId);
- editorIntent.putExtra(EditPostActivity.EXTRA_IS_PAGE, false);
+ editorIntent.putExtra(EditPostActivityConstants.EXTRA_POST_LOCAL_ID, localPostId);
+ editorIntent.putExtra(EditPostActivityConstants.EXTRA_IS_PAGE, false);
taskStackBuilder.addNextIntent(mainActivityIntent);
taskStackBuilder.addNextIntent(editorIntent);
@@ -493,11 +454,11 @@ public static void openEditorForReblog(
);
Intent editorIntent = new Intent(activity, EditPostActivity.class);
- editorIntent.putExtra(EditPostActivity.EXTRA_REBLOG_POST_TITLE, post.getTitle());
- editorIntent.putExtra(EditPostActivity.EXTRA_REBLOG_POST_QUOTE, post.getExcerpt());
- editorIntent.putExtra(EditPostActivity.EXTRA_REBLOG_POST_IMAGE, post.getFeaturedImage());
- editorIntent.putExtra(EditPostActivity.EXTRA_REBLOG_POST_CITATION, post.getUrl());
- editorIntent.setAction(EditPostActivity.ACTION_REBLOG);
+ editorIntent.putExtra(EditPostActivityConstants.EXTRA_REBLOG_POST_TITLE, post.getTitle());
+ editorIntent.putExtra(EditPostActivityConstants.EXTRA_REBLOG_POST_QUOTE, post.getExcerpt());
+ editorIntent.putExtra(EditPostActivityConstants.EXTRA_REBLOG_POST_IMAGE, post.getFeaturedImage());
+ editorIntent.putExtra(EditPostActivityConstants.EXTRA_REBLOG_POST_CITATION, post.getUrl());
+ editorIntent.setAction(EditPostActivityConstants.ACTION_REBLOG);
addNewPostForResult(editorIntent, activity, site, false, reblogSource, -1, null);
}
@@ -992,135 +953,59 @@ public static void viewBlogAdmin(Context context, SiteModel site) {
openUrlExternal(context, site.getAdminUrl());
}
- public static void addNewPostForResult(
+ public static void addNewPostWithContentFromAIForResult(
Activity activity,
SiteModel site,
boolean isPromo,
PagePostCreationSourcesDetail source,
- final int promptId,
- final EntryPoint entryPoint
- ) {
- addNewPostForResult(
- new Intent(activity, EditPostActivity.class), activity, site, isPromo, source, promptId, entryPoint
- );
- }
-
- public static void addNewPostForResult(
- Intent intent,
- Activity activity,
- SiteModel site,
- boolean isPromo,
- PagePostCreationSourcesDetail source,
- final int promptId,
- final EntryPoint entryPoint
+ final String content
) {
if (site == null) {
return;
}
+ Intent intent = new Intent(activity, EditPostActivity.class);
intent.putExtra(WordPress.SITE, site);
- intent.putExtra(EditPostActivity.EXTRA_IS_PAGE, false);
- intent.putExtra(EditPostActivity.EXTRA_IS_PROMO, isPromo);
+ intent.putExtra(EditPostActivityConstants.EXTRA_IS_PAGE, false);
+ intent.putExtra(EditPostActivityConstants.EXTRA_IS_PROMO, isPromo);
intent.putExtra(AnalyticsUtils.EXTRA_CREATION_SOURCE_DETAIL, source);
- intent.putExtra(EditPostActivity.EXTRA_PROMPT_ID, promptId);
- intent.putExtra(EditPostActivity.EXTRA_ENTRY_POINT, entryPoint);
+ intent.putExtra(EditPostActivityConstants.EXTRA_VOICE_CONTENT, content);
activity.startActivityForResult(intent, RequestCodes.EDIT_POST);
}
- public static void addNewStoryForResult(
- Activity activity,
- SiteModel site,
- PagePostCreationSourcesDetail source
- ) {
- if (site == null) {
- return;
- }
-
- Intent intent = new Intent(activity, StoryComposerActivity.class);
- intent.putExtra(WordPress.SITE, site);
- intent.putExtra(AnalyticsUtils.EXTRA_CREATION_SOURCE_DETAIL, source);
- intent.putExtra(MediaPickerConstants.EXTRA_LAUNCH_WPSTORIES_CAMERA_REQUESTED, true);
- activity.startActivityForResult(intent, RequestCodes.CREATE_STORY);
- }
-
- public static void addNewStoryWithMediaIdsForResult(
+ public static void addNewPostForResult(
Activity activity,
SiteModel site,
+ boolean isPromo,
PagePostCreationSourcesDetail source,
- long[] mediaIds
+ final int promptId,
+ final EntryPoint entryPoint
) {
- if (site == null) {
- return;
- }
-
- Intent intent = new Intent(activity, StoryComposerActivity.class);
- intent.putExtra(WordPress.SITE, site);
- intent.putExtra(MediaBrowserActivity.RESULT_IDS, mediaIds);
- intent.putExtra(AnalyticsUtils.EXTRA_CREATION_SOURCE_DETAIL, source);
- activity.startActivityForResult(intent, RequestCodes.CREATE_STORY);
+ addNewPostForResult(
+ new Intent(activity, EditPostActivity.class), activity, site, isPromo, source, promptId, entryPoint
+ );
}
- public static void addNewStoryWithMediaUrisForResult(
+ public static void addNewPostForResult(
+ Intent intent,
Activity activity,
SiteModel site,
+ boolean isPromo,
PagePostCreationSourcesDetail source,
- String[] mediaUris
+ final int promptId,
+ final EntryPoint entryPoint
) {
if (site == null) {
return;
}
- Intent intent = new Intent(activity, StoryComposerActivity.class);
intent.putExtra(WordPress.SITE, site);
- intent.putExtra(MediaPickerConstants.EXTRA_MEDIA_URIS, mediaUris);
+ intent.putExtra(EditPostActivityConstants.EXTRA_IS_PAGE, false);
+ intent.putExtra(EditPostActivityConstants.EXTRA_IS_PROMO, isPromo);
intent.putExtra(AnalyticsUtils.EXTRA_CREATION_SOURCE_DETAIL, source);
- activity.startActivityForResult(intent, RequestCodes.CREATE_STORY);
- }
-
-
- public static void editStoryForResult(
- Activity activity,
- SiteModel site,
- LocalId localPostId,
- int storyIndex,
- boolean allStorySlidesAreEditable,
- boolean launchedFromGutenberg,
- String storyBlockId
- ) {
- if (site == null) {
- return;
- }
-
- Intent intent = new Intent(activity, StoryComposerActivity.class);
- intent.putExtra(WordPress.SITE, site);
- intent.putExtra(KEY_POST_LOCAL_ID, localPostId.getValue());
- intent.putExtra(KEY_STORY_INDEX, storyIndex);
- intent.putExtra(KEY_LAUNCHED_FROM_GUTENBERG, launchedFromGutenberg);
- intent.putExtra(KEY_ALL_UNFLATTENED_LOADED_SLIDES, allStorySlidesAreEditable);
- intent.putExtra(ARG_STORY_BLOCK_ID, storyBlockId);
- activity.startActivityForResult(intent, RequestCodes.EDIT_STORY);
- }
-
- public static void editEmptyStoryForResult(
- Activity activity,
- SiteModel site,
- LocalId localPostId,
- int storyIndex,
- String storyBlockId
- ) {
- if (site == null) {
- return;
- }
-
- AnalyticsTracker.track(Stat.STORY_BLOCK_ADD_MEDIA_TAPPED);
- Intent intent = new Intent(activity, StoryComposerActivity.class);
- intent.putExtra(WordPress.SITE, site);
- intent.putExtra(KEY_LAUNCHED_FROM_GUTENBERG, true);
- intent.putExtra(MediaPickerConstants.EXTRA_LAUNCH_WPSTORIES_MEDIA_PICKER_REQUESTED, true);
- intent.putExtra(KEY_POST_LOCAL_ID, localPostId.getValue());
- intent.putExtra(KEY_STORY_INDEX, storyIndex);
- intent.putExtra(ARG_STORY_BLOCK_ID, storyBlockId);
- activity.startActivityForResult(intent, RequestCodes.EDIT_STORY);
+ intent.putExtra(EditPostActivityConstants.EXTRA_PROMPT_ID, promptId);
+ intent.putExtra(EditPostActivityConstants.EXTRA_ENTRY_POINT, entryPoint);
+ activity.startActivityForResult(intent, RequestCodes.EDIT_POST);
}
public static void editPostOrPageForResult(Activity activity, SiteModel site, PostModel post) {
@@ -1147,8 +1032,8 @@ public static void editPostOrPageForResult(Intent intent, Activity activity, Sit
// PostModel objects can be quite large, since content field is not size restricted,
// in order to avoid issues like TransactionTooLargeException it's better to pass the id of the post.
// However, we still want to keep passing the SiteModel to avoid confusion around local & remote ids.
- intent.putExtra(EditPostActivity.EXTRA_POST_LOCAL_ID, postLocalId);
- intent.putExtra(EditPostActivity.EXTRA_LOAD_AUTO_SAVE_REVISION, loadAutoSaveRevision);
+ intent.putExtra(EditPostActivityConstants.EXTRA_POST_LOCAL_ID, postLocalId);
+ intent.putExtra(EditPostActivityConstants.EXTRA_LOAD_AUTO_SAVE_REVISION, loadAutoSaveRevision);
activity.startActivityForResult(intent, RequestCodes.EDIT_POST);
}
@@ -1167,16 +1052,16 @@ public static void editPageForResult(Intent intent, @NonNull Fragment fragment,
public static void editLandingPageForResult(@NonNull Fragment fragment, @NonNull SiteModel site, int homeLocalId,
boolean isNewSite) {
Intent intent = new Intent(fragment.getContext(), EditPostActivity.class);
- intent.putExtra(EditPostActivity.EXTRA_IS_LANDING_EDITOR, true);
- intent.putExtra(EditPostActivity.EXTRA_IS_LANDING_EDITOR_OPENED_FOR_NEW_SITE, isNewSite);
+ intent.putExtra(EditPostActivityConstants.EXTRA_IS_LANDING_EDITOR, true);
+ intent.putExtra(EditPostActivityConstants.EXTRA_IS_LANDING_EDITOR_OPENED_FOR_NEW_SITE, isNewSite);
editPageForResult(intent, fragment, site, homeLocalId, false, RequestCodes.EDIT_LANDING_PAGE);
}
public static void editPageForResult(Intent intent, @NonNull Fragment fragment, @NonNull SiteModel site,
int pageLocalId, boolean loadAutoSaveRevision, int requestCode) {
intent.putExtra(WordPress.SITE, site);
- intent.putExtra(EditPostActivity.EXTRA_POST_LOCAL_ID, pageLocalId);
- intent.putExtra(EditPostActivity.EXTRA_LOAD_AUTO_SAVE_REVISION, loadAutoSaveRevision);
+ intent.putExtra(EditPostActivityConstants.EXTRA_POST_LOCAL_ID, pageLocalId);
+ intent.putExtra(EditPostActivityConstants.EXTRA_LOAD_AUTO_SAVE_REVISION, loadAutoSaveRevision);
fragment.startActivityForResult(intent, requestCode);
}
@@ -1190,11 +1075,11 @@ public static void addNewPageForResult(
) {
Intent intent = new Intent(activity, EditPostActivity.class);
intent.putExtra(WordPress.SITE, site);
- intent.putExtra(EditPostActivity.EXTRA_IS_PAGE, true);
- intent.putExtra(EditPostActivity.EXTRA_IS_PROMO, false);
- intent.putExtra(EditPostActivity.EXTRA_PAGE_TITLE, title);
- intent.putExtra(EditPostActivity.EXTRA_PAGE_CONTENT, content);
- intent.putExtra(EditPostActivity.EXTRA_PAGE_TEMPLATE, template);
+ intent.putExtra(EditPostActivityConstants.EXTRA_IS_PAGE, true);
+ intent.putExtra(EditPostActivityConstants.EXTRA_IS_PROMO, false);
+ intent.putExtra(EditPostActivityConstants.EXTRA_PAGE_TITLE, title);
+ intent.putExtra(EditPostActivityConstants.EXTRA_PAGE_CONTENT, content);
+ intent.putExtra(EditPostActivityConstants.EXTRA_PAGE_TEMPLATE, template);
intent.putExtra(AnalyticsUtils.EXTRA_CREATION_SOURCE_DETAIL, source);
activity.startActivityForResult(intent, RequestCodes.EDIT_POST);
}
@@ -1208,11 +1093,11 @@ public static void addNewPageForResult(
@NonNull PagePostCreationSourcesDetail source) {
Intent intent = new Intent(fragment.getContext(), EditPostActivity.class);
intent.putExtra(WordPress.SITE, site);
- intent.putExtra(EditPostActivity.EXTRA_IS_PAGE, true);
- intent.putExtra(EditPostActivity.EXTRA_IS_PROMO, false);
- intent.putExtra(EditPostActivity.EXTRA_PAGE_TITLE, title);
- intent.putExtra(EditPostActivity.EXTRA_PAGE_CONTENT, content);
- intent.putExtra(EditPostActivity.EXTRA_PAGE_TEMPLATE, template);
+ intent.putExtra(EditPostActivityConstants.EXTRA_IS_PAGE, true);
+ intent.putExtra(EditPostActivityConstants.EXTRA_IS_PROMO, false);
+ intent.putExtra(EditPostActivityConstants.EXTRA_PAGE_TITLE, title);
+ intent.putExtra(EditPostActivityConstants.EXTRA_PAGE_CONTENT, content);
+ intent.putExtra(EditPostActivityConstants.EXTRA_PAGE_TEMPLATE, template);
intent.putExtra(AnalyticsUtils.EXTRA_CREATION_SOURCE_DETAIL, source);
fragment.startActivityForResult(intent, RequestCodes.EDIT_POST);
}
@@ -1381,22 +1266,6 @@ public static void viewNotificationsSettings(Activity activity) {
activity.startActivity(intent);
}
- public static void viewStories(Activity activity, SiteModel site, StorySaveResult event) {
- Intent intent = new Intent(activity, StoryComposerActivity.class);
- intent.putExtra(KEY_STORY_SAVE_RESULT, event);
- intent.putExtra(WordPress.SITE, site);
-
- // we need to have a way to cancel the related error notification when the user comes
- // from tapping on MANAGE on the snackbar (otherwise they'll be able to discard the
- // errored story but the error notification will remain existing in the system dashboard)
- intent.setAction(String.valueOf(FrameSaveNotifier.getNotificationIdForError(
- StoryComposerActivity.BASE_FRAME_MEDIA_ERROR_NOTIFICATION_ID,
- event.getStoryIndex()
- )));
-
- activity.startActivity(intent);
- }
-
public static void viewJetpackSecuritySettingsForResult(Activity activity, SiteModel site) {
AnalyticsTracker.track(Stat.SITE_SETTINGS_JETPACK_SECURITY_SETTINGS_VIEWED);
Intent intent = new Intent(activity, JetpackSecuritySettingsActivity.class);
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityNavigator.kt b/WordPress/src/main/java/org/wordpress/android/ui/ActivityNavigator.kt
index 9378e0842f6f..235a0be1e594 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityNavigator.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityNavigator.kt
@@ -49,7 +49,7 @@ class ActivityNavigator @Inject constructor() {
fun navigateToCampaignDetailPage(
context: Context,
- campaignId: Int,
+ campaignId: String,
campaignDetailPageSource: CampaignDetailPageSource
) {
context.startActivity(
@@ -201,4 +201,11 @@ class ActivityNavigator @Inject constructor() {
.addNextIntent(intent)
.startActivities()
}
+
+ fun openIneligibleForVoiceToContent(
+ context: Context,
+ url: String
+ ) {
+ WPWebViewActivity.openUrlByUsingGlobalWPCOMCredentials(context, url)
+ }
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java
index f25fecd8c170..dcaa0d31f6d5 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java
@@ -35,6 +35,7 @@
import org.wordpress.android.fluxc.store.SiteStore;
import org.wordpress.android.fluxc.tools.FluxCImageLoader;
import org.wordpress.android.ui.posts.EditPostActivity;
+import org.wordpress.android.ui.posts.EditPostActivityConstants;
import org.wordpress.android.util.SiteUtils;
import org.wordpress.android.util.ToastUtils;
@@ -142,8 +143,8 @@ public void onClick(DialogInterface dialog, int which) {
shortcutIntent.setAction(Intent.ACTION_MAIN);
shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- shortcutIntent.putExtra(EditPostActivity.EXTRA_QUICKPRESS_BLOG_ID, siteIds[position]);
- shortcutIntent.putExtra(EditPostActivity.EXTRA_IS_QUICKPRESS, true);
+ shortcutIntent.putExtra(EditPostActivityConstants.EXTRA_QUICKPRESS_BLOG_ID, siteIds[position]);
+ shortcutIntent.putExtra(EditPostActivityConstants.EXTRA_IS_QUICKPRESS, true);
String shortcutName = quickPressShortcutName.getText().toString();
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/CollapseFullScreenDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/CollapseFullScreenDialogFragment.java
index 795801d2a42d..f57910c7cb0e 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/CollapseFullScreenDialogFragment.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/CollapseFullScreenDialogFragment.java
@@ -22,7 +22,6 @@
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
-import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.view.MenuItemCompat;
@@ -36,6 +35,7 @@
import com.google.android.material.elevation.ElevationOverlayProvider;
import org.wordpress.android.R;
+import org.wordpress.android.databinding.CollapseFullScreenDialogFragmentBinding;
/**
* A {@link DialogFragment} implementing the full-screen dialog pattern defined in the
@@ -56,6 +56,7 @@ public class CollapseFullScreenDialogFragment extends DialogFragment {
private static final String ARG_HIDE_ACTIVITY_BAR = "ARG_HIDE_ACTIVITY_BAR";
private static final String ARG_TITLE = "ARG_TITLE";
private static final int ID_ACTION = 1;
+ private CollapseFullScreenDialogFragmentBinding mBinding;
public static final String TAG = CollapseFullScreenDialogFragment.class.getSimpleName();
@@ -120,7 +121,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
- mFragment = getChildFragmentManager().findFragmentById(R.id.full_screen_dialog_fragment_content);
+ mFragment = getChildFragmentManager().findFragmentById(mBinding.fullScreenDialogFragmentContent.getId());
}
mController = new CollapseFullScreenDialogController() {
@@ -171,7 +172,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
hideActivityBar();
}
- ViewGroup view = (ViewGroup) inflater.inflate(R.layout.collapse_full_screen_dialog_fragment, container, false);
+ mBinding = CollapseFullScreenDialogFragmentBinding.inflate(inflater, container, false);
+ ViewGroup view = mBinding.getRoot();
initToolbar(view);
setThemeBackground(view);
view.setFocusableInTouchMode(true);
@@ -270,20 +272,19 @@ private void initBuilderArguments() {
* @param view {@link View}
*/
private void initToolbar(View view) {
- Toolbar toolbar = view.findViewById(R.id.full_screen_dialog_fragment_toolbar);
-
ElevationOverlayProvider elevationOverlayProvider = new ElevationOverlayProvider(view.getContext());
float appbarElevation = getResources().getDimension(R.dimen.appbar_elevation);
int elevatedColor = elevationOverlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded(appbarElevation);
- toolbar.setBackgroundColor(elevatedColor);
+ mBinding.fullScreenDialogFragmentToolbar.setBackgroundColor(elevatedColor);
- toolbar.setTitle(mTitle);
- toolbar.setNavigationContentDescription(R.string.description_collapse);
- toolbar.setNavigationIcon(ContextCompat.getDrawable(view.getContext(), R.drawable.ic_chevron_down_white_24dp));
- toolbar.setNavigationOnClickListener(v -> onCollapseClicked());
+ mBinding.fullScreenDialogFragmentToolbar.setTitle(mTitle);
+ mBinding.fullScreenDialogFragmentToolbar.setNavigationContentDescription(R.string.description_collapse);
+ mBinding.fullScreenDialogFragmentToolbar.setNavigationIcon(ContextCompat.getDrawable(view.getContext(),
+ R.drawable.ic_chevron_down_white_24dp));
+ mBinding.fullScreenDialogFragmentToolbar.setNavigationOnClickListener(v -> onCollapseClicked());
if (!mAction.isEmpty()) {
- Menu menu = toolbar.getMenu();
+ Menu menu = mBinding.fullScreenDialogFragmentToolbar.getMenu();
mMenuAction = menu.add(0, ID_ACTION, 0, this.mAction);
mMenuAction.setIcon(R.drawable.ic_send_white_24dp);
MenuItemCompat.setIconTintList(mMenuAction,
@@ -520,4 +521,10 @@ public Builder setTitle(@StringRes int textId) {
return this;
}
}
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mBinding = null;
+ }
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionResultActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionResultActivity.java
index 7bc61f8a5342..f83839062fbc 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionResultActivity.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionResultActivity.java
@@ -141,7 +141,7 @@ private void finishAndGoBackToSource() {
if (mSource == JetpackConnectionSource.STATS) {
SiteModel site = (SiteModel) getIntent().getSerializableExtra(SITE);
mDispatcher.dispatch(SiteActionBuilder.newFetchSitesAction(SiteUtils.getFetchSitesPayload()));
- ActivityLauncher.viewBlogStatsAfterJetpackSetup(this, site, StatsLaunchedFrom.JETPACK_CONNECTION);
+ ActivityLauncher.viewBlogStatsAfterJetpackSetup(this, site, StatsLaunchedFrom.QUICK_ACTIONS);
}
finish();
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java b/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java
index e46b5cd8a090..41de68d08d9c 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java
@@ -71,10 +71,6 @@ public class RequestCodes {
// Other
public static final int SELECTED_USER_MENTION = 7000;
- // Story creator
- public static final int CREATE_STORY = 8000;
- public static final int EDIT_STORY = 8001;
-
// Reader Interests
public static final int READER_INTERESTS = 9001;
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ShareIntentReceiverFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/ShareIntentReceiverFragment.java
index ee975b117526..c4165c37e291 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/ShareIntentReceiverFragment.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/ShareIntentReceiverFragment.java
@@ -19,12 +19,14 @@
import org.wordpress.android.WordPress;
import org.wordpress.android.fluxc.store.AccountStore;
import org.wordpress.android.ui.main.SitePickerAdapter;
-import org.wordpress.android.ui.main.SitePickerAdapter.SiteList;
import org.wordpress.android.ui.main.SitePickerAdapter.ViewHolderHandler;
+import org.wordpress.android.ui.main.SiteRecord;
import org.wordpress.android.ui.media.MediaBrowserActivity;
import org.wordpress.android.ui.posts.EditPostActivity;
import org.wordpress.android.util.image.ImageManager;
+import java.util.List;
+
import javax.inject.Inject;
public class ShareIntentReceiverFragment extends Fragment {
@@ -185,7 +187,7 @@ public HeaderViewHolder onCreateViewHolder(LayoutInflater layoutInflater, ViewGr
}
@Override
- public void onBindViewHolder(HeaderViewHolder holder, SiteList sites) {
+ public void onBindViewHolder(HeaderViewHolder holder, List sites) {
if (!isAdded()) {
return;
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/WebViewActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/WebViewActivity.java
index 1999b39538e2..c71e716e6f3d 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/WebViewActivity.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/WebViewActivity.java
@@ -13,13 +13,15 @@
import androidx.appcompat.widget.Toolbar;
import org.wordpress.android.R;
-import org.wordpress.android.WordPress;
import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.fluxc.network.UserAgent;
import org.wordpress.android.util.extensions.CompatExtensionsKt;
import java.util.HashMap;
import java.util.Map;
+import javax.inject.Inject;
+
/**
* Basic activity for displaying a WebView.
*/
@@ -35,6 +37,8 @@ public abstract class WebViewActivity extends LocaleAwareActivity {
protected WebView mWebView;
+ @Inject UserAgent mUserAgent;
+
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
supportRequestWindowFeature(Window.FEATURE_PROGRESS);
@@ -64,7 +68,9 @@ public void handleOnBackPressed() {
mWebView = (WebView) findViewById(R.id.webView);
mWebView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
// Setting this user agent makes Calypso sites hide any WordPress UIs (e.g. Masterbar, banners, etc.).
- mWebView.getSettings().setUserAgentString(WordPress.getUserAgent());
+ if (mUserAgent != null) {
+ mWebView.getSettings().setUserAgentString(mUserAgent.toString());
+ }
configureWebView();
if (savedInstanceState == null) {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java
index fea289f1e55f..0d67b97b833d 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java
@@ -56,12 +56,11 @@
import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Click;
import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Flow;
import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Source;
-import org.wordpress.android.ui.accounts.login.LoginPrologueFragment;
import org.wordpress.android.ui.accounts.login.LoginPrologueListener;
import org.wordpress.android.ui.accounts.login.LoginPrologueRevampedFragment;
import org.wordpress.android.ui.accounts.login.jetpack.LoginNoSitesFragment;
import org.wordpress.android.ui.accounts.login.jetpack.LoginSiteCheckErrorFragment;
-import org.wordpress.android.ui.main.SitePickerActivity;
+import org.wordpress.android.ui.main.ChooseSiteActivity;
import org.wordpress.android.ui.notifications.services.NotificationsUpdateServiceStarter;
import org.wordpress.android.ui.posts.BasicFragmentDialog;
import org.wordpress.android.ui.posts.BasicFragmentDialog.BasicDialogPositiveClickInterface;
@@ -77,7 +76,6 @@
import org.wordpress.android.util.WPActivityUtils;
import org.wordpress.android.util.WPUrlUtils;
import org.wordpress.android.util.config.ContactSupportFeatureConfig;
-import org.wordpress.android.util.config.LandingScreenRevampFeatureConfig;
import org.wordpress.android.widgets.WPSnackbar;
import java.util.ArrayList;
@@ -138,9 +136,6 @@ private enum SmartLockHelperState {
@Inject protected SiteStore mSiteStore;
@Inject protected ViewModelProvider.Factory mViewModelFactory;
@Inject BuildConfigWrapper mBuildConfigWrapper;
-
- @Inject LandingScreenRevampFeatureConfig mLandingScreenRevampFeatureConfig;
-
@Inject ContactSupportFeatureConfig mContactSupportFeatureConfig;
@Override
@@ -232,11 +227,7 @@ private void initViewModel() {
}
private void loginFromPrologue() {
- if (mLandingScreenRevampFeatureConfig.isEnabled()) {
- showFragment(new LoginPrologueRevampedFragment(), LoginPrologueRevampedFragment.TAG);
- } else {
- showFragment(new LoginPrologueFragment(), LoginPrologueFragment.TAG);
- }
+ showFragment(new LoginPrologueRevampedFragment(), LoginPrologueRevampedFragment.TAG);
mIsSmartLockTriggeredFromPrologue = true;
mIsSiteLoginAvailableFromPrologue = true;
initSmartLockIfNotFinished(true);
@@ -279,11 +270,6 @@ private void addGoogleFragment(GoogleFragment googleFragment, String tag) {
fragmentTransaction.commit();
}
- private LoginPrologueFragment getLoginPrologueFragment() {
- Fragment fragment = getSupportFragmentManager().findFragmentByTag(LoginPrologueFragment.TAG);
- return fragment == null ? null : (LoginPrologueFragment) fragment;
- }
-
private LoginPrologueRevampedFragment getLoginPrologueRevampedFragment() {
Fragment fragment = getSupportFragmentManager().findFragmentByTag(LoginPrologueRevampedFragment.TAG);
return fragment == null ? null : (LoginPrologueRevampedFragment) fragment;
@@ -352,7 +338,7 @@ private void loggedInAndFinish(ArrayList oldSitesIds, boolean doLoginUp
if (newSitesIds.size() > 0) {
Intent intent = new Intent();
- intent.putExtra(SitePickerActivity.KEY_SITE_LOCAL_ID, newSitesIds.get(0));
+ intent.putExtra(ChooseSiteActivity.KEY_SITE_LOCAL_ID, newSitesIds.get(0));
setResult(Activity.RESULT_OK, intent);
} else {
AppLog.e(T.MAIN, "Couldn't detect newly added self-hosted site. "
@@ -448,7 +434,7 @@ private void startLogin() {
return;
}
- if (getLoginPrologueFragment() == null && getLoginPrologueRevampedFragment() == null) {
+ if (getLoginPrologueRevampedFragment() == null) {
// prologue fragment is not shown so, the email screen will be the initial screen on the fragment container
showFragment(LoginEmailFragment.newInstance(mIsSignupFromLoginEnabled), LoginEmailFragment.TAG);
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginEpilogueActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginEpilogueActivity.java
index 4440ffb959e0..79f8c6140185 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginEpilogueActivity.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginEpilogueActivity.java
@@ -24,7 +24,7 @@
import org.wordpress.android.ui.accounts.login.LoginEpilogueListener;
import org.wordpress.android.ui.accounts.login.jetpack.LoginNoSitesFragment;
import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginFragment;
-import org.wordpress.android.ui.main.SitePickerActivity;
+import org.wordpress.android.ui.main.ChooseSiteActivity;
import org.wordpress.android.ui.mysite.SelectedSiteRepository;
import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource;
@@ -126,7 +126,7 @@ private void showPostSignupInterstitialScreen() {
}
private void selectSite(int localId) {
- setResult(RESULT_OK, new Intent().putExtra(SitePickerActivity.KEY_SITE_LOCAL_ID, localId));
+ setResult(RESULT_OK, new Intent().putExtra(ChooseSiteActivity.KEY_SITE_LOCAL_ID, localId));
finish();
}
@@ -167,16 +167,16 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
&& data != null
) {
int newSiteLocalID = data.getIntExtra(
- SitePickerActivity.KEY_SITE_LOCAL_ID,
+ ChooseSiteActivity.KEY_SITE_LOCAL_ID,
SelectedSiteRepository.UNAVAILABLE
);
boolean isTitleTaskCompleted = data.getBooleanExtra(
- SitePickerActivity.KEY_SITE_TITLE_TASK_COMPLETED,
+ ChooseSiteActivity.KEY_SITE_TITLE_TASK_COMPLETED,
false
);
setResult(RESULT_OK, new Intent()
- .putExtra(SitePickerActivity.KEY_SITE_LOCAL_ID, newSiteLocalID)
- .putExtra(SitePickerActivity.KEY_SITE_TITLE_TASK_COMPLETED, isTitleTaskCompleted)
+ .putExtra(ChooseSiteActivity.KEY_SITE_LOCAL_ID, newSiteLocalID)
+ .putExtra(ChooseSiteActivity.KEY_SITE_TITLE_TASK_COMPLETED, isTitleTaskCompleted)
.putExtra(KEY_SITE_CREATED_FROM_LOGIN_EPILOGUE, true)
);
finish();
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignupEpilogueActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignupEpilogueActivity.java
index 12992ae5e703..dae2831880dc 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignupEpilogueActivity.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignupEpilogueActivity.java
@@ -48,7 +48,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
protected void addSignupEpilogueFragment(String name, String email, String photoUrl, String username,
boolean isEmail) {
- SignupEpilogueFragment signupEpilogueSocialFragment = SignupEpilogueFragment.newInstance(
+ SignupEpilogueFragment signupEpilogueSocialFragment = SignupEpilogueFragment.Companion.newInstance(
name, email, photoUrl, username, isEmail);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.fragment_container, signupEpilogueSocialFragment, SignupEpilogueFragment.TAG);
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SmartLockHelper.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SmartLockHelper.java
index 0bac7f2b6a63..e0d0b0fe1278 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SmartLockHelper.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SmartLockHelper.java
@@ -13,7 +13,6 @@
import com.google.android.gms.auth.api.Auth;
import com.google.android.gms.auth.api.credentials.Credential;
import com.google.android.gms.auth.api.credentials.CredentialRequest;
-import com.google.android.gms.auth.api.credentials.CredentialRequestResult;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.common.api.CommonStatusCodes;
@@ -82,33 +81,30 @@ public void smartLockAutoFill(@NonNull final Callback callback) {
.setPasswordLoginSupported(true)
.build();
Auth.CredentialsApi.request(mCredentialsClient, credentialRequest).setResultCallback(
- new ResultCallback() {
- @Override
- public void onResult(@NonNull CredentialRequestResult result) {
- Status status = result.getStatus();
- if (status.isSuccess()) {
- Credential credential = result.getCredential();
- callback.onCredentialRetrieved(credential);
- } else {
- if (status.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED) {
- try {
- Activity activity = getActivityAndCheckAvailability();
- if (activity == null) {
- return;
- }
- // Prompt the user to choose a saved credential
- status.startResolutionForResult(activity, RequestCodes.SMART_LOCK_READ);
- } catch (IntentSender.SendIntentException e) {
- AppLog.d(T.NUX, "SmartLock: Failed to send resolution for credential request");
-
- callback.onCredentialsUnavailable();
- }
- } else {
- // The user must create an account or log in manually.
- AppLog.d(T.NUX, "SmartLock: Unsuccessful credential request.");
+ result -> {
+ Activity currentActivity = getActivityAndCheckAvailability();
+ if (currentActivity == null) {
+ return;
+ }
+ Status status = result.getStatus();
+ if (status.isSuccess()) {
+ Credential credential = result.getCredential();
+ callback.onCredentialRetrieved(credential);
+ } else {
+ if (status.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED) {
+ try {
+ // Prompt the user to choose a saved credential
+ status.startResolutionForResult(currentActivity, RequestCodes.SMART_LOCK_READ);
+ } catch (IntentSender.SendIntentException e) {
+ AppLog.d(T.NUX, "SmartLock: Failed to send resolution for credential request");
callback.onCredentialsUnavailable();
}
+ } else {
+ // The user must create an account or log in manually.
+ AppLog.d(T.NUX, "SmartLock: Unsuccessful credential request.");
+
+ callback.onCredentialsUnavailable();
}
}
});
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/LoginEpilogueFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/LoginEpilogueFragment.java
index a637848415a3..81dd6ad51340 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/LoginEpilogueFragment.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/LoginEpilogueFragment.java
@@ -31,16 +31,16 @@
import org.wordpress.android.ui.main.SitePickerAdapter;
import org.wordpress.android.ui.main.SitePickerAdapter.OnDataLoadedListener;
import org.wordpress.android.ui.main.SitePickerAdapter.OnSiteClickListener;
-import org.wordpress.android.ui.main.SitePickerAdapter.SiteList;
import org.wordpress.android.ui.main.SitePickerAdapter.SitePickerMode;
-import org.wordpress.android.ui.main.SitePickerAdapter.SiteRecord;
import org.wordpress.android.ui.main.SitePickerAdapter.ViewHolderHandler;
+import org.wordpress.android.ui.main.SiteRecord;
import org.wordpress.android.util.BuildConfigWrapper;
import org.wordpress.android.util.StringUtils;
import org.wordpress.android.util.analytics.AnalyticsUtils;
import org.wordpress.android.util.image.ImageManager;
import java.util.ArrayList;
+import java.util.List;
import javax.inject.Inject;
@@ -255,7 +255,7 @@ public LoginHeaderViewHolder onCreateViewHolder(LayoutInflater layoutInflater, V
}
@Override
- public void onBindViewHolder(LoginHeaderViewHolder holder, SiteList sites) {
+ public void onBindViewHolder(LoginHeaderViewHolder holder, List sites) {
bindHeaderViewHolder(holder, sites);
}
};
@@ -275,7 +275,7 @@ public LoginFooterViewHolder onCreateViewHolder(LayoutInflater layoutInflater, V
}
@Override
- public void onBindViewHolder(LoginFooterViewHolder holder, SiteList sites) {
+ public void onBindViewHolder(LoginFooterViewHolder holder, List sites) {
bindFooterViewHolder(holder, sites);
}
};
@@ -321,7 +321,7 @@ public void onResume() {
mParentViewModel.onLoginEpilogueResume(mDoLoginUpdate);
}
- private void bindHeaderViewHolder(LoginHeaderViewHolder holder, SiteList sites) {
+ private void bindHeaderViewHolder(LoginHeaderViewHolder holder, List sites) {
if (!isAdded()) {
return;
}
@@ -354,7 +354,7 @@ private void bindHeaderViewHolder(LoginHeaderViewHolder holder, SiteList sites)
}
}
- private void bindFooterViewHolder(LoginFooterViewHolder holder, SiteList sites) {
+ private void bindFooterViewHolder(LoginFooterViewHolder holder, List sites) {
holder.itemView.setVisibility((mShowAndReturn || BuildConfig.IS_JETPACK_APP) ? View.GONE : View.VISIBLE);
holder.itemView.setOnClickListener(v -> {
if (mLoginEpilogueListener != null) {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/LoginHeaderViewHolder.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/LoginHeaderViewHolder.java
index e677cf3f4598..783717fbec79 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/LoginHeaderViewHolder.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/LoginHeaderViewHolder.java
@@ -7,17 +7,22 @@
import android.widget.LinearLayout;
import android.widget.TextView;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
+import com.gravatar.AvatarQueryOptions;
+import com.gravatar.AvatarUrl;
+import com.gravatar.DefaultAvatarOption;
+import com.gravatar.types.Email;
+
import org.wordpress.android.R;
import org.wordpress.android.fluxc.model.AccountModel;
import org.wordpress.android.fluxc.model.SiteModel;
-import org.wordpress.android.util.GravatarUtils;
import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.WPAvatarUtils;
import org.wordpress.android.util.image.ImageManager;
-import static org.wordpress.android.util.GravatarUtils.DefaultImage.STATUS_404;
import static org.wordpress.android.util.image.ImageType.AVATAR_WITHOUT_BACKGROUND;
/**
@@ -96,11 +101,21 @@ private int getAvatarSize(Context context) {
return context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_large);
}
- private String constructGravatarUrl(Context context, AccountModel account) {
- return GravatarUtils.fixGravatarUrl(account.getAvatarUrl(), getAvatarSize(context), STATUS_404);
+ private @NonNull String constructGravatarUrl(Context context, @Nullable AccountModel account) {
+ if (account == null || account.getAvatarUrl() == null) {
+ return "";
+ }
+ return WPAvatarUtils.rewriteAvatarUrl(account.getAvatarUrl(), getAvatarSize(context),
+ DefaultAvatarOption.Status404.INSTANCE);
}
- private String constructGravatarUrl(Context context, SiteModel site) {
- return GravatarUtils.gravatarFromEmail(site.getEmail(), getAvatarSize(context), STATUS_404);
+ private @NonNull String constructGravatarUrl(Context context, @Nullable SiteModel site) {
+ if (site == null || site.getEmail() == null) {
+ return "";
+ }
+ return new AvatarUrl(
+ new Email(site.getEmail()),
+ new AvatarQueryOptions(getAvatarSize(context), DefaultAvatarOption.Status404.INSTANCE, null, null)
+ ).url().toString();
}
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SignupEpilogueFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SignupEpilogueFragment.java
deleted file mode 100644
index ece755888abb..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SignupEpilogueFragment.java
+++ /dev/null
@@ -1,846 +0,0 @@
-package org.wordpress.android.ui.accounts.signup;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Bundle;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver.OnGlobalLayoutListener;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.LayoutRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.view.ContextThemeWrapper;
-import androidx.core.widget.NestedScrollView;
-import androidx.core.widget.NestedScrollView.OnScrollChangeListener;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.yalantis.ucrop.UCrop;
-import com.yalantis.ucrop.UCropActivity;
-
-import org.greenrobot.eventbus.Subscribe;
-import org.greenrobot.eventbus.ThreadMode;
-import org.wordpress.android.R;
-import org.wordpress.android.WordPress;
-import org.wordpress.android.analytics.AnalyticsTracker;
-import org.wordpress.android.analytics.AnalyticsTracker.Stat;
-import org.wordpress.android.fluxc.Dispatcher;
-import org.wordpress.android.fluxc.action.AccountAction;
-import org.wordpress.android.fluxc.generated.AccountActionBuilder;
-import org.wordpress.android.fluxc.store.AccountStore;
-import org.wordpress.android.fluxc.store.AccountStore.AccountUsernameActionType;
-import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged;
-import org.wordpress.android.fluxc.store.AccountStore.OnUsernameChanged;
-import org.wordpress.android.fluxc.store.AccountStore.PushAccountSettingsPayload;
-import org.wordpress.android.fluxc.store.AccountStore.PushUsernamePayload;
-import org.wordpress.android.login.LoginBaseFormFragment;
-import org.wordpress.android.login.widgets.WPLoginInputRow;
-import org.wordpress.android.networking.GravatarApi;
-import org.wordpress.android.ui.FullScreenDialogFragment;
-import org.wordpress.android.ui.FullScreenDialogFragment.OnConfirmListener;
-import org.wordpress.android.ui.FullScreenDialogFragment.OnDismissListener;
-import org.wordpress.android.ui.FullScreenDialogFragment.OnShownListener;
-import org.wordpress.android.ui.RequestCodes;
-import org.wordpress.android.ui.accounts.UnifiedLoginTracker;
-import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Click;
-import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Step;
-import org.wordpress.android.ui.photopicker.MediaPickerConstants;
-import org.wordpress.android.ui.photopicker.MediaPickerLauncher;
-import org.wordpress.android.ui.photopicker.PhotoPickerActivity;
-import org.wordpress.android.ui.prefs.AppPrefsWrapper;
-import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic;
-import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter;
-import org.wordpress.android.util.AppLog;
-import org.wordpress.android.util.AppLog.T;
-import org.wordpress.android.util.GravatarUtils;
-import org.wordpress.android.util.MediaUtils;
-import org.wordpress.android.util.StringUtils;
-import org.wordpress.android.util.ToastUtils;
-import org.wordpress.android.util.WPMediaUtils;
-import org.wordpress.android.util.extensions.ContextExtensionsKt;
-import org.wordpress.android.util.extensions.ViewExtensionsKt;
-import org.wordpress.android.util.image.ImageManager;
-import org.wordpress.android.util.image.ImageManager.RequestListener;
-import org.wordpress.android.util.image.ImageType;
-import org.wordpress.android.widgets.WPTextView;
-
-import java.io.File;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-
-import javax.inject.Inject;
-
-import static org.wordpress.android.analytics.AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_GRAVATAR_GALLERY_PICKED;
-import static org.wordpress.android.analytics.AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_GRAVATAR_SHOT_NEW;
-
-public class SignupEpilogueFragment extends LoginBaseFormFragment
- implements OnConfirmListener, OnDismissListener, OnShownListener {
- private EditText mEditTextDisplayName;
- private EditText mEditTextUsername;
- private FullScreenDialogFragment mDialog;
- private SignupEpilogueListener mSignupEpilogueListener;
-
- protected ImageView mHeaderAvatarAdd;
- protected String mDisplayName;
- protected String mEmailAddress;
- protected String mPhotoUrl;
- protected String mUsername;
- protected WPLoginInputRow mInputPassword;
- protected ImageView mHeaderAvatar;
- protected WPTextView mHeaderDisplayName;
- protected WPTextView mHeaderEmailAddress;
- protected View mBottomShadow;
- protected NestedScrollView mScrollView;
- protected boolean mIsAvatarAdded;
- protected boolean mIsEmailSignup;
-
- private boolean mIsUpdatingDisplayName = false;
- private boolean mIsUpdatingPassword = false;
- private boolean mHasUpdatedPassword = false;
- private boolean mHasMadeUpdates = false;
-
- private static final String ARG_DISPLAY_NAME = "ARG_DISPLAY_NAME";
- private static final String ARG_EMAIL_ADDRESS = "ARG_EMAIL_ADDRESS";
- private static final String ARG_IS_EMAIL_SIGNUP = "ARG_IS_EMAIL_SIGNUP";
- private static final String ARG_PHOTO_URL = "ARG_PHOTO_URL";
- private static final String ARG_USERNAME = "ARG_USERNAME";
- private static final String KEY_DISPLAY_NAME = "KEY_DISPLAY_NAME";
- private static final String KEY_EMAIL_ADDRESS = "KEY_EMAIL_ADDRESS";
- private static final String KEY_IS_AVATAR_ADDED = "KEY_IS_AVATAR_ADDED";
- private static final String KEY_PHOTO_URL = "KEY_PHOTO_URL";
- private static final String KEY_USERNAME = "KEY_USERNAME";
- private static final String KEY_IS_UPDATING_DISPLAY_NAME = "KEY_IS_UPDATING_DISPLAY_NAME";
- private static final String KEY_IS_UPDATING_PASSWORD = "KEY_IS_UPDATING_PASSWORD";
- private static final String KEY_HAS_UPDATED_PASSWORD = "KEY_HAS_UPDATED_PASSWORD";
- private static final String KEY_HAS_MADE_UPDATES = "KEY_HAS_MADE_UPDATES";
-
- private static final String SOURCE = "source";
- private static final String SOURCE_SIGNUP_EPILOGUE = "signup_epilogue";
-
- public static final String TAG = "signup_epilogue_fragment_tag";
-
- @Inject protected AccountStore mAccount;
- @Inject protected Dispatcher mDispatcher;
- @Inject protected ImageManager mImageManager;
- @Inject protected AppPrefsWrapper mAppPrefsWrapper;
- @Inject protected UnifiedLoginTracker mUnifiedLoginTracker;
- @Inject protected SignupUtils mSignupUtils;
- @Inject protected MediaPickerLauncher mMediaPickerLauncher;
-
- public static SignupEpilogueFragment newInstance(String displayName, String emailAddress,
- String photoUrl, String username,
- boolean isEmailSignup) {
- SignupEpilogueFragment signupEpilogueFragment = new SignupEpilogueFragment();
- Bundle args = new Bundle();
- args.putString(ARG_DISPLAY_NAME, displayName);
- args.putString(ARG_EMAIL_ADDRESS, emailAddress);
- args.putString(ARG_PHOTO_URL, photoUrl);
- args.putString(ARG_USERNAME, username);
- args.putBoolean(ARG_IS_EMAIL_SIGNUP, isEmailSignup);
- signupEpilogueFragment.setArguments(args);
- return signupEpilogueFragment;
- }
-
- @Override
- protected @LayoutRes int getContentLayout() {
- return 0; // no content layout; entire view is inflated in createMainView
- }
-
- @Override
- protected @LayoutRes int getProgressBarText() {
- return R.string.signup_updating_account;
- }
-
- @Override
- protected void setupLabel(@NonNull TextView label) {
- // no label in this screen
- }
-
- @Override
- protected ViewGroup createMainView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- return (ViewGroup) inflater.inflate(R.layout.signup_epilogue, container, false);
- }
-
- @Override
- protected void setupContent(ViewGroup rootView) {
- final FrameLayout headerAvatarLayout = rootView.findViewById(R.id.login_epilogue_header_avatar_layout);
- headerAvatarLayout.setEnabled(mIsEmailSignup);
- headerAvatarLayout.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- mUnifiedLoginTracker.trackClick(Click.SELECT_AVATAR);
- mMediaPickerLauncher.showGravatarPicker(SignupEpilogueFragment.this);
- }
- });
- headerAvatarLayout.setOnLongClickListener(new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(View view) {
- ToastUtils.showToast(getActivity(), getString(R.string.content_description_add_avatar),
- ToastUtils.Duration.SHORT);
- return true;
- }
- });
- ViewExtensionsKt.redirectContextClickToLongPressListener(headerAvatarLayout);
- mHeaderAvatarAdd = rootView.findViewById(R.id.login_epilogue_header_avatar_add);
- mHeaderAvatarAdd.setVisibility(mIsEmailSignup ? View.VISIBLE : View.GONE);
- mHeaderAvatar = rootView.findViewById(R.id.login_epilogue_header_avatar);
- mHeaderDisplayName = rootView.findViewById(R.id.login_epilogue_header_title);
- mHeaderDisplayName.setText(mDisplayName);
- mHeaderEmailAddress = rootView.findViewById(R.id.login_epilogue_header_subtitle);
- mHeaderEmailAddress.setText(mEmailAddress);
- WPLoginInputRow inputDisplayName = rootView.findViewById(R.id.signup_epilogue_input_display);
- mEditTextDisplayName = inputDisplayName.getEditText();
- mEditTextDisplayName.setText(mDisplayName);
- mEditTextDisplayName.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- }
-
- @Override
- public void afterTextChanged(Editable s) {
- mDisplayName = s.toString();
- mHeaderDisplayName.setText(mDisplayName);
- }
- });
- WPLoginInputRow inputUsername = rootView.findViewById(R.id.signup_epilogue_input_username);
- mEditTextUsername = inputUsername.getEditText();
- mEditTextUsername.setText(mUsername);
- mEditTextUsername.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- mUnifiedLoginTracker.trackClick(Click.EDIT_USERNAME);
- launchDialog();
- }
- });
- mEditTextUsername.setOnFocusChangeListener(new View.OnFocusChangeListener() {
- @Override
- public void onFocusChange(View view, boolean hasFocus) {
- if (hasFocus) {
- launchDialog();
- }
- }
- });
- mEditTextUsername.setOnKeyListener(new View.OnKeyListener() {
- @Override
- public boolean onKey(View view, int keyCode, KeyEvent event) {
- // Consume keyboard events except for Enter (i.e. click/tap) and Tab (i.e. focus/navigation).
- // The onKey method returns true if the listener has consumed the event and false otherwise
- // allowing hardware keyboard users to tap and navigate, but not input text as expected.
- // This allows the username changer to launch using the keyboard.
- return !(keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_TAB);
- }
- });
- mInputPassword = rootView.findViewById(R.id.signup_epilogue_input_password);
- mInputPassword.setVisibility(mIsEmailSignup ? View.VISIBLE : View.GONE);
- final WPTextView passwordDetail = rootView.findViewById(R.id.signup_epilogue_input_password_detail);
- passwordDetail.setVisibility(mIsEmailSignup ? View.VISIBLE : View.GONE);
-
- // Set focus on static text field to avoid showing keyboard on start.
- mHeaderEmailAddress.requestFocus();
-
- mBottomShadow = rootView.findViewById(R.id.bottom_shadow);
- mScrollView = rootView.findViewById(R.id.scroll_view);
- mScrollView.setOnScrollChangeListener(
- (OnScrollChangeListener) (v, scrollX, scrollY, oldScrollX, oldScrollY) -> showBottomShadowIfNeeded());
- // We must use onGlobalLayout here otherwise canScrollVertically will always return false
- mScrollView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
- @Override public void onGlobalLayout() {
- mScrollView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
- showBottomShadowIfNeeded();
- }
- });
- }
-
- private void showBottomShadowIfNeeded() {
- if (mScrollView != null) {
- final boolean canScrollDown = mScrollView.canScrollVertically(1);
- if (mBottomShadow != null) {
- mBottomShadow.setVisibility(canScrollDown ? View.VISIBLE : View.GONE);
- }
- }
- }
-
- @Override
- protected void setupBottomButton(Button button) {
- button.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- mUnifiedLoginTracker.trackClick(Click.CONTINUE);
- updateAccountOrContinue();
- }
- });
- }
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ((WordPress) getActivity().getApplication()).component().inject(this);
- mDispatcher.dispatch(AccountActionBuilder.newFetchAccountAction());
-
- mDisplayName = getArguments().getString(ARG_DISPLAY_NAME);
- mEmailAddress = getArguments().getString(ARG_EMAIL_ADDRESS);
- mPhotoUrl = StringUtils.notNullStr(getArguments().getString(ARG_PHOTO_URL));
- mUsername = getArguments().getString(ARG_USERNAME);
- mIsEmailSignup = getArguments().getBoolean(ARG_IS_EMAIL_SIGNUP);
- }
-
- @Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- if (savedInstanceState == null) {
- // Start loading reader tags so they will be available asap
- ReaderUpdateServiceStarter.startService(WordPress.getContext(),
- EnumSet.of(ReaderUpdateLogic.UpdateTask.TAGS));
-
- mUnifiedLoginTracker.track(Step.SUCCESS);
- if (mIsEmailSignup) {
- AnalyticsTracker.track(AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_VIEWED);
-
- // Start progress and wait for account to be fetched before populating views when
- // email does not exist in account store.
- if (TextUtils.isEmpty(mAccountStore.getAccount().getEmail())) {
- startProgress(false);
- } else {
- // Skip progress and populate views when email does exist in account store.
- populateViews();
- }
- } else {
- AnalyticsTracker.track(AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_VIEWED);
- new DownloadAvatarAndUploadGravatarThread(mPhotoUrl, mEmailAddress, mAccount.getAccessToken()).start();
- mImageManager.loadIntoCircle(mHeaderAvatar, ImageType.AVATAR_WITHOUT_BACKGROUND, mPhotoUrl);
- }
- } else {
- mDialog = (FullScreenDialogFragment) getParentFragmentManager()
- .findFragmentByTag(FullScreenDialogFragment.TAG);
-
- if (mDialog != null) {
- mDialog.setOnConfirmListener(this);
- mDialog.setOnDismissListener(this);
- }
-
- mDisplayName = savedInstanceState.getString(KEY_DISPLAY_NAME);
- mUsername = savedInstanceState.getString(KEY_USERNAME);
- mIsAvatarAdded = savedInstanceState.getBoolean(KEY_IS_AVATAR_ADDED);
-
- if (mIsEmailSignup) {
- mPhotoUrl = StringUtils.notNullStr(savedInstanceState.getString(KEY_PHOTO_URL));
- mEmailAddress = savedInstanceState.getString(KEY_EMAIL_ADDRESS);
- mHeaderEmailAddress.setText(mEmailAddress);
- mHeaderAvatarAdd.setVisibility(mIsAvatarAdded ? View.GONE : View.VISIBLE);
- }
- mImageManager.loadIntoCircle(mHeaderAvatar, ImageType.AVATAR_WITHOUT_BACKGROUND, mPhotoUrl);
-
- mIsUpdatingDisplayName = savedInstanceState.getBoolean(KEY_IS_UPDATING_DISPLAY_NAME);
- mIsUpdatingPassword = savedInstanceState.getBoolean(KEY_IS_UPDATING_PASSWORD);
- mHasUpdatedPassword = savedInstanceState.getBoolean(KEY_HAS_UPDATED_PASSWORD);
- mHasMadeUpdates = savedInstanceState.getBoolean(KEY_HAS_MADE_UPDATES);
- }
- }
-
- @Override
- @SuppressWarnings("deprecation")
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
-
- if (isAdded()) {
- switch (resultCode) {
- case Activity.RESULT_OK:
- switch (requestCode) {
- case RequestCodes.PHOTO_PICKER:
- if (data != null) {
- String[] mediaUriStringsArray =
- data.getStringArrayExtra(MediaPickerConstants.EXTRA_MEDIA_URIS);
-
- if (mediaUriStringsArray != null && mediaUriStringsArray.length > 0) {
- PhotoPickerActivity.PhotoPickerMediaSource source =
- PhotoPickerActivity.PhotoPickerMediaSource.fromString(
- data.getStringExtra(MediaPickerConstants.EXTRA_MEDIA_SOURCE)
- );
- AnalyticsTracker.Stat stat =
- source == PhotoPickerActivity.PhotoPickerMediaSource.ANDROID_CAMERA
- ? SIGNUP_EMAIL_EPILOGUE_GRAVATAR_SHOT_NEW
- : SIGNUP_EMAIL_EPILOGUE_GRAVATAR_GALLERY_PICKED;
- AnalyticsTracker.track(stat);
- Uri imageUri = Uri.parse(mediaUriStringsArray[0]);
-
- if (imageUri != null) {
- boolean wasSuccess = WPMediaUtils.fetchMediaAndDoNext(
- getActivity(), imageUri,
- new WPMediaUtils.MediaFetchDoNext() {
- @Override
- public void doNext(Uri uri) {
- startCropActivity(uri);
- }
- });
-
- if (!wasSuccess) {
- AppLog.e(T.UTILS, "Can't download picked or captured image");
- }
- } else {
- AppLog.e(T.UTILS, "Can't parse media string");
- }
- } else {
- AppLog.e(T.UTILS, "Can't resolve picked or captured image");
- }
- }
-
- break;
- case UCrop.REQUEST_CROP:
- AnalyticsTracker.track(AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_GRAVATAR_CROPPED);
- WPMediaUtils.fetchMediaAndDoNext(getActivity(), UCrop.getOutput(data),
- new WPMediaUtils.MediaFetchDoNext() {
- @Override
- public void doNext(Uri uri) {
- startGravatarUpload(MediaUtils.getRealPathFromURI(
- getActivity(), uri));
- }
- });
-
- break;
- }
-
- break;
- case UCrop.RESULT_ERROR:
- AppLog.e(T.NUX, "Image cropping failed", UCrop.getError(data));
- ToastUtils.showToast(getActivity(), R.string.error_cropping_image, ToastUtils.Duration.SHORT);
- break;
- }
- }
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
-
- if (context instanceof SignupEpilogueListener) {
- mSignupEpilogueListener = (SignupEpilogueListener) context;
- } else {
- throw new RuntimeException(context.toString() + " must implement SignupEpilogueListener");
- }
- }
-
- @Override
- public void onConfirm(@Nullable Bundle result) {
- if (result != null) {
- mUsername = result.getString(UsernameChangerFullScreenDialogFragment.RESULT_USERNAME);
- mEditTextUsername.setText(mUsername);
- }
- }
-
- @Override
- public void onDismiss() {
- Map props = new HashMap<>();
- props.put(SOURCE, SOURCE_SIGNUP_EPILOGUE);
- AnalyticsTracker.track(Stat.CHANGE_USERNAME_DISMISSED, props);
- mDialog = null;
- }
-
- @Override
- public void onShown() {
- Map props = new HashMap<>();
- props.put(SOURCE, SOURCE_SIGNUP_EPILOGUE);
- AnalyticsTracker.track(AnalyticsTracker.Stat.CHANGE_USERNAME_DISPLAYED, props);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putString(KEY_PHOTO_URL, mPhotoUrl);
- outState.putString(KEY_DISPLAY_NAME, mDisplayName);
- outState.putString(KEY_EMAIL_ADDRESS, mEmailAddress);
- outState.putString(KEY_USERNAME, mUsername);
- outState.putBoolean(KEY_IS_AVATAR_ADDED, mIsAvatarAdded);
- outState.putBoolean(KEY_IS_UPDATING_DISPLAY_NAME, mIsUpdatingDisplayName);
- outState.putBoolean(KEY_IS_UPDATING_PASSWORD, mIsUpdatingPassword);
- outState.putBoolean(KEY_HAS_UPDATED_PASSWORD, mHasUpdatedPassword);
- outState.putBoolean(KEY_HAS_MADE_UPDATES, mHasMadeUpdates);
- }
-
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- }
-
- @Override
- public void afterTextChanged(Editable s) {
- }
-
- @Override
- protected void onHelp() {
- }
-
- @Override
- protected void onLoginFinished() {
- endProgress();
- }
-
- @SuppressWarnings("unused")
- @Subscribe(threadMode = ThreadMode.MAIN)
- public void onAccountChanged(OnAccountChanged event) {
- if (event.isError()) {
- if (mIsUpdatingDisplayName) {
- mIsUpdatingDisplayName = false;
- AnalyticsTracker.track(mIsEmailSignup
- ? AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_DISPLAY_NAME_FAILED
- : AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_DISPLAY_NAME_FAILED);
- } else if (mIsUpdatingPassword) {
- mIsUpdatingPassword = false;
- }
-
- AppLog.e(T.API, "SignupEpilogueFragment.onAccountChanged: "
- + event.error.type + " - " + event.error.message);
- endProgress();
-
- if (isPasswordInErrorMessage(event.error.message)) {
- showErrorDialogWithCloseButton(event.error.message);
- } else {
- showErrorDialog(getString(R.string.signup_epilogue_error_generic));
- }
- // Wait to populate epilogue for email interface until account is fetched and email address
- // is available since flow is coming from magic link with no instance argument values.
- } else if (mIsEmailSignup && event.causeOfChange == AccountAction.FETCH_ACCOUNT
- && !TextUtils.isEmpty(mAccountStore.getAccount().getEmail())) {
- endProgress();
- populateViews();
- } else if (event.causeOfChange == AccountAction.PUSH_SETTINGS) {
- mHasMadeUpdates = true;
-
- if (mIsUpdatingDisplayName) {
- mIsUpdatingDisplayName = false;
- AnalyticsTracker.track(mIsEmailSignup
- ? AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_DISPLAY_NAME_SUCCEEDED
- : AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_DISPLAY_NAME_SUCCEEDED);
- } else if (mIsUpdatingPassword) {
- mIsUpdatingPassword = false;
- mHasUpdatedPassword = true;
- }
-
- updateAccountOrContinue();
- }
- }
-
- @SuppressWarnings("unused")
- @Subscribe(threadMode = ThreadMode.MAIN)
- public void onUsernameChanged(OnUsernameChanged event) {
- if (event.isError()) {
- AnalyticsTracker.track(mIsEmailSignup
- ? AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_USERNAME_FAILED
- : AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_USERNAME_FAILED);
- AppLog.e(T.API, "SignupEpilogueFragment.onUsernameChanged: "
- + event.error.type + " - " + event.error.message);
- endProgress();
- showErrorDialog(getString(R.string.signup_epilogue_error_generic));
- } else {
- mHasMadeUpdates = true;
- AnalyticsTracker.track(mIsEmailSignup
- ? AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_USERNAME_SUCCEEDED
- : AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_USERNAME_SUCCEEDED);
- updateAccountOrContinue();
- }
- }
-
- protected boolean changedDisplayName() {
- return !TextUtils.equals(mAccount.getAccount().getDisplayName(), mDisplayName);
- }
-
- protected boolean changedPassword() {
- return !TextUtils.isEmpty(mInputPassword.getEditText().getText().toString());
- }
-
- protected boolean changedUsername() {
- return !TextUtils.equals(mAccount.getAccount().getUserName(), mUsername);
- }
-
- private boolean isPasswordInErrorMessage(String message) {
- String lowercaseMessage = message.toLowerCase(Locale.getDefault());
- String lowercasePassword = getString(R.string.password).toLowerCase(Locale.getDefault());
- return lowercaseMessage.contains(lowercasePassword);
- }
-
- protected void launchDialog() {
- AnalyticsTracker.track(mIsEmailSignup
- ? AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_USERNAME_TAPPED
- : AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_USERNAME_TAPPED);
-
- final Bundle bundle = UsernameChangerFullScreenDialogFragment.newBundle(
- mEditTextDisplayName.getText().toString(), mEditTextUsername.getText().toString());
-
- mDialog = new FullScreenDialogFragment.Builder(getContext())
- .setTitle(R.string.username_changer_title)
- .setAction(R.string.username_changer_action)
- .setToolbarTheme(org.wordpress.android.login.R.style.ThemeOverlay_LoginFlow_Toolbar)
- .setOnConfirmListener(this)
- .setOnDismissListener(this)
- .setOnShownListener(this)
- .setContent(UsernameChangerFullScreenDialogFragment.class, bundle)
- .build();
-
- mDialog.show(requireActivity().getSupportFragmentManager(), FullScreenDialogFragment.TAG);
- }
-
- protected void loadAvatar(final String avatarUrl, String injectFilePath) {
- final boolean newAvatarUploaded = injectFilePath != null && !injectFilePath.isEmpty();
- if (newAvatarUploaded) {
- // Remove specific URL entry from bitmap cache. Update it via injected request cache.
- WordPress.getBitmapCache().removeSimilar(avatarUrl);
- // Changing the signature invalidates Glide's cache
- mAppPrefsWrapper.setAvatarVersion(mAppPrefsWrapper.getAvatarVersion() + 1);
- }
-
- Bitmap bitmap = WordPress.getBitmapCache().get(avatarUrl);
- // Avatar's API doesn't synchronously update the image at avatarUrl. There is a replication lag
- // (cca 5s), before the old avatar is replaced with the new avatar. Therefore we need to use this workaround,
- // which temporary saves the new image into a local bitmap cache.
- if (bitmap != null) {
- mImageManager.load(mHeaderAvatar, bitmap);
- } else {
- mImageManager.loadIntoCircle(mHeaderAvatar, ImageType.AVATAR_WITHOUT_BACKGROUND,
- newAvatarUploaded ? injectFilePath : avatarUrl, new RequestListener() {
- @Override
- public void onLoadFailed(@Nullable Exception e, @Nullable Object model) {
- AppLog.e(T.NUX, "Uploading image to Gravatar succeeded, but setting image view failed");
- showErrorDialogWithCloseButton(getString(R.string.signup_epilogue_error_avatar_view));
- }
-
- @Override
- public void onResourceReady(@NonNull Drawable resource, @Nullable Object model) {
- if (newAvatarUploaded && resource instanceof BitmapDrawable) {
- Bitmap bitmap = ((BitmapDrawable) resource).getBitmap();
- // create a copy since the original bitmap may by automatically recycled
- bitmap = bitmap.copy(bitmap.getConfig(), true);
- WordPress.getBitmapCache().put(avatarUrl, bitmap);
- }
- }
- }, mAppPrefsWrapper.getAvatarVersion());
- }
- }
-
- private void populateViews() {
- mEmailAddress = mAccountStore.getAccount().getEmail();
- mDisplayName = mSignupUtils.createDisplayNameFromEmail(mEmailAddress);
- mUsername = !TextUtils.isEmpty(mAccountStore.getAccount().getUserName())
- ? mAccountStore.getAccount().getUserName() : mSignupUtils.createUsernameFromEmail(mEmailAddress);
- mHeaderDisplayName.setText(mDisplayName);
- mHeaderEmailAddress.setText(mEmailAddress);
- mEditTextDisplayName.setText(mDisplayName);
- mEditTextUsername.setText(mUsername);
- // Set fragment arguments to know if account should be updated when values change.
- Bundle args = new Bundle();
- args.putString(ARG_DISPLAY_NAME, mDisplayName);
- args.putString(ARG_EMAIL_ADDRESS, mEmailAddress);
- args.putString(ARG_PHOTO_URL, mPhotoUrl);
- args.putString(ARG_USERNAME, mUsername);
- args.putBoolean(ARG_IS_EMAIL_SIGNUP, mIsEmailSignup);
- setArguments(args);
- }
-
- protected void showErrorDialog(String message) {
- DialogInterface.OnClickListener dialogListener = new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- switch (which) {
- case DialogInterface.BUTTON_NEGATIVE:
- undoChanges();
- break;
- case DialogInterface.BUTTON_POSITIVE:
- updateAccountOrContinue();
- break;
- // DialogInterface.BUTTON_NEUTRAL is intentionally ignored. Just dismiss dialog.
- }
- }
- };
-
- AlertDialog dialog = new MaterialAlertDialogBuilder(getActivity())
- .setMessage(message)
- .setNeutralButton(R.string.login_error_button, dialogListener)
- .setNegativeButton(R.string.signup_epilogue_error_button_negative, dialogListener)
- .setPositiveButton(R.string.signup_epilogue_error_button_positive, dialogListener)
- .create();
- dialog.show();
- }
-
- protected void showErrorDialogWithCloseButton(String message) {
- AlertDialog dialog = new MaterialAlertDialogBuilder(getActivity())
- .setMessage(message)
- .setPositiveButton(R.string.login_error_button, null)
- .create();
- dialog.show();
- }
-
- protected void startCropActivity(Uri uri) {
- final Context baseContext = getActivity();
-
- if (baseContext != null) {
- final Context context = new ContextThemeWrapper(baseContext, R.style.WordPress_NoActionBar);
-
- UCrop.Options options = new UCrop.Options();
- options.setShowCropGrid(false);
- options.setStatusBarColor(ContextExtensionsKt.getColorFromAttribute(
- context, android.R.attr.statusBarColor
- ));
- options.setToolbarColor(ContextExtensionsKt.getColorFromAttribute(context, R.attr.wpColorAppBar));
- options.setToolbarWidgetColor(ContextExtensionsKt.getColorFromAttribute(
- context, com.google.android.material.R.attr.colorOnSurface
- ));
- options.setAllowedGestures(UCropActivity.SCALE, UCropActivity.NONE, UCropActivity.NONE);
- options.setHideBottomControls(true);
-
- UCrop.of(uri, Uri.fromFile(new File(context.getCacheDir(), "cropped.jpg")))
- .withAspectRatio(1, 1)
- .withOptions(options)
- .start(context, this);
- }
- }
-
- protected void startGravatarUpload(final String filePath) {
- if (!TextUtils.isEmpty(filePath)) {
- final File file = new File(filePath);
-
- if (file.exists()) {
- startProgress(false);
-
- GravatarApi.uploadGravatar(file, mAccountStore.getAccount().getEmail(), mAccountStore.getAccessToken(),
- new GravatarApi.GravatarUploadListener() {
- @Override
- public void onSuccess() {
- endProgress();
- mPhotoUrl = GravatarUtils.fixGravatarUrl(mAccount.getAccount().getAvatarUrl(),
- getResources().getDimensionPixelSize(R.dimen.avatar_sz_large));
- loadAvatar(mPhotoUrl, filePath);
- mHeaderAvatarAdd.setVisibility(View.GONE);
- mIsAvatarAdded = true;
- }
-
- @Override
- public void onError() {
- endProgress();
- showErrorDialogWithCloseButton(getString(R.string.signup_epilogue_error_avatar));
- AppLog.e(T.NUX, "Uploading image to Gravatar failed");
- }
- });
- } else {
- ToastUtils.showToast(getActivity(), R.string.error_locating_image, ToastUtils.Duration.SHORT);
- }
- } else {
- ToastUtils.showToast(getActivity(), R.string.error_locating_image, ToastUtils.Duration.SHORT);
- }
- }
-
- protected void undoChanges() {
- mDisplayName = !TextUtils.isEmpty(mAccountStore.getAccount().getDisplayName())
- ? mAccountStore.getAccount().getDisplayName() : getArguments().getString(ARG_DISPLAY_NAME);
- mEditTextDisplayName.setText(mDisplayName);
- mUsername = !TextUtils.isEmpty(mAccountStore.getAccount().getUserName())
- ? mAccountStore.getAccount().getUserName() : getArguments().getString(ARG_USERNAME);
- mEditTextUsername.setText(mUsername);
- mInputPassword.getEditText().setText("");
- updateAccountOrContinue();
- }
-
- protected void updateAccountOrContinue() {
- if (changedUsername()) {
- startProgressIfNeeded();
- updateUsername();
- } else if (changedDisplayName()) {
- startProgressIfNeeded();
- mIsUpdatingDisplayName = true;
- updateDisplayName();
- } else if (changedPassword() && !mHasUpdatedPassword) {
- startProgressIfNeeded();
- mIsUpdatingPassword = true;
- updatePassword();
- } else if (mSignupEpilogueListener != null) {
- if (!mHasMadeUpdates) {
- AnalyticsTracker.track(mIsEmailSignup
- ? AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_UNCHANGED
- : AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_UNCHANGED);
- }
- endProgressIfNeeded();
- mSignupEpilogueListener.onContinue();
- }
- }
-
- private void updateDisplayName() {
- PushAccountSettingsPayload payload = new PushAccountSettingsPayload();
- payload.params = new HashMap<>();
- payload.params.put("display_name", mDisplayName);
- mDispatcher.dispatch(AccountActionBuilder.newPushSettingsAction(payload));
- }
-
- private void updatePassword() {
- PushAccountSettingsPayload payload = new PushAccountSettingsPayload();
- payload.params = new HashMap<>();
- payload.params.put("password", mInputPassword.getEditText().getText().toString());
- mDispatcher.dispatch(AccountActionBuilder.newPushSettingsAction(payload));
- }
-
- private void updateUsername() {
- PushUsernamePayload payload = new PushUsernamePayload(
- mUsername, AccountUsernameActionType.KEEP_OLD_SITE_AND_ADDRESS);
- mDispatcher.dispatch(AccountActionBuilder.newPushUsernameAction(payload));
- }
-
- private class DownloadAvatarAndUploadGravatarThread extends Thread {
- private String mEmail;
- private String mToken;
- private String mUrl;
-
- DownloadAvatarAndUploadGravatarThread(String url, String email, String token) {
- mUrl = url;
- mEmail = email;
- mToken = token;
- }
-
- @Override
- public void run() {
- try {
- Uri uri = MediaUtils.downloadExternalMedia(getContext(), Uri.parse(mUrl));
- File file = new File(new URI(uri.toString()));
- GravatarApi.uploadGravatar(file, mEmail, mToken,
- new GravatarApi.GravatarUploadListener() {
- @Override
- public void onSuccess() {
- AppLog.i(T.NUX, "Google avatar download and Gravatar upload succeeded.");
- }
-
- @Override
- public void onError() {
- AppLog.i(T.NUX, "Google avatar download and Gravatar upload failed.");
- }
- });
- } catch (NullPointerException | URISyntaxException exception) {
- AppLog.e(T.NUX, "Google avatar download and Gravatar upload failed - "
- + exception.toString() + " - " + exception.getMessage());
- }
- }
- }
-}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SignupEpilogueFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SignupEpilogueFragment.kt
new file mode 100644
index 000000000000..fbe47aef6cc4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SignupEpilogueFragment.kt
@@ -0,0 +1,906 @@
+@file:Suppress("DEPRECATION")
+
+package org.wordpress.android.ui.accounts.signup
+
+import android.app.Activity
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextUtils
+import android.text.TextWatcher
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.OnFocusChangeListener
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.widget.Button
+import android.widget.EditText
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.LayoutRes
+import androidx.appcompat.view.ContextThemeWrapper
+import androidx.core.widget.NestedScrollView
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.gravatar.services.AvatarService
+import com.gravatar.services.Result
+import com.gravatar.types.Email
+import com.yalantis.ucrop.UCrop
+import com.yalantis.ucrop.UCropActivity
+import kotlinx.coroutines.launch
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+import org.wordpress.android.R
+import org.wordpress.android.WordPress
+import org.wordpress.android.WordPress.Companion.getBitmapCache
+import org.wordpress.android.analytics.AnalyticsTracker
+import org.wordpress.android.analytics.AnalyticsTracker.Stat
+import org.wordpress.android.fluxc.Dispatcher
+import org.wordpress.android.fluxc.action.AccountAction
+import org.wordpress.android.fluxc.generated.AccountActionBuilder
+import org.wordpress.android.fluxc.store.AccountStore
+import org.wordpress.android.fluxc.store.AccountStore.AccountUsernameActionType
+import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged
+import org.wordpress.android.fluxc.store.AccountStore.OnUsernameChanged
+import org.wordpress.android.fluxc.store.AccountStore.PushAccountSettingsPayload
+import org.wordpress.android.fluxc.store.AccountStore.PushUsernamePayload
+import org.wordpress.android.login.LoginBaseFormFragment
+import org.wordpress.android.login.widgets.WPLoginInputRow
+import org.wordpress.android.ui.FullScreenDialogFragment
+import org.wordpress.android.ui.FullScreenDialogFragment.OnShownListener
+import org.wordpress.android.ui.RequestCodes
+import org.wordpress.android.ui.accounts.UnifiedLoginTracker
+import org.wordpress.android.ui.photopicker.MediaPickerConstants
+import org.wordpress.android.ui.photopicker.MediaPickerLauncher
+import org.wordpress.android.ui.photopicker.PhotoPickerActivity.PhotoPickerMediaSource
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
+import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask
+import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter
+import org.wordpress.android.util.AppLog
+import org.wordpress.android.util.MediaUtils
+import org.wordpress.android.util.StringUtils
+import org.wordpress.android.util.ToastUtils
+import org.wordpress.android.util.WPAvatarUtils
+import org.wordpress.android.util.WPMediaUtils
+import org.wordpress.android.util.extensions.getColorFromAttribute
+import org.wordpress.android.util.extensions.redirectContextClickToLongPressListener
+import org.wordpress.android.util.image.ImageManager
+import org.wordpress.android.util.image.ImageType
+import org.wordpress.android.widgets.WPTextView
+import java.io.File
+import java.net.URI
+import java.net.URISyntaxException
+import java.util.EnumSet
+import java.util.Locale
+import javax.inject.Inject
+
+@Suppress("LargeClass")
+class SignupEpilogueFragment : LoginBaseFormFragment(),
+ FullScreenDialogFragment.OnConfirmListener, FullScreenDialogFragment.OnDismissListener,
+ OnShownListener {
+ private lateinit var mEditTextDisplayName: EditText
+ private lateinit var mEditTextUsername: EditText
+ private var mDialog: FullScreenDialogFragment? = null
+ private var mSignupEpilogueListener: SignupEpilogueListener? = null
+
+ private lateinit var mHeaderAvatarAdd: ImageView
+ private var mDisplayName: String? = null
+ private lateinit var mEmailAddress: String
+ private lateinit var mPhotoUrl: String
+ private var mUsername: String? = null
+ private lateinit var mInputPassword: WPLoginInputRow
+ private lateinit var mHeaderAvatar: ImageView
+ private lateinit var mHeaderDisplayName: WPTextView
+ private lateinit var mHeaderEmailAddress: WPTextView
+ private var mBottomShadow: View? = null
+ private lateinit var mScrollView: NestedScrollView
+ private var mIsAvatarAdded: Boolean = false
+ private var mIsEmailSignup: Boolean = false
+
+ private var mIsUpdatingDisplayName = false
+ private var mIsUpdatingPassword = false
+ private var mHasUpdatedPassword = false
+ private var mHasMadeUpdates = false
+
+ @Inject
+ lateinit var mAccount: AccountStore
+
+ @Inject
+ lateinit var dispatcher: Dispatcher
+
+ @Inject
+ lateinit var mImageManager: ImageManager
+
+ @Inject
+ lateinit var mAppPrefsWrapper: AppPrefsWrapper
+
+ @Inject
+ lateinit var mUnifiedLoginTracker: UnifiedLoginTracker
+
+ @Inject
+ lateinit var mSignupUtils: SignupUtils
+
+ @Inject
+ lateinit var mMediaPickerLauncher: MediaPickerLauncher
+
+ @Inject
+ lateinit var mAvatarService: AvatarService
+
+ @LayoutRes
+ override fun getContentLayout(): Int {
+ return 0 // no content layout; entire view is inflated in createMainView
+ }
+
+ @LayoutRes
+ override fun getProgressBarText(): Int {
+ return R.string.signup_updating_account
+ }
+
+ override fun setupLabel(label: TextView) {
+ // no label in this screen
+ }
+
+ override fun createMainView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): ViewGroup {
+ return inflater.inflate(R.layout.signup_epilogue, container, false) as ViewGroup
+ }
+
+ @Suppress("LongMethod")
+ override fun setupContent(rootView: ViewGroup) {
+ val headerAvatarLayout =
+ rootView.findViewById(R.id.login_epilogue_header_avatar_layout)
+ headerAvatarLayout.isEnabled = mIsEmailSignup
+ headerAvatarLayout.setOnClickListener {
+ mUnifiedLoginTracker.trackClick(UnifiedLoginTracker.Click.SELECT_AVATAR)
+ mMediaPickerLauncher.showGravatarPicker(this@SignupEpilogueFragment)
+ }
+ headerAvatarLayout.setOnLongClickListener {
+ ToastUtils.showToast(
+ activity, getString(R.string.content_description_add_avatar),
+ ToastUtils.Duration.SHORT
+ )
+ true
+ }
+ headerAvatarLayout.redirectContextClickToLongPressListener()
+ mHeaderAvatarAdd = rootView.findViewById(R.id.login_epilogue_header_avatar_add)
+ mHeaderAvatarAdd.setVisibility(if (mIsEmailSignup) View.VISIBLE else View.GONE)
+ mHeaderAvatar = rootView.findViewById(R.id.login_epilogue_header_avatar)
+ mHeaderDisplayName = rootView.findViewById(R.id.login_epilogue_header_title)
+ mHeaderDisplayName.text = mDisplayName
+ mHeaderEmailAddress = rootView.findViewById(R.id.login_epilogue_header_subtitle)
+ mHeaderEmailAddress.text = mEmailAddress
+ val inputDisplayName =
+ rootView.findViewById(R.id.signup_epilogue_input_display)
+ mEditTextDisplayName = inputDisplayName.editText
+ mEditTextDisplayName.setText(mDisplayName)
+ mEditTextDisplayName.addTextChangedListener(object : TextWatcher {
+ @Suppress("EmptyFunctionBlock")
+ override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+ }
+
+ override fun afterTextChanged(s: Editable) {
+ mDisplayName = s.toString()
+ mHeaderDisplayName.text = mDisplayName
+ }
+ })
+ val inputUsername =
+ rootView.findViewById(R.id.signup_epilogue_input_username)
+ mEditTextUsername = inputUsername.editText
+ mEditTextUsername.setText(mUsername)
+ mEditTextUsername.setOnClickListener {
+ mUnifiedLoginTracker.trackClick(UnifiedLoginTracker.Click.EDIT_USERNAME)
+ launchDialog()
+ }
+ mEditTextUsername.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
+ if (hasFocus) {
+ launchDialog()
+ }
+ }
+ mEditTextUsername.setOnKeyListener { _, keyCode, _ ->
+ // Consume keyboard events except for Enter (i.e. click/tap) and Tab (i.e. focus/navigation).
+ // The onKey method returns true if the listener has consumed the event and false otherwise
+ // allowing hardware keyboard users to tap and navigate, but not input text as expected.
+ // This allows the username changer to launch using the keyboard.
+ !(keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_TAB)
+ }
+ mInputPassword = rootView.findViewById(R.id.signup_epilogue_input_password)
+ mInputPassword.visibility = if (mIsEmailSignup) View.VISIBLE else View.GONE
+ val passwordDetail =
+ rootView.findViewById(R.id.signup_epilogue_input_password_detail)
+ passwordDetail.visibility = if (mIsEmailSignup) View.VISIBLE else View.GONE
+
+ // Set focus on static text field to avoid showing keyboard on start.
+ mHeaderEmailAddress.requestFocus()
+
+ mBottomShadow = rootView.findViewById(R.id.bottom_shadow)
+ mScrollView = rootView.findViewById(R.id.scroll_view)
+ mScrollView.setOnScrollChangeListener { _: NestedScrollView?, _: Int, _: Int, _: Int, _: Int ->
+ showBottomShadowIfNeeded()
+ }
+ // We must use onGlobalLayout here otherwise canScrollVertically will always return false
+ mScrollView.getViewTreeObserver()
+ .addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
+ override fun onGlobalLayout() {
+ mScrollView.getViewTreeObserver().removeOnGlobalLayoutListener(this)
+ showBottomShadowIfNeeded()
+ }
+ })
+ }
+
+ private fun showBottomShadowIfNeeded() {
+ val canScrollDown = mScrollView.canScrollVertically(1)
+ if (mBottomShadow != null) {
+ mBottomShadow!!.visibility =
+ if (canScrollDown) View.VISIBLE else View.GONE
+ }
+ }
+
+ override fun setupBottomButton(button: Button) {
+ button.setOnClickListener {
+ mUnifiedLoginTracker.trackClick(UnifiedLoginTracker.Click.CONTINUE)
+ updateAccountOrContinue()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ (requireActivity().application as WordPress).component().inject(this)
+ dispatcher.dispatch(AccountActionBuilder.newFetchAccountAction())
+
+ mDisplayName = requireArguments().getString(ARG_DISPLAY_NAME)
+ mEmailAddress = StringUtils.notNullStr(requireArguments().getString(ARG_EMAIL_ADDRESS))
+ mPhotoUrl = StringUtils.notNullStr(requireArguments().getString(ARG_PHOTO_URL))
+ mUsername = requireArguments().getString(ARG_USERNAME)
+ mIsEmailSignup = requireArguments().getBoolean(ARG_IS_EMAIL_SIGNUP)
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ if (savedInstanceState == null) {
+ // Start loading reader tags so they will be available asap
+ ReaderUpdateServiceStarter.startService(
+ WordPress.getContext(),
+ EnumSet.of(UpdateTask.TAGS)
+ )
+
+ mUnifiedLoginTracker.track(step = UnifiedLoginTracker.Step.SUCCESS)
+ if (mIsEmailSignup) {
+ AnalyticsTracker.track(Stat.SIGNUP_EMAIL_EPILOGUE_VIEWED)
+
+ // Start progress and wait for account to be fetched before populating views when
+ // email does not exist in account store.
+ if (TextUtils.isEmpty(mAccountStore.account.email)) {
+ startProgress(false)
+ } else {
+ // Skip progress and populate views when email does exist in account store.
+ populateViews()
+ }
+ } else {
+ AnalyticsTracker.track(Stat.SIGNUP_SOCIAL_EPILOGUE_VIEWED)
+ mAccount.accessToken?.let { accessToken ->
+ DownloadAvatarAndUploadGravatarThread(
+ mPhotoUrl,
+ mEmailAddress,
+ accessToken
+ ).start()
+ mImageManager.loadIntoCircle(
+ mHeaderAvatar,
+ ImageType.AVATAR_WITHOUT_BACKGROUND,
+ (mPhotoUrl)
+ )
+ }
+ }
+ } else {
+ mDialog = parentFragmentManager
+ .findFragmentByTag(FullScreenDialogFragment.TAG) as FullScreenDialogFragment?
+
+ if (mDialog != null) {
+ mDialog!!.setOnConfirmListener(this)
+ mDialog!!.setOnDismissListener(this)
+ }
+
+ mDisplayName = savedInstanceState.getString(KEY_DISPLAY_NAME)
+ mUsername = savedInstanceState.getString(KEY_USERNAME)
+ mIsAvatarAdded = savedInstanceState.getBoolean(KEY_IS_AVATAR_ADDED)
+
+ if (mIsEmailSignup) {
+ mPhotoUrl = StringUtils.notNullStr(savedInstanceState.getString(KEY_PHOTO_URL))
+ mEmailAddress = StringUtils.notNullStr(savedInstanceState.getString(KEY_EMAIL_ADDRESS))
+ mHeaderEmailAddress.text = mEmailAddress
+ mHeaderAvatarAdd.visibility = if (mIsAvatarAdded) View.GONE else View.VISIBLE
+ }
+ mImageManager.loadIntoCircle(
+ mHeaderAvatar,
+ ImageType.AVATAR_WITHOUT_BACKGROUND,
+ mPhotoUrl
+ )
+
+ mIsUpdatingDisplayName = savedInstanceState.getBoolean(KEY_IS_UPDATING_DISPLAY_NAME)
+ mIsUpdatingPassword = savedInstanceState.getBoolean(KEY_IS_UPDATING_PASSWORD)
+ mHasUpdatedPassword = savedInstanceState.getBoolean(KEY_HAS_UPDATED_PASSWORD)
+ mHasMadeUpdates = savedInstanceState.getBoolean(KEY_HAS_MADE_UPDATES)
+ }
+ }
+
+ @Suppress("deprecation", "NestedBlockDepth", "LongMethod")
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+
+ if (isAdded) {
+ when (resultCode) {
+ Activity.RESULT_OK -> when (requestCode) {
+ RequestCodes.PHOTO_PICKER -> if (data != null) {
+ val mediaUriStringsArray =
+ data.getStringArrayExtra(MediaPickerConstants.EXTRA_MEDIA_URIS)
+
+ if (mediaUriStringsArray != null && mediaUriStringsArray.size > 0) {
+ val source =
+ PhotoPickerMediaSource.fromString(
+ data.getStringExtra(MediaPickerConstants.EXTRA_MEDIA_SOURCE)
+ )
+ val stat =
+ if (source == PhotoPickerMediaSource.ANDROID_CAMERA) {
+ Stat.SIGNUP_EMAIL_EPILOGUE_GRAVATAR_SHOT_NEW
+ } else {
+ Stat.SIGNUP_EMAIL_EPILOGUE_GRAVATAR_GALLERY_PICKED
+ }
+ AnalyticsTracker.track(stat)
+ val imageUri = Uri.parse(mediaUriStringsArray[0])
+
+ if (imageUri != null) {
+ val wasSuccess = WPMediaUtils.fetchMediaAndDoNext(
+ activity, imageUri
+ ) { uri -> startCropActivity(uri) }
+
+ if (!wasSuccess) {
+ AppLog.e(
+ AppLog.T.UTILS,
+ "Can't download picked or captured image"
+ )
+ }
+ } else {
+ AppLog.e(AppLog.T.UTILS, "Can't parse media string")
+ }
+ } else {
+ AppLog.e(AppLog.T.UTILS, "Can't resolve picked or captured image")
+ }
+ }
+
+ UCrop.REQUEST_CROP -> {
+ AnalyticsTracker.track(Stat.SIGNUP_EMAIL_EPILOGUE_GRAVATAR_CROPPED)
+ WPMediaUtils.fetchMediaAndDoNext(
+ activity, UCrop.getOutput((data)!!)
+ ) { uri ->
+ startGravatarUpload(
+ MediaUtils.getRealPathFromURI(
+ activity, uri
+ )
+ )
+ }
+ }
+ }
+
+ UCrop.RESULT_ERROR -> {
+ AppLog.e(
+ AppLog.T.NUX, "Image cropping failed", UCrop.getError(
+ (data)!!
+ )
+ )
+ ToastUtils.showToast(
+ activity,
+ R.string.error_cropping_image,
+ ToastUtils.Duration.SHORT
+ )
+ }
+ }
+ }
+ }
+
+ @Suppress("TooGenericExceptionThrown")
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+
+ if (context is SignupEpilogueListener) {
+ mSignupEpilogueListener = context
+ } else {
+ throw RuntimeException("$context must implement SignupEpilogueListener")
+ }
+ }
+
+ override fun onConfirm(result: Bundle?) {
+ if (result != null) {
+ mUsername = result.getString(BaseUsernameChangerFullScreenDialogFragment.RESULT_USERNAME)
+ mEditTextUsername.setText(mUsername)
+ }
+ }
+
+ override fun onDismiss() {
+ val props: MutableMap = HashMap()
+ props[SOURCE] = SOURCE_SIGNUP_EPILOGUE
+ AnalyticsTracker.track(Stat.CHANGE_USERNAME_DISMISSED, props)
+ mDialog = null
+ }
+
+ override fun onShown() {
+ val props: MutableMap = HashMap()
+ props[SOURCE] = SOURCE_SIGNUP_EPILOGUE
+ AnalyticsTracker.track(Stat.CHANGE_USERNAME_DISPLAYED, props)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putString(KEY_PHOTO_URL, mPhotoUrl)
+ outState.putString(KEY_DISPLAY_NAME, mDisplayName)
+ outState.putString(KEY_EMAIL_ADDRESS, mEmailAddress)
+ outState.putString(KEY_USERNAME, mUsername)
+ outState.putBoolean(KEY_IS_AVATAR_ADDED, mIsAvatarAdded)
+ outState.putBoolean(KEY_IS_UPDATING_DISPLAY_NAME, mIsUpdatingDisplayName)
+ outState.putBoolean(KEY_IS_UPDATING_PASSWORD, mIsUpdatingPassword)
+ outState.putBoolean(KEY_HAS_UPDATED_PASSWORD, mHasUpdatedPassword)
+ outState.putBoolean(KEY_HAS_MADE_UPDATES, mHasMadeUpdates)
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun afterTextChanged(s: Editable) {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onHelp() {
+ }
+
+ override fun onLoginFinished() {
+ endProgress()
+ }
+
+ @Suppress("unused")
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ override fun onAccountChanged(event: OnAccountChanged) {
+ if (event.isError) {
+ if (mIsUpdatingDisplayName) {
+ mIsUpdatingDisplayName = false
+ AnalyticsTracker.track(
+ if (mIsEmailSignup
+ ) Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_DISPLAY_NAME_FAILED
+ else Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_DISPLAY_NAME_FAILED
+ )
+ } else if (mIsUpdatingPassword) {
+ mIsUpdatingPassword = false
+ }
+
+ AppLog.e(
+ AppLog.T.API, ("SignupEpilogueFragment.onAccountChanged: "
+ + event.error.type + " - " + event.error.message)
+ )
+ endProgress()
+
+ if (isPasswordInErrorMessage(event.error.message)) {
+ showErrorDialogWithCloseButton(event.error.message)
+ } else {
+ showErrorDialog(getString(R.string.signup_epilogue_error_generic))
+ }
+ // Wait to populate epilogue for email interface until account is fetched and email address
+ // is available since flow is coming from magic link with no instance argument values.
+ } else if (mIsEmailSignup && (event.causeOfChange == AccountAction.FETCH_ACCOUNT
+ ) && !TextUtils.isEmpty(mAccountStore.account.email)
+ ) {
+ endProgress()
+ populateViews()
+ } else if (event.causeOfChange == AccountAction.PUSH_SETTINGS) {
+ mHasMadeUpdates = true
+
+ if (mIsUpdatingDisplayName) {
+ mIsUpdatingDisplayName = false
+ AnalyticsTracker.track(
+ if (mIsEmailSignup
+ ) Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_DISPLAY_NAME_SUCCEEDED
+ else Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_DISPLAY_NAME_SUCCEEDED
+ )
+ } else if (mIsUpdatingPassword) {
+ mIsUpdatingPassword = false
+ mHasUpdatedPassword = true
+ }
+
+ updateAccountOrContinue()
+ }
+ }
+
+ @Suppress("unused")
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ fun onUsernameChanged(event: OnUsernameChanged) {
+ if (event.isError) {
+ AnalyticsTracker.track(
+ if (mIsEmailSignup
+ ) Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_USERNAME_FAILED
+ else Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_USERNAME_FAILED
+ )
+ AppLog.e(
+ AppLog.T.API, ("SignupEpilogueFragment.onUsernameChanged: "
+ + event.error.type + " - " + event.error.message)
+ )
+ endProgress()
+ showErrorDialog(getString(R.string.signup_epilogue_error_generic))
+ } else {
+ mHasMadeUpdates = true
+ AnalyticsTracker.track(
+ if (mIsEmailSignup
+ ) Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_USERNAME_SUCCEEDED
+ else Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_USERNAME_SUCCEEDED
+ )
+ updateAccountOrContinue()
+ }
+ }
+
+ private fun changedDisplayName(): Boolean {
+ return !TextUtils.equals(mAccount.account.displayName, mDisplayName)
+ }
+
+ private fun changedPassword(): Boolean {
+ return !TextUtils.isEmpty(mInputPassword.editText.text.toString())
+ }
+
+ private fun changedUsername(): Boolean {
+ return !TextUtils.equals(mAccount.account.userName, mUsername)
+ }
+
+ private fun isPasswordInErrorMessage(message: String): Boolean {
+ val lowercaseMessage = message.lowercase(Locale.getDefault())
+ val lowercasePassword = getString(R.string.password).lowercase(Locale.getDefault())
+ return lowercaseMessage.contains(lowercasePassword)
+ }
+
+ private fun launchDialog() {
+ AnalyticsTracker.track(
+ if (mIsEmailSignup
+ ) Stat.SIGNUP_EMAIL_EPILOGUE_USERNAME_TAPPED
+ else Stat.SIGNUP_SOCIAL_EPILOGUE_USERNAME_TAPPED
+ )
+
+ val bundle: Bundle = BaseUsernameChangerFullScreenDialogFragment.newBundle(
+ mEditTextDisplayName.text.toString(), mEditTextUsername.text.toString()
+ )
+
+ mDialog = FullScreenDialogFragment.Builder(requireContext())
+ .setTitle(R.string.username_changer_title)
+ .setAction(R.string.username_changer_action)
+ .setToolbarTheme(org.wordpress.android.login.R.style.ThemeOverlay_LoginFlow_Toolbar)
+ .setOnConfirmListener(this)
+ .setOnDismissListener(this)
+ .setOnShownListener(this)
+ .setContent(UsernameChangerFullScreenDialogFragment::class.java, bundle)
+ .build()
+
+ mDialog?.show(requireActivity().supportFragmentManager, FullScreenDialogFragment.TAG)
+ }
+
+ private fun loadAvatar(avatarUrl: String, injectFilePath: String) {
+ val newAvatarUploaded = injectFilePath.isNotEmpty()
+ if (newAvatarUploaded) {
+ // Remove specific URL entry from bitmap cache. Update it via injected request cache.
+ getBitmapCache().removeSimilar(avatarUrl)
+ // Changing the signature invalidates Glide's cache
+ mAppPrefsWrapper.avatarVersion += 1
+ }
+
+ val bitmap = getBitmapCache()[avatarUrl]
+ // Avatar's API doesn't synchronously update the image at avatarUrl. There is a replication lag
+ // (cca 5s), before the old avatar is replaced with the new avatar. Therefore we need to use this workaround,
+ // which temporary saves the new image into a local bitmap cache.
+ if (bitmap != null) {
+ mImageManager.load((mHeaderAvatar), bitmap)
+ } else {
+ mImageManager.loadIntoCircle(
+ mHeaderAvatar,
+ ImageType.AVATAR_WITHOUT_BACKGROUND,
+ if (newAvatarUploaded) {
+ injectFilePath
+ } else {
+ avatarUrl
+ },
+ object : ImageManager.RequestListener {
+ override fun onLoadFailed(e: Exception?, model: Any?) {
+ AppLog.e(
+ AppLog.T.NUX,
+ "Uploading image to Gravatar succeeded, but setting image view failed"
+ )
+ showErrorDialogWithCloseButton(getString(R.string.signup_epilogue_error_avatar_view))
+ }
+
+ @Suppress("NAME_SHADOWING")
+ override fun onResourceReady(resource: Drawable, model: Any?) {
+ if (newAvatarUploaded && resource is BitmapDrawable) {
+ var bitmap = resource.bitmap
+ // create a copy since the original bitmap may by automatically recycled
+ bitmap = bitmap.copy(bitmap.config, true)
+ getBitmapCache().put((avatarUrl), bitmap)
+ }
+ }
+ },
+ mAppPrefsWrapper.avatarVersion
+ )
+ }
+ }
+
+ private fun populateViews() {
+ mEmailAddress = mAccountStore.account.email
+ mDisplayName = mSignupUtils.createDisplayNameFromEmail(mEmailAddress)
+ mUsername = if (!TextUtils.isEmpty(mAccountStore.account.userName)
+ ) mAccountStore.account.userName else mSignupUtils.createUsernameFromEmail(mEmailAddress)
+ mHeaderDisplayName.text = mDisplayName
+ mHeaderEmailAddress.text = mEmailAddress
+ mEditTextDisplayName.setText(mDisplayName)
+ mEditTextUsername.setText(mUsername)
+ // Set fragment arguments to know if account should be updated when values change.
+ val args = Bundle()
+ args.putString(ARG_DISPLAY_NAME, mDisplayName)
+ args.putString(ARG_EMAIL_ADDRESS, mEmailAddress)
+ args.putString(ARG_PHOTO_URL, mPhotoUrl)
+ args.putString(ARG_USERNAME, mUsername)
+ args.putBoolean(ARG_IS_EMAIL_SIGNUP, mIsEmailSignup)
+ arguments = args
+ }
+
+ private fun showErrorDialog(message: String?) {
+ val dialogListener: DialogInterface.OnClickListener =
+ DialogInterface.OnClickListener { _, which ->
+ when (which) {
+ DialogInterface.BUTTON_NEGATIVE -> undoChanges()
+ DialogInterface.BUTTON_POSITIVE -> updateAccountOrContinue()
+ }
+ }
+
+ val dialog = MaterialAlertDialogBuilder(requireActivity())
+ .setMessage(message)
+ .setNeutralButton(R.string.login_error_button, dialogListener)
+ .setNegativeButton(R.string.signup_epilogue_error_button_negative, dialogListener)
+ .setPositiveButton(R.string.signup_epilogue_error_button_positive, dialogListener)
+ .create()
+ dialog.show()
+ }
+
+ private fun showErrorDialogWithCloseButton(message: String?) {
+ val dialog = MaterialAlertDialogBuilder(requireActivity())
+ .setMessage(message)
+ .setPositiveButton(R.string.login_error_button, null)
+ .create()
+ dialog.show()
+ }
+
+ private fun startCropActivity(uri: Uri?) {
+ val baseContext: Context? = activity
+
+ if (baseContext != null) {
+ val context: Context = ContextThemeWrapper(baseContext, R.style.WordPress_NoActionBar)
+
+ val options = UCrop.Options()
+ options.setShowCropGrid(false)
+ options.setStatusBarColor(
+ context.getColorFromAttribute(
+ android.R.attr.statusBarColor
+ )
+ )
+ options.setToolbarColor(context.getColorFromAttribute(R.attr.wpColorAppBar))
+ options.setToolbarWidgetColor(
+ context.getColorFromAttribute(
+ com.google.android.material.R.attr.colorOnSurface
+ )
+ )
+ options.setAllowedGestures(UCropActivity.SCALE, UCropActivity.NONE, UCropActivity.NONE)
+ options.setHideBottomControls(true)
+
+ UCrop.of((uri)!!, Uri.fromFile(File(context.cacheDir, "cropped.jpg")))
+ .withAspectRatio(1f, 1f)
+ .withOptions(options)
+ .start(context, this)
+ }
+ }
+
+ private fun startGravatarUpload(filePath: String) {
+ if (!TextUtils.isEmpty(filePath)) {
+ val file = File(filePath)
+ if (file.exists()) {
+ mAccountStore.accessToken?.let { accessToken ->
+ startProgress(false)
+ lifecycleScope.launch {
+ val result = mAvatarService.upload(
+ file, Email(mAccountStore.account.email),
+ accessToken
+ )
+ when (result) {
+ is Result.Success -> {
+ endProgress()
+ AnalyticsTracker.track(Stat.ME_GRAVATAR_UPLOADED)
+ mPhotoUrl = WPAvatarUtils.rewriteAvatarUrl(
+ mAccount.account.avatarUrl,
+ resources.getDimensionPixelSize(R.dimen.avatar_sz_large)
+ )
+ loadAvatar(mPhotoUrl, filePath)
+ mHeaderAvatarAdd.visibility = View.GONE
+ mIsAvatarAdded = true
+ }
+
+ is Result.Failure -> {
+ endProgress()
+ showErrorDialogWithCloseButton(getString(R.string.signup_epilogue_error_avatar))
+ val properties: MutableMap = HashMap()
+ properties["error_type"] = result.error
+ AnalyticsTracker.track(Stat.ME_GRAVATAR_UPLOAD_EXCEPTION, properties)
+ AppLog.e(AppLog.T.NUX, "Uploading image to Gravatar failed")
+ }
+ }
+ }
+ }
+ } else {
+ ToastUtils.showToast(
+ activity,
+ R.string.error_locating_image,
+ ToastUtils.Duration.SHORT
+ )
+ }
+ } else {
+ ToastUtils.showToast(activity, R.string.error_locating_image, ToastUtils.Duration.SHORT)
+ }
+ }
+
+ private fun undoChanges() {
+ mDisplayName = if (!TextUtils.isEmpty(mAccountStore.account.displayName)
+ ) mAccountStore.account.displayName else requireArguments().getString(ARG_DISPLAY_NAME)
+ mEditTextDisplayName.setText(mDisplayName)
+ mUsername = if (!TextUtils.isEmpty(mAccountStore.account.userName)
+ ) mAccountStore.account.userName else requireArguments().getString(ARG_USERNAME)
+ mEditTextUsername.setText(mUsername)
+ mInputPassword.editText.setText("")
+ updateAccountOrContinue()
+ }
+
+ private fun updateAccountOrContinue() {
+ if (changedUsername()) {
+ startProgressIfNeeded()
+ updateUsername()
+ } else if (changedDisplayName()) {
+ startProgressIfNeeded()
+ mIsUpdatingDisplayName = true
+ updateDisplayName()
+ } else if (changedPassword() && !mHasUpdatedPassword) {
+ startProgressIfNeeded()
+ mIsUpdatingPassword = true
+ updatePassword()
+ } else if (mSignupEpilogueListener != null) {
+ if (!mHasMadeUpdates) {
+ AnalyticsTracker.track(
+ if (mIsEmailSignup
+ ) Stat.SIGNUP_EMAIL_EPILOGUE_UNCHANGED
+ else Stat.SIGNUP_SOCIAL_EPILOGUE_UNCHANGED
+ )
+ }
+ endProgressIfNeeded()
+ mSignupEpilogueListener!!.onContinue()
+ }
+ }
+
+ private fun updateDisplayName() {
+ val payload = PushAccountSettingsPayload()
+ payload.params = HashMap()
+ payload.params["display_name"] = mDisplayName
+ dispatcher.dispatch(AccountActionBuilder.newPushSettingsAction(payload))
+ }
+
+ private fun updatePassword() {
+ val payload = PushAccountSettingsPayload()
+ payload.params = HashMap()
+ payload.params["password"] = mInputPassword.editText.text.toString()
+ dispatcher.dispatch(AccountActionBuilder.newPushSettingsAction(payload))
+ }
+
+ private fun updateUsername() {
+ mUsername?.let {
+ val payload = PushUsernamePayload(it, AccountUsernameActionType.KEEP_OLD_SITE_AND_ADDRESS)
+ dispatcher.dispatch(AccountActionBuilder.newPushUsernameAction(payload))
+ }
+ }
+
+ private inner class DownloadAvatarAndUploadGravatarThread(
+ private val mUrl: String,
+ private val mEmail: String,
+ private val mToken: String
+ ) : Thread() {
+ override fun run() {
+ @Suppress("TooGenericExceptionCaught")
+ try {
+ val uri = MediaUtils.downloadExternalMedia(context, Uri.parse(mUrl))
+ val file = File(URI(uri.toString()))
+ lifecycleScope.launch {
+ when (val result = mAvatarService.upload(file, Email(mEmail), mToken)) {
+ is Result.Success -> {
+ AppLog.i(
+ AppLog.T.NUX,
+ "Google avatar download and Gravatar upload succeeded."
+ )
+ AnalyticsTracker.track(Stat.ME_GRAVATAR_UPLOADED)
+ }
+
+ is Result.Failure -> {
+ AppLog.i(
+ AppLog.T.NUX,
+ "Google avatar download and Gravatar upload failed."
+ )
+ val properties: MutableMap = HashMap()
+ properties["error_type"] = result.error
+ AnalyticsTracker.track(Stat.ME_GRAVATAR_UPLOAD_EXCEPTION, properties)
+ }
+ }
+ }
+ } catch (exception: NullPointerException) {
+ AppLog.e(
+ AppLog.T.NUX, ("Google avatar download and Gravatar upload failed - "
+ + exception.toString() + " - " + exception.message)
+ )
+ } catch (exception: URISyntaxException) {
+ AppLog.e(
+ AppLog.T.NUX, ("Google avatar download and Gravatar upload failed - "
+ + exception.toString() + " - " + exception.message)
+ )
+ }
+ }
+ }
+
+ companion object {
+ private const val ARG_DISPLAY_NAME = "ARG_DISPLAY_NAME"
+ private const val ARG_EMAIL_ADDRESS = "ARG_EMAIL_ADDRESS"
+ private const val ARG_IS_EMAIL_SIGNUP = "ARG_IS_EMAIL_SIGNUP"
+ private const val ARG_PHOTO_URL = "ARG_PHOTO_URL"
+ private const val ARG_USERNAME = "ARG_USERNAME"
+ private const val KEY_DISPLAY_NAME = "KEY_DISPLAY_NAME"
+ private const val KEY_EMAIL_ADDRESS = "KEY_EMAIL_ADDRESS"
+ private const val KEY_IS_AVATAR_ADDED = "KEY_IS_AVATAR_ADDED"
+ private const val KEY_PHOTO_URL = "KEY_PHOTO_URL"
+ private const val KEY_USERNAME = "KEY_USERNAME"
+ private const val KEY_IS_UPDATING_DISPLAY_NAME = "KEY_IS_UPDATING_DISPLAY_NAME"
+ private const val KEY_IS_UPDATING_PASSWORD = "KEY_IS_UPDATING_PASSWORD"
+ private const val KEY_HAS_UPDATED_PASSWORD = "KEY_HAS_UPDATED_PASSWORD"
+ private const val KEY_HAS_MADE_UPDATES = "KEY_HAS_MADE_UPDATES"
+
+ private const val SOURCE = "source"
+ private const val SOURCE_SIGNUP_EPILOGUE = "signup_epilogue"
+
+ const val TAG: String = "signup_epilogue_fragment_tag"
+
+ fun newInstance(
+ displayName: String?, emailAddress: String?,
+ photoUrl: String?, username: String?,
+ isEmailSignup: Boolean
+ ): SignupEpilogueFragment {
+ val signupEpilogueFragment = SignupEpilogueFragment()
+ val args = Bundle()
+ args.putString(ARG_DISPLAY_NAME, displayName)
+ args.putString(ARG_EMAIL_ADDRESS, emailAddress)
+ args.putString(ARG_PHOTO_URL, photoUrl)
+ args.putString(ARG_USERNAME, username)
+ args.putBoolean(ARG_IS_EMAIL_SIGNUP, isEmailSignup)
+ signupEpilogueFragment.arguments = args
+ return signupEpilogueFragment
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/avatars/AvatarViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/avatars/AvatarViewHolder.kt
index 495f8b8d9726..a606202ea5cd 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/avatars/AvatarViewHolder.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/avatars/AvatarViewHolder.kt
@@ -3,17 +3,17 @@ package org.wordpress.android.ui.avatars
import android.view.ViewGroup
import org.wordpress.android.databinding.AvatarItemBinding
import org.wordpress.android.ui.avatars.TrainOfAvatarsItem.AvatarItem
-import org.wordpress.android.util.GravatarUtils
import org.wordpress.android.util.extensions.viewBinding
import org.wordpress.android.util.image.ImageManager
import org.wordpress.android.util.image.ImageType
+import org.wordpress.android.util.WPAvatarUtils
class AvatarViewHolder(
parent: ViewGroup,
private val imageManager: ImageManager
) : TrainOfAvatarsViewHolder(parent.viewBinding(AvatarItemBinding::inflate)) {
fun bind(avatarDetails: AvatarItem) = with(binding) {
- val likerAvatarUrl = GravatarUtils.fixGravatarUrl(
+ val likerAvatarUrl = WPAvatarUtils.rewriteAvatarUrl(
avatarDetails.userAvatarUrl,
itemView.context.resources.getDimensionPixelSize(AVATAR_SIZE_DIMEN)
)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanner.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanner.kt
index e5df791ee239..e5e52f1abab1 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanner.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanner.kt
@@ -6,6 +6,9 @@ import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
import androidx.camera.core.ImageProxy
+import androidx.camera.core.resolutionselector.AspectRatioStrategy
+import androidx.camera.core.resolutionselector.ResolutionSelector
+import androidx.camera.core.resolutionselector.ResolutionStrategy
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.Column
@@ -18,21 +21,20 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
import org.wordpress.android.ui.compose.theme.AppTheme
import androidx.camera.core.Preview as CameraPreview
@Composable
fun BarcodeScanner(
codeScanner: CodeScanner,
- onScannedResult: (Flow) -> Unit
+ onScannedResult: CodeScannerCallback
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val cameraProviderFuture = remember {
ProcessCameraProvider.getInstance(context)
}
+
Column(
modifier = Modifier.fillMaxSize()
) {
@@ -42,39 +44,42 @@ fun BarcodeScanner(
val preview = CameraPreview.Builder().build()
preview.setSurfaceProvider(previewView.surfaceProvider)
val selector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
- val imageAnalysis = ImageAnalysis.Builder().setTargetResolution(
- Size(
- previewView.width,
- previewView.height
- )
- )
+ val imageAnalysis = ImageAnalysis.Builder()
+ .setResolutionSelector(ResolutionSelector.Builder()
+ .setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
+ .setResolutionStrategy(
+ ResolutionStrategy(
+ Size(
+ previewView.width,
+ previewView.height
+ ),
+ ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+ )
+ ).build())
.setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(context)) { imageProxy ->
- onScannedResult(codeScanner.startScan(imageProxy))
+ val callback = object : CodeScannerCallback {
+ override fun run(status: CodeScannerStatus?) {
+ status?.let { onScannedResult.run(it) }
+ }
+ }
+ codeScanner.startScan(imageProxy, callback)
}
try {
cameraProviderFuture.get().bindToLifecycle(lifecycleOwner, selector, preview, imageAnalysis)
} catch (e: IllegalStateException) {
- onScannedResult(
- flowOf(
- CodeScannerStatus.Failure(
- e.message
- ?: "Illegal state exception while binding camera provider to lifecycle",
- CodeScanningErrorType.Other(e)
- )
- )
- )
+ onScannedResult.run(CodeScannerStatus.Failure(
+ e.message
+ ?: "Illegal state exception while binding camera provider to lifecycle",
+ CodeScanningErrorType.Other(e)
+ ))
} catch (e: IllegalArgumentException) {
- onScannedResult(
- flowOf(
- CodeScannerStatus.Failure(
- e.message
- ?: "Illegal argument exception while binding camera provider to lifecycle",
- CodeScanningErrorType.Other(e)
- )
- )
- )
+ onScannedResult.run(CodeScannerStatus.Failure(
+ e.message
+ ?: "Illegal argument exception while binding camera provider to lifecycle",
+ CodeScanningErrorType.Other(e)
+ ))
}
previewView
},
@@ -84,8 +89,8 @@ fun BarcodeScanner(
}
class DummyCodeScanner : CodeScanner {
- override fun startScan(imageProxy: ImageProxy): Flow {
- return flowOf(CodeScannerStatus.Success("", GoogleBarcodeFormatMapper.BarcodeFormat.FormatUPCA))
+ override fun startScan(imageProxy: ImageProxy, callback: CodeScannerCallback) {
+ callback.run(CodeScannerStatus.Success("", GoogleBarcodeFormatMapper.BarcodeFormat.FormatUPCA))
}
}
@@ -94,6 +99,10 @@ class DummyCodeScanner : CodeScanner {
@Composable
private fun BarcodeScannerScreenPreview() {
AppTheme {
- BarcodeScanner(codeScanner = DummyCodeScanner(), onScannedResult = {})
+ BarcodeScanner(codeScanner = DummyCodeScanner(), onScannedResult = object : CodeScannerCallback {
+ override fun run(status: CodeScannerStatus?) {
+ // no-ops
+ }
+ })
}
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScannerScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScannerScreen.kt
index 01a270db9ece..9960cd675da0 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScannerScreen.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScannerScreen.kt
@@ -15,7 +15,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.wordpress.android.R
-import kotlinx.coroutines.flow.Flow
import org.wordpress.android.ui.compose.theme.AppTheme
@Composable
@@ -23,7 +22,7 @@ fun BarcodeScannerScreen(
codeScanner: CodeScanner,
permissionState: BarcodeScanningViewModel.PermissionState,
onResult: (Boolean) -> Unit,
- onScannedResult: (Flow) -> Unit,
+ onScannedResult: CodeScannerCallback,
) {
val cameraPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningFragment.kt
index a05fd7c67769..434a5a4d8286 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningFragment.kt
@@ -12,11 +12,7 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.viewModels
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.launch
import org.wordpress.android.ui.compose.theme.AppTheme
import org.wordpress.android.util.WPPermissionUtils
import javax.inject.Inject
@@ -52,12 +48,10 @@ class BarcodeScanningFragment : Fragment() {
shouldShowRequestPermissionRationale(KEY_CAMERA_PERMISSION)
)
},
- onScannedResult = { codeScannerStatus ->
- viewLifecycleOwner.lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
- codeScannerStatus.collect { status ->
- setResultAndPopStack(status)
- }
+ onScannedResult = object : CodeScannerCallback {
+ override fun run(status: CodeScannerStatus?) {
+ if (status != null) {
+ setResultAndPopStack(status)
}
}
},
@@ -90,8 +84,10 @@ class BarcodeScanningFragment : Fragment() {
}
private fun setResultAndPopStack(status: CodeScannerStatus) {
- setFragmentResult(KEY_BARCODE_SCANNING_REQUEST, bundleOf(KEY_BARCODE_SCANNING_SCAN_STATUS to status))
- requireActivity().supportFragmentManager.popBackStackImmediate()
+ if (isAdded) {
+ setFragmentResult(KEY_BARCODE_SCANNING_REQUEST, bundleOf(KEY_BARCODE_SCANNING_SCAN_STATUS to status))
+ requireActivity().supportFragmentManager.popBackStackImmediate()
+ }
}
companion object {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/CodeScanner.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/CodeScanner.kt
index 6dde760e0bf0..bcecd01735e4 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/CodeScanner.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/CodeScanner.kt
@@ -2,11 +2,14 @@ package org.wordpress.android.ui.barcodescanner
import android.os.Parcelable
import androidx.camera.core.ImageProxy
-import kotlinx.coroutines.flow.Flow
import kotlinx.parcelize.Parcelize
interface CodeScanner {
- fun startScan(imageProxy: ImageProxy): Flow
+ fun startScan(imageProxy: ImageProxy, callback: CodeScannerCallback)
+}
+
+interface CodeScannerCallback {
+ fun run(status: CodeScannerStatus?)
}
sealed class CodeScannerStatus : Parcelable {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleMLKitCodeScanner.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleMLKitCodeScanner.kt
index 2e0d339c473e..428bbc5abef0 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleMLKitCodeScanner.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleMLKitCodeScanner.kt
@@ -3,10 +3,6 @@ package org.wordpress.android.ui.barcodescanner
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.common.Barcode
-import kotlinx.coroutines.channels.ProducerScope
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.callbackFlow
import javax.inject.Inject
class GoogleMLKitCodeScanner @Inject constructor(
@@ -17,53 +13,41 @@ class GoogleMLKitCodeScanner @Inject constructor(
) : CodeScanner {
private var barcodeFound = false
@androidx.camera.core.ExperimentalGetImage
- override fun startScan(imageProxy: ImageProxy): Flow {
- return callbackFlow {
- val barcodeTask = barcodeScanner.process(inputImageProvider.provideImage(imageProxy))
- barcodeTask.addOnCompleteListener {
- // We must call image.close() on received images when finished using them.
- // Otherwise, new images may not be received or the camera may stall.
- imageProxy.close()
- }
- barcodeTask.addOnSuccessListener { barcodeList ->
- // The check for barcodeFound is done because the startScan method will be called
- // continuously by the library as long as we are in the scanning screen.
- // There will be a good chance that the same barcode gets identified multiple times and as a result
- // success callback will be called multiple times.
- if (!barcodeList.isNullOrEmpty() && !barcodeFound) {
- barcodeFound = true
- handleScanSuccess(barcodeList.firstOrNull())
- this@callbackFlow.close()
- }
- }
- barcodeTask.addOnFailureListener { exception ->
- this@callbackFlow.trySend(
- CodeScannerStatus.Failure(
- error = exception.message,
- type = errorMapper.mapGoogleMLKitScanningErrors(exception)
- )
- )
- this@callbackFlow.close()
+ override fun startScan(imageProxy: ImageProxy, callback: CodeScannerCallback) {
+ val barcodeTask = barcodeScanner.process(inputImageProvider.provideImage(imageProxy))
+ barcodeTask.addOnCompleteListener {
+ // We must call image.close() on received images when finished using them.
+ // Otherwise, new images may not be received or the camera may stall.
+ imageProxy.close()
+ }
+ barcodeTask.addOnSuccessListener { barcodeList ->
+ // The check for barcodeFound is done because the startScan method will be called
+ // continuously by the library as long as we are in the scanning screen.
+ // There will be a good chance that the same barcode gets identified multiple times and as a result
+ // success callback will be called multiple times.
+ if (!barcodeList.isNullOrEmpty() && !barcodeFound) {
+ barcodeFound = true
+ callback.run(handleScanSuccess(barcodeList.firstOrNull()))
}
-
- awaitClose()
+ }
+ barcodeTask.addOnFailureListener { exception ->
+ callback.run(CodeScannerStatus.Failure(
+ error = exception.message,
+ type = errorMapper.mapGoogleMLKitScanningErrors(exception)
+ ))
}
}
- private fun ProducerScope.handleScanSuccess(code: Barcode?) {
- code?.rawValue?.let {
- trySend(
- CodeScannerStatus.Success(
- it,
- barcodeFormatMapper.mapBarcodeFormat(code.format)
- )
+ private fun handleScanSuccess(code: Barcode?): CodeScannerStatus {
+ return code?.rawValue?.let {
+ CodeScannerStatus.Success(
+ it,
+ barcodeFormatMapper.mapBarcodeFormat(code.format)
)
} ?: run {
- trySend(
- CodeScannerStatus.Failure(
- error = "Failed to find a valid raw value!",
- type = CodeScanningErrorType.Other(Throwable("Empty raw value"))
- )
+ CodeScannerStatus.Failure(
+ error = "Failed to find a valid raw value!",
+ type = CodeScanningErrorType.Other(Throwable("Empty raw value"))
)
}
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/BlazeFeatureUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/BlazeFeatureUtils.kt
index ba72a052d6f5..3cace3456986 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/BlazeFeatureUtils.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/BlazeFeatureUtils.kt
@@ -1,12 +1,12 @@
package org.wordpress.android.ui.blaze
-import org.wordpress.android.WordPress
import org.wordpress.android.analytics.AnalyticsTracker
import org.wordpress.android.fluxc.model.PostModel
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.model.page.PageModel
import org.wordpress.android.fluxc.model.page.PageStatus
import org.wordpress.android.fluxc.model.post.PostStatus
+import org.wordpress.android.fluxc.network.UserAgent
import org.wordpress.android.ui.WPWebViewActivity
import org.wordpress.android.ui.blaze.blazecampaigns.campaigndetail.CampaignDetailPageSource
import org.wordpress.android.ui.blaze.blazecampaigns.campaignlisting.CampaignListingPageSource
@@ -18,6 +18,7 @@ import org.wordpress.android.util.config.BlazeManageCampaignFeatureConfig
import javax.inject.Inject
class BlazeFeatureUtils @Inject constructor(
+ private val userAgent: UserAgent,
private val analyticsTrackerWrapper: AnalyticsTrackerWrapper,
private val appPrefsWrapper: AppPrefsWrapper,
private val blazeFeatureConfig: BlazeFeatureConfig,
@@ -153,7 +154,7 @@ class BlazeFeatureUtils @Inject constructor(
)
}
- fun getUserAgent() = WordPress.getUserAgent()
+ fun getUserAgent() = userAgent.toString()
fun getAuthenticationPostData(authenticationUrl: String,
urlToLoad: String,
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/BlazeCampaignPage.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/BlazeCampaignPage.kt
index 58f41f95e207..bbd081cc570b 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/BlazeCampaignPage.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/BlazeCampaignPage.kt
@@ -10,6 +10,6 @@ import org.wordpress.android.ui.blaze.blazecampaigns.campaignlisting.CampaignLis
@SuppressLint("ParcelCreator")
sealed class BlazeCampaignPage : Parcelable {
data class CampaignListingPage(val source: CampaignListingPageSource) : BlazeCampaignPage()
- data class CampaignDetailsPage(val campaignId: Int, val source: CampaignDetailPageSource) : BlazeCampaignPage()
+ data class CampaignDetailsPage(val campaignId: String, val source: CampaignDetailPageSource) : BlazeCampaignPage()
object Done: BlazeCampaignPage()
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/BlazeCampaignParentActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/BlazeCampaignParentActivity.kt
index ea2d7ff10373..c24cd976f6a4 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/BlazeCampaignParentActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/BlazeCampaignParentActivity.kt
@@ -1,10 +1,10 @@
package org.wordpress.android.ui.blaze.blazecampaigns
-import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import dagger.hilt.android.AndroidEntryPoint
import org.wordpress.android.R
+import org.wordpress.android.ui.LocaleAwareActivity
import org.wordpress.android.ui.blaze.blazecampaigns.campaigndetail.CampaignDetailFragment
import org.wordpress.android.ui.blaze.blazecampaigns.campaignlisting.CampaignListingFragment
import org.wordpress.android.util.extensions.getParcelableExtraCompat
@@ -12,7 +12,7 @@ import org.wordpress.android.util.extensions.getParcelableExtraCompat
const val ARG_EXTRA_BLAZE_CAMPAIGN_PAGE = "blaze_campaign_page"
@AndroidEntryPoint
-class BlazeCampaignParentActivity : AppCompatActivity() {
+class BlazeCampaignParentActivity : LocaleAwareActivity() {
private val viewModel: CampaignViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailFragment.kt
index de7cc0bf6afa..5e5a81df9e06 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailFragment.kt
@@ -54,10 +54,10 @@ private const val CAMPAIGN_DETAIL_CAMPAIGN_ID = "campaign_detail_campaign_id"
@AndroidEntryPoint
class CampaignDetailFragment : Fragment(), CampaignDetailWebViewClient.CampaignDetailWebViewClientListener {
companion object {
- fun newInstance(campaignId: Int, source: CampaignDetailPageSource) = CampaignDetailFragment().apply {
+ fun newInstance(campaignId: String, source: CampaignDetailPageSource) = CampaignDetailFragment().apply {
arguments = Bundle().apply {
putSerializable(CAMPAIGN_DETAIL_PAGE_SOURCE, source)
- putInt(CAMPAIGN_DETAIL_CAMPAIGN_ID, campaignId)
+ putString(CAMPAIGN_DETAIL_CAMPAIGN_ID, campaignId)
}
}
}
@@ -110,7 +110,7 @@ class CampaignDetailFragment : Fragment(), CampaignDetailWebViewClient.CampaignD
?: CampaignDetailPageSource.UNKNOWN
}
- private fun getCampaignId() = requireArguments().getInt(CAMPAIGN_DETAIL_CAMPAIGN_ID)
+ private fun getCampaignId() = requireArguments().getString(CAMPAIGN_DETAIL_CAMPAIGN_ID) ?: ""
override fun onRedirectToExternalBrowser(url: String) = viewModel.onRedirectToExternalBrowser(url)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailViewModel.kt
index a7230da28acc..b50e151d2a81 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailViewModel.kt
@@ -30,7 +30,7 @@ class CampaignDetailViewModel @Inject constructor(
@Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher
) : ScopedViewModel(bgDispatcher) {
private lateinit var pageSource: CampaignDetailPageSource
- private var campaignId: Int = 0
+ private var campaignId: String = ""
private val _actionEvents = Channel(Channel.BUFFERED)
val actionEvents = _actionEvents.receiveAsFlow()
@@ -38,7 +38,7 @@ class CampaignDetailViewModel @Inject constructor(
private val _uiState = MutableStateFlow(CampaignDetailUiState.Preparing)
val uiState = _uiState as StateFlow
- fun start(campaignId: Int, campaignDetailPageSource: CampaignDetailPageSource) {
+ fun start(campaignId: String, campaignDetailPageSource: CampaignDetailPageSource) {
this.campaignId = campaignId
this.pageSource = campaignDetailPageSource
@@ -76,7 +76,7 @@ class CampaignDetailViewModel @Inject constructor(
pathComponents = arrayOf(
ADVERTISING_PATH,
CAMPAIGNS_PATH,
- campaignId.toString(),
+ campaignId,
extractAndSanitizeSiteUrl()
),
source = pageSource.trackingName
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListUseCases.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListUseCases.kt
index 18b43200e49b..bc5502271a59 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListUseCases.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListUseCases.kt
@@ -9,13 +9,26 @@ class FetchCampaignListUseCase @Inject constructor(
private val store: BlazeCampaignsStore,
private val mapper: CampaignListingUIModelMapper
) {
+ companion object {
+ const val PAGE_SIZE = 10
+ }
+
@Suppress("ReturnCount")
- suspend fun execute(site: SiteModel, page: Int): Result> {
- val result = store.fetchBlazeCampaigns(site, page)
- if (result.isError || result.model == null) return Result.Failure(GenericError)
+ suspend fun execute(
+ site: SiteModel,
+ offset: Int,
+ pageSize: Int = PAGE_SIZE
+ ): Result {
+ val result = store.fetchBlazeCampaigns(site = site, offset = offset, perPage = pageSize)
+ if (result.isError || result.model == null) return Result.Failure(GenericResult)
val campaigns = result.model!!.campaigns
if (campaigns.isEmpty()) return Result.Failure(NoCampaigns)
- return Result.Success(mapper.mapToCampaignModels(campaigns))
+ return Result.Success(
+ FetchedCampaignsResult(
+ campaigns = mapper.mapToCampaignModels(campaigns),
+ totalItems = result.model!!.totalItems
+ )
+ )
}
}
@@ -24,14 +37,19 @@ class GetCampaignListFromDbUseCase @Inject constructor(
private val mapper: CampaignListingUIModelMapper
) {
suspend fun execute(site: SiteModel): Result> {
- val result = store.getBlazeCampaigns(site)
- if (result.campaigns.isEmpty()) return Result.Failure(NoCampaigns)
- return Result.Success(mapper.mapToCampaignModels(result.campaigns))
+ val campaigns = store.getBlazeCampaigns(site)
+ if (campaigns.isEmpty()) return Result.Failure(NoCampaigns)
+ return Result.Success(mapper.mapToCampaignModels(campaigns))
}
}
-sealed interface NetworkError
+data class FetchedCampaignsResult(
+ val campaigns: List,
+ val totalItems: Int
+)
+
+sealed interface NetworkResult
-object GenericError : NetworkError
+object GenericResult : NetworkResult
-object NoCampaigns : NetworkError
+object NoCampaigns : NetworkResult
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingUIModelMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingUIModelMapper.kt
index e0543e72162f..920c8f180de7 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingUIModelMapper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingUIModelMapper.kt
@@ -6,8 +6,7 @@ import org.wordpress.android.ui.stats.refresh.utils.ONE_THOUSAND
import org.wordpress.android.ui.stats.refresh.utils.StatsUtils
import org.wordpress.android.ui.utils.UiString
import javax.inject.Inject
-
-const val CENTS_IN_DOLLARS = 100
+import kotlin.math.roundToInt
class CampaignListingUIModelMapper @Inject constructor(
private val statsUtils: StatsUtils
@@ -24,7 +23,7 @@ class CampaignListingUIModelMapper @Inject constructor(
featureImageUrl = campaignModel.imageUrl,
impressions = mapToStatsStringIfNeeded(campaignModel.impressions),
clicks = mapToStatsStringIfNeeded(campaignModel.clicks),
- budget = convertToDollars(campaignModel.budgetCents)
+ budget = UiString.UiStringText("$${campaignModel.totalBudget.roundToInt()}")
)
}
@@ -36,8 +35,4 @@ class CampaignListingUIModelMapper @Inject constructor(
null
}
}
-
- private fun convertToDollars(budgetCents: Long): UiString {
- return UiString.UiStringText("$" + (budgetCents / CENTS_IN_DOLLARS).toString())
- }
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingViewModel.kt
index 182a29884a17..b92dbd672800 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingViewModel.kt
@@ -50,8 +50,7 @@ class CampaignListingViewModel @Inject constructor(
private val _onSelectedSiteMissing = MutableLiveData()
val onSelectedSiteMissing = _onSelectedSiteMissing as LiveData
- private var page = 1
- private var limitPerPage: Int = 10
+ private var offset = 0
private var isLastPage: Boolean = false
fun start(campaignListingPageSource: CampaignListingPageSource) {
@@ -79,11 +78,11 @@ class CampaignListingViewModel @Inject constructor(
if (networkUtilsWrapper.isNetworkAvailable().not()) {
showNoNetworkError()
} else {
- when (val campaignResult = fetchCampaignListUseCase.execute(site, page)) {
- is Result.Success -> showCampaigns(campaignResult.value)
+ when (val campaignResult = fetchCampaignListUseCase.execute(site, offset)) {
+ is Result.Success -> showCampaigns(campaignResult.value.campaigns)
is Result.Failure -> {
when (campaignResult.value) {
- is GenericError -> showGenericError()
+ is GenericResult -> showGenericError()
is NoCampaigns -> showNoCampaigns()
}
}
@@ -119,7 +118,6 @@ class CampaignListingViewModel @Inject constructor(
(_uiState.value as CampaignListingUiState.Success).pagingDetails.loadingNext.not() &&
isLastPage.not()
) {
- page++
showLoadingMore()
fetchMoreCampaigns()
}
@@ -131,16 +129,18 @@ class CampaignListingViewModel @Inject constructor(
disableLoadingMore()
showSnackBar(R.string.campaign_listing_page_error_refresh_no_network_available)
} else {
- when (val campaignResult = fetchCampaignListUseCase.execute(site, page)) {
+ when (val campaignResult = fetchCampaignListUseCase.execute(site, offset)) {
is Result.Success -> {
val currentUiState = _uiState.value as CampaignListingUiState.Success
- isLastPage = campaignResult.value.isEmpty() || campaignResult.value.size < limitPerPage
- showCampaigns(currentUiState.campaigns + campaignResult.value)
+ val allCampaigns = currentUiState.campaigns + campaignResult.value.campaigns
+ isLastPage = allCampaigns.size >= campaignResult.value.totalItems
+ offset += allCampaigns.size
+ showCampaigns(allCampaigns)
}
is Result.Failure -> {
when (campaignResult.value) {
- is GenericError -> {
+ is GenericResult -> {
disableLoadingMore()
showSnackBar(R.string.campaign_listing_page_error_refresh_could_not_fetch_campaigns)
}
@@ -168,7 +168,7 @@ class CampaignListingViewModel @Inject constructor(
}
private fun onCampaignClicked(campaignModel: CampaignModel) {
- _navigation.postValue(Event(CampaignListingNavigation.CampaignDetailPage(campaignModel.id.toInt())))
+ _navigation.postValue(Event(CampaignListingNavigation.CampaignDetailPage(campaignModel.id)))
}
private fun showNoCampaigns() {
@@ -180,23 +180,23 @@ class CampaignListingViewModel @Inject constructor(
}
fun refreshCampaigns() {
- page = 1
launch {
_refresh.postValue(true)
if (!networkUtilsWrapper.isNetworkAvailable()) {
_refresh.postValue(false)
showSnackBar(R.string.campaign_listing_page_error_refresh_no_network_available)
} else {
- when (val campaignResult = fetchCampaignListUseCase.execute(site, page)) {
+ offset = 0
+ when (val campaignResult = fetchCampaignListUseCase.execute(site, offset)) {
is Result.Success -> {
_refresh.postValue(false)
isLastPage = false
- showCampaigns(campaignResult.value)
+ showCampaigns(campaignResult.value.campaigns)
}
is Result.Failure -> {
when (campaignResult.value) {
- is GenericError -> {
+ is GenericResult -> {
_refresh.postValue(false)
showSnackBar(R.string.campaign_listing_page_error_refresh_could_not_fetch_campaigns)
}
@@ -223,7 +223,7 @@ enum class CampaignListingPageSource(val trackingName: String) {
sealed class CampaignListingNavigation {
data class CampaignDetailPage(
- val campaignId: Int,
+ val campaignId: String,
val campaignDetailPageSource: CampaignDetailPageSource = CampaignDetailPageSource.CAMPAIGN_LISTING_PAGE
) : CampaignListingNavigation()
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazeoverlay/BlazeViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazeoverlay/BlazeViewModel.kt
index 826cd921feae..6189bf25283d 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazeoverlay/BlazeViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazeoverlay/BlazeViewModel.kt
@@ -22,7 +22,7 @@ class BlazeViewModel @Inject constructor(
private val blazeFeatureUtils: BlazeFeatureUtils,
dispatcher: Dispatcher,
mediaStore: MediaStore,
- private val siteSelectedSiteRepository: SelectedSiteRepository
+ private val selectedSiteRepository: SelectedSiteRepository
) : ViewModel() {
private lateinit var blazeFlowSource: BlazeFlowSource
@@ -32,6 +32,8 @@ class BlazeViewModel @Inject constructor(
private val _promoteUiState = MutableLiveData()
val promoteUiState: LiveData = _promoteUiState
+ private val _onSelectedSiteMissing = MutableLiveData()
+ val onSelectedSiteMissing = _onSelectedSiteMissing as LiveData
private val featuredImageTracker =
PostListFeaturedImageTracker(dispatcher = dispatcher, mediaStore = mediaStore)
@@ -40,6 +42,11 @@ class BlazeViewModel @Inject constructor(
blazeUIModel?.let { initializePromoteContentUIState(it) } ?: run {
initializePromoteSiteUIState(shouldShowOverlay)
}
+ val site = selectedSiteRepository.getSelectedSite()
+ if (site == null) {
+ _onSelectedSiteMissing.value = Unit
+ return
+ }
}
private fun initializePromoteContentUIState(blazeUIModel: BlazeUIModel) {
@@ -52,7 +59,7 @@ class BlazeViewModel @Inject constructor(
private fun initializePromotePostUIState(postModel: PostUIModel) {
val updatedPostModel = postModel.copy(
url = UrlUtils.removeScheme(postModel.url),
- featuredImageUrl = siteSelectedSiteRepository.getSelectedSite()?.let {
+ featuredImageUrl = selectedSiteRepository.getSelectedSite()?.let {
featuredImageTracker.getFeaturedImageUrl(
it,
postModel.featuredImageId
@@ -71,10 +78,12 @@ class BlazeViewModel @Inject constructor(
private fun initializePromotePageUIState(pageModel: PageUIModel) {
val updatedPageModel = pageModel.copy(
url = UrlUtils.removeScheme(pageModel.url),
- featuredImageUrl = featuredImageTracker.getFeaturedImageUrl(
- siteSelectedSiteRepository.getSelectedSite()!!,
- pageModel.featuredImageId
- )
+ featuredImageUrl = selectedSiteRepository.getSelectedSite()?.let {
+ featuredImageTracker.getFeaturedImageUrl(
+ it,
+ pageModel.featuredImageId
+ )
+ }
)
_promoteUiState.value = BlazeUiState.PromoteScreen.PromotePage(updatedPageModel)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazepromote/BlazePromoteParentActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazepromote/BlazePromoteParentActivity.kt
index 1a3f3279a7ec..1e84224789cd 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazepromote/BlazePromoteParentActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazepromote/BlazePromoteParentActivity.kt
@@ -2,9 +2,9 @@ package org.wordpress.android.ui.blaze.blazepromote
import android.os.Bundle
import androidx.activity.viewModels
-import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import org.wordpress.android.R
+import org.wordpress.android.ui.LocaleAwareActivity
import org.wordpress.android.ui.blaze.BlazeFlowSource
import org.wordpress.android.ui.blaze.BlazeUIModel
import org.wordpress.android.ui.blaze.BlazeUiState
@@ -18,7 +18,7 @@ const val ARG_BLAZE_FLOW_SOURCE = "blaze_flow_source"
const val ARG_BLAZE_SHOULD_SHOW_OVERLAY = "blaze_flow_should_show_overlay"
@AndroidEntryPoint
-class BlazePromoteParentActivity : AppCompatActivity() {
+class BlazePromoteParentActivity : LocaleAwareActivity() {
private val viewModel: BlazeViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
@@ -47,6 +47,10 @@ class BlazePromoteParentActivity : AppCompatActivity() {
else -> {}
}
}
+
+ viewModel.onSelectedSiteMissing.observe(this) {
+ finish()
+ }
}
private fun getSource(): BlazeFlowSource {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazepromote/BlazePromoteWebViewViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazepromote/BlazePromoteWebViewViewModel.kt
index 80a90d5749d6..1f07b9ead1aa 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazepromote/BlazePromoteWebViewViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazepromote/BlazePromoteWebViewViewModel.kt
@@ -129,6 +129,9 @@ class BlazePromoteWebViewViewModel @Inject constructor(
}
private fun onHeaderCancelActionClick() {
+ if (::blazeFlowStep.isInitialized.not()) {
+ blazeFlowStep = BlazeFlowStep.UNSPECIFIED
+ }
blazeFeatureUtils.trackFlowCanceled(blazeFlowSource, blazeFlowStep)
postActionEvent(BlazeActionEvent.FinishActivity)
}
@@ -259,6 +262,10 @@ class BlazePromoteWebViewViewModel @Inject constructor(
val nonDismissibleStep = nonDismissableHashConfig.getValue()
val completedStep = completedStepHashConfig.getValue()
+ if (::blazeFlowStep.isInitialized.not()) {
+ blazeFlowStep = BlazeFlowStep.UNSPECIFIED
+ }
+
if (blazeFlowStep.label == nonDismissibleStep) return
if (blazeFlowStep.label == completedStep || blazeFlowStep == BlazeFlowStep.CAMPAIGNS_LIST) {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt
index 9bac2972def3..2fcff83c7a75 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt
@@ -2,7 +2,7 @@ package org.wordpress.android.ui.bloggingprompts
import org.wordpress.android.models.ReaderTag
import org.wordpress.android.models.ReaderTagType
-import org.wordpress.android.ui.reader.services.post.ReaderPostLogic
+import org.wordpress.android.ui.reader.repository.ReaderPostRepository
import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper
import javax.inject.Inject
@@ -23,7 +23,7 @@ class BloggingPromptsPostTagProvider @Inject constructor(
promptIdTag,
promptIdTag,
promptIdTag,
- ReaderPostLogic.formatFullEndpointForTag(promptIdTag),
+ ReaderPostRepository.formatFullEndpointForTag(promptIdTag),
ReaderTagType.FOLLOWED,
)
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingDialogFragment.kt
index 4c2bdd1450cc..7191a73dfb2c 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingDialogFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingDialogFragment.kt
@@ -25,8 +25,8 @@ import org.wordpress.android.ui.bloggingprompts.onboarding.BloggingPromptsOnboar
import org.wordpress.android.ui.bloggingprompts.onboarding.BloggingPromptsOnboardingDialogFragment.DialogType.ONBOARDING
import org.wordpress.android.ui.bloggingprompts.onboarding.BloggingPromptsOnboardingUiState.Ready
import org.wordpress.android.ui.featureintroduction.FeatureIntroductionDialogFragment
-import org.wordpress.android.ui.main.SitePickerActivity
-import org.wordpress.android.ui.main.SitePickerAdapter.SitePickerMode.BLOGGING_PROMPTS_MODE
+import org.wordpress.android.ui.main.ChooseSiteActivity
+import org.wordpress.android.ui.main.SitePickerMode
import org.wordpress.android.ui.main.UpdateSelectedSiteListener
import org.wordpress.android.ui.mysite.SelectedSiteRepository
import org.wordpress.android.ui.posts.PostUtils.EntryPoint.BLOGGING_PROMPTS_INTRODUCTION
@@ -52,7 +52,7 @@ class BloggingPromptsOnboardingDialogFragment : FeatureIntroductionDialogFragmen
private val sitePickerLauncher = registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val selectedSiteLocalId = result.data?.getIntExtra(
- SitePickerActivity.KEY_SITE_LOCAL_ID,
+ ChooseSiteActivity.KEY_SITE_LOCAL_ID,
SelectedSiteRepository.UNAVAILABLE
) ?: SelectedSiteRepository.UNAVAILABLE
viewModel.onSiteSelected(selectedSiteLocalId)
@@ -180,9 +180,9 @@ class BloggingPromptsOnboardingDialogFragment : FeatureIntroductionDialogFragmen
}
is OpenSitePicker -> {
- val intent = Intent(context, SitePickerActivity::class.java).apply {
- putExtra(SitePickerActivity.KEY_SITE_LOCAL_ID, action.selectedSite)
- putExtra(SitePickerActivity.KEY_SITE_PICKER_MODE, BLOGGING_PROMPTS_MODE)
+ val intent = Intent(context, ChooseSiteActivity::class.java).apply {
+ action.selectedSite?.id?.let { putExtra(ChooseSiteActivity.KEY_SITE_LOCAL_ID, it) }
+ putExtra(ChooseSiteActivity.KEY_SITE_PICKER_MODE, SitePickerMode.SIMPLE.name)
}
sitePickerLauncher.launch(intent)
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/promptslist/compose/BloggingPromptsListScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/promptslist/compose/BloggingPromptsListScreen.kt
index e499fad2f4b2..a809a3bdc48c 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/promptslist/compose/BloggingPromptsListScreen.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/promptslist/compose/BloggingPromptsListScreen.kt
@@ -112,8 +112,8 @@ private fun FetchErrorContent() {
@Composable
private fun NetworkErrorContent() {
EmptyContent(
- title = stringResource(R.string.blogging_prompts_list_error_network_title),
- subtitle = stringResource(R.string.blogging_prompts_list_error_network_subtitle),
+ title = stringResource(R.string.no_connection_error_title),
+ subtitle = stringResource(R.string.no_connection_error_description),
image = R.drawable.img_illustration_cloud_off_152dp,
modifier = Modifier.fillMaxSize(),
)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java
index 03f93d8622ac..3a89b0e25e65 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java
@@ -31,6 +31,9 @@
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.elevation.ElevationOverlayProvider;
import com.google.android.material.snackbar.Snackbar;
+import com.gravatar.AvatarQueryOptions;
+import com.gravatar.AvatarUrl;
+import com.gravatar.types.Email;
import org.apache.commons.text.StringEscapeUtils;
import org.greenrobot.eventbus.EventBus;
@@ -96,11 +99,11 @@
import org.wordpress.android.util.ColorUtils;
import org.wordpress.android.util.DateTimeUtils;
import org.wordpress.android.util.EditTextUtils;
-import org.wordpress.android.util.GravatarUtils;
import org.wordpress.android.util.HtmlUtils;
import org.wordpress.android.util.NetworkUtils;
import org.wordpress.android.util.SiteUtils;
import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.WPAvatarUtils;
import org.wordpress.android.util.WPLinkMovementMethod;
import org.wordpress.android.util.analytics.AnalyticsUtils;
import org.wordpress.android.util.extensions.ContextExtensionsKt;
@@ -814,9 +817,10 @@ private void showCommentWhenNonNull(
int avatarSz = getResources().getDimensionPixelSize(R.dimen.avatar_sz_large);
String avatarUrl = "";
if (comment.getAuthorProfileImageUrl() != null) {
- avatarUrl = GravatarUtils.fixGravatarUrl(comment.getAuthorProfileImageUrl(), avatarSz);
+ avatarUrl = WPAvatarUtils.rewriteAvatarUrl(comment.getAuthorProfileImageUrl(), avatarSz);
} else if (comment.getAuthorEmail() != null) {
- avatarUrl = GravatarUtils.gravatarFromEmail(comment.getAuthorEmail(), avatarSz);
+ avatarUrl = new AvatarUrl(new Email(comment.getAuthorEmail()),
+ new AvatarQueryOptions(avatarSz, null, null, null)).url().toString();
}
mImageManager.loadIntoCircle(binding.imageAvatar, ImageType.AVATAR_WITH_BACKGROUND, avatarUrl);
@@ -1354,7 +1358,7 @@ private boolean canEdit(@NonNull SiteModel site) {
}
private boolean canLike(@NonNull SiteModel site) {
- return mEnabledActions.contains(EnabledActions.ACTION_LIKE)
+ return mEnabledActions.contains(EnabledActions.ACTION_LIKE_COMMENT)
&& SiteUtils.isAccessedViaWPComRest(site);
}
@@ -1387,7 +1391,7 @@ private void showCommentAsNotification(
* this user made on someone else's blog
*/
if (note != null) {
- mEnabledActions = note.getEnabledActions();
+ mEnabledActions = note.getEnabledCommentActions();
}
// Set 'Reply to (Name)' in comment reply EditText if it's a reasonable size
@@ -1471,6 +1475,11 @@ private void likeComment(
mCommentsStoreAdapter.dispatch(CommentActionBuilder.newLikeCommentAction(
new RemoteLikeCommentPayload(site, comment, actionBinding.btnLike.isActivated()))
);
+ if (mNote != null) {
+ EventBus.getDefault().postSticky(new NotificationEvents
+ .OnNoteCommentLikeChanged(mNote, actionBinding.btnLike.isActivated()));
+ }
+
actionBinding.btnLike.announceForAccessibility(
getText(actionBinding.btnLike.isActivated() ? R.string.comment_liked_talkback
: R.string.comment_unliked_talkback)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentListAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentListAdapter.kt
index e1f1997ed617..c84c375e05c2 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentListAdapter.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentListAdapter.kt
@@ -13,7 +13,7 @@ import org.wordpress.android.ui.comments.unified.UnifiedCommentListItem.NextPage
import org.wordpress.android.ui.comments.unified.UnifiedCommentListItem.SubHeader
import org.wordpress.android.ui.utils.AnimationUtilsWrapper
import org.wordpress.android.ui.utils.UiHelpers
-import org.wordpress.android.util.GravatarUtilsWrapper
+import org.wordpress.android.util.WPAvatarUtilsWrapper
import org.wordpress.android.util.image.ImageManager
import org.wordpress.android.viewmodel.ResourceProvider
import javax.inject.Inject
@@ -35,7 +35,7 @@ class UnifiedCommentListAdapter(context: Context) : ListAdapter LoadStateViewHolder(parent)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentViewHolder.kt
index b0b79887eca7..1caae111024a 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentViewHolder.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentViewHolder.kt
@@ -2,13 +2,15 @@ package org.wordpress.android.ui.comments.unified
import android.text.TextUtils
import android.view.ViewGroup
+import com.gravatar.AvatarQueryOptions
+import com.gravatar.AvatarUrl
+import com.gravatar.types.Email
import org.wordpress.android.R
import org.wordpress.android.databinding.CommentListItemBinding
import org.wordpress.android.ui.comments.unified.UnifiedCommentListItem.Comment
import org.wordpress.android.ui.utils.AnimationUtilsWrapper
import org.wordpress.android.ui.utils.UiHelpers
-import org.wordpress.android.util.GravatarUtils
-import org.wordpress.android.util.GravatarUtilsWrapper
+import org.wordpress.android.util.WPAvatarUtilsWrapper
import org.wordpress.android.util.extensions.viewBinding
import org.wordpress.android.util.image.ImageManager
import org.wordpress.android.util.image.ImageType.AVATAR_WITH_BACKGROUND
@@ -21,7 +23,7 @@ class UnifiedCommentViewHolder(
private val uiHelpers: UiHelpers,
private val commentListUiUtils: CommentListUiUtils,
private val resourceProvider: ResourceProvider,
- private val gravatarUtilsWrapper: GravatarUtilsWrapper,
+ private val avatarUtilsWrapper: WPAvatarUtilsWrapper,
private val animationUtilsWrapper: AnimationUtilsWrapper
) : UnifiedCommentListViewHolder(parent.viewBinding(CommentListItemBinding::inflate)) {
fun bind(item: Comment) = with(binding) {
@@ -76,15 +78,15 @@ class UnifiedCommentViewHolder(
private fun getGravatarUrl(comment: Comment): String {
return if (!TextUtils.isEmpty(comment.authorAvatarUrl)) {
- gravatarUtilsWrapper.fixGravatarUrl(
+ avatarUtilsWrapper.rewriteAvatarUrl(
comment.authorAvatarUrl,
resourceProvider.getDimensionPixelSize(R.dimen.avatar_sz_medium)
)
} else if (!TextUtils.isEmpty(comment.authorEmail)) {
- GravatarUtils.gravatarFromEmail(
- comment.authorEmail,
- resourceProvider.getDimensionPixelSize(R.dimen.avatar_sz_medium)
- )
+ AvatarUrl(
+ Email(comment.authorEmail),
+ AvatarQueryOptions(preferredSize = resourceProvider.getDimensionPixelSize(R.dimen.avatar_sz_medium))
+ ).url().toString()
} else {
""
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnrepliedCommentsUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnrepliedCommentsUtils.kt
index 6bf811ae637f..4c8cdfb89b94 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnrepliedCommentsUtils.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnrepliedCommentsUtils.kt
@@ -40,17 +40,17 @@ class UnrepliedCommentsUtils @Inject constructor(
return topLevelComments
}
- private fun isMyComment(comment: CommentEntity): Boolean {
- val myEmail: String
+ fun isMyComment(comment: CommentEntity): Boolean {
+ val myEmail: String?
val selectedSite = selectedSiteRepository.getSelectedSite() ?: return false
// if site is self hosted, we want to use email associate with it, even if we are logged into wpcom
myEmail = if (!selectedSite.isUsingWpComRestApi) {
selectedSite.email
} else {
- val account: AccountModel = accountStore.account
- account.email
+ val account: AccountModel? = accountStore.account
+ account?.email
}
- return comment.authorEmail == myEmail
+ return myEmail != null && comment.authorEmail == myEmail
}
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/ImageButton.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/ImageButton.kt
index b48e648319ac..8ec3f0f00af8 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/ImageButton.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/ImageButton.kt
@@ -39,10 +39,10 @@ fun PreviewDrawButton() {
Color.Gray,
shape = RoundedCornerShape(6.dp)
),
- drawableLeft = Drawable(R.drawable.ic_story_icon_24dp),
- drawableRight = Drawable(R.drawable.ic_story_icon_24dp),
- drawableTop = Drawable(R.drawable.ic_story_icon_24dp),
- drawableBottom = Drawable(R.drawable.ic_story_icon_24dp),
+ drawableLeft = Drawable(R.drawable.ic_pages_white_24dp),
+ drawableRight = Drawable(R.drawable.ic_pages_white_24dp),
+ drawableTop = Drawable(R.drawable.ic_pages_white_24dp),
+ drawableBottom = Drawable(R.drawable.ic_pages_white_24dp),
button = Button(text = UiString.UiStringText("Button Text")),
onClick = {}
)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/menu/dropdown/JetpackDropdownMenu.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/menu/dropdown/JetpackDropdownMenu.kt
index ab48ff30a30b..6056300e5b3c 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/menu/dropdown/JetpackDropdownMenu.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/menu/dropdown/JetpackDropdownMenu.kt
@@ -32,6 +32,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@@ -180,7 +181,7 @@ private fun CascadeColumnScope.SubMenuHeader(
}
Image(
painter = painterResource(backIconResource),
- contentDescription = null,
+ contentDescription = stringResource(R.string.reader_label_toolbar_back),
colorFilter = ColorFilter.tint(MenuColors.itemContentColor()),
)
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/views/TrainOfAvatarsView.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/views/TrainOfAvatarsView.kt
index c887301a7ad5..ad76a38cc315 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/compose/views/TrainOfAvatarsView.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/views/TrainOfAvatarsView.kt
@@ -22,7 +22,7 @@ import org.wordpress.android.ui.compose.components.TrainOfIcons
import org.wordpress.android.ui.compose.components.TrainOfIconsModel
import org.wordpress.android.ui.compose.theme.AppTheme
import org.wordpress.android.util.DisplayUtils
-import org.wordpress.android.util.GravatarUtils
+import org.wordpress.android.util.WPAvatarUtils
@Suppress("MemberVisibilityCanBePrivate", "unused")
class TrainOfAvatarsView @JvmOverloads constructor(
@@ -113,7 +113,7 @@ class TrainOfAvatarsView @JvmOverloads constructor(
// returning null for a model will cause the Composable to render a placeholder
private fun avatarModels(): List = avatarsState.value
- .map { GravatarUtils.fixGravatarUrl(it.userAvatarUrl, iconSize) }
+ .map { WPAvatarUtils.rewriteAvatarUrl(it.userAvatarUrl, iconSize) }
.map {
TrainOfIconsModel(
it.takeIf { gravatarUrl -> gravatarUrl.isNotEmpty() }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/debug/preferences/DebugPrefs.kt b/WordPress/src/main/java/org/wordpress/android/ui/debug/preferences/DebugPrefs.kt
new file mode 100644
index 000000000000..7dda8cc7bf70
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/debug/preferences/DebugPrefs.kt
@@ -0,0 +1,10 @@
+package org.wordpress.android.ui.debug.preferences
+
+import kotlin.reflect.KClass
+
+/**
+ * Class used to track debuggable shared preferences and will show up in [DebugSharedPreferenceFlagsActivity].
+ */
+enum class DebugPrefs(val key: String, val type: KClass<*>) {
+ ALWAYS_SHOW_ANNOUNCEMENT("prefs_always_show_announcement", Boolean::class)
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/debug/preferences/DebugSharedPreferenceFlagsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/debug/preferences/DebugSharedPreferenceFlagsViewModel.kt
index b7c99e4487e6..a88c1cb1ed4f 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/debug/preferences/DebugSharedPreferenceFlagsViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/debug/preferences/DebugSharedPreferenceFlagsViewModel.kt
@@ -18,7 +18,13 @@ class DebugSharedPreferenceFlagsViewModel @Inject constructor(
val flags = prefsWrapper.getAllPrefs().mapNotNull { (key, value) ->
if (value is Boolean) key to value else null
}.toMap()
- _uiStateFlow.value = flags
+
+ val explicitFlags = DebugPrefs.entries.mapNotNull {
+ // Only supporting boolean for now.
+ if (it.type == Boolean::class) it else null
+ }.associate { it.key to prefsWrapper.getDebugBooleanPref(it.key, false) }
+
+ _uiStateFlow.value = flags + explicitFlags
}
fun setFlag(key: String, value: Boolean) {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/handlers/StatsLinkHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/handlers/StatsLinkHandler.kt
index ceed1dbb5b12..c083f900c464 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/handlers/StatsLinkHandler.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/handlers/StatsLinkHandler.kt
@@ -2,11 +2,11 @@ package org.wordpress.android.ui.deeplinks.handlers
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction
+import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenJetpackStaticPosterView
import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenStats
import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenStatsForSite
import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenStatsForSiteAndTimeframe
import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenStatsForTimeframe
-import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenJetpackStaticPosterView
import org.wordpress.android.ui.deeplinks.DeepLinkUriUtils
import org.wordpress.android.ui.deeplinks.DeepLinkingIntentReceiverViewModel.Companion.APPLINK_SCHEME
import org.wordpress.android.ui.deeplinks.DeepLinkingIntentReceiverViewModel.Companion.HOST_WORDPRESS_COM
@@ -32,7 +32,8 @@ class StatsLinkHandler
val pathSegments = uri.pathSegments
val length = pathSegments.size
val site = pathSegments.getOrNull(length - 1)?.toSite()
- val statsTimeframe = pathSegments.getOrNull(length - 2)?.toStatsTimeframe()
+ val timeframeIndex = if (site == null) (length - 1) else (length - 2)
+ val statsTimeframe = pathSegments.getOrNull(timeframeIndex)?.toStatsTimeframe()
return when {
jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage() -> OpenJetpackStaticPosterView
site != null && statsTimeframe != null -> {
@@ -98,6 +99,7 @@ class StatsLinkHandler
"month" -> StatsTimeframe.MONTH
"year" -> StatsTimeframe.YEAR
"insights" -> StatsTimeframe.INSIGHTS
+ "subscribers" -> StatsTimeframe.SUBSCRIBERS
else -> null
}
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/engagement/GetLikesUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/engagement/GetLikesUseCase.kt
index 7f0e4090de34..396034e13522 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/engagement/GetLikesUseCase.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/engagement/GetLikesUseCase.kt
@@ -1,9 +1,11 @@
package org.wordpress.android.ui.engagement
+import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.suspendCancellableCoroutine
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.wordpress.android.R
@@ -36,9 +38,7 @@ import org.wordpress.android.util.AppLog
import org.wordpress.android.util.AppLog.T
import org.wordpress.android.util.NetworkUtilsWrapper
import javax.inject.Inject
-import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
// NOTE: Do not remove the commentStore and postStore fields; commentStore seems needed so that the store is registered
// when we dispatch events; postStore added to keep the rational even if not strictly needed as of today.
@@ -50,7 +50,7 @@ class GetLikesUseCase @Inject constructor(
@Suppress("Unused") val postStore: PostStore,
val accountStore: AccountStore
) {
- private var getLikesContinuations = mutableMapOf>>()
+ private var getLikesContinuations = mutableMapOf>>()
init {
dispatcher.register(this)
@@ -128,7 +128,7 @@ class GetLikesUseCase @Inject constructor(
fingerPrint: LikeGroupFingerPrint,
paginationParams: PaginationParams
): OnChanged<*> {
- return suspendCoroutine {
+ return suspendCancellableCoroutine {
getLikesContinuations[category.getActionKey(fingerPrint.siteId, fingerPrint.postOrCommentId)] = it
when (category) {
POST_LIKE -> {
@@ -203,7 +203,9 @@ class GetLikesUseCase @Inject constructor(
val key = POST_LIKE.getActionKey(event.siteId, event.postId)
getLikesContinuations[key]?.let {
- it.resume(event)
+ if(it.isActive) {
+ it.resume(event)
+ }
getLikesContinuations.remove(key)
}
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/engagement/LikedItemViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/engagement/LikedItemViewHolder.kt
index 55347a487000..d86bbfe11fc2 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/engagement/LikedItemViewHolder.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/engagement/LikedItemViewHolder.kt
@@ -9,7 +9,7 @@ import org.wordpress.android.R
import org.wordpress.android.ui.engagement.AuthorName.AuthorNameCharSequence
import org.wordpress.android.ui.engagement.AuthorName.AuthorNameString
import org.wordpress.android.ui.engagement.EngageItem.LikedItem
-import org.wordpress.android.util.GravatarUtils
+import org.wordpress.android.util.WPAvatarUtils
import org.wordpress.android.util.extensions.getDrawableResIdFromAttribute
import org.wordpress.android.util.image.ImageManager
import org.wordpress.android.util.image.ImageType
@@ -33,7 +33,7 @@ class LikedItemViewHolder(
this.name.text = authorName
this.snippet.text = likedItem.postOrCommentText
- val avatarUrl = GravatarUtils.fixGravatarUrl(
+ val avatarUrl = WPAvatarUtils.rewriteAvatarUrl(
likedItem.authorAvatarUrl,
rootView.context.resources.getDimensionPixelSize(R.dimen.avatar_sz_small)
)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/engagement/LikerViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/engagement/LikerViewHolder.kt
index f6b81e415bd1..e4fc6b615215 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/engagement/LikerViewHolder.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/engagement/LikerViewHolder.kt
@@ -7,7 +7,7 @@ import android.widget.TextView
import org.wordpress.android.R
import org.wordpress.android.ui.engagement.EngageItem.Liker
import org.wordpress.android.ui.engagement.EngagedListNavigationEvent.OpenUserProfileBottomSheet.UserProfile
-import org.wordpress.android.util.GravatarUtils
+import org.wordpress.android.util.WPAvatarUtils
import org.wordpress.android.util.image.ImageManager
import org.wordpress.android.util.image.ImageType
import org.wordpress.android.viewmodel.ResourceProvider
@@ -30,7 +30,7 @@ class LikerViewHolder(
liker.login
}
- val likerAvatarUrl = GravatarUtils.fixGravatarUrl(
+ val likerAvatarUrl = WPAvatarUtils.rewriteAvatarUrl(
liker.userAvatarUrl,
likerRootView.context.resources.getDimensionPixelSize(R.dimen.avatar_sz_medium)
)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/engagement/ListScenarioUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/engagement/ListScenarioUtils.kt
index 6d02ba429ca6..50f5221b7183 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/engagement/ListScenarioUtils.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/engagement/ListScenarioUtils.kt
@@ -30,7 +30,7 @@ class ListScenarioUtils @Inject constructor(
val notificationsUtilsWrapper: NotificationsUtilsWrapper
) {
fun mapLikeNoteToListScenario(note: Note, context: Context): ListScenario {
- require(note.isLikeType) { "mapLikeNoteToListScenario > unexpected note type ${note.type}" }
+ require(note.isLikeType) { "mapLikeNoteToListScenario > unexpected note type ${note.rawType}" }
val imageType = AVATAR_WITH_BACKGROUND
val headerNoteBlock = HeaderNoteBlock(
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/engagement/UserProfileBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/engagement/UserProfileBottomSheetFragment.kt
index 76b2484b91bb..b0c6e5a13d83 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/engagement/UserProfileBottomSheetFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/engagement/UserProfileBottomSheetFragment.kt
@@ -19,10 +19,10 @@ import org.wordpress.android.R
import org.wordpress.android.WordPress
import org.wordpress.android.ui.engagement.BottomSheetUiState.UserProfileUiState
import org.wordpress.android.ui.utils.UiHelpers
-import org.wordpress.android.util.GravatarUtils
import org.wordpress.android.util.PhotonUtils
import org.wordpress.android.util.PhotonUtils.Quality.HIGH
import org.wordpress.android.util.UrlUtils
+import org.wordpress.android.util.WPAvatarUtils
import org.wordpress.android.util.image.ImageManager
import org.wordpress.android.util.image.ImageType.AVATAR_WITH_BACKGROUND
import org.wordpress.android.util.image.ImageType.BLAVATAR
@@ -114,7 +114,7 @@ class UserProfileBottomSheetFragment : BottomSheetDialogFragment() {
imageManager.loadIntoCircle(
userAvatar,
AVATAR_WITH_BACKGROUND,
- GravatarUtils.fixGravatarUrl(state.userAvatarUrl, avatarSz)
+ WPAvatarUtils.rewriteAvatarUrl(state.userAvatarUrl, avatarSz)
)
userName.text = state.userName
userLogin.text = if (state.userLogin.isNotBlank()) {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailContainerFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailContainerFragment.java
index f51a98efabfa..61d2d2a59700 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailContainerFragment.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailContainerFragment.java
@@ -172,6 +172,9 @@ private ArrayList mapRevisions() {
mRevision = getArguments().getParcelable(EXTRA_CURRENT_REVISION);
final long[] previousRevisionsIds = getArguments().getLongArray(EXTRA_PREVIOUS_REVISIONS_IDS);
+ if (previousRevisionsIds == null) {
+ return null;
+ }
final List revisionModels = new ArrayList<>();
final long postId = getArguments().getLong(EXTRA_POST_ID);
final long siteId = getArguments().getLong(EXTRA_SITE_ID);
@@ -192,7 +195,9 @@ private ArrayList mapRevisionModelsToRevisions(@Nullable final List revisions = new ArrayList<>();
for (int i = 0; i < revisionModels.size(); i++) {
final RevisionModel current = revisionModels.get(i);
- revisions.add(new Revision(current));
+ if (current != null) {
+ revisions.add(new Revision(current));
+ }
}
return revisions;
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalBrandingUtil.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalBrandingUtil.kt
index cd3b272ec97b..4178035a1378 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalBrandingUtil.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalBrandingUtil.kt
@@ -60,7 +60,7 @@ class JetpackFeatureRemovalBrandingUtil @Inject constructor(
fun getBrandingTextByPhase(screen: JetpackPoweredScreen): UiString {
return when (jetpackFeatureRemovalPhaseHelper.getCurrentPhase()) {
- PhaseStaticPosters -> UiStringRes(R.string.wp_jetpack_feature_removal_static_posters_phase)
+ PhaseStaticPosters -> UiStringRes(R.string.wp_jetpack_powered)
PhaseThree -> (screen as? JetpackPoweredScreen.WithDynamicText)?.let { screenWithDynamicText ->
getDynamicBrandingForScreen(screenWithDynamicText)
} ?: UiStringRes(JetpackBrandingUiState.RES_JP_POWERED)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalPhaseHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalPhaseHelper.kt
index 49ec25df3d7f..1d353a18b1ca 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalPhaseHelper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalPhaseHelper.kt
@@ -1,5 +1,7 @@
package org.wordpress.android.ui.jetpackoverlay
+import org.wordpress.android.analytics.AnalyticsTracker
+import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseFour
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseNewUsers
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseOne
@@ -9,7 +11,9 @@ import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseS
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseStaticPosters
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalSiteCreationPhase.PHASE_ONE
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalSiteCreationPhase.PHASE_TWO
+import org.wordpress.android.ui.main.WPMainNavigationView.PageType
import org.wordpress.android.util.BuildConfigWrapper
+import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
import org.wordpress.android.util.config.JetpackFeatureRemovalNewUsersConfig
import org.wordpress.android.util.config.JetpackFeatureRemovalPhaseFourConfig
import org.wordpress.android.util.config.JetpackFeatureRemovalPhaseOneConfig
@@ -40,7 +44,8 @@ class JetpackFeatureRemovalPhaseHelper @Inject constructor(
private val jetpackFeatureRemovalNewUsersConfig: JetpackFeatureRemovalNewUsersConfig,
private val jetpackFeatureRemovalSelfHostedUsersConfig: JetpackFeatureRemovalSelfHostedUsersConfig,
private val jetpackFeatureRemovalStaticPostersConfig: JetpackFeatureRemovalStaticPostersConfig,
- private val jetpackPhaseFourOverlayFrequencyConfig: PhaseFourOverlayFrequencyConfig
+ private val jetpackPhaseFourOverlayFrequencyConfig: PhaseFourOverlayFrequencyConfig,
+ private val analyticsTrackerWrapper: AnalyticsTrackerWrapper
) {
fun getCurrentPhase(): JetpackFeatureRemovalPhase? {
return if (buildConfigWrapper.isJetpackApp) null
@@ -86,9 +91,6 @@ class JetpackFeatureRemovalPhaseHelper @Inject constructor(
}
}
- @Suppress("FunctionOnlyReturningConstant")
- fun shouldShowStoryPost(): Boolean = false
-
fun shouldShowJetpackPoweredEditorFeatures(): Boolean {
val currentPhase = getCurrentPhase() ?: return true
return when (currentPhase) {
@@ -129,6 +131,26 @@ class JetpackFeatureRemovalPhaseHelper @Inject constructor(
}
}
+ @JvmOverloads
+ fun trackPageAccessedEventIfNeeded(pageType: PageType, site: SiteModel? = null) {
+ when (pageType) {
+ PageType.MY_SITE -> analyticsTrackerWrapper.track(AnalyticsTracker.Stat.MY_SITE_ACCESSED, site)
+ PageType.READER -> {
+ if (arePosterizedPagesVisible()) {
+ analyticsTrackerWrapper.track(AnalyticsTracker.Stat.READER_ACCESSED)
+ }
+ }
+
+ PageType.NOTIFS -> {
+ if (arePosterizedPagesVisible()) {
+ analyticsTrackerWrapper.track(AnalyticsTracker.Stat.NOTIFICATIONS_ACCESSED)
+ }
+ }
+
+ PageType.ME -> analyticsTrackerWrapper.track(AnalyticsTracker.Stat.ME_ACCESSED)
+ }
+ }
+
fun shouldShowNotifications(): Boolean {
val currentPhase = getCurrentPhase() ?: return true
return when (currentPhase) {
@@ -156,6 +178,8 @@ class JetpackFeatureRemovalPhaseHelper @Inject constructor(
fun getPhaseFourOverlayFrequency(): Int {
return jetpackPhaseFourOverlayFrequencyConfig.getValue()
}
+
+ private fun arePosterizedPagesVisible() = !shouldShowStaticPage()
}
// Global overlay frequency is the frequency at which the overlay is shown across the features
// no matter which feature was accessed last time
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPreviewFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPreviewFragment.kt
index 03292ed578f8..83d878c0642a 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPreviewFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPreviewFragment.kt
@@ -18,6 +18,7 @@ import androidx.lifecycle.ViewModelProvider
import org.wordpress.android.R
import org.wordpress.android.WordPress
import org.wordpress.android.databinding.LayoutPickerPreviewFragmentBinding
+import org.wordpress.android.fluxc.network.UserAgent
import org.wordpress.android.ui.FullscreenBottomSheetDialogFragment
import org.wordpress.android.ui.PreviewMode.DESKTOP
import org.wordpress.android.ui.PreviewMode.MOBILE
@@ -38,6 +39,9 @@ private const val JS_EVALUATION_DELAY = 250L
private const val JS_READY_CALLBACK_ID = 926L
abstract class LayoutPreviewFragment : FullscreenBottomSheetDialogFragment() {
+ @Inject
+ lateinit var userAgent: UserAgent
+
@Inject
lateinit var displayUtilsWrapper: DisplayUtilsWrapper
@@ -125,7 +129,7 @@ abstract class LayoutPreviewFragment : FullscreenBottomSheetDialogFragment() {
binding?.previewTypeSelectorButton?.setOnClickListener { viewModel.onPreviewModePressed() }
- binding?.webView?.settings?.userAgentString = WordPress.getUserAgent()
+ binding?.webView?.settings?.userAgentString = userAgent.toString()
binding?.webView?.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/AddSiteDialog.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/AddSiteDialog.kt
new file mode 100644
index 000000000000..36e507e32543
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/AddSiteDialog.kt
@@ -0,0 +1,46 @@
+package org.wordpress.android.ui.main
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.widget.ArrayAdapter
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.wordpress.android.R
+import org.wordpress.android.analytics.AnalyticsTracker
+import org.wordpress.android.ui.ActivityLauncher
+import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource
+
+/**
+ * Dialog to prompt the user to add a site.
+ */
+class AddSiteDialog : DialogFragment() {
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val source =
+ SiteCreationSource.fromString(requireArguments().getString(ChooseSiteActivity.KEY_ARG_SITE_CREATION_SOURCE))
+ val items = arrayOf(
+ getString(R.string.site_picker_create_wpcom),
+ getString(R.string.site_picker_add_self_hosted)
+ )
+ val builder = MaterialAlertDialogBuilder(requireActivity())
+ builder.setTitle(R.string.site_picker_add_a_site)
+ builder.setAdapter(
+ ArrayAdapter(requireActivity(), R.layout.add_new_site_dialog_item, R.id.text, items)
+ ) { _: DialogInterface?, which: Int ->
+ if (which == 0) {
+ ActivityLauncher.newBlogForResult(activity, source)
+ } else {
+ ActivityLauncher.addSelfHostedSiteForResult(activity)
+ }
+ }
+ AnalyticsTracker.track(
+ AnalyticsTracker.Stat.ADD_SITE_ALERT_DISPLAYED,
+ mapOf(ChooseSiteActivity.KEY_SOURCE to source.label)
+ )
+ return builder.create()
+ }
+
+ companion object {
+ const val ADD_SITE_DIALOG_TAG = "add_site_dialog"
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/AddSiteHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/AddSiteHandler.kt
new file mode 100644
index 000000000000..93f89f24f49a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/AddSiteHandler.kt
@@ -0,0 +1,36 @@
+package org.wordpress.android.ui.main
+
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentActivity
+import org.wordpress.android.BuildConfig
+import org.wordpress.android.ui.ActivityLauncher
+import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource
+
+/**
+ * Helper class to handle adding a site.
+ */
+object AddSiteHandler {
+ fun addSite(activity: FragmentActivity, hasAccessToken: Boolean, source: SiteCreationSource) {
+ if (hasAccessToken) {
+ if (!BuildConfig.ENABLE_ADD_SELF_HOSTED_SITE) {
+ ActivityLauncher.newBlogForResult(activity, source)
+ } else {
+ // user is signed into wordpress app, so use the dialog to enable choosing whether to
+ // create a new wp.com blog or add a self-hosted one
+ showAddSiteDialog(activity, source)
+ }
+ } else {
+ // user doesn't have an access token, so simply enable adding self-hosted
+ ActivityLauncher.addSelfHostedSiteForResult(activity)
+ }
+ }
+
+ private fun showAddSiteDialog(activity: FragmentActivity, source: SiteCreationSource) {
+ val dialog: DialogFragment = AddSiteDialog()
+ val args = Bundle()
+ args.putString(ChooseSiteActivity.KEY_ARG_SITE_CREATION_SOURCE, source.label)
+ dialog.arguments = args
+ dialog.show(activity.supportFragmentManager, AddSiteDialog.ADD_SITE_DIALOG_TAG)
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/ChooseSiteActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/ChooseSiteActivity.kt
new file mode 100644
index 000000000000..db0a2e524e16
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/ChooseSiteActivity.kt
@@ -0,0 +1,401 @@
+package org.wordpress.android.ui.main
+
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.activity.viewModels
+import androidx.appcompat.widget.SearchView
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import dagger.hilt.android.AndroidEntryPoint
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+import org.wordpress.android.R
+import org.wordpress.android.analytics.AnalyticsTracker
+import org.wordpress.android.analytics.AnalyticsTracker.Stat
+import org.wordpress.android.databinding.ChooseSiteActivityBinding
+import org.wordpress.android.fluxc.Dispatcher
+import org.wordpress.android.fluxc.generated.SiteActionBuilder
+import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.fluxc.store.AccountStore
+import org.wordpress.android.fluxc.store.SiteStore
+import org.wordpress.android.fluxc.store.SiteStore.OnSiteChanged
+import org.wordpress.android.fluxc.store.SiteStore.OnSiteRemoved
+import org.wordpress.android.ui.ActivityId
+import org.wordpress.android.ui.LocaleAwareActivity
+import org.wordpress.android.ui.RequestCodes
+import org.wordpress.android.ui.mysite.SelectedSiteRepository
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
+import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource
+import org.wordpress.android.util.AccessibilityUtils
+import org.wordpress.android.util.ActivityUtils
+import org.wordpress.android.util.AppLog
+import org.wordpress.android.util.DeviceUtils
+import org.wordpress.android.util.SiteUtils
+import org.wordpress.android.util.ToastUtils
+import org.wordpress.android.util.WPSwipeToRefreshHelper
+import org.wordpress.android.util.helpers.SwipeToRefreshHelper
+import org.wordpress.android.widgets.WPDialogSnackbar
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class ChooseSiteActivity : LocaleAwareActivity() {
+ private val viewModel: SiteViewModel by viewModels()
+ private val adapter = ChooseSiteAdapter()
+ private val mode by lazy { SitePickerMode.valueOf(intent.getStringExtra(KEY_SITE_PICKER_MODE)!!) }
+ private val localId: Int? by lazy { intent.getIntExtra(KEY_SITE_LOCAL_ID, -1).takeIf { it != -1 } }
+ private lateinit var binding: ChooseSiteActivityBinding
+ private lateinit var menuSearch: MenuItem
+ private lateinit var menuEditPin: MenuItem
+ private lateinit var refreshHelper: SwipeToRefreshHelper
+ private var searchKeyword: String? = null
+
+ @Inject
+ lateinit var accountStore: AccountStore
+
+ @Inject
+ lateinit var siteStore: SiteStore
+
+ @Inject
+ lateinit var dispatcher: Dispatcher
+
+ @Inject
+ lateinit var appPrefsWrapper: AppPrefsWrapper
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ChooseSiteActivityBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ setSupportActionBar(binding.toolbarMain)
+ if (savedInstanceState == null) {
+ AnalyticsTracker.track(Stat.SITE_SWITCHER_DISPLAYED)
+ }
+ binding.toolbarMain.setNavigationOnClickListener {
+ AnalyticsTracker.track(Stat.SITE_SWITCHER_DISMISSED)
+ finish()
+ }
+ binding.buttonAddSite.setOnClickListener {
+ AnalyticsTracker.track(Stat.SITE_SWITCHER_ADD_SITE_TAPPED)
+ AddSiteHandler.addSite(this, accountStore.hasAccessToken(), SiteCreationSource.MY_SITE)
+ }
+ binding.progress.isVisible = true
+ setupRecycleView()
+
+ viewModel.sites.observe(this) {
+ binding.progress.isVisible = false
+ binding.recyclerView.isVisible = it.isNotEmpty()
+ binding.actionableEmptyView.isVisible = it.isEmpty()
+ adapter.setSites(it)
+ }
+
+ refreshHelper = WPSwipeToRefreshHelper.buildSwipeToRefreshHelper(binding.ptrLayout) {
+ refreshHelper.isRefreshing = true
+ dispatcher.dispatch(SiteActionBuilder.newFetchSitesAction(SiteUtils.getFetchSitesPayload()))
+ }
+
+ localId?.let {
+ appPrefsWrapper.addRecentSiteLocalId(it)
+ adapter.selectedSiteId = it
+ }
+
+ viewModel.loadSites(mode)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ dispatcher.register(this)
+ isRunning = true
+ }
+
+ override fun onResume() {
+ super.onResume()
+ ActivityId.trackLastActivity(ActivityId.SITE_PICKER)
+ }
+
+ override fun onStop() {
+ dispatcher.unregister(this)
+ isRunning = false
+ super.onStop()
+ }
+
+ @Suppress("unused")
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ fun onSiteChanged(event: OnSiteChanged) {
+ if (refreshHelper.isRefreshing) {
+ refreshHelper.isRefreshing = false
+ }
+ if (event.isError.not()) {
+ viewModel.loadSites(mode, searchKeyword)
+ }
+ }
+
+ @Suppress("unused")
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ fun onSiteRemoved(event: OnSiteRemoved) {
+ if (event.isError.not()) {
+ viewModel.loadSites(mode, searchKeyword)
+ } else {
+ // shouldn't happen
+ AppLog.e(AppLog.T.DB, "Encountered unexpected error while attempting to remove site: " + event.error)
+ ToastUtils.showToast(this, R.string.site_picker_remove_site_error)
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ super.onCreateOptionsMenu(menu)
+ menuInflater.inflate(R.menu.choose_site, menu)
+ menuSearch = menu.findItem(R.id.menu_search)
+ menuEditPin = menu.findItem(R.id.menu_pin)
+ return true
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ super.onPrepareOptionsMenu(menu)
+ if (adapter.mode == ActionMode.Pin) {
+ // restore state
+ enablePinSitesMode()
+ }
+ setupMenuVisibility()
+ setupSearchView()
+ return true
+ }
+
+ private fun setupMenuVisibility() {
+ if (mode == SitePickerMode.DEFAULT) {
+ menuEditPin.isVisible = true
+ binding.layoutAddSite.isVisible = true
+ } else {
+ menuEditPin.isVisible = false
+ binding.layoutAddSite.isVisible = false
+ }
+ }
+
+ private fun setupSearchView() {
+ val searchView = menuSearch.actionView as SearchView
+ searchView.maxWidth = Integer.MAX_VALUE
+ menuSearch.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
+ override fun onMenuItemActionExpand(item: MenuItem): Boolean {
+ binding.layoutAddSite.isVisible = false
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String): Boolean {
+ if (!DeviceUtils.getInstance().hasHardwareKeyboard(this@ChooseSiteActivity)) {
+ ActivityUtils.hideKeyboardForced(searchView)
+ }
+ return true
+ }
+
+ override fun onQueryTextChange(newText: String): Boolean {
+ searchKeyword = newText
+ AnalyticsTracker.track(Stat.SITE_SWITCHER_SEARCH_PERFORMED)
+ viewModel.loadSites(mode, newText)
+ return true
+ }
+ })
+ return true
+ }
+
+ override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
+ searchKeyword = null
+ searchView.setOnQueryTextListener(null)
+ viewModel.loadSites(mode)
+ invalidateOptionsMenu()
+ return true
+ }
+ })
+
+ // Restore search keyword
+ if (searchKeyword != null) {
+ // this is a workaround to set the search keyword after the search view is expanded
+ // due to searchKeyword will be cleared after the search view has been expanded first time
+ val keyword = searchKeyword // copy the keyword
+ menuSearch.expandActionView()
+ searchView.post { searchView.setQuery(keyword, true) }
+ searchView.clearFocus()
+ }
+ }
+
+ @Suppress("ReturnCount")
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.menu_pin -> {
+ if (adapter.mode is ActionMode.Pin) {
+ disablePinSitesMode()
+ AnalyticsTracker.track(
+ Stat.SITE_SWITCHER_TOGGLED_PIN_TAPPED,
+ mapOf(TRACK_PROPERTY_STATE to TRACK_PROPERTY_STATE_DONE)
+ )
+ } else {
+ enablePinSitesMode()
+ AnalyticsTracker.track(
+ Stat.SITE_SWITCHER_TOGGLED_PIN_TAPPED,
+ mapOf(TRACK_PROPERTY_STATE to TRACK_PROPERTY_STATE_EDIT)
+ )
+ }
+ return true
+ }
+
+ R.id.menu_search -> return true
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ /**
+ * Enable pin sites mode via menu items
+ */
+ private fun enablePinSitesMode() {
+ menuEditPin.setIcon(null)
+ menuEditPin.title = getString(R.string.label_done_button)
+ adapter.setActionMode(ActionMode.Pin)
+ }
+
+ /**
+ * Disable pin sites mode via menu items
+ */
+ private fun disablePinSitesMode() {
+ menuSearch.isVisible = true
+ menuEditPin.setIcon(R.drawable.pin_filled)
+ menuEditPin.title = getString(R.string.site_picker_edit_pins)
+ adapter.setActionMode(ActionMode.None)
+ }
+
+ private fun setupRecycleView() {
+ binding.recyclerView.layoutManager = LinearLayoutManager(this)
+ binding.recyclerView.adapter = adapter.apply {
+ onReload = { viewModel.loadSites(this@ChooseSiteActivity.mode, searchKeyword) }
+ onSiteClicked = { selectSite(it) }
+ onSiteLongClicked = { onSiteLongClick(it) }
+ }
+ binding.recyclerView.scrollBarStyle = View.SCROLLBARS_OUTSIDE_OVERLAY
+ binding.recyclerView.setEmptyView(binding.actionableEmptyView)
+ }
+
+ private fun selectSite(siteRecord: SiteRecord) {
+ AnalyticsTracker.track(
+ Stat.SITE_SWITCHER_SITE_TAPPED,
+ mapOf(TRACK_PROPERTY_SECTION to viewModel.getSection(siteRecord.localId))
+ )
+ appPrefsWrapper.addRecentSiteLocalId(siteRecord.localId)
+ setResult(RESULT_OK, Intent().putExtra(KEY_SITE_LOCAL_ID, siteRecord.localId))
+ finish()
+ }
+
+ private fun onSiteLongClick(siteRecord: SiteRecord) {
+ val site: SiteModel = siteStore.getSiteByLocalId(siteRecord.localId) ?: return
+ if (site.isUsingWpComRestApi.not()) {
+ showRemoveSelfHostedSiteDialog(site)
+ }
+ }
+
+ private fun showRemoveSelfHostedSiteDialog(site: SiteModel) {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(resources.getText(R.string.remove_account))
+ .setMessage(resources.getText(R.string.sure_to_remove_account))
+ .setPositiveButton(
+ resources.getText(R.string.yes)
+ ) { _: DialogInterface?, _: Int ->
+ dispatcher.dispatch(
+ SiteActionBuilder.newRemoveSiteAction(site)
+ )
+ }
+ .setNegativeButton(resources.getText(R.string.no), null)
+ .setCancelable(false)
+ .create()
+ .show()
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ when (requestCode) {
+ RequestCodes.ADD_ACCOUNT, RequestCodes.CREATE_SITE -> if (resultCode == RESULT_OK) {
+ viewModel.loadSites(mode)
+ if (data?.getBooleanExtra(KEY_SITE_CREATED_BUT_NOT_FETCHED, false) == true) {
+ showSiteCreatedButNotFetchedSnackbar()
+ } else {
+ val intent = data ?: Intent()
+ intent.putExtra(WPMainActivity.ARG_CREATE_SITE, RequestCodes.CREATE_SITE)
+ setResult(resultCode, intent)
+ finish()
+ }
+ }
+ }
+
+ // Enable the block editor on sites created on mobile
+ if (requestCode == RequestCodes.CREATE_SITE) {
+ if (data != null) {
+ val newSiteLocalID = data.getIntExtra(
+ KEY_SITE_LOCAL_ID,
+ SelectedSiteRepository.UNAVAILABLE
+ )
+ SiteUtils.enableBlockEditorOnSiteCreation(dispatcher, siteStore, newSiteLocalID)
+ }
+ }
+ }
+
+ private fun showSiteCreatedButNotFetchedSnackbar() {
+ val duration = AccessibilityUtils
+ .getSnackbarDuration(this, resources.getInteger(R.integer.site_creation_snackbar_duration))
+ val message = getString(R.string.site_created_but_not_fetched_snackbar_message)
+ WPDialogSnackbar.make(binding.coordinatorLayout, message, duration).show()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putString(KEY_SEARCH_KEYWORD, searchKeyword)
+ outState.putString(KEY_ACTION_MODE, adapter.mode.value)
+ }
+
+ override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+ super.onRestoreInstanceState(savedInstanceState)
+
+ searchKeyword = savedInstanceState.getString(KEY_SEARCH_KEYWORD)
+
+ savedInstanceState.getString(KEY_ACTION_MODE)?.let { actionMode ->
+ adapter.setActionMode(ActionMode.from(actionMode))
+ }
+ }
+
+ companion object {
+ const val KEY_ARG_SITE_CREATION_SOURCE = "ARG_SITE_CREATION_SOURCE"
+ const val KEY_SOURCE = "source"
+ const val KEY_SITE_LOCAL_ID = "local_id"
+ const val KEY_SITE_PICKER_MODE = "key_site_picker_mode"
+ const val KEY_SITE_TITLE_TASK_COMPLETED = "key_site_title_task_completed"
+ const val KEY_SITE_CREATED_BUT_NOT_FETCHED = "key_site_created_but_not_fetched"
+ const val KEY_SEARCH_KEYWORD = "key_search_keyword"
+ const val KEY_ACTION_MODE = "key_action_mode"
+ private const val TRACK_PROPERTY_STATE = "state"
+ private const val TRACK_PROPERTY_STATE_EDIT = "edit"
+ private const val TRACK_PROPERTY_STATE_DONE = "done"
+ private const val TRACK_PROPERTY_SECTION = "section"
+
+ @JvmStatic
+ var isRunning = false
+ }
+}
+
+
+/**
+ * Mode for the site picker
+ */
+enum class SitePickerMode {
+ /**
+ * Show everything
+ */
+ DEFAULT,
+
+ /**
+ * Show all sites, hide the "Add Site" button and hide the "Edit Pins" button
+ */
+ SIMPLE,
+
+ /**
+ * Hide self-hosted sites for purchasing a domain for a WPCOM site
+ * Also hide the "Add Site" button and hide the "Edit Pins" button
+ */
+ WPCOM_SITES_ONLY
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/ChooseSiteAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/ChooseSiteAdapter.kt
new file mode 100644
index 000000000000..dbbdc9098a6d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/ChooseSiteAdapter.kt
@@ -0,0 +1,63 @@
+package org.wordpress.android.ui.main
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import org.wordpress.android.databinding.ItemChooseSiteBinding
+
+class ChooseSiteAdapter : RecyclerView.Adapter() {
+ private val sites = ArrayList()
+
+ var mode: ActionMode = ActionMode.None
+ private set
+
+ var onReload = {}
+ var onSiteClicked: (SiteRecord) -> Unit = {}
+ var onSiteLongClicked: (SiteRecord) -> Unit = {}
+ var selectedSiteId: Int? = null
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChooseSiteViewHolder =
+ ChooseSiteViewHolder(ItemChooseSiteBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+
+ override fun getItemCount(): Int = sites.size
+
+ override fun onBindViewHolder(holder: ChooseSiteViewHolder, position: Int) {
+ holder.bind(mode, sites.getOrNull(position - 1), sites[position], selectedSiteId)
+ holder.onPinUpdated = { onReload() }
+ holder.onClicked = { onSiteClicked(it) }
+ holder.onLongClicked = { onSiteLongClicked(it) }
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun setSites(newSites: List) {
+ sites.clear()
+ sites.addAll(newSites)
+ notifyDataSetChanged()
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun setActionMode(actionMode: ActionMode) {
+ mode = actionMode
+ notifyDataSetChanged()
+ }
+}
+
+/**
+ * For displaying the UI of "Edit Pins"
+ */
+sealed class ActionMode(val value: String) {
+ data object None : ActionMode(NONE)
+ data object Pin : ActionMode(PIN)
+
+ companion object {
+ fun from(value: String): ActionMode = when (value) {
+ PIN -> Pin
+ NONE -> None
+ else -> throw IllegalArgumentException("Unknown value: $value")
+ }
+
+ private const val PIN = "pin"
+ private const val NONE = "none"
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/ChooseSiteViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/ChooseSiteViewHolder.kt
new file mode 100644
index 000000000000..9e169ba7f29f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/ChooseSiteViewHolder.kt
@@ -0,0 +1,171 @@
+package org.wordpress.android.ui.main
+
+import android.content.res.ColorStateList
+import android.view.ViewGroup.MarginLayoutParams
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.ColorUtils
+import androidx.core.view.isVisible
+import androidx.core.widget.ImageViewCompat
+import androidx.recyclerview.widget.RecyclerView
+import org.wordpress.android.R
+import org.wordpress.android.WordPress
+import org.wordpress.android.analytics.AnalyticsTracker
+import org.wordpress.android.databinding.ItemChooseSiteBinding
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
+import org.wordpress.android.util.extensions.getColorFromAttribute
+import org.wordpress.android.util.extensions.isDarkTheme
+import org.wordpress.android.util.image.ImageManager
+import javax.inject.Inject
+
+class ChooseSiteViewHolder(private val binding: ItemChooseSiteBinding) : RecyclerView.ViewHolder(binding.root) {
+ @Inject
+ lateinit var imageManager: ImageManager
+
+ @Inject
+ lateinit var appPrefs: AppPrefsWrapper
+
+ var onPinUpdated = { _: SiteRecord -> }
+ var onClicked = { _: SiteRecord -> }
+ var onLongClicked = { _: SiteRecord -> }
+
+ init {
+ (itemView.context.applicationContext as WordPress).component().inject(this)
+ }
+
+ fun bind(mode: ActionMode, previousSite: SiteRecord?, site: SiteRecord, selectedId: Int?) {
+ handleAvatar(site)
+
+ handleHeader(previousSite, site)
+
+ binding.textTitle.text = site.blogNameOrHomeURL
+ binding.textDomain.text = site.homeURL
+ binding.pin.isVisible = mode is ActionMode.Pin
+
+ handlePinButton(site)
+
+ if (mode is ActionMode.Pin) {
+ binding.layoutContainer.setOnClickListener(null)
+ binding.layoutContainer.setOnLongClickListener(null)
+ } else {
+ binding.layoutContainer.setOnClickListener { onClicked(site) }
+ binding.layoutContainer.setOnLongClickListener {
+ onLongClicked(site)
+ true
+ }
+ }
+
+ handleHighlight(site, selectedId)
+ }
+
+ private fun handlePinButton(site: SiteRecord) {
+ val isPinned = site.isPinned()
+ binding.pin.setImageResource(if (isPinned) R.drawable.pin_filled else R.drawable.pin)
+ binding.pin.setOnClickListener {
+ if (isPinned) {
+ appPrefs.pinnedSiteLocalIds = appPrefs.pinnedSiteLocalIds.apply { remove(site.localId) }
+ AnalyticsTracker.track(
+ AnalyticsTracker.Stat.SITE_SWITCHER_PIN_UPDATED,
+ mapOf(
+ TRACK_PROPERTY_BLOG_ID to site.siteId,
+ TRACK_PROPERTY_PINNED to false
+ )
+ )
+ } else {
+ appPrefs.pinnedSiteLocalIds = appPrefs.pinnedSiteLocalIds.apply { add(site.localId) }
+ AnalyticsTracker.track(
+ AnalyticsTracker.Stat.SITE_SWITCHER_PIN_UPDATED,
+ mapOf(
+ TRACK_PROPERTY_BLOG_ID to site.siteId,
+ TRACK_PROPERTY_PINNED to true
+ )
+ )
+ }
+ onPinUpdated(site)
+ }
+ val color = if (isPinned) binding.root.context.getColor(R.color.inline_action_filled)
+ else binding.root.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium)
+ ImageViewCompat.setImageTintList(binding.pin, ColorStateList.valueOf(color))
+ }
+
+ private fun handleAvatar(site: SiteRecord) {
+ imageManager.load(binding.avatar, site.blavatarType, site.blavatarUrl)
+ val isDarkTheme = itemView.resources.configuration.isDarkTheme()
+ val borderColor = ContextCompat.getColor(
+ itemView.context,
+ if (isDarkTheme) R.color.white_translucent_10
+ else R.color.black_translucent_10
+ )
+ binding.avatar.strokeColor = ColorStateList.valueOf(borderColor)
+ }
+
+ private fun handleHeader(previousSite: SiteRecord?, site: SiteRecord) {
+ when {
+ previousSite == null && site.isPinned() -> {
+ binding.header.text = itemView.context.getString(R.string.site_picker_pinned_sites)
+ binding.header.isVisible = true
+ setHeaderTopMargin(previousSite)
+ }
+
+ (previousSite == null || previousSite.isPinned()) && site.isPinned().not() -> {
+ binding.header.text = itemView.context.getString(R.string.site_picker_recent_sites)
+ binding.header.isVisible = true
+ setHeaderTopMargin(previousSite)
+ }
+
+ (previousSite == null || previousSite.isRecent() || previousSite.isPinned()) &&
+ site.isPinned().not() &&
+ site.isRecent().not() -> {
+ binding.header.text = itemView.context.getString(R.string.site_picker_all_sites)
+ binding.header.isVisible = true
+ setHeaderTopMargin(previousSite)
+ }
+
+ else -> {
+ binding.header.isVisible = false
+ }
+ }
+ }
+
+ private fun setHeaderTopMargin(previousSite: SiteRecord?) {
+ val resId = previousSite?.let { R.dimen.margin_extra_large } ?: R.dimen.margin_small
+ (binding.header.layoutParams as MarginLayoutParams).apply {
+ setMargins(
+ leftMargin,
+ itemView.context.resources.getDimensionPixelSize(resId),
+ rightMargin,
+ bottomMargin
+ )
+ }.let { binding.header.layoutParams = it }
+ }
+
+ private fun handleHighlight(site: SiteRecord, selectedId: Int?) {
+ val isSelected = site.localId == (selectedId ?: appPrefs.getSelectedSite())
+ if (isSelected) {
+ // highlight the selected site
+ ColorUtils.setAlphaComponent(
+ itemView.context.getColorFromAttribute(com.google.android.material.R.attr.colorOnSurface),
+ itemView.context.resources.getInteger(R.integer.selected_list_item_opacity)
+ ).let { color ->
+ binding.layoutContainer.setBackgroundColor(color)
+ }
+ } else {
+ // clear the highlight
+ binding.layoutContainer.background = null
+ }
+ }
+
+ private fun SiteRecord?.isPinned(): Boolean = when (this) {
+ null -> false
+ else -> appPrefs.pinnedSiteLocalIds.contains(localId)
+ }
+
+ private fun SiteRecord?.isRecent(): Boolean = when (this) {
+ null -> false
+ else -> appPrefs.getRecentSiteLocalIds().contains(localId)
+ }
+
+ companion object {
+ private const val TRACK_PROPERTY_BLOG_ID = "blog_id"
+ private const val TRACK_PROPERTY_PINNED = "pinned"
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MainActionListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/MainActionListItem.kt
index cfbac905fc29..755e674dfc0c 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/main/MainActionListItem.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MainActionListItem.kt
@@ -13,8 +13,8 @@ sealed class MainActionListItem {
CREATE_NEW_PAGE,
CREATE_NEW_PAGE_FROM_PAGES_CARD,
CREATE_NEW_POST,
- CREATE_NEW_STORY,
- ANSWER_BLOGGING_PROMPT
+ ANSWER_BLOGGING_PROMPT,
+ CREATE_NEW_POST_FROM_AUDIO
}
data class CreateAction(
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt
index 347e700f21d3..8375e18630f7 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt
@@ -18,12 +18,17 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
+import com.gravatar.services.AvatarService
+import com.gravatar.services.Result
+import com.gravatar.types.Email
import com.yalantis.ucrop.UCrop
import com.yalantis.ucrop.UCrop.Options
import com.yalantis.ucrop.UCropActivity
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@@ -36,6 +41,7 @@ import org.wordpress.android.analytics.AnalyticsTracker.Stat.ME_GRAVATAR_GALLERY
import org.wordpress.android.analytics.AnalyticsTracker.Stat.ME_GRAVATAR_SHOT_NEW
import org.wordpress.android.analytics.AnalyticsTracker.Stat.ME_GRAVATAR_TAPPED
import org.wordpress.android.analytics.AnalyticsTracker.Stat.ME_GRAVATAR_UPLOADED
+import org.wordpress.android.analytics.AnalyticsTracker.Stat.ME_GRAVATAR_UPLOAD_EXCEPTION
import org.wordpress.android.databinding.MeFragmentBinding
import org.wordpress.android.designsystem.DesignSystemActivity
import org.wordpress.android.fluxc.Dispatcher
@@ -44,8 +50,6 @@ import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged
import org.wordpress.android.fluxc.store.PostStore
import org.wordpress.android.fluxc.store.SiteStore
import org.wordpress.android.models.JetpackPoweredScreen
-import org.wordpress.android.networking.GravatarApi
-import org.wordpress.android.networking.GravatarApi.GravatarUploadListener
import org.wordpress.android.ui.ActivityLauncher
import org.wordpress.android.ui.RequestCodes
import org.wordpress.android.ui.about.UnifiedAboutActivity
@@ -145,6 +149,9 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener {
@Inject
lateinit var domainManagementFeatureConfig: DomainManagementFeatureConfig
+ @Inject
+ lateinit var avatarService: AvatarService
+
private val viewModel: MeViewModel by viewModels()
private val shouldShowDomainButton
@@ -669,24 +676,25 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener {
}
val file = File(filePath)
if (!file.exists()) {
- ToastUtils.showToast(
- activity,
- R.string.error_locating_image,
- SHORT
- )
+ ToastUtils.showToast(activity, R.string.error_locating_image, SHORT)
return
}
binding?.showGravatarProgressBar(true)
- GravatarApi.uploadGravatar(file, accountStore.account.email, accountStore.accessToken,
- object : GravatarUploadListener {
- override fun onSuccess() {
- EventBus.getDefault().post(GravatarUploadFinished(filePath, true))
+ lifecycleScope.launch {
+ val result =
+ avatarService.upload(file, Email(accountStore.account.email), accountStore.accessToken.orEmpty())
+ when (result) {
+ is Result.Failure -> {
+ AnalyticsTracker.track(ME_GRAVATAR_UPLOAD_EXCEPTION, mapOf("error_type" to result.error.name))
+ EventBus.getDefault().post(GravatarUploadFinished(filePath, false))
}
- override fun onError() {
- EventBus.getDefault().post(GravatarUploadFinished(filePath, false))
+ is Result.Success -> {
+ AnalyticsTracker.track(ME_GRAVATAR_UPLOADED)
+ EventBus.getDefault().post(GravatarUploadFinished(filePath, true))
}
- })
+ }
+ }
}
class GravatarUploadFinished internal constructor(val filePath: String, val success: Boolean)
@@ -695,7 +703,6 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener {
fun onEventMainThread(event: GravatarUploadFinished) {
binding?.showGravatarProgressBar(false)
if (event.success) {
- AnalyticsTracker.track(ME_GRAVATAR_UPLOADED)
binding?.loadAvatar(event.filePath)
binding?.gravatarSyncView?.gravatarSyncContainer?.visibility = View.VISIBLE
} else {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java
deleted file mode 100644
index 12aa1ebfe301..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java
+++ /dev/null
@@ -1,910 +0,0 @@
-package org.wordpress.android.ui.main;
-
-import android.app.Dialog;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.ArrayAdapter;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.view.ActionMode;
-import androidx.appcompat.widget.SearchView;
-import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.recyclerview.widget.DefaultItemAnimator;
-import androidx.recyclerview.widget.LinearLayoutManager;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-
-import org.greenrobot.eventbus.Subscribe;
-import org.greenrobot.eventbus.ThreadMode;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.wordpress.android.BuildConfig;
-import org.wordpress.android.R;
-import org.wordpress.android.WordPress;
-import org.wordpress.android.analytics.AnalyticsTracker;
-import org.wordpress.android.analytics.AnalyticsTracker.Stat;
-import org.wordpress.android.databinding.SitePickerActivityBinding;
-import org.wordpress.android.fluxc.Dispatcher;
-import org.wordpress.android.fluxc.generated.SiteActionBuilder;
-import org.wordpress.android.fluxc.model.SiteModel;
-import org.wordpress.android.fluxc.store.AccountStore;
-import org.wordpress.android.fluxc.store.SiteStore;
-import org.wordpress.android.fluxc.store.SiteStore.OnSiteChanged;
-import org.wordpress.android.fluxc.store.SiteStore.OnSiteRemoved;
-import org.wordpress.android.fluxc.store.StatsStore;
-import org.wordpress.android.ui.ActivityId;
-import org.wordpress.android.ui.ActivityLauncher;
-import org.wordpress.android.ui.LocaleAwareActivity;
-import org.wordpress.android.ui.RequestCodes;
-import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginFragment;
-import org.wordpress.android.ui.main.SitePickerAdapter.SiteList;
-import org.wordpress.android.ui.main.SitePickerAdapter.SitePickerMode;
-import org.wordpress.android.ui.main.SitePickerAdapter.SiteRecord;
-import org.wordpress.android.ui.mysite.SelectedSiteRepository;
-import org.wordpress.android.ui.prefs.AppPrefs;
-import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource;
-import org.wordpress.android.util.AccessibilityUtils;
-import org.wordpress.android.util.ActivityUtils;
-import org.wordpress.android.util.AppLog;
-import org.wordpress.android.util.BuildConfigWrapper;
-import org.wordpress.android.util.DeviceUtils;
-import org.wordpress.android.util.NetworkUtils;
-import org.wordpress.android.util.SiteUtils;
-import org.wordpress.android.util.ToastUtils;
-import org.wordpress.android.util.helpers.Debouncer;
-import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
-import org.wordpress.android.viewmodel.main.SitePickerViewModel;
-import org.wordpress.android.viewmodel.main.SitePickerViewModel.Action.ContinueReblogTo;
-import org.wordpress.android.viewmodel.main.SitePickerViewModel.Action.NavigateToState;
-import org.wordpress.android.widgets.WPDialogSnackbar;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import javax.inject.Inject;
-
-import static org.wordpress.android.util.WPSwipeToRefreshHelper.buildSwipeToRefreshHelper;
-
-import dagger.hilt.android.AndroidEntryPoint;
-
-@AndroidEntryPoint
-public class SitePickerActivity extends LocaleAwareActivity
- implements SitePickerAdapter.OnSiteClickListener,
- SitePickerAdapter.OnSelectedCountChangedListener,
- SearchView.OnQueryTextListener {
- public static final String KEY_SITE_LOCAL_ID = "local_id";
- public static final String KEY_SITE_CREATED_BUT_NOT_FETCHED = "key_site_created_but_not_fetched";
- public static final String KEY_SITE_TITLE_TASK_COMPLETED = "key_site_title_task_completed";
-
- public static final String KEY_SITE_PICKER_MODE = "key_site_picker_mode";
-
- private static final String KEY_IS_IN_SEARCH_MODE = "is_in_search_mode";
- private static final String KEY_LAST_SEARCH = "last_search";
- private static final String KEY_REFRESHING = "refreshing_sites";
-
- // Used for preserving selection states after configuration change.
- private static final String KEY_SELECTED_POSITIONS = "selected_positions";
- private static final String KEY_IS_IN_EDIT_MODE = "is_in_edit_mode";
- private static final String KEY_IS_SHOW_MENU_ENABLED = "is_show_menu_enabled";
- private static final String KEY_IS_HIDE_MENU_ENABLED = "is_hide_menu_enabled";
-
- private static final String ARG_SITE_CREATION_SOURCE = "ARG_SITE_CREATION_SOURCE";
- private static final String SOURCE = "source";
- private static final String TRACK_PROPERTY_STATE = "state";
- private static final String TRACK_PROPERTY_STATE_EDIT = "edit";
- private static final String TRACK_PROPERTY_STATE_DONE = "done";
- private static final String TRACK_PROPERTY_BLOG_ID = "blog_id";
- private static final String TRACK_PROPERTY_VISIBLE = "visible";
-
- @Nullable private SitePickerAdapter mAdapter;
- @Nullable private SwipeToRefreshHelper mSwipeToRefreshHelper;
- @Nullable private ActionMode mActionMode;
- @Nullable private ActionMode mReblogActionMode;
- @Nullable private MenuItem mMenuEdit;
- @Nullable private MenuItem mMenuAdd;
- @Nullable private MenuItem mMenuSearch;
- @Nullable private SearchView mSearchView;
- private int mCurrentLocalId;
- @Nullable private SitePickerMode mSitePickerMode;
- @NonNull private final Debouncer mDebouncer = new Debouncer();
- @Nullable private SitePickerViewModel mViewModel;
-
- @NonNull private HashSet mSelectedPositions = new HashSet<>();
- private boolean mIsInEditMode;
-
- private boolean mShowMenuEnabled = false;
- private boolean mHideMenuEnabled = false;
-
-
- @Inject AccountStore mAccountStore;
- @Inject SiteStore mSiteStore;
- @Inject Dispatcher mDispatcher;
- @Inject StatsStore mStatsStore;
- @Inject ViewModelProvider.Factory mViewModelFactory;
- @Inject BuildConfigWrapper mBuildConfigWrapper;
-
- @Nullable private SitePickerActivityBinding mBinding = null;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mViewModel = new ViewModelProvider(this, mViewModelFactory).get(SitePickerViewModel.class);
-
- mBinding = SitePickerActivityBinding.inflate(getLayoutInflater());
- setContentView(mBinding.getRoot());
- restoreSavedInstanceState(savedInstanceState);
- setupActionBar(mBinding);
- setupRecycleView(mBinding);
-
- mSwipeToRefreshHelper = initSwipeToRefreshHelper(mBinding);
- if (savedInstanceState != null) {
- mSwipeToRefreshHelper.setRefreshing(savedInstanceState.getBoolean(KEY_REFRESHING, false));
- } else {
- AnalyticsTracker.track(Stat.SITE_SWITCHER_DISPLAYED);
- }
-
- mViewModel.getOnActionTriggered().observe(
- this,
- unitEvent -> unitEvent.applyIfNotHandled(action -> {
- switch (action.getActionType()) {
- case NAVIGATE_TO_STATE:
- if (!(mSitePickerMode != null && mSitePickerMode.isReblogMode())) break;
- switch (((NavigateToState) action).getNavigateState()) {
- case TO_SITE_SELECTED:
- mSitePickerMode = SitePickerMode.REBLOG_CONTINUE_MODE;
- if (getAdapter().getIsInSearchMode()) {
- disableSearchMode(mBinding);
- }
-
- if (mReblogActionMode == null) {
- startSupportActionMode(new ReblogActionModeCallback());
- }
-
- SiteRecord site = ((NavigateToState) action).getSiteForReblog();
- if (site != null && mReblogActionMode != null) {
- mReblogActionMode.setTitle(site.getBlogNameOrHomeURL());
- }
- break;
- case TO_NO_SITE_SELECTED:
- mSitePickerMode = SitePickerMode.REBLOG_SELECT_MODE;
- getAdapter().clearReblogSelection();
- break;
- }
- break;
- case CONTINUE_REBLOG_TO:
- if (!(mSitePickerMode != null && mSitePickerMode.isReblogMode())) break;
- SiteRecord siteToReblog = ((ContinueReblogTo) action).getSiteForReblog();
- if (siteToReblog != null) selectSiteAndFinish(siteToReblog);
- break;
- case ASK_FOR_SITE_SELECTION:
- if (!(mSitePickerMode != null && mSitePickerMode.isReblogMode())) break;
- if (BuildConfig.DEBUG) {
- throw new IllegalStateException(
- "SitePickerActivity > Selected site was null while attempting to reblog"
- );
- } else {
- AppLog.e(
- AppLog.T.READER,
- "SitePickerActivity > Selected site was null while attempting to reblog"
- );
- ToastUtils.showToast(this, R.string.site_picker_ask_site_select);
- }
- break;
- case SHOW_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY:
- WPJetpackIndividualPluginFragment.show(getSupportFragmentManager());
- break;
- }
- return null;
- }));
- // If the picker is already in editing mode from previous configuration, re-enable the editing mode.
- if (mIsInEditMode) {
- startEditingVisibility(mBinding);
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
- ActivityId.trackLastActivity(ActivityId.SITE_PICKER);
- }
-
- @Override
- protected void onSaveInstanceState(@NonNull Bundle outState) {
- outState.putInt(KEY_SITE_LOCAL_ID, mCurrentLocalId);
- outState.putBoolean(KEY_IS_IN_SEARCH_MODE, getAdapter().getIsInSearchMode());
- outState.putString(KEY_LAST_SEARCH, getAdapter().getLastSearch());
- if (mSwipeToRefreshHelper != null) {
- outState.putBoolean(KEY_REFRESHING, mSwipeToRefreshHelper.isRefreshing());
- } else {
- outState.putBoolean(KEY_REFRESHING, false);
- }
- outState.putSerializable(KEY_SITE_PICKER_MODE, mSitePickerMode);
-
- outState.putSerializable(KEY_SELECTED_POSITIONS, getAdapter().getSelectedPositions());
- outState.putBoolean(KEY_IS_IN_EDIT_MODE, mIsInEditMode);
- outState.putBoolean(KEY_IS_SHOW_MENU_ENABLED, mShowMenuEnabled);
- outState.putBoolean(KEY_IS_HIDE_MENU_ENABLED, mHideMenuEnabled);
-
- super.onSaveInstanceState(outState);
- }
-
- @Override
- public boolean onCreateOptionsMenu(@NonNull Menu menu) {
- super.onCreateOptionsMenu(menu);
- getMenuInflater().inflate(R.menu.site_picker, menu);
- mMenuSearch = menu.findItem(R.id.menu_search);
- mMenuEdit = menu.findItem(R.id.menu_edit);
- mMenuAdd = menu.findItem(R.id.menu_add);
- return true;
- }
-
- @Override
- public boolean onPrepareOptionsMenu(@NonNull Menu menu) {
- super.onPrepareOptionsMenu(menu);
- if (mBinding != null && mMenuSearch != null) {
- updateMenuItemVisibility();
- mSearchView = getSearchView(mMenuSearch);
- setupSearchView(mBinding, mMenuSearch, mSearchView);
- return true;
- } else {
- return false;
- }
- }
-
- private void updateMenuItemVisibility() {
- if (mMenuAdd == null || mMenuEdit == null || mMenuSearch == null) {
- return;
- }
-
- if (getAdapter().getIsInSearchMode()
- || mSitePickerMode == null
- || mSitePickerMode.isReblogMode()
- || mSitePickerMode.isBloggingPromptsMode()
- || mSitePickerMode == SitePickerMode.SIMPLE_MODE) {
- mMenuEdit.setVisible(false);
- mMenuAdd.setVisible(false);
- } else {
- // don't allow editing visibility unless there are multiple wp.com and jetpack sites
- mMenuEdit.setVisible(mSiteStore.getSitesAccessedViaWPComRestCount() > 1);
- mMenuAdd.setVisible(mBuildConfigWrapper.isSiteCreationEnabled());
- }
-
- // no point showing search if there aren't multiple blogs
- mMenuSearch.setVisible(mSiteStore.getSitesCount() > 1);
- }
-
- @Override
- public boolean onOptionsItemSelected(@NonNull MenuItem item) {
- int itemId = item.getItemId();
- if (itemId == android.R.id.home) {
- AnalyticsTracker.track(Stat.SITE_SWITCHER_DISMISSED);
- getOnBackPressedDispatcher().onBackPressed();
- return true;
- } else if (itemId == R.id.menu_edit) {
- if (mBinding != null) {
- AnalyticsTracker.track(Stat.SITE_SWITCHER_TOGGLED_EDIT_TAPPED,
- Collections.singletonMap(TRACK_PROPERTY_STATE, TRACK_PROPERTY_STATE_EDIT));
- startEditingVisibility(mBinding);
- return true;
- }
- } else if (itemId == R.id.menu_add) {
- AnalyticsTracker.track(Stat.SITE_SWITCHER_ADD_SITE_TAPPED);
- addSite(this, mAccountStore.hasAccessToken(), SiteCreationSource.MY_SITE);
- return true;
- } else if (itemId == R.id.continue_flow) {
- if (mViewModel != null) mViewModel.onContinueFlowSelected();
- }
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
-
- switch (requestCode) {
- case RequestCodes.ADD_ACCOUNT:
- case RequestCodes.CREATE_SITE:
- if (resultCode == RESULT_OK) {
- debounceLoadSites();
- if (data == null) {
- data = new Intent();
- }
- if (data.getBooleanExtra(KEY_SITE_CREATED_BUT_NOT_FETCHED, false)) {
- if (mBinding != null) {
- showSiteCreatedButNotFetchedSnackbar(mBinding);
- }
- } else {
- data.putExtra(WPMainActivity.ARG_CREATE_SITE, RequestCodes.CREATE_SITE);
- setResult(resultCode, data);
- finish();
- }
- }
- break;
- }
-
- // Enable the block editor on sites created on mobile
- if (requestCode == RequestCodes.CREATE_SITE) {
- if (data != null) {
- int newSiteLocalID = data.getIntExtra(
- SitePickerActivity.KEY_SITE_LOCAL_ID,
- SelectedSiteRepository.UNAVAILABLE
- );
- SiteUtils.enableBlockEditorOnSiteCreation(mDispatcher, mSiteStore, newSiteLocalID);
- }
- }
- }
-
- @Override
- protected void onStop() {
- mDispatcher.unregister(this);
- mDebouncer.shutdown();
- super.onStop();
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- mDispatcher.register(this);
- }
-
- @SuppressWarnings("unused")
- @Subscribe(threadMode = ThreadMode.MAIN)
- public void onSiteRemoved(OnSiteRemoved event) {
- if (!event.isError()) {
- debounceLoadSites();
- } else {
- // shouldn't happen
- AppLog.e(AppLog.T.DB, "Encountered unexpected error while attempting to remove site: " + event.error);
- ToastUtils.showToast(this, R.string.site_picker_remove_site_error);
- }
- }
-
- @SuppressWarnings("unused")
- @Subscribe(threadMode = ThreadMode.MAIN)
- public void onSiteChanged(OnSiteChanged event) {
- if (mSwipeToRefreshHelper != null && mSwipeToRefreshHelper.isRefreshing()) {
- mSwipeToRefreshHelper.setRefreshing(false);
- }
- debounceLoadSites();
- }
-
- private void debounceLoadSites() {
- mDebouncer.debounce(Void.class, () -> {
- if (!isFinishing()) {
- getAdapter().loadSites();
- }
- }, 200, TimeUnit.MILLISECONDS);
- }
-
- @NonNull
- private SwipeToRefreshHelper initSwipeToRefreshHelper(@NonNull SitePickerActivityBinding binding) {
- return buildSwipeToRefreshHelper(
- binding.ptrLayout,
- () -> {
- if (isFinishing()) {
- return;
- }
- if (!NetworkUtils.checkConnection(this) || !mAccountStore.hasAccessToken()) {
- if (mSwipeToRefreshHelper != null) mSwipeToRefreshHelper.setRefreshing(false);
- return;
- }
- mDispatcher.dispatch(SiteActionBuilder.newFetchSitesAction(SiteUtils.getFetchSitesPayload()));
- }
- );
- }
-
- private void setupRecycleView(@NonNull SitePickerActivityBinding binding) {
- binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
- binding.recyclerView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
-
- binding.recyclerView.setItemAnimator(
- (mSitePickerMode != null && mSitePickerMode.isReblogMode()) ? new DefaultItemAnimator() : null
- );
-
- binding.recyclerView.setAdapter(getAdapter());
-
- binding.actionableEmptyView.updateLayoutForSearch(true, 0);
- binding.recyclerView.setEmptyView(binding.actionableEmptyView);
- }
-
- @SuppressWarnings("unchecked")
- private void restoreSavedInstanceState(@Nullable Bundle savedInstanceState) {
- boolean isInSearchMode = false;
- String lastSearch = "";
-
- if (savedInstanceState != null) {
- mCurrentLocalId = savedInstanceState.getInt(KEY_SITE_LOCAL_ID);
- isInSearchMode = savedInstanceState.getBoolean(KEY_IS_IN_SEARCH_MODE);
- lastSearch = savedInstanceState.getString(KEY_LAST_SEARCH);
- mSitePickerMode = (SitePickerMode) savedInstanceState.getSerializable(KEY_SITE_PICKER_MODE);
-
- mSelectedPositions = (HashSet) savedInstanceState.getSerializable(KEY_SELECTED_POSITIONS);
- mIsInEditMode = savedInstanceState.getBoolean(KEY_IS_IN_EDIT_MODE);
- mShowMenuEnabled = savedInstanceState.getBoolean(KEY_IS_SHOW_MENU_ENABLED);
- mHideMenuEnabled = savedInstanceState.getBoolean(KEY_IS_HIDE_MENU_ENABLED);
- } else if (getIntent() != null) {
- mCurrentLocalId = getIntent().getIntExtra(KEY_SITE_LOCAL_ID, SelectedSiteRepository.UNAVAILABLE);
- mSitePickerMode = (SitePickerMode) getIntent().getSerializableExtra(KEY_SITE_PICKER_MODE);
- }
-
- mAdapter = createNewAdapter(lastSearch, isInSearchMode);
- }
-
- private void setupActionBar(@NonNull SitePickerActivityBinding binding) {
- setSupportActionBar(binding.toolbarMain);
- ActionBar actionBar = getSupportActionBar();
- if (actionBar != null) {
- actionBar.setHomeButtonEnabled(true);
- actionBar.setDisplayHomeAsUpEnabled(true);
- actionBar.setTitle(R.string.site_picker_title);
-
- if (mSitePickerMode == SitePickerMode.REBLOG_CONTINUE_MODE && mReblogActionMode == null) {
- if (mViewModel != null) mViewModel.onRefreshReblogActionMode();
- }
- }
- }
-
- private void setIsInSearchModeAndSetNewAdapter(boolean isInSearchMode) {
- String lastSearch = getAdapter().getLastSearch();
- mAdapter = createNewAdapter(lastSearch, isInSearchMode);
- }
-
- @NonNull
- private SitePickerAdapter getAdapter() {
- if (mAdapter == null) {
- mAdapter = createNewAdapter("", false);
- }
- return mAdapter;
- }
-
- @NonNull
- private SitePickerAdapter createNewAdapter(String lastSearch, boolean isInSearchMode) {
- SitePickerAdapter adapter = new SitePickerAdapter(
- this,
- R.layout.site_picker_listitem,
- mCurrentLocalId,
- lastSearch,
- isInSearchMode,
- new SitePickerAdapter.OnDataLoadedListener() {
- @Override
- public void onBeforeLoad(boolean isEmpty) {
- if (mBinding != null && isEmpty) {
- showProgress(mBinding, true);
- }
- }
-
- @Override
- public void onAfterLoad() {
- if (mBinding != null && mViewModel != null) {
- showProgress(mBinding, false);
- if (mSitePickerMode == SitePickerMode.REBLOG_CONTINUE_MODE && !isInSearchMode) {
- getAdapter().findAndSelect(mCurrentLocalId);
- int scrollPos = getAdapter().getItemPosByLocalId(mCurrentLocalId);
- if (scrollPos > -1) {
- mBinding.recyclerView.scrollToPosition(scrollPos);
- }
- }
- mViewModel.onSiteListLoaded();
- }
- }
- },
- mSitePickerMode,
- mIsInEditMode);
- adapter.setOnSiteClickListener(this);
- adapter.setOnSelectedCountChangedListener(this);
- return adapter;
- }
-
- private void saveSiteVisibility(SiteRecord siteRecord) {
- Set siteRecords = new HashSet<>();
- siteRecords.add(siteRecord);
- saveSitesVisibility(siteRecords);
- }
-
- private void saveSitesVisibility(Set changeSet) {
- boolean skippedCurrentSite = false;
- String currentSiteName = null;
- SiteList hiddenSites = getAdapter().getHiddenSites();
- List siteList = new ArrayList<>();
- for (SiteRecord siteRecord : changeSet) {
- SiteModel siteModel = mSiteStore.getSiteByLocalId(siteRecord.getLocalId());
- if (siteModel != null) {
- if (hiddenSites.contains(siteRecord)) {
- if (siteRecord.getLocalId() == mCurrentLocalId) {
- skippedCurrentSite = true;
- currentSiteName = siteRecord.getBlogNameOrHomeURL();
- continue;
- }
- siteModel.setIsVisible(false);
- // Remove stats data for hidden sites
- mStatsStore.deleteSiteData(siteModel);
- } else {
- siteModel.setIsVisible(true);
- }
- // Save the site
- mDispatcher.dispatch(SiteActionBuilder.newUpdateSiteAction(siteModel));
- siteList.add(siteModel);
- trackVisibility(Long.toString(siteModel.getSiteId()), siteModel.isVisible());
- }
- }
-
- updateVisibilityOfSitesOnRemote(siteList);
-
- // let user know the current site wasn't hidden
- if (skippedCurrentSite) {
- String cantHideCurrentSite = getString(R.string.site_picker_cant_hide_current_site);
- ToastUtils.showToast(this,
- String.format(cantHideCurrentSite, currentSiteName),
- ToastUtils.Duration.LONG);
- }
- }
-
- private void updateVisibilityOfSitesOnRemote(List siteList) {
- // Example json format for the request: {"sites":{"100001":{"visible":false}}}
- JSONObject jsonObject = new JSONObject();
- try {
- JSONObject sites = new JSONObject();
- for (SiteModel siteModel : siteList) {
- JSONObject visible = new JSONObject();
- visible.put("visible", siteModel.isVisible());
- sites.put(Long.toString(siteModel.getSiteId()), visible);
- }
- jsonObject.put("sites", sites);
- } catch (JSONException e) {
- AppLog.e(AppLog.T.API, "Could not build me/sites json object");
- }
-
- if (jsonObject.length() == 0) {
- return;
- }
-
- WordPress.getRestClientUtilsV1_1().post("me/sites", jsonObject, null,
- response -> AppLog.v(AppLog.T.API, "Site visibility successfully updated"),
- volleyError -> AppLog
- .e(AppLog.T.API, "An error occurred while updating site visibility: " + volleyError));
- }
-
- private void updateActionModeTitle() {
- if (mActionMode != null) {
- int numSelected = getAdapter().getNumSelected();
- String cabSelected = getString(R.string.cab_selected);
- mActionMode.setTitle(String.format(cabSelected, numSelected));
- }
- }
-
- @NonNull
- private SearchView getSearchView(@NonNull MenuItem menuSearch) {
- SearchView searchView = (SearchView) menuSearch.getActionView();
- searchView.setMaxWidth(Integer.MAX_VALUE);
- return searchView;
- }
-
- private void setupSearchView(
- @NonNull SitePickerActivityBinding binding,
- @NonNull MenuItem menuSearch,
- @NonNull SearchView searchView
- ) {
- menuSearch.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
- @Override
- public boolean onMenuItemActionExpand(@NonNull MenuItem item) {
- if (!getAdapter().getIsInSearchMode() && mMenuEdit != null && mMenuAdd != null) {
- enableSearchMode(binding);
- mMenuEdit.setVisible(false);
- mMenuAdd.setVisible(false);
-
- searchView.setOnQueryTextListener(SitePickerActivity.this);
- }
-
- return true;
- }
-
- @Override
- public boolean onMenuItemActionCollapse(@NonNull MenuItem item) {
- disableSearchMode(binding);
- searchView.setOnQueryTextListener(null);
- return true;
- }
- });
-
- setQueryIfInSearch(menuSearch, searchView);
- }
-
- private void setQueryIfInSearch(
- @NonNull MenuItem menuSearch,
- @NonNull SearchView searchView
- ) {
- if (getAdapter().getIsInSearchMode()) {
- menuSearch.expandActionView();
- searchView.setOnQueryTextListener(SitePickerActivity.this);
- searchView.setQuery(getAdapter().getLastSearch(), true);
- }
- }
-
- private void enableSearchMode(@NonNull SitePickerActivityBinding binding) {
- setIsInSearchModeAndSetNewAdapter(true);
- binding.recyclerView.swapAdapter(getAdapter(), true);
- }
-
- private void disableSearchMode(@NonNull SitePickerActivityBinding binding) {
- hideSoftKeyboard();
- setIsInSearchModeAndSetNewAdapter(false);
- binding.recyclerView.swapAdapter(getAdapter(), true);
- invalidateOptionsMenu();
- }
-
- private void hideSoftKeyboard() {
- if (!DeviceUtils.getInstance().hasHardwareKeyboard(this)) {
- ActivityUtils.hideKeyboardForced(mSearchView);
- }
- }
-
- @Override
- public void onSelectedCountChanged(int numSelected) {
- if (mActionMode != null) {
- updateActionModeTitle();
- mShowMenuEnabled = getAdapter().getNumHiddenSelected() > 0;
- mHideMenuEnabled = getAdapter().getNumVisibleSelected() > 0;
- mActionMode.invalidate();
- }
- }
-
- @Override
- public boolean onSiteLongClick(final SiteRecord siteRecord) {
- final SiteModel site = mSiteStore.getSiteByLocalId(siteRecord.getLocalId());
- if (site == null) {
- return false;
- }
- if (site.isUsingWpComRestApi()) {
- if (mBinding == null || mActionMode != null) {
- return false;
- }
- startEditingVisibility(mBinding);
- } else {
- showRemoveSelfHostedSiteDialog(site);
- }
-
- return true;
- }
-
- @Override
- public void onSiteClick(SiteRecord siteRecord) {
- if (mSitePickerMode != null && mSitePickerMode.isReblogMode() && mViewModel != null) {
- mCurrentLocalId = siteRecord.getLocalId();
- mViewModel.onSiteForReblogSelected(siteRecord);
- } else if (mActionMode == null) {
- selectSiteAndFinish(siteRecord);
- }
- }
-
- @Override
- public boolean onQueryTextSubmit(@NonNull String s) {
- hideSoftKeyboard();
- return true;
- }
-
- @Override
- public boolean onQueryTextChange(@NonNull String s) {
- if (getAdapter().getIsInSearchMode()) {
- AnalyticsTracker.track(Stat.SITE_SWITCHER_SEARCH_PERFORMED);
- getAdapter().setLastSearch(s);
- getAdapter().searchSites(s);
- }
- return true;
- }
-
- public void showProgress(@NonNull SitePickerActivityBinding binding, boolean show) {
- binding.progress.setVisibility(show ? View.VISIBLE : View.GONE);
- }
-
- private void selectSiteAndFinish(@NonNull SiteRecord siteRecord) {
- hideSoftKeyboard();
- AppPrefs.addRecentlyPickedSiteId(siteRecord.getLocalId());
- setResult(RESULT_OK, new Intent().putExtra(KEY_SITE_LOCAL_ID, siteRecord.getLocalId()));
- // If the site is hidden, make sure to make it visible
- if (siteRecord.isHidden()) {
- siteRecord.setHidden(false);
- saveSiteVisibility(siteRecord);
- }
- finish();
- }
-
- private final class ReblogActionModeCallback implements ActionMode.Callback {
- @Override
- public boolean onCreateActionMode(@NonNull ActionMode mode, @NonNull Menu menu) {
- mReblogActionMode = mode;
- mode.getMenuInflater().inflate(R.menu.site_picker_reblog_action_mode, menu);
- return true;
- }
-
- @Override
- public boolean onPrepareActionMode(@NonNull ActionMode mode, @NonNull Menu menu) {
- return true;
- }
-
- @Override
- public boolean onActionItemClicked(@NonNull ActionMode mode, @NonNull MenuItem item) {
- int itemId = item.getItemId();
-
- if (itemId == R.id.continue_flow && mViewModel != null) {
- mViewModel.onContinueFlowSelected();
- }
-
- return true;
- }
-
- @Override
- public void onDestroyActionMode(@NonNull ActionMode mode) {
- if (mViewModel != null) mViewModel.onReblogActionBackSelected();
- mReblogActionMode = null;
- }
- }
-
- private final class ActionModeCallback implements ActionMode.Callback {
- private boolean mHasChanges;
- private Set mChangeSet;
-
- @Override
- public boolean onCreateActionMode(@NonNull ActionMode mode, @NonNull Menu menu) {
- mActionMode = mode;
- mHasChanges = false;
- mChangeSet = new HashSet<>();
- updateActionModeTitle();
- mode.getMenuInflater().inflate(R.menu.site_picker_action_mode, menu);
- return true;
- }
-
- @Override
- public boolean onPrepareActionMode(@NonNull ActionMode mode, @NonNull Menu menu) {
- MenuItem mnuShow = menu.findItem(R.id.menu_show);
- mnuShow.setEnabled(mShowMenuEnabled);
-
- MenuItem mnuHide = menu.findItem(R.id.menu_hide);
- mnuHide.setEnabled(mHideMenuEnabled);
-
- MenuItem mnuSelectAll = menu.findItem(R.id.menu_select_all);
- mnuSelectAll.setEnabled(getAdapter().getNumSelected() != getAdapter().getItemCount());
-
- MenuItem mnuDeselectAll = menu.findItem(R.id.menu_deselect_all);
- mnuDeselectAll.setEnabled(getAdapter().getNumSelected() > 0);
-
- return true;
- }
-
- @Override
- public boolean onActionItemClicked(@NonNull ActionMode mode, @NonNull MenuItem item) {
- int itemId = item.getItemId();
- if (itemId == R.id.menu_show) {
- Set changeSet = getAdapter().setVisibilityForSelectedSites(true);
- mChangeSet.addAll(changeSet);
- mHasChanges = true;
- if (mActionMode != null) mActionMode.finish();
- } else if (itemId == R.id.menu_hide) {
- Set changeSet = getAdapter().setVisibilityForSelectedSites(false);
- mChangeSet.addAll(changeSet);
- mHasChanges = true;
- if (mActionMode != null) mActionMode.finish();
- } else if (itemId == R.id.menu_select_all) {
- getAdapter().selectAll();
- } else if (itemId == R.id.menu_deselect_all) {
- getAdapter().deselectAll();
- }
- return true;
- }
-
- @Override
- public void onDestroyActionMode(@NonNull ActionMode actionMode) {
- if (mHasChanges) {
- saveSitesVisibility(mChangeSet);
- }
- AnalyticsTracker.track(Stat.SITE_SWITCHER_TOGGLED_EDIT_TAPPED,
- Collections.singletonMap(TRACK_PROPERTY_STATE, TRACK_PROPERTY_STATE_DONE));
- getAdapter().setEnableEditMode(false, mSelectedPositions);
- mActionMode = null;
- mIsInEditMode = false;
- mSelectedPositions.clear();
- }
- }
-
- public static void addSite(FragmentActivity activity, boolean hasAccessToken, SiteCreationSource source) {
- if (hasAccessToken) {
- if (!BuildConfig.ENABLE_ADD_SELF_HOSTED_SITE) {
- ActivityLauncher.newBlogForResult(activity, source);
- } else {
- // user is signed into wordpress app, so use the dialog to enable choosing whether to
- // create a new wp.com blog or add a self-hosted one
- showAddSiteDialog(activity, source);
- }
- } else {
- // user doesn't have an access token, so simply enable adding self-hosted
- ActivityLauncher.addSelfHostedSiteForResult(activity);
- }
- }
-
- private static void showAddSiteDialog(FragmentActivity activity, SiteCreationSource source) {
- DialogFragment dialog = new AddSiteDialog();
- Bundle args = new Bundle();
- args.putString(ARG_SITE_CREATION_SOURCE, source.getLabel());
- dialog.setArguments(args);
- dialog.show(activity.getSupportFragmentManager(), AddSiteDialog.ADD_SITE_DIALOG_TAG);
- }
-
- /**
- * Dialog which appears after user taps "Add site" - enables choosing whether to create
- * a new wp.com blog or add an existing self-hosted one.
- *
- * @apiNote Must pass {@link SitePickerActivity#ARG_SITE_CREATION_SOURCE} in arguments when creating this dialog.
- */
- public static class AddSiteDialog extends DialogFragment {
- static final String ADD_SITE_DIALOG_TAG = "add_site_dialog";
-
- @NonNull
- @Override
- public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
- SiteCreationSource source =
- SiteCreationSource.fromString(requireArguments().getString(ARG_SITE_CREATION_SOURCE));
- CharSequence[] items =
- {getString(R.string.site_picker_create_wpcom),
- getString(R.string.site_picker_add_self_hosted)};
- MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
- builder.setTitle(R.string.site_picker_add_site);
- builder.setAdapter(
- new ArrayAdapter<>(requireActivity(), R.layout.add_new_site_dialog_item, R.id.text, items),
- (dialog, which) -> {
- if (which == 0) {
- ActivityLauncher.newBlogForResult(getActivity(), source);
- } else {
- ActivityLauncher.addSelfHostedSiteForResult(getActivity());
- }
- });
-
- AnalyticsTracker.track(Stat.ADD_SITE_ALERT_DISPLAYED, Collections.singletonMap(SOURCE, source.getLabel()));
- return builder.create();
- }
- }
-
- private void startEditingVisibility(@NonNull SitePickerActivityBinding binding) {
- binding.recyclerView.setItemAnimator(new DefaultItemAnimator());
- getAdapter().setEnableEditMode(true, mSelectedPositions);
- startSupportActionMode(new ActionModeCallback());
- mIsInEditMode = true;
- }
-
- private void showRemoveSelfHostedSiteDialog(@NonNull final SiteModel site) {
- MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(this);
- dialogBuilder.setTitle(getResources().getText(R.string.remove_account));
- dialogBuilder.setMessage(getResources().getText(R.string.sure_to_remove_account));
- dialogBuilder.setPositiveButton(getResources().getText(R.string.yes),
- (dialog, whichButton) -> mDispatcher.dispatch(SiteActionBuilder.newRemoveSiteAction(site)));
- dialogBuilder.setNegativeButton(getResources().getText(R.string.no), null);
- dialogBuilder.setCancelable(false);
- dialogBuilder.create().show();
- }
-
- private void showSiteCreatedButNotFetchedSnackbar(@NonNull SitePickerActivityBinding binding) {
- int duration = AccessibilityUtils
- .getSnackbarDuration(this, getResources().getInteger(R.integer.site_creation_snackbar_duration));
- String message = getString(R.string.site_created_but_not_fetched_snackbar_message);
- WPDialogSnackbar.make(binding.coordinatorLayout, message, duration).show();
- }
-
- private void trackVisibility(String blogId, boolean isVisible) {
- Map props = new HashMap<>();
- props.put(TRACK_PROPERTY_BLOG_ID, blogId);
- props.put(TRACK_PROPERTY_VISIBLE, isVisible ? "1" : "0");
- AnalyticsTracker.track(Stat.SITE_SWITCHER_TOGGLE_BLOG_VISIBLE, props);
- }
-}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerAdapter.java
index 3fa52d583828..803bc477f6aa 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerAdapter.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerAdapter.java
@@ -3,7 +3,6 @@
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.AsyncTask;
-import android.text.TextUtils;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
@@ -23,24 +22,20 @@
import org.wordpress.android.fluxc.model.SiteModel;
import org.wordpress.android.fluxc.store.AccountStore;
import org.wordpress.android.fluxc.store.SiteStore;
+import org.wordpress.android.ui.main.utils.SiteRecordUtil;
import org.wordpress.android.ui.mysite.SelectedSiteRepository;
import org.wordpress.android.ui.prefs.AppPrefs;
import org.wordpress.android.util.AppLog;
import org.wordpress.android.util.BuildConfigWrapper;
-import org.wordpress.android.util.extensions.ContextExtensionsKt;
import org.wordpress.android.util.DisplayUtils;
-import org.wordpress.android.util.SiteUtils;
import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.extensions.ContextExtensionsKt;
import org.wordpress.android.util.extensions.ViewExtensionsKt;
-import org.wordpress.android.util.image.BlavatarShape;
import org.wordpress.android.util.image.ImageManager;
-import org.wordpress.android.util.image.ImageType;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashSet;
import java.util.List;
-import java.util.Locale;
import java.util.Set;
import javax.inject.Inject;
@@ -65,7 +60,7 @@ public interface OnDataLoadedListener {
public interface ViewHolderHandler {
T onCreateViewHolder(LayoutInflater layoutInflater, ViewGroup parent, boolean attachToRoot);
- void onBindViewHolder(T holder, SiteList sites);
+ void onBindViewHolder(T holder, List sites);
}
/**
@@ -73,10 +68,10 @@ public interface ViewHolderHandler {
*/
public enum SitePickerMode {
DEFAULT_MODE,
- REBLOG_SELECT_MODE,
- REBLOG_CONTINUE_MODE,
- BLOGGING_PROMPTS_MODE,
- SIMPLE_MODE;
+ REBLOG_SELECT_MODE, // used when the site is not selected yet (first step of reblogging)
+ REBLOG_CONTINUE_MODE, // used when the site is selected (second step of reblogging)
+ BLOGGING_PROMPTS_MODE, // used when choose a site for prompts
+ SIMPLE_MODE; // used when select a non-self-hosted site for purchasing a domain
public boolean isReblogMode() {
return this == REBLOG_SELECT_MODE || this == REBLOG_CONTINUE_MODE;
@@ -89,9 +84,9 @@ public boolean isBloggingPromptsMode() {
private final @LayoutRes int mItemLayoutReourceId;
- private static int mBlavatarSz;
+ static int mBlavatarSz;
- private SiteList mSites = new SiteList();
+ @NonNull private List mSites = new ArrayList<>();
private final int mCurrentLocalId;
private int mSelectedLocalId;
@@ -110,7 +105,7 @@ public boolean isBloggingPromptsMode() {
private final boolean mShowAndReturn;
private boolean mShowSelfHostedSites = true;
private String mLastSearch;
- private SiteList mAllSites;
+ private List mAllSites = new ArrayList<>();
@Nullable private final ArrayList mIgnoreSitesIds;
private OnSiteClickListener mSiteSelectedListener;
@@ -219,7 +214,6 @@ public SitePickerAdapter(Context context,
setHasStableIds(true);
mLastSearch = StringUtils.notNullStr(lastSearch);
- mAllSites = new SiteList();
mIsInSearchMode = isInSearchMode;
mItemLayoutReourceId = itemLayoutResourceId;
mCurrentLocalId = currentLocalBlogId;
@@ -281,7 +275,7 @@ public long getItemId(int position) {
} else if (viewType == VIEW_TYPE_FOOTER) {
return -2;
} else {
- return getItem(position).mLocalId;
+ return getItem(position).getLocalId();
}
}
@@ -308,6 +302,7 @@ void setOnSelectedCountChangedListener(OnSelectedCountChangedListener listener)
mSelectedCountListener = listener;
}
+ @SuppressLint("NotifyDataSetChanged")
public void setOnSiteClickListener(OnSiteClickListener listener) {
mSiteSelectedListener = listener;
notifyDataSetChanged();
@@ -344,24 +339,24 @@ public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder,
final SiteViewHolder holder = (SiteViewHolder) viewHolder;
holder.mTxtTitle.setText(site.getBlogNameOrHomeURL());
- holder.mTxtDomain.setText(site.mHomeURL);
- mImageManager.loadImageWithCorners(holder.mImgBlavatar, site.getBlavatarType(), site.mBlavatarUrl,
+ holder.mTxtDomain.setText(site.getHomeURL());
+ mImageManager.loadImageWithCorners(holder.mImgBlavatar, site.getBlavatarType(), site.getBlavatarUrl(),
DisplayUtils.dpToPx(holder.itemView.getContext(), 4));
- if ((site.mLocalId == mCurrentLocalId && !mIsMultiSelectEnabled
+ if ((site.getLocalId() == mCurrentLocalId && !mIsMultiSelectEnabled
&& mSitePickerMode == SitePickerMode.DEFAULT_MODE)
|| (mIsMultiSelectEnabled && isItemSelected(position))
- || (mSitePickerMode == SitePickerMode.REBLOG_CONTINUE_MODE && mSelectedLocalId == site.mLocalId)) {
+ || (mSitePickerMode == SitePickerMode.REBLOG_CONTINUE_MODE && mSelectedLocalId == site.getLocalId())) {
holder.mLayoutContainer.setBackgroundColor(mSelectedItemBackground);
} else {
holder.mLayoutContainer.setBackground(null);
}
// different styling for visible/hidden sites
- if (holder.mIsSiteHidden == null || holder.mIsSiteHidden != site.mIsHidden) {
- holder.mIsSiteHidden = site.mIsHidden;
- holder.mTxtTitle.setAlpha(site.mIsHidden ? mDisabledSiteOpacity : 1f);
- holder.mImgBlavatar.setAlpha(site.mIsHidden ? mDisabledSiteOpacity : 1f);
+ if (holder.mIsSiteHidden == null || holder.mIsSiteHidden != site.isHidden()) {
+ holder.mIsSiteHidden = site.isHidden();
+ holder.mTxtTitle.setAlpha(site.isHidden() ? mDisabledSiteOpacity : 1f);
+ holder.mImgBlavatar.setAlpha(site.isHidden() ? mDisabledSiteOpacity : 1f);
}
if (holder.mItemDivider != null) {
@@ -370,10 +365,10 @@ public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder,
}
if (holder.mDivider != null) {
// only show divider after last recent pick
- boolean showDivider = site.mIsRecentPick
+ boolean showDivider = site.isRecentPick()
&& !mIsInSearchMode
&& position < getItemCount() - 1
- && !getItem(position + 1).mIsRecentPick;
+ && !getItem(position + 1).isRecentPick();
holder.mDivider.setVisibility(showDivider ? View.VISIBLE : View.GONE);
}
@@ -386,7 +381,7 @@ && position < getItemCount() - 1
} else if (mSiteSelectedListener != null) {
if (mSitePickerMode.isReblogMode()) {
mSitePickerMode = SitePickerMode.REBLOG_CONTINUE_MODE;
- mSelectedLocalId = site.mLocalId;
+ mSelectedLocalId = site.getLocalId();
selectSingleItem(clickedPosition);
}
@@ -439,6 +434,7 @@ private void selectSingleItem(final int newItemPosition) {
notifyItemChanged(mSelectedItemPos);
}
+ @SuppressLint("NotifyDataSetChanged")
public void setSingleItemSelectionEnabled(final boolean enabled) {
if (enabled != mIsSingleItemSelectionEnabled) {
mIsSingleItemSelectionEnabled = enabled;
@@ -447,18 +443,18 @@ public void setSingleItemSelectionEnabled(final boolean enabled) {
}
public void findAndSelect(final int lastUsedBlogLocalId) {
- int positionInSitesArray = mSites.indexOfSiteId(lastUsedBlogLocalId);
+ int positionInSitesArray = SiteRecordUtil.indexOf(mSites, lastUsedBlogLocalId);
if (positionInSitesArray != -1) {
selectSingleItem(positionInSitesArray + getPositionOffset());
}
}
public int getSelectedItemLocalId() {
- return mSites.size() != 0 ? getItem(mSelectedItemPos).mLocalId : -1;
+ return mSites.size() != 0 ? getItem(mSelectedItemPos).getLocalId() : -1;
}
public int getItemPosByLocalId(int localId) {
- int positionInSitesArray = mSites.indexOfSiteId(localId);
+ int positionInSitesArray = SiteRecordUtil.indexOf(mSites, localId);
return mSites.size() != 0 && positionInSitesArray > -1 ? positionInSitesArray : -1;
}
@@ -475,9 +471,10 @@ boolean getIsInSearchMode() {
return mIsInSearchMode;
}
+ @SuppressLint("NotifyDataSetChanged")
void searchSites(String searchText) {
mLastSearch = searchText;
- mSites = filteredSitesByText(mAllSites);
+ mSites = SiteRecordUtil.filteredSites(mAllSites, searchText);
notifyDataSetChanged();
}
@@ -531,7 +528,7 @@ int getNumSelected() {
int getNumHiddenSelected() {
int numHidden = 0;
for (Integer i : mSelectedPositions) {
- if (isValidPosition(i) && mSites.get(i).mIsHidden) {
+ if (isValidPosition(i) && mSites.get(i).isHidden()) {
numHidden++;
}
}
@@ -541,7 +538,7 @@ int getNumHiddenSelected() {
int getNumVisibleSelected() {
int numVisible = 0;
for (Integer i : mSelectedPositions) {
- if (i < mSites.size() && !mSites.get(i).mIsHidden) {
+ if (i < mSites.size() && !mSites.get(i).isHidden()) {
numVisible++;
}
}
@@ -573,6 +570,7 @@ private void setItemSelected(int position, boolean isSelected) {
}
}
+ @SuppressLint("NotifyDataSetChanged")
void selectAll() {
if (mSelectedPositions.size() == mSites.size()) {
return;
@@ -589,6 +587,7 @@ void selectAll() {
}
}
+ @SuppressLint("NotifyDataSetChanged")
void deselectAll() {
if (mSelectedPositions.size() == 0) {
return;
@@ -602,14 +601,15 @@ void deselectAll() {
}
}
+ @SuppressLint("NotifyDataSetChanged")
void clearReblogSelection() {
mSitePickerMode = SitePickerMode.REBLOG_SELECT_MODE;
notifyDataSetChanged();
}
@NonNull
- private SiteList getSelectedSites() {
- SiteList sites = new SiteList();
+ private List getSelectedSites() {
+ ArrayList sites = new ArrayList<>();
if (!mIsMultiSelectEnabled) {
return sites;
}
@@ -627,10 +627,10 @@ public HashSet getSelectedPositions() {
return mSelectedPositions;
}
- SiteList getHiddenSites() {
- SiteList hiddenSites = new SiteList();
+ List getHiddenSites() {
+ ArrayList hiddenSites = new ArrayList<>();
for (SiteRecord site : mSites) {
- if (site.mIsHidden) {
+ if (site.isHidden()) {
hiddenSites.add(site);
}
}
@@ -638,23 +638,24 @@ SiteList getHiddenSites() {
return hiddenSites;
}
+ @SuppressLint("NotifyDataSetChanged")
Set setVisibilityForSelectedSites(boolean makeVisible) {
- SiteList sites = getSelectedSites();
+ List sites = getSelectedSites();
Set changeSet = new HashSet<>();
if (sites.size() > 0) {
ArrayList recentIds = AppPrefs.getRecentlyPickedSiteIds();
int selectedSiteLocalId = mSelectedSiteRepository.getSelectedSiteLocalId();
for (SiteRecord site : sites) {
- int index = mAllSites.indexOfSite(site);
+ int index = SiteRecordUtil.indexOf(mAllSites, site);
if (index > -1) {
SiteRecord siteRecord = mAllSites.get(index);
- if (siteRecord.mIsHidden == makeVisible) {
+ if (siteRecord.isHidden() == makeVisible) {
changeSet.add(siteRecord);
- siteRecord.mIsHidden = !makeVisible;
+ siteRecord.setHidden(!makeVisible);
if (!makeVisible
- && siteRecord.mLocalId != selectedSiteLocalId
- && recentIds.contains(siteRecord.mLocalId)) {
- AppPrefs.removeRecentlyPickedSiteId(siteRecord.mLocalId);
+ && siteRecord.getLocalId() != selectedSiteLocalId
+ && recentIds.contains(siteRecord.getLocalId())) {
+ AppPrefs.removeRecentlyPickedSiteId(siteRecord.getLocalId());
}
}
}
@@ -673,31 +674,14 @@ void loadSites() {
new LoadSitesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
- private SiteList filteredSitesByTextIfInSearchMode(SiteList sites) {
+ private List filteredSitesByTextIfInSearchMode(List sites) {
if (!mIsInSearchMode) {
return sites;
} else {
- return filteredSitesByText(sites);
+ return SiteRecordUtil.filteredSites(sites, mLastSearch);
}
}
- private SiteList filteredSitesByText(SiteList sites) {
- SiteList filteredSiteList = new SiteList();
-
- for (int i = 0; i < sites.size(); i++) {
- SiteRecord record = sites.get(i);
- String siteNameLowerCase = record.mBlogName.toLowerCase(Locale.getDefault());
- String hostNameLowerCase = record.mHomeURL.toLowerCase(Locale.ROOT);
-
- if (siteNameLowerCase.contains(mLastSearch.toLowerCase(Locale.getDefault())) || hostNameLowerCase
- .contains(mLastSearch.toLowerCase(Locale.ROOT))) {
- filteredSiteList.add(record);
- }
- }
-
- return filteredSiteList;
- }
-
public List getBlogsForCurrentView() {
if (mSitePickerMode.isReblogMode() || mSitePickerMode.isBloggingPromptsMode()) {
// If we are reblogging we only want to select or search into the WPCom visible sites.
@@ -727,7 +711,7 @@ public List getBlogsForCurrentView() {
*/
@SuppressWarnings("deprecation")
@SuppressLint("StaticFieldLeak")
- private class LoadSitesTask extends AsyncTask {
+ private class LoadSitesTask extends AsyncTask[]> {
@Override
protected void onPreExecute() {
super.onPreExecute();
@@ -741,7 +725,7 @@ protected void onCancelled() {
}
@Override
- protected SiteList[] doInBackground(Void... params) {
+ protected List[] doInBackground(Void... params) {
List siteModels = getBlogsForCurrentView();
if (mIgnoreSitesIds != null) {
@@ -754,20 +738,11 @@ protected SiteList[] doInBackground(Void... params) {
siteModels = unignoredSiteModels;
}
- SiteList sites = new SiteList(siteModels);
+ List sites = SiteRecordUtil.createRecords(siteModels);
// sort primary blog to the top, otherwise sort by blog/host
final long primaryBlogId = mAccountStore.getAccount().getPrimarySiteId();
- Collections.sort(sites, (site1, site2) -> {
- if (primaryBlogId > 0 && !mIsInSearchMode) {
- if (site1.mSiteId == primaryBlogId) {
- return -1;
- } else if (site2.mSiteId == primaryBlogId) {
- return 1;
- }
- }
- return site1.getBlogNameOrHomeURL().compareToIgnoreCase(site2.getBlogNameOrHomeURL());
- });
+ sites = SiteRecordUtil.sort(sites, primaryBlogId);
// flag recently-picked sites and move them to the top if there are enough sites and
// the user isn't searching
@@ -775,27 +750,29 @@ protected SiteList[] doInBackground(Void... params) {
ArrayList pickedIds = AppPrefs.getRecentlyPickedSiteIds();
for (int i = pickedIds.size() - 1; i > -1; i--) {
int thisId = pickedIds.get(i);
- int indexOfSite = sites.indexOfSiteId(thisId);
+ int indexOfSite = SiteRecordUtil.indexOf(sites, thisId);
if (indexOfSite > -1) {
SiteRecord site = sites.remove(indexOfSite);
- site.mIsRecentPick = true;
+ site.setRecentPick(true);
sites.add(0, site);
}
}
}
- if (mSites == null || !mSites.isSameList(sites)) {
- SiteList allSites = (SiteList) sites.clone();
- SiteList filteredSites = filteredSitesByTextIfInSearchMode(sites);
+ if (!SiteRecordUtil.isSameList(mSites, sites)) {
+ List[] arrayOfLists = new List[2];
+ arrayOfLists[0] = sites;
+ arrayOfLists[1] = filteredSitesByTextIfInSearchMode(sites);
- return new SiteList[]{allSites, filteredSites};
+ return arrayOfLists;
}
return null;
}
+ @SuppressLint("NotifyDataSetChanged")
@Override
- protected void onPostExecute(SiteList[] updatedSiteLists) {
+ protected void onPostExecute(List[] updatedSiteLists) {
if (updatedSiteLists != null) {
mAllSites = updatedSiteLists[0];
mSites = updatedSiteLists[1];
@@ -804,113 +781,4 @@ protected void onPostExecute(SiteList[] updatedSiteLists) {
mDataLoadedListener.onAfterLoad();
}
}
-
- /**
- * SiteRecord is a simplified version of the full account (blog) record
- */
- public static class SiteRecord {
- private final int mLocalId;
- private final long mSiteId;
- private final String mBlogName;
- private final String mHomeURL;
- private final String mBlavatarUrl;
- private final ImageType mBlavatarType;
- private boolean mIsHidden;
- private boolean mIsRecentPick;
-
- public SiteRecord(SiteModel siteModel) {
- mLocalId = siteModel.getId();
- mSiteId = siteModel.getSiteId();
- mBlogName = SiteUtils.getSiteNameOrHomeURL(siteModel);
- mHomeURL = SiteUtils.getHomeURLOrHostName(siteModel);
- mBlavatarUrl = SiteUtils.getSiteIconUrl(siteModel, mBlavatarSz);
- mBlavatarType = SiteUtils.getSiteImageType(
- siteModel.isWpForTeamsSite(), BlavatarShape.SQUARE_WITH_ROUNDED_CORNERES);
- mIsHidden = !siteModel.isVisible();
- }
-
- public String getBlogNameOrHomeURL() {
- if (TextUtils.isEmpty(mBlogName)) {
- return mHomeURL;
- }
- return mBlogName;
- }
-
- public int getLocalId() {
- return mLocalId;
- }
-
- public boolean isHidden() {
- return mIsHidden;
- }
-
- public void setHidden(boolean hidden) {
- mIsHidden = hidden;
- }
-
- public String getHomeURL() {
- return mHomeURL;
- }
-
- public String getBlavatarUrl() {
- return mBlavatarUrl;
- }
-
- public ImageType getBlavatarType() {
- return mBlavatarType;
- }
-
- public long getSiteId() {
- return mSiteId;
- }
- }
-
- public static class SiteList extends ArrayList {
- SiteList() {
- }
-
- SiteList(List siteModels) {
- if (siteModels != null) {
- for (SiteModel siteModel : siteModels) {
- add(new SiteRecord(siteModel));
- }
- }
- }
-
- boolean isSameList(SiteList sites) {
- if (sites == null || sites.size() != this.size()) {
- return false;
- }
- int i;
- for (SiteRecord site : sites) {
- i = indexOfSite(site);
- if (i == -1
- || this.get(i).mIsHidden != site.mIsHidden
- || this.get(i).mIsRecentPick != site.mIsRecentPick) {
- return false;
- }
- }
- return true;
- }
-
- int indexOfSite(SiteRecord site) {
- if (site != null && site.mSiteId > 0) {
- for (int i = 0; i < size(); i++) {
- if (site.mSiteId == this.get(i).mSiteId) {
- return i;
- }
- }
- }
- return -1;
- }
-
- int indexOfSiteId(int localId) {
- for (int i = 0; i < size(); i++) {
- if (localId == this.get(i).mLocalId) {
- return i;
- }
- }
- return -1;
- }
- }
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerContract.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerContract.kt
index 8f8fe81ca374..a05c672887f7 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerContract.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerContract.kt
@@ -10,14 +10,14 @@ import org.wordpress.android.ui.mysite.SelectedSiteRepository
class SitePickerContract (private val siteStoreProvider: () -> SiteStore) : ActivityResultContract() {
override fun createIntent(context: Context, input: Unit) =
- Intent(context, SitePickerActivity::class.java).apply {
- putExtra(SitePickerActivity.KEY_SITE_PICKER_MODE, SitePickerAdapter.SitePickerMode.SIMPLE_MODE)
+ Intent(context, ChooseSiteActivity::class.java).apply {
+ putExtra(ChooseSiteActivity.KEY_SITE_PICKER_MODE, SitePickerMode.WPCOM_SITES_ONLY.name)
}
override fun parseResult(resultCode: Int, intent: Intent?) =
if (resultCode == AppCompatActivity.RESULT_OK) {
intent?.getIntExtra(
- SitePickerActivity.KEY_SITE_LOCAL_ID,
+ ChooseSiteActivity.KEY_SITE_LOCAL_ID,
SelectedSiteRepository.UNAVAILABLE,
)?.let { siteLocalId ->
siteStoreProvider().getSiteByLocalId(siteLocalId)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/SiteRecord.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/SiteRecord.kt
new file mode 100644
index 000000000000..38ad81ba5f4d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/SiteRecord.kt
@@ -0,0 +1,37 @@
+package org.wordpress.android.ui.main
+
+import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.util.SiteUtils
+import org.wordpress.android.util.image.BlavatarShape
+import org.wordpress.android.util.image.ImageType
+
+/**
+ * SiteRecord is a simplified version of the full account (blog) record
+ */
+class SiteRecord(siteModel: SiteModel) {
+ val localId: Int
+ val siteId: Long
+ val blogName: String
+ val homeURL: String
+ val blavatarUrl: String
+ val blavatarType: ImageType
+ var isHidden: Boolean
+ var isRecentPick = false
+
+ init {
+ localId = siteModel.id
+ siteId = siteModel.siteId
+ blogName = SiteUtils.getSiteNameOrHomeURL(siteModel)
+ homeURL = SiteUtils.getHomeURLOrHostName(siteModel)
+ blavatarUrl = SiteUtils.getSiteIconUrl(siteModel, SitePickerAdapter.mBlavatarSz)
+ blavatarType = SiteUtils.getSiteImageType(
+ siteModel.isWpForTeamsSite, BlavatarShape.SQUARE_WITH_ROUNDED_CORNERES
+ )
+ isHidden = !siteModel.isVisible
+ }
+
+ val blogNameOrHomeURL: String
+ get() = blogName.ifEmpty {
+ homeURL
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/SiteViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/SiteViewModel.kt
new file mode 100644
index 000000000000..935ca04c60a7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/SiteViewModel.kt
@@ -0,0 +1,87 @@
+package org.wordpress.android.ui.main
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.CoroutineDispatcher
+import org.wordpress.android.fluxc.store.SiteStore
+import org.wordpress.android.modules.BG_THREAD
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
+import org.wordpress.android.viewmodel.ScopedViewModel
+import java.util.Locale
+import javax.inject.Inject
+import javax.inject.Named
+
+@HiltViewModel
+class SiteViewModel @Inject constructor(
+ @Named(BG_THREAD) bgDispatcher: CoroutineDispatcher,
+ private val siteStore: SiteStore,
+ private val appPrefsWrapper: AppPrefsWrapper
+) : ScopedViewModel(bgDispatcher) {
+ private val _sites = MutableLiveData>()
+ val sites: LiveData> = _sites
+
+ /**
+ * Load for sites by keyword.
+ * If the keyword is not set, all sites are returned.
+ */
+ fun loadSites(sitePickerMode: SitePickerMode, keyword: String? = null) = launch {
+ if (keyword == null || keyword.trim().isEmpty()) {
+ _sites.postValue(getSites(sitePickerMode))
+ return@launch
+ }
+ getSites(sitePickerMode).filter { record ->
+ val siteName: String = record.blogName.lowercase(Locale.getDefault())
+ val hostName: String = record.homeURL.lowercase(Locale.ROOT)
+
+ siteName.contains(keyword.lowercase(Locale.getDefault())) ||
+ hostName.contains(keyword.lowercase(Locale.ROOT))
+ }.let { _sites.postValue(it) }
+ }
+
+ /**
+ * Returns a list of sites to display in the site picker.
+ * If mode is [SitePickerMode.WPCOM_SITES_ONLY], only WPCOM sites are returned.
+ */
+ private fun getSites(mode: SitePickerMode): List {
+ val result = if (mode == SitePickerMode.WPCOM_SITES_ONLY) {
+ siteStore.sitesAccessedViaWPComRest
+ } else {
+ siteStore.sites
+ }
+ return result.map { SiteRecord(it) }.let { sortSites(it) }
+ }
+
+ /**
+ * Make the pinned sites appear first in the list, followed by the recent sites.
+ * Then sort the list of sites by blog name or home URL.
+ */
+ private fun sortSites(records: List): List {
+ val allSites = records.toMutableList()
+
+ val pinnedSites = appPrefsWrapper.pinnedSiteLocalIds
+ .mapNotNull { pinnedId -> allSites.firstOrNull { it.localId == pinnedId } }
+ .toMutableList()
+
+ val recentSites = appPrefsWrapper.getRecentSiteLocalIds()
+ .mapNotNull { pinnedId -> allSites.firstOrNull { it.localId == pinnedId } }
+ .toMutableList()
+ .apply { removeAll(pinnedSites) }
+
+ allSites.apply {
+ removeAll(pinnedSites)
+ removeAll(recentSites)
+ }
+
+ return pinnedSites + recentSites + allSites.sortedBy { it.blogNameOrHomeURL }
+ }
+
+ /**
+ * @return the section name for the site with the given local ID.
+ */
+ fun getSection(localId: Int): String = when {
+ appPrefsWrapper.pinnedSiteLocalIds.contains(localId) -> "pinned"
+ appPrefsWrapper.getRecentSiteLocalIds().contains(localId) -> "recent"
+ else -> "all"
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java
index b2a55812a397..2c730a43acae 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java
@@ -10,6 +10,7 @@
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
+import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.View;
import android.view.ViewGroup;
@@ -27,12 +28,9 @@
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
-import com.google.android.gms.tasks.Task;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
-import com.google.android.play.core.review.ReviewInfo;
-import com.google.android.play.core.review.ReviewManager;
-import com.google.android.play.core.review.ReviewManagerFactory;
+import com.google.android.play.core.install.model.AppUpdateType;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
@@ -67,6 +65,8 @@
import org.wordpress.android.fluxc.store.SiteStore.OnSiteChanged;
import org.wordpress.android.fluxc.store.SiteStore.OnSiteEditorsChanged;
import org.wordpress.android.fluxc.store.SiteStore.OnSiteRemoved;
+import org.wordpress.android.inappupdate.IInAppUpdateManager;
+import org.wordpress.android.inappupdate.InAppUpdateListener;
import org.wordpress.android.login.LoginAnalyticsListener;
import org.wordpress.android.networking.ConnectionChangeReceiver;
import org.wordpress.android.push.GCMMessageHandler;
@@ -108,8 +108,9 @@
import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository;
import org.wordpress.android.ui.notifications.NotificationEvents;
import org.wordpress.android.ui.notifications.NotificationsListFragment;
+import org.wordpress.android.ui.notifications.NotificationsListViewModel;
import org.wordpress.android.ui.notifications.SystemNotificationsTracker;
-import org.wordpress.android.ui.notifications.adapters.NotesAdapter;
+import org.wordpress.android.ui.notifications.adapters.Filter;
import org.wordpress.android.ui.notifications.receivers.NotificationsPendingDraftsReceiver;
import org.wordpress.android.ui.notifications.utils.NotificationsActions;
import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
@@ -118,6 +119,7 @@
import org.wordpress.android.ui.posts.BasicFragmentDialog.BasicDialogNegativeClickInterface;
import org.wordpress.android.ui.posts.BasicFragmentDialog.BasicDialogPositiveClickInterface;
import org.wordpress.android.ui.posts.EditPostActivity;
+import org.wordpress.android.ui.posts.EditPostActivityConstants;
import org.wordpress.android.ui.posts.PostUtils.EntryPoint;
import org.wordpress.android.ui.posts.QuickStartPromptDialogFragment.QuickStartPromptClickInterface;
import org.wordpress.android.ui.prefs.AppPrefs;
@@ -127,20 +129,21 @@
import org.wordpress.android.ui.prefs.privacy.banner.PrivacyBannerFragment;
import org.wordpress.android.ui.quickstart.QuickStartMySitePrompts;
import org.wordpress.android.ui.quickstart.QuickStartTracker;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
import org.wordpress.android.ui.reader.ReaderFragment;
+import org.wordpress.android.ui.reader.comments.ThreadedCommentsActionSource;
import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask;
import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter;
import org.wordpress.android.ui.reader.tracker.ReaderTracker;
-import org.wordpress.android.ui.review.ReviewViewModel;
import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource;
import org.wordpress.android.ui.stats.StatsTimeframe;
import org.wordpress.android.ui.stats.refresh.utils.StatsLaunchedFrom;
-import org.wordpress.android.ui.stories.intro.StoriesIntroDialogFragment;
import org.wordpress.android.ui.uploads.UploadActionUseCase;
import org.wordpress.android.ui.uploads.UploadUtils;
import org.wordpress.android.ui.uploads.UploadUtilsWrapper;
import org.wordpress.android.ui.utils.JetpackAppMigrationFlowUtils;
import org.wordpress.android.ui.utils.UiString.UiStringRes;
+import org.wordpress.android.ui.voicetocontent.VoiceToContentDialogFragment;
import org.wordpress.android.ui.whatsnew.FeatureAnnouncementDialogFragment;
import org.wordpress.android.util.AniUtils;
import org.wordpress.android.util.AppLog;
@@ -170,7 +173,8 @@
import org.wordpress.android.viewmodel.main.WPMainActivityViewModel.FocusPointInfo;
import org.wordpress.android.viewmodel.mlp.ModalLayoutPickerViewModel;
import org.wordpress.android.viewmodel.mlp.ModalLayoutPickerViewModel.CreatePageDashboardSource;
-import org.wordpress.android.widgets.AppRatingDialog;
+import org.wordpress.android.widgets.AppReviewManager;
+import org.wordpress.android.widgets.WPSnackbar;
import org.wordpress.android.workers.notification.createsite.CreateSiteNotificationScheduler;
import org.wordpress.android.workers.weeklyroundup.WeeklyRoundupScheduler;
@@ -183,14 +187,15 @@
import static androidx.lifecycle.Lifecycle.State.STARTED;
import static org.wordpress.android.WordPress.SITE;
-import static org.wordpress.android.editor.gutenberg.GutenbergEditorFragment.ARG_STORY_BLOCK_ID;
import static org.wordpress.android.fluxc.store.SiteStore.CompleteQuickStartVariant.NEXT_STEPS;
import static org.wordpress.android.login.LoginAnalyticsListener.CreatedAccountSource.EMAIL;
import static org.wordpress.android.push.NotificationsProcessingService.ARG_NOTIFICATION_TYPE;
import static org.wordpress.android.ui.JetpackConnectionSource.NOTIFICATIONS;
-import static org.wordpress.android.util.extensions.InAppReviewExtensionsKt.logException;
import dagger.hilt.android.AndroidEntryPoint;
+import kotlin.Unit;
+import kotlin.jvm.functions.Function0;
+import kotlin.jvm.functions.Function3;
/**
* Main activity which hosts sites, reader, me and notifications pages
@@ -250,8 +255,8 @@ public class WPMainActivity extends LocaleAwareActivity implements
private WPMainActivityViewModel mViewModel;
private ModalLayoutPickerViewModel mMLPViewModel;
- @NonNull private ReviewViewModel mReviewViewModel;
private BloggingRemindersViewModel mBloggingRemindersViewModel;
+ private NotificationsListViewModel mNotificationsViewModel;
private FloatingActionButton mFloatingActionButton;
private static final String MAIN_BOTTOM_SHEET_TAG = "MAIN_BOTTOM_SHEET_TAG";
private static final String BLOGGING_REMINDERS_BOTTOM_SHEET_TAG = "BLOGGING_REMINDERS_BOTTOM_SHEET_TAG";
@@ -290,6 +295,8 @@ public class WPMainActivity extends LocaleAwareActivity implements
@Inject BuildConfigWrapper mBuildConfigWrapper;
+ @Inject IInAppUpdateManager mInAppUpdateManager;
+
@Inject GCMRegistrationScheduler mGCMRegistrationScheduler;
@Inject ActivityNavigator mActivityNavigator;
@@ -495,7 +502,7 @@ && getIntent().getExtras().getBoolean(ARG_CONTINUE_JETPACK_CONNECT, false)) {
}
if (canShowAppRatingPrompt) {
- AppRatingDialog.INSTANCE.showRateDialogIfNeeded(getSupportFragmentManager());
+ AppReviewManager.INSTANCE.showRateDialogIfNeeded(getSupportFragmentManager());
}
scheduleLocalNotifications();
@@ -677,7 +684,6 @@ private void initViewModel() {
mViewModel = new ViewModelProvider(this, mViewModelFactory).get(WPMainActivityViewModel.class);
mMLPViewModel = new ViewModelProvider(this, mViewModelFactory).get(ModalLayoutPickerViewModel.class);
- mReviewViewModel = new ViewModelProvider(this, mViewModelFactory).get(ReviewViewModel.class);
mBloggingRemindersViewModel =
new ViewModelProvider(this, mViewModelFactory).get(BloggingRemindersViewModel.class);
@@ -706,6 +712,9 @@ private void initViewModel() {
mViewModel.getCreateAction().observe(this, createAction -> {
switch (createAction) {
+ case CREATE_NEW_POST_FROM_AUDIO:
+ launchVoiceToContent();
+ break;
case CREATE_NEW_POST:
handleNewPostAction(PagePostCreationSourcesDetail.POST_FROM_MY_SITE, -1, null);
break;
@@ -713,9 +722,6 @@ private void initViewModel() {
case CREATE_NEW_PAGE_FROM_PAGES_CARD:
triggerCreatePageFlow(createAction);
break;
- case CREATE_NEW_STORY:
- handleNewStoryAction();
- break;
case ANSWER_BLOGGING_PROMPT:
case NO_ACTION:
break; // noop - we handle ANSWER_BLOGGING_PROMPT through live data event
@@ -746,14 +752,17 @@ private void initViewModel() {
.show(getSupportFragmentManager(), FeatureAnnouncementDialogFragment.TAG);
});
- mFloatingActionButton.setOnClickListener(v -> mViewModel.onFabClicked(getSelectedSite()));
+ mFloatingActionButton.setOnClickListener(v -> {
+ PageType selectedPage = getSelectedPage();
+ if (selectedPage != null) mViewModel.onFabClicked(getSelectedSite(), selectedPage);
+ });
mFloatingActionButton.setOnLongClickListener(v -> {
if (v.isHapticFeedbackEnabled()) {
v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
- int messageId = mViewModel.getCreateContentMessageId(getSelectedSite());
+ int messageId = mViewModel.getCreateContentMessageId(getSelectedSite(), getSelectedPage());
Toast.makeText(v.getContext(), messageId, Toast.LENGTH_SHORT).show();
return true;
@@ -776,11 +785,6 @@ private void initViewModel() {
});
});
- mReviewViewModel.getLaunchReview().observe(this, event -> event.applyIfNotHandled(unit -> {
- launchInAppReviews();
- return null;
- }));
-
BloggingReminderUtils.observeBottomSheet(
mBloggingRemindersViewModel.isBottomSheetShowing(),
this,
@@ -830,7 +834,7 @@ private void initViewModel() {
// initialized with the most restrictive rights case. This is OK and will be frequently checked
// to normalize the UI state whenever mSelectedSite changes.
// It also means that the ViewModel must accept a nullable SiteModel.
- mViewModel.start(getSelectedSite());
+ mViewModel.start(getSelectedSite(), mBottomNav.getCurrentSelectedPage());
}
private void triggerCreatePageFlow(ActionType actionType) {
@@ -848,20 +852,6 @@ private void triggerCreatePageFlow(ActionType actionType) {
}
}
- private void launchInAppReviews() {
- ReviewManager manager = ReviewManagerFactory.create(this);
- Task request = manager.requestReviewFlow();
- request.addOnCompleteListener(task -> {
- if (task.isSuccessful()) {
- ReviewInfo reviewInfo = task.getResult();
- Task flow = manager.launchReviewFlow(this, reviewInfo);
- flow.addOnFailureListener(e -> AppLog.e(T.MAIN, "Error launching google review API flow.", e));
- } else {
- logException(task);
- }
- });
- }
-
private CreatePageDashboardSource getCreatePageDashboardSourceFromActionType(ActionType actionType) {
if (actionType == ActionType.CREATE_NEW_PAGE_FROM_PAGES_CARD) {
return CreatePageDashboardSource.PAGES_CARD;
@@ -1059,11 +1049,34 @@ public void onTokenInvalid() {
// we processed the voice reply, so we exit this function immediately
return;
} else {
- boolean shouldShowKeyboard =
- getIntent().getBooleanExtra(NotificationsListFragment.NOTE_INSTANT_REPLY_EXTRA, false);
- NotificationsListFragment
- .openNoteForReply(this, noteId, shouldShowKeyboard, null,
- NotesAdapter.FILTERS.FILTER_ALL, true);
+ if (mNotificationsViewModel == null) {
+ mNotificationsViewModel = new ViewModelProvider(this).get(NotificationsListViewModel.class);
+ }
+ mNotificationsViewModel.openNote(noteId, new Function3() {
+ @Nullable @Override
+ public Unit invoke(@NonNull Long siteId, @NonNull Long postId,
+ @NonNull Long commentId) {
+ ReaderActivityLauncher.showReaderComments(
+ WPMainActivity.this,
+ siteId,
+ postId,
+ commentId,
+ ThreadedCommentsActionSource.COMMENT_NOTIFICATION.getSourceDescription()
+ );
+ return null;
+ }
+ }, new Function0() {
+ @Nullable @Override
+ public Unit invoke() {
+ boolean shouldShowKeyboard = getIntent().getBooleanExtra(
+ NotificationsListFragment.NOTE_INSTANT_REPLY_EXTRA,
+ false);
+ NotificationsListFragment.openNoteForReply(WPMainActivity.this, noteId,
+ shouldShowKeyboard, null, Filter.ALL, true);
+ return null;
+ }
+ }
+ );
}
} else {
AppLog.e(T.NOTIFS, "app launched from a PN that doesn't have a note_id in it!!");
@@ -1167,12 +1180,36 @@ protected void onResume() {
mViewModel.onResume(
getSelectedSite(),
- mSelectedSiteRepository.hasSelectedSite() && mBottomNav != null
- && mBottomNav.getCurrentSelectedPage() == PageType.MY_SITE
+ mSelectedSiteRepository.hasSelectedSite(),
+ getSelectedPage()
);
+
+ if (AppReviewManager.INSTANCE.shouldShowInAppReviewsPrompt()) {
+ AppReviewManager.INSTANCE.launchInAppReviews(this);
+ }
+ checkForInAppUpdate();
+
mIsChangingConfiguration = false;
}
+ private void checkForInAppUpdate() {
+ mInAppUpdateManager.checkForAppUpdate(this, mInAppUpdateListener);
+ }
+
+ @NonNull final InAppUpdateListener mInAppUpdateListener = new InAppUpdateListener() {
+ @Override public void onAppUpdateDownloaded() {
+ popupSnackbarForCompleteUpdate();
+ }
+ };
+
+ private void popupSnackbarForCompleteUpdate() {
+ WPSnackbar.make(findViewById(R.id.coordinator), R.string.in_app_update_available, Snackbar.LENGTH_INDEFINITE)
+ .setAction(R.string.in_app_update_restart, v -> {
+ mInAppUpdateManager.completeAppUpdate();
+ })
+ .show();
+ }
+
private void checkQuickStartNotificationStatus() {
SiteModel selectedSite = getSelectedSite();
long selectedSiteLocalId = mSelectedSiteRepository.getSelectedSiteLocalId();
@@ -1233,8 +1270,9 @@ public void onPageChanged(int position) {
}
mViewModel.onPageChanged(
- mSiteStore.hasSite() && pageType == PageType.MY_SITE,
- getSelectedSite()
+ getSelectedSite(),
+ mSiteStore.hasSite(),
+ pageType
);
}
@@ -1271,42 +1309,32 @@ private void handleNewPostAction(PagePostCreationSourcesDetail source,
ActivityLauncher.addNewPostForResult(this, getSelectedSite(), false, source, promptId, entryPoint);
}
- private void handleNewStoryAction() {
+ private void launchVoiceToContent() {
if (!mSiteStore.hasSite()) {
// No site yet - Move to My Sites fragment that shows the create new site screen
mBottomNav.setCurrentSelectedPage(PageType.MY_SITE);
return;
}
-
- SiteModel selectedSite = getSelectedSite();
- if (selectedSite != null) {
- // TODO: evaluate to include the QuickStart logic like in the handleNewPostAction
- if (AppPrefs.shouldShowStoriesIntro()) {
- StoriesIntroDialogFragment.newInstance(selectedSite)
- .show(getSupportFragmentManager(), StoriesIntroDialogFragment.TAG);
- } else {
- mMediaPickerLauncher.showStoriesPhotoPickerForResultAndTrack(this, selectedSite);
- }
- }
+ VoiceToContentDialogFragment.newInstance().show(getSupportFragmentManager(), VoiceToContentDialogFragment.TAG);
}
private void trackLastVisiblePage(@NonNull final PageType pageType) {
switch (pageType) {
case MY_SITE:
ActivityId.trackLastActivity(ActivityId.MY_SITE);
- mAnalyticsTrackerWrapper.track(AnalyticsTracker.Stat.MY_SITE_ACCESSED, getSelectedSite());
+ mJetpackFeatureRemovalPhaseHelper.trackPageAccessedEventIfNeeded(PageType.MY_SITE, getSelectedSite());
break;
case READER:
ActivityId.trackLastActivity(ActivityId.READER);
- AnalyticsTracker.track(AnalyticsTracker.Stat.READER_ACCESSED);
+ mJetpackFeatureRemovalPhaseHelper.trackPageAccessedEventIfNeeded(PageType.READER);
break;
case NOTIFS:
ActivityId.trackLastActivity(ActivityId.NOTIFICATIONS);
- AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATIONS_ACCESSED);
+ mJetpackFeatureRemovalPhaseHelper.trackPageAccessedEventIfNeeded(PageType.NOTIFS);
break;
case ME:
ActivityId.trackLastActivity(ActivityId.ME);
- AnalyticsTracker.track(Stat.ME_ACCESSED);
+ mJetpackFeatureRemovalPhaseHelper.trackPageAccessedEventIfNeeded(PageType.ME);
break;
default:
break;
@@ -1330,7 +1358,7 @@ public void setReaderPageActive() {
private void setSite(Intent data) {
if (data != null) {
int siteLocalId = data.getIntExtra(
- SitePickerActivity.KEY_SITE_LOCAL_ID,
+ ChooseSiteActivity.KEY_SITE_LOCAL_ID,
SelectedSiteRepository.UNAVAILABLE
);
SiteModel site = mSiteStore.getSiteByLocalId(siteLocalId);
@@ -1343,6 +1371,7 @@ private void setSite(Intent data) {
@Override
@SuppressWarnings("deprecation")
public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ Log.e("WPMainActivity", "onActivityResult: " + requestCode + " " + resultCode);
super.onActivityResult(requestCode, resultCode, data);
if (!mSelectedSiteRepository.hasSelectedSite()) {
initSelectedSite();
@@ -1353,22 +1382,23 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK || data == null || isFinishing()) {
return;
}
- int localId = data.getIntExtra(EditPostActivity.EXTRA_POST_LOCAL_ID, 0);
+ int localId = data.getIntExtra(EditPostActivityConstants.EXTRA_POST_LOCAL_ID, 0);
final SiteModel site = (SiteModel) data.getSerializableExtra(WordPress.SITE);
final PostModel post = mPostStore.getPostByLocalPostId(localId);
if (EditPostActivity.checkToRestart(data)) {
ActivityLauncher.editPostOrPageForResult(data, WPMainActivity.this, site,
- data.getIntExtra(EditPostActivity.EXTRA_POST_LOCAL_ID, 0));
+ data.getIntExtra(EditPostActivityConstants.EXTRA_POST_LOCAL_ID, 0));
// a restart will happen so, no need to continue here
break;
}
- if (site != null && post != null) {
+ View snackbarAttachView = findViewById(R.id.coordinator);
+ if (site != null && post != null && snackbarAttachView != null) {
mUploadUtilsWrapper.handleEditPostResultSnackbars(
this,
- findViewById(R.id.coordinator),
+ snackbarAttachView,
data,
post,
site,
@@ -1376,22 +1406,13 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
v -> UploadUtils.publishPost(WPMainActivity.this, post, site, mDispatcher),
isFirstTimePublishing -> {
mBloggingRemindersViewModel.onPublishingPost(site.getId(), isFirstTimePublishing);
- mReviewViewModel.onPublishingPost(isFirstTimePublishing);
+ if (isFirstTimePublishing) {
+ AppReviewManager.INSTANCE.onPostPublished();
+ }
}
);
}
break;
- case RequestCodes.CREATE_STORY:
- SiteModel selectedSite = getSelectedSite();
- if (selectedSite != null) {
- boolean isNewStory = data == null || data.getStringExtra(ARG_STORY_BLOCK_ID) == null;
- mBloggingRemindersViewModel.onPublishingPost(
- selectedSite.getId(),
- isNewStory
- );
- mReviewViewModel.onPublishingPost(isNewStory);
- }
- break;
case RequestCodes.CREATE_SITE:
QuickStartUtils.cancelQuickStartReminder(this);
AppPrefs.setQuickStartNoticeRequired(false);
@@ -1400,7 +1421,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
// Enable the block editor on sites created on mobile
if (data != null) {
int newSiteLocalID = data.getIntExtra(
- SitePickerActivity.KEY_SITE_LOCAL_ID,
+ ChooseSiteActivity.KEY_SITE_LOCAL_ID,
SelectedSiteRepository.UNAVAILABLE
);
SiteUtils.enableBlockEditorOnSiteCreation(mDispatcher, mSiteStore, newSiteLocalID);
@@ -1434,7 +1455,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
case RequestCodes.SITE_PICKER:
boolean isSameSiteSelected = data != null
&& data.getIntExtra(
- SitePickerActivity.KEY_SITE_LOCAL_ID,
+ ChooseSiteActivity.KEY_SITE_LOCAL_ID,
SelectedSiteRepository.UNAVAILABLE
) == mSelectedSiteRepository.getSelectedSiteLocalId();
@@ -1467,6 +1488,23 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
case RequestCodes.DOMAIN_REGISTRATION:
passOnActivityResultToMySiteFragment(requestCode, resultCode, data);
break;
+ case IInAppUpdateManager.APP_UPDATE_FLEXIBLE_REQUEST_CODE:
+ handleUpdateResult(resultCode, AppUpdateType.FLEXIBLE);
+ break;
+ case IInAppUpdateManager.APP_UPDATE_IMMEDIATE_REQUEST_CODE:
+ handleUpdateResult(resultCode, AppUpdateType.IMMEDIATE);
+ break;
+ }
+ }
+
+ // Handles the result of the app update request
+ private void handleUpdateResult(int resultCode, int updateType) {
+ if (resultCode == RESULT_OK) {
+ // The user accepted the update
+ mInAppUpdateManager.onUserAcceptedAppUpdate(updateType);
+ } else if (resultCode == RESULT_CANCELED) {
+ // The user denied the update
+ mInAppUpdateManager.cancelAppUpdate(updateType);
}
}
@@ -1670,7 +1708,7 @@ private void handleSiteRemoved() {
showSignInForResultBasedOnIsJetpackAppBuildConfig(this);
return;
}
- if (mViewModel.getHasMultipleSites()) {
+ if (mViewModel.getHasMultipleSites() && !ChooseSiteActivity.isRunning()) {
ActivityLauncher.showSitePickerForResult(this, mViewModel.getFirstSite());
}
}
@@ -1768,19 +1806,24 @@ public void onPostUploaded(OnPostUploaded event) {
}
}
- mUploadUtilsWrapper.onPostUploadedSnackbarHandler(
- this,
- findViewById(R.id.coordinator),
- event.isError(),
- event.isFirstTimePublish,
- event.post,
- null,
- targetSite,
- isFirstTimePublishing -> {
- mBloggingRemindersViewModel.onPublishingPost(targetSite.getId(), isFirstTimePublishing);
- mReviewViewModel.onPublishingPost(isFirstTimePublishing);
- }
- );
+ View snackbarAttachView = findViewById(R.id.coordinator);
+ if (snackbarAttachView != null) {
+ mUploadUtilsWrapper.onPostUploadedSnackbarHandler(
+ this,
+ snackbarAttachView,
+ event.isError(),
+ event.isFirstTimePublish,
+ event.post,
+ null,
+ targetSite,
+ isFirstTimePublishing -> {
+ mBloggingRemindersViewModel.onPublishingPost(targetSite.getId(), isFirstTimePublishing);
+ if (isFirstTimePublishing) {
+ AppReviewManager.INSTANCE.onPostPublished();
+ }
+ }
+ );
+ }
}
}
}
@@ -1888,6 +1931,7 @@ public void onSetPromptReminderClick(final int siteId) {
onActivityResult(RequestCodes.SITE_PICKER, resultCode, data);
}
+
// We dismiss the QuickStart SnackBar every time activity is paused because
// SnackBar sometimes do not appear when another SnackBar is still visible, even in other activities (weird)
@Override
@@ -1951,4 +1995,9 @@ private void showOpenPageMessageIfNeeded() {
}
}
}
+
+ @Nullable
+ private PageType getSelectedPage() {
+ return mBottomNav != null ? mBottomNav.getCurrentSelectedPage() : null;
+ }
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTracker.kt
new file mode 100644
index 000000000000..05d6e57d6c4c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTracker.kt
@@ -0,0 +1,66 @@
+package org.wordpress.android.ui.main.analytics
+
+import org.wordpress.android.analytics.AnalyticsTracker
+import org.wordpress.android.ui.main.MainActionListItem
+import org.wordpress.android.ui.main.WPMainNavigationView
+import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution
+import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
+import java.util.Locale
+import javax.inject.Inject
+
+class MainCreateSheetTracker @Inject constructor(
+ private val analyticsTracker: AnalyticsTrackerWrapper,
+) {
+ fun trackActionTapped(page: WPMainNavigationView.PageType, actionType: MainActionListItem.ActionType) {
+ val stat = when (page) {
+ WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_ACTION_TAPPED
+ WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_SHEET_ACTION_TAPPED
+ else -> return
+ }
+ val properties = mapOf("action" to actionType.name.lowercase(Locale.ROOT))
+ analyticsTracker.track(stat, properties)
+ }
+
+ fun trackAnswerPromptActionTapped(page: WPMainNavigationView.PageType, attribution: BloggingPromptAttribution) {
+ val properties = mapOf("attribution" to attribution.value).filterValues { it.isNotBlank() }
+ val stat = when (page) {
+ WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_ANSWER_PROMPT_TAPPED
+ WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_SHEET_ANSWER_PROMPT_TAPPED
+ else -> return
+ }
+ analyticsTracker.track(stat, properties)
+ }
+
+ fun trackHelpPromptActionTapped(page: WPMainNavigationView.PageType) {
+ val stat = when (page) {
+ WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_PROMPT_HELP_TAPPED
+ WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_SHEET_PROMPT_HELP_TAPPED
+ else -> return
+ }
+ analyticsTracker.track(stat)
+ }
+
+ fun trackSheetShown(page: WPMainNavigationView.PageType) {
+ val stat = when (page) {
+ WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_SHOWN
+ WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_SHEET_SHOWN
+ else -> return
+ }
+ analyticsTracker.track(stat)
+ }
+
+ fun trackFabShown(page: WPMainNavigationView.PageType) {
+ val stat = when (page) {
+ WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_FAB_SHOWN
+ WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_FAB_SHOWN
+ else -> return
+ }
+ analyticsTracker.track(stat)
+ }
+
+ fun trackCreateActionsSheetCard(actions: List) {
+ if (actions.any { it is MainActionListItem.AnswerBloggingPromptAction }) {
+ analyticsTracker.track(AnalyticsTracker.Stat.BLOGGING_PROMPTS_CREATE_SHEET_CARD_VIEWED)
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/migration/JetpackMigrationViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/migration/JetpackMigrationViewModel.kt
index 979d709290ed..29584be672ee 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/migration/JetpackMigrationViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/migration/JetpackMigrationViewModel.kt
@@ -65,7 +65,7 @@ import org.wordpress.android.ui.utils.UiString
import org.wordpress.android.ui.utils.UiString.UiStringRes
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.AppLog.T
-import org.wordpress.android.util.GravatarUtilsWrapper
+import org.wordpress.android.util.WPAvatarUtilsWrapper
import org.wordpress.android.util.JetpackMigrationLanguageUtil
import org.wordpress.android.util.LocaleManagerWrapper
import org.wordpress.android.util.SiteUtilsWrapper
@@ -81,7 +81,7 @@ class JetpackMigrationViewModel @Inject constructor(
@Named(UI_THREAD) mainDispatcher: CoroutineDispatcher,
private val dispatcher: Dispatcher,
private val siteUtilsWrapper: SiteUtilsWrapper,
- private val gravatarUtilsWrapper: GravatarUtilsWrapper,
+ private val avatarUtilsWrapper: WPAvatarUtilsWrapper,
private val contextProvider: ContextProvider,
private val preventDuplicateNotifsFeatureConfig: PreventDuplicateNotifsFeatureConfig,
private val appPrefsWrapper: AppPrefsWrapper,
@@ -386,7 +386,7 @@ class JetpackMigrationViewModel @Inject constructor(
postActionEvent(FinishActivity)
}
- private fun resizeAvatarUrl(avatarUrl: String) = gravatarUtilsWrapper.fixGravatarUrlWithResource(
+ private fun resizeAvatarUrl(avatarUrl: String) = avatarUtilsWrapper.rewriteAvatarUrlWithResource(
avatarUrl,
R.dimen.jp_migration_user_avatar_size
)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelper.kt
new file mode 100644
index 000000000000..7255a908245a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelper.kt
@@ -0,0 +1,37 @@
+package org.wordpress.android.ui.main.utils
+
+import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper
+import org.wordpress.android.ui.main.WPMainNavigationView.PageType
+import org.wordpress.android.ui.voicetocontent.VoiceToContentFeatureUtils
+import org.wordpress.android.util.BuildConfigWrapper
+import org.wordpress.android.util.SiteUtilsWrapper
+import org.wordpress.android.util.config.ReaderFloatingButtonFeatureConfig
+import javax.inject.Inject
+
+class MainCreateSheetHelper @Inject constructor(
+ private val voiceToContentFeatureUtils: VoiceToContentFeatureUtils,
+ private val readerFloatingButtonFeatureConfig: ReaderFloatingButtonFeatureConfig,
+ private val bloggingPromptsSettingsHelper: BloggingPromptsSettingsHelper,
+ private val buildConfig: BuildConfigWrapper,
+ private val siteUtils: SiteUtilsWrapper,
+) {
+ fun shouldShowFabForPage(page: PageType?): Boolean {
+ val enabledForPage = page == PageType.MY_SITE ||
+ (page == PageType.READER && readerFloatingButtonFeatureConfig.isEnabled())
+ return buildConfig.isCreateFabEnabled && enabledForPage
+ }
+
+ @Suppress("FunctionOnlyReturningConstant")
+ fun canCreatePost(): Boolean = true // for completeness
+
+ fun canCreatePage(site: SiteModel?, page: PageType?): Boolean {
+ return siteUtils.hasFullAccessToContent(site) && page == PageType.MY_SITE
+ }
+
+ fun canCreatePostFromAudio(site: SiteModel?): Boolean {
+ return voiceToContentFeatureUtils.isVoiceToContentEnabled() && siteUtils.hasFullAccessToContent(site)
+ }
+
+ suspend fun canCreatePromptAnswer(): Boolean = bloggingPromptsSettingsHelper.shouldShowPromptsFeature()
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/utils/MeGravatarLoader.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/utils/MeGravatarLoader.kt
index ecd43e0eb385..f4b6ad1464fc 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/main/utils/MeGravatarLoader.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/utils/MeGravatarLoader.kt
@@ -5,7 +5,7 @@ import android.widget.ImageView
import org.wordpress.android.R
import org.wordpress.android.WordPress
import org.wordpress.android.ui.prefs.AppPrefsWrapper
-import org.wordpress.android.util.GravatarUtils
+import org.wordpress.android.util.WPAvatarUtils
import org.wordpress.android.util.image.ImageManager
import org.wordpress.android.util.image.ImageManager.RequestListener
import org.wordpress.android.util.image.ImageType
@@ -58,6 +58,6 @@ class MeGravatarLoader @Inject constructor(
fun constructGravatarUrl(rawAvatarUrl: String): String {
val avatarSz = resourseProvider.getDimensionPixelSize(R.dimen.avatar_sz_extra_small)
- return GravatarUtils.fixGravatarUrl(rawAvatarUrl, avatarSz)
+ return WPAvatarUtils.rewriteAvatarUrl(rawAvatarUrl, avatarSz)
}
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/utils/SiteRecordUtil.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/utils/SiteRecordUtil.kt
new file mode 100644
index 000000000000..3481f13da1fa
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/utils/SiteRecordUtil.kt
@@ -0,0 +1,70 @@
+package org.wordpress.android.ui.main.utils
+
+import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.ui.main.SiteRecord
+import java.util.Locale
+
+object SiteRecordUtil {
+ /**
+ * Sorts the list of sites by blog name or home URL and moves the primary site to the top.
+ */
+ @JvmStatic
+ fun sort(sites: List, primarySiteId: Long): List {
+ val list = sites.sortedBy { it.blogNameOrHomeURL }.toMutableList()
+ val primarySite = list.firstOrNull { it.siteId == primarySiteId }
+ list.remove(primarySite)
+ return if (primarySite == null) list else (listOf(primarySite) + list)
+ }
+
+ @JvmStatic
+ fun sort(sites: List, pinnedSiteLocalIds:Set): List {
+ val sortedSites = sites.sortedBy { it.blogNameOrHomeURL }
+ .toMutableList()
+
+ val pinnedSites = sites.filter { pinnedSiteLocalIds.contains(it.localId) }
+ pinnedSites.forEach { sortedSites.remove(it) }
+
+ return pinnedSites + sortedSites
+ }
+
+ /**
+ * Returns the index of the site with the given local ID.
+ */
+ @JvmStatic
+ fun indexOf(sites: List, localId: Int) = sites.indexOfFirst { it.localId == localId }
+
+ /**
+ * Returns the index of the given site.
+ */
+ @JvmStatic
+ fun indexOf(sites: List, siteRecord: SiteRecord) = sites.indexOfFirst { it.siteId == siteRecord.siteId }
+
+ @Suppress("ReturnCount")
+ @JvmStatic
+ fun isSameList(currentSites: List, anotherSites: List): Boolean {
+ if (currentSites.size != anotherSites.size) {
+ return false
+ }
+
+ for (i in currentSites.indices) {
+ if (currentSites[i].siteId != anotherSites[i].siteId ||
+ currentSites[i].isHidden != anotherSites[i].isHidden ||
+ currentSites[i].isRecentPick != anotherSites[i].isRecentPick
+ ) return false
+ }
+ return true
+ }
+
+ @JvmStatic
+ fun filteredSites(sites: List, searchKeyword: String) =
+ sites.filter { record ->
+ val siteName: String = record.blogName.lowercase(Locale.getDefault())
+ val hostName: String = record.homeURL.lowercase(Locale.ROOT)
+
+ siteName.contains(searchKeyword.lowercase(Locale.getDefault())) ||
+ hostName.contains(searchKeyword.lowercase(Locale.ROOT))
+ }
+
+ @JvmStatic
+ fun createRecords(siteModels: List) = siteModels.map { SiteRecord(it) }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/ExoPlayerUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/media/ExoPlayerUtils.kt
index adc9f5779c82..1fe579a18b6f 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/media/ExoPlayerUtils.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/ExoPlayerUtils.kt
@@ -19,13 +19,14 @@ import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
import com.google.android.exoplayer2.util.Util
import dagger.Reusable
-import org.wordpress.android.WordPress
+import org.wordpress.android.fluxc.network.UserAgent
import org.wordpress.android.ui.utils.AuthenticationUtils
import javax.inject.Inject
@Reusable
@Suppress("DEPRECATION")
class ExoPlayerUtils @Inject constructor(
+ private val userAgent: UserAgent,
private val authenticationUtils: AuthenticationUtils,
private val appContext: Context
) {
@@ -33,7 +34,7 @@ class ExoPlayerUtils @Inject constructor(
fun buildHttpDataSourceFactory(url: String): DefaultHttpDataSourceFactory {
if (httpDataSourceFactory == null) {
- httpDataSourceFactory = DefaultHttpDataSourceFactory(WordPress.getUserAgent())
+ httpDataSourceFactory = DefaultHttpDataSourceFactory(userAgent.toString())
}
httpDataSourceFactory?.defaultRequestProperties?.set(authenticationUtils.getAuthHeaders(url))
return httpDataSourceFactory as DefaultHttpDataSourceFactory
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java
index d434f5024f09..8235a7c84dd6 100755
--- a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java
@@ -91,7 +91,7 @@
import org.wordpress.android.util.WPMediaUtils;
import org.wordpress.android.util.WPPermissionUtils;
import org.wordpress.android.util.analytics.AnalyticsUtils;
-import org.wordpress.android.widgets.AppRatingDialog;
+import org.wordpress.android.widgets.AppReviewManager;
import org.wordpress.android.widgets.QuickStartFocusPoint;
import java.util.ArrayList;
@@ -795,6 +795,10 @@ public void onMediaCapturePathReady(String mediaCapturePath) {
mMediaCapturePath = mediaCapturePath;
}
+ @Override public void onCameraError(String errorMessage) {
+ ToastUtils.showToast(this, errorMessage, LONG);
+ }
+
private void showMediaToastError(@StringRes int message, @Nullable String messageDetail) {
if (isFinishing()) {
return;
@@ -1093,7 +1097,7 @@ private void addMediaToUploadService(@NonNull ArrayList mediaModels)
}
UploadService.uploadMedia(this, mediaModels, "MediaBrowserActivity#addMediaToUploadService");
- AppRatingDialog.INSTANCE.incrementInteractions(APP_REVIEWS_EVENT_INCREMENTED_BY_UPLOADING_MEDIA);
+ AppReviewManager.INSTANCE.incrementInteractions(APP_REVIEWS_EVENT_INCREMENTED_BY_UPLOADING_MEDIA);
}
private void queueFileForUpload(Uri uri, String mimeType) {
@@ -1149,7 +1153,13 @@ private void startMediaDeleteService(ArrayList mediaToDelete) {
intent.putExtra(MediaDeleteService.MEDIA_LIST_KEY, mediaToDelete);
doBindDeleteService(intent);
}
- startService(intent);
+ try {
+ startService(intent);
+ } catch (IllegalStateException e) {
+ // This can happen if the app still appears to be running in the background
+ // see: https://github.com/wordpress-mobile/WordPress-Android/issues/18638
+ AppLog.e(AppLog.T.MEDIA, "Unable to start MediaDeleteService: " + e.getMessage());
+ }
}
}
@@ -1193,10 +1203,11 @@ public void onSiteChanged(OnSiteChanged event) {
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(UploadService.UploadErrorEvent event) {
EventBus.getDefault().removeStickyEvent(event);
- if (event.mediaModelList != null && !event.mediaModelList.isEmpty()) {
+ View snackbarAttachView = findViewById(R.id.tab_layout);
+ if (event.mediaModelList != null && !event.mediaModelList.isEmpty() && snackbarAttachView != null) {
mUploadUtilsWrapper.onMediaUploadedSnackbarHandler(
this,
- findViewById(R.id.tab_layout),
+ snackbarAttachView,
true,
!TextUtils.isEmpty(event.errorMessage)
&& event.errorMessage.contains(getString(R.string.error_media_quota_exceeded))
@@ -1213,10 +1224,10 @@ public void onEventMainThread(UploadService.UploadErrorEvent event) {
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(UploadService.UploadMediaSuccessEvent event) {
EventBus.getDefault().removeStickyEvent(event);
- if (event.mediaModelList != null && !event.mediaModelList.isEmpty()) {
- mUploadUtilsWrapper.onMediaUploadedSnackbarHandler(this,
- findViewById(R.id.tab_layout), false,
- event.mediaModelList, mSite, event.successMessage);
+ View snackbarAttachView = findViewById(R.id.tab_layout);
+ if (event.mediaModelList != null && !event.mediaModelList.isEmpty() && snackbarAttachView != null) {
+ mUploadUtilsWrapper.onMediaUploadedSnackbarHandler(this, snackbarAttachView, false, event.mediaModelList,
+ mSite, event.successMessage);
updateMediaGridForTheseMedia(event.mediaModelList);
}
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserType.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserType.java
index f30ab8f2ca27..c4995c671eb8 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserType.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserType.java
@@ -14,8 +14,7 @@ public enum MediaBrowserType {
GUTENBERG_SINGLE_MEDIA_PICKER, // select a single image or video to insert into a post
GUTENBERG_MEDIA_PICKER, // select multiple images or videos to insert into a post
GUTENBERG_SINGLE_FILE_PICKER, // select a file to insert into a post
- GUTENBERG_SINGLE_AUDIO_FILE_PICKER, // select an audio file to insert into a post
- WP_STORIES_MEDIA_PICKER; // select multiple images or videos to insert as Story frames in a Story
+ GUTENBERG_SINGLE_AUDIO_FILE_PICKER; // select an audio file to insert into a post
public boolean isPicker() {
return this != BROWSER;
@@ -42,7 +41,6 @@ public boolean isImagePicker() {
|| this == GUTENBERG_SINGLE_IMAGE_PICKER
|| this == GUTENBERG_SINGLE_MEDIA_PICKER
|| this == GUTENBERG_MEDIA_PICKER
- || this == WP_STORIES_MEDIA_PICKER
|| this == GUTENBERG_SINGLE_FILE_PICKER;
}
@@ -53,7 +51,6 @@ public boolean isVideoPicker() {
|| this == GUTENBERG_SINGLE_VIDEO_PICKER
|| this == GUTENBERG_SINGLE_MEDIA_PICKER
|| this == GUTENBERG_MEDIA_PICKER
- || this == WP_STORIES_MEDIA_PICKER
|| this == GUTENBERG_SINGLE_FILE_PICKER;
}
@@ -76,10 +73,6 @@ public boolean isGutenbergPicker() {
|| this == GUTENBERG_SINGLE_AUDIO_FILE_PICKER;
}
- public boolean isWPStoriesPicker() {
- return this == WP_STORIES_MEDIA_PICKER;
- }
-
public boolean isSingleFilePicker() {
return this == GUTENBERG_SINGLE_FILE_PICKER;
}
@@ -96,8 +89,7 @@ public boolean canMultiselect() {
return this == EDITOR_PICKER
|| this == AZTEC_EDITOR_PICKER
|| this == GUTENBERG_IMAGE_PICKER
- || this == GUTENBERG_VIDEO_PICKER
- || this == WP_STORIES_MEDIA_PICKER;
+ || this == GUTENBERG_VIDEO_PICKER;
}
public boolean canFilter() {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java
index 3d96fd3386c7..b1fe511abc3b 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java
@@ -67,6 +67,7 @@
import org.wordpress.android.fluxc.generated.MediaActionBuilder;
import org.wordpress.android.fluxc.model.MediaModel;
import org.wordpress.android.fluxc.model.SiteModel;
+import org.wordpress.android.fluxc.network.UserAgent;
import org.wordpress.android.fluxc.store.MediaStore;
import org.wordpress.android.fluxc.store.MediaStore.MediaPayload;
import org.wordpress.android.fluxc.store.MediaStore.OnMediaChanged;
@@ -153,6 +154,8 @@ private enum MediaType {
private MediaType mMediaType;
+ @Inject UserAgent mUserAgent;
+
@Inject MediaStore mMediaStore;
@Inject Dispatcher mDispatcher;
@Inject ImageManager mImageManager;
@@ -1031,7 +1034,7 @@ private void saveMediaToDevice() {
}
request.allowScanningByMediaScanner();
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
- request.addRequestHeader("User-Agent", WordPress.getUserAgent());
+ request.addRequestHeader("User-Agent", mUserAgent.toString());
mDownloadId = dm.enqueue(request);
invalidateOptionsMenu();
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadReadyListener.java b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadReadyListener.java
index b8ebe1d28fc1..fafcc37c834f 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadReadyListener.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadReadyListener.java
@@ -1,5 +1,6 @@
package org.wordpress.android.ui.media.services;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wordpress.android.fluxc.model.PostModel;
@@ -14,7 +15,7 @@ public interface MediaUploadReadyListener {
// TODO: We're passing a SiteModel parameter here in order to debug a crash on SaveStoryGutenbergBlockUseCase.
// Once that's done, the parameter should be replaced with a site url String, like it was before.
// See: https://git.io/JqfhK
- PostModel replaceMediaFileWithUrlInPost(@Nullable PostModel post, String localMediaId, MediaFile mediaFile,
+ PostModel replaceMediaFileWithUrlInPost(@Nullable PostModel post, @NonNull String localMediaId, MediaFile mediaFile,
@Nullable SiteModel site);
PostModel markMediaUploadFailedInPost(@Nullable PostModel post, String localMediaId, MediaFile mediaFile);
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActivity.kt
index 46d141ac72c7..4732b8ddac8e 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActivity.kt
@@ -27,7 +27,6 @@ import org.wordpress.android.ui.mediapicker.MediaPickerActivity.MediaPickerMedia
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.Companion.newInstance
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerAction
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerAction.OpenCameraForPhotos
-import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerAction.OpenCameraForWPStories
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerAction.OpenSystemPicker
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerAction.SwitchMediaPicker
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerListener
@@ -37,7 +36,6 @@ import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.GIF_LIBR
import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.STOCK_LIBRARY
import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.WP_LIBRARY
import org.wordpress.android.ui.photopicker.MediaPickerConstants
-import org.wordpress.android.ui.photopicker.MediaPickerConstants.EXTRA_LAUNCH_WPSTORIES_CAMERA_REQUESTED
import org.wordpress.android.ui.photopicker.MediaPickerConstants.EXTRA_MEDIA_ID
import org.wordpress.android.ui.photopicker.MediaPickerConstants.EXTRA_MEDIA_QUEUED_URIS
import org.wordpress.android.ui.photopicker.MediaPickerConstants.EXTRA_MEDIA_SOURCE
@@ -49,6 +47,7 @@ import org.wordpress.android.ui.posts.editor.ImageEditorTracker
import org.wordpress.android.ui.utils.UiHelpers
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.AppLog.T.MEDIA
+import org.wordpress.android.util.ToastUtils
import org.wordpress.android.util.WPMediaUtils
import org.wordpress.android.util.extensions.getSerializableCompat
import org.wordpress.android.util.extensions.getSerializableExtraCompat
@@ -251,13 +250,6 @@ class MediaPickerActivity : LocaleAwareActivity(), MediaPickerListener {
WPMediaUtils.launchChooserWithContext(this, openSystemPicker, uiHelpers, MEDIA_LIBRARY)
}
- private fun launchWPStoriesCamera() {
- val intent = Intent()
- .putExtra(EXTRA_LAUNCH_WPSTORIES_CAMERA_REQUESTED, true)
- setResult(Activity.RESULT_OK, intent)
- finish()
- }
-
private fun Intent.putUris(
mediaUris: List
) {
@@ -321,12 +313,27 @@ class MediaPickerActivity : LocaleAwareActivity(), MediaPickerListener {
is OpenSystemPicker -> {
launchChooserWithContext(action, uiHelpers)
}
- is OpenCameraForWPStories -> launchWPStoriesCamera()
is SwitchMediaPicker -> {
startActivityForResult(buildIntent(this, action.mediaPickerSetup, site, localPostId), PHOTO_PICKER)
}
OpenCameraForPhotos -> {
- WPMediaUtils.launchCamera(this, BuildConfig.APPLICATION_ID) { mediaCapturePath = it }
+ WPMediaUtils.launchCamera(
+ this,
+ BuildConfig.APPLICATION_ID,
+ object : WPMediaUtils.LaunchCameraCallback {
+ override fun onMediaCapturePathReady(mediaCapturePath: String?) {
+ this@MediaPickerActivity.mediaCapturePath = mediaCapturePath
+ }
+
+ override fun onCameraError(errorMessage: String?) {
+ ToastUtils.showToast(
+ this@MediaPickerActivity,
+ errorMessage,
+ ToastUtils.Duration.SHORT
+ )
+ }
+ }
+ )
}
}
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt
index f5a87762415c..54a41e988294 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt
@@ -45,7 +45,6 @@ import org.wordpress.android.ui.mediapicker.MediaNavigationEvent.PreviewUrl
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerIconType.ANDROID_CHOOSE_FROM_DEVICE
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerIconType.CAPTURE_PHOTO
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerIconType.SWITCH_SOURCE
-import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerIconType.WP_STORIES_CAPTURE
import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource
import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.ActionModeUiModel
import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.BrowseMenuUiModel.BrowseAction.DEVICE
@@ -86,7 +85,6 @@ class MediaPickerFragment : Fragment(), MenuProvider {
enum class MediaPickerIconType {
ANDROID_CHOOSE_FROM_DEVICE,
SWITCH_SOURCE,
- WP_STORIES_CAPTURE,
CAPTURE_PHOTO;
companion object {
@@ -117,7 +115,6 @@ class MediaPickerFragment : Fragment(), MenuProvider {
val allowMultipleSelection: Boolean
) : MediaPickerAction()
- data class OpenCameraForWPStories(val allowMultipleSelection: Boolean) : MediaPickerAction()
object OpenCameraForPhotos : MediaPickerAction()
data class SwitchMediaPicker(val mediaPickerSetup: MediaPickerSetup) : MediaPickerAction()
}
@@ -128,8 +125,6 @@ class MediaPickerFragment : Fragment(), MenuProvider {
) : MediaPickerIcon(ANDROID_CHOOSE_FROM_DEVICE)
data class SwitchSource(val dataSource: DataSource) : MediaPickerIcon(SWITCH_SOURCE)
-
- object WpStoriesCapture : MediaPickerIcon(WP_STORIES_CAPTURE)
object CapturePhoto : MediaPickerIcon(CAPTURE_PHOTO)
fun toBundle(bundle: Bundle) {
@@ -145,7 +140,6 @@ class MediaPickerFragment : Fragment(), MenuProvider {
bundle.putInt(KEY_LAST_TAPPED_ICON_DATA_SOURCE, this.dataSource.ordinal)
}
is CapturePhoto -> Unit // Do nothing
- is WpStoriesCapture -> Unit // Do nothing
}
}
@@ -164,7 +158,6 @@ class MediaPickerFragment : Fragment(), MenuProvider {
}.toSet()
ChooseFromAndroidDevice(allowedTypes)
}
- WP_STORIES_CAPTURE -> WpStoriesCapture
CAPTURE_PHOTO -> CapturePhoto
SWITCH_SOURCE -> {
val ordinal = bundle.getInt(KEY_LAST_TAPPED_ICON_DATA_SOURCE, -1)
@@ -693,6 +686,9 @@ class MediaPickerFragment : Fragment(), MenuProvider {
// devices lower than API 33.
permissions.add(permission.READ_EXTERNAL_STORAGE)
}
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ permissions.add(permission.ACCESS_MEDIA_LOCATION)
+ }
requestPermissions(permissions.toTypedArray(), WPPermissionUtils.PHOTO_PICKER_MEDIA_PERMISSION_REQUEST_CODE)
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerSetup.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerSetup.kt
index 5cb2ebf4c0ca..3752aee242d0 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerSetup.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerSetup.kt
@@ -24,7 +24,7 @@ data class MediaPickerSetup(
}
enum class CameraSetup {
- STORIES, ENABLED, HIDDEN
+ ENABLED, HIDDEN
}
fun toBundle(bundle: Bundle) {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerTracker.kt
index 874a7687211c..79e2186af4e5 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerTracker.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerTracker.kt
@@ -10,7 +10,6 @@ import org.wordpress.android.analytics.AnalyticsTracker.Stat.MEDIA_PICKER_OPEN_D
import org.wordpress.android.analytics.AnalyticsTracker.Stat.MEDIA_PICKER_OPEN_GIF_LIBRARY
import org.wordpress.android.analytics.AnalyticsTracker.Stat.MEDIA_PICKER_OPEN_STOCK_LIBRARY
import org.wordpress.android.analytics.AnalyticsTracker.Stat.MEDIA_PICKER_OPEN_WP_MEDIA
-import org.wordpress.android.analytics.AnalyticsTracker.Stat.MEDIA_PICKER_OPEN_WP_STORIES_CAPTURE
import org.wordpress.android.analytics.AnalyticsTracker.Stat.MEDIA_PICKER_PREVIEW_OPENED
import org.wordpress.android.analytics.AnalyticsTracker.Stat.MEDIA_PICKER_RECENT_MEDIA_SELECTED
import org.wordpress.android.analytics.AnalyticsTracker.Stat.MEDIA_PICKER_SEARCH_COLLAPSED
@@ -80,10 +79,6 @@ class MediaPickerTracker
fun trackIconClick(icon: MediaPickerIcon, mediaPickerSetup: MediaPickerSetup) {
when (icon) {
- is MediaPickerIcon.WpStoriesCapture -> analyticsTrackerWrapper.track(
- MEDIA_PICKER_OPEN_WP_STORIES_CAPTURE,
- mediaPickerSetup.toProperties()
- )
is MediaPickerIcon.ChooseFromAndroidDevice -> analyticsTrackerWrapper.track(
MEDIA_PICKER_OPEN_DEVICE_LIBRARY,
mediaPickerSetup.toProperties()
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModel.kt
index 78fd876ae461..829bd0f78252 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModel.kt
@@ -25,17 +25,14 @@ import org.wordpress.android.ui.mediapicker.MediaNavigationEvent.PreviewUrl
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.ChooserContext
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerAction
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerAction.OpenCameraForPhotos
-import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerAction.OpenCameraForWPStories
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerAction.OpenSystemPicker
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerAction.SwitchMediaPicker
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerIcon
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerIcon.CapturePhoto
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerIcon.ChooseFromAndroidDevice
import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerIcon.SwitchSource
-import org.wordpress.android.ui.mediapicker.MediaPickerFragment.MediaPickerIcon.WpStoriesCapture
import org.wordpress.android.ui.mediapicker.MediaPickerSetup.CameraSetup.ENABLED
import org.wordpress.android.ui.mediapicker.MediaPickerSetup.CameraSetup.HIDDEN
-import org.wordpress.android.ui.mediapicker.MediaPickerSetup.CameraSetup.STORIES
import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.DEVICE
import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.GIF_LIBRARY
import org.wordpress.android.ui.mediapicker.MediaPickerSetup.DataSource.STOCK_LIBRARY
@@ -496,7 +493,7 @@ class MediaPickerViewModel @Inject constructor(
private fun clickIcon(icon: MediaPickerIcon) {
mediaPickerTracker.trackIconClick(icon, mediaPickerSetup)
- if (icon is WpStoriesCapture || icon is CapturePhoto) {
+ if (icon is CapturePhoto) {
if (!permissionsHandler.hasPermissionsToTakePhoto()) {
_onCameraPermissionsRequested.value = Event(Unit)
lastTappedIcon = icon
@@ -510,7 +507,6 @@ class MediaPickerViewModel @Inject constructor(
private fun clickOnCamera() {
when (mediaPickerSetup.cameraSetup) {
- STORIES -> clickIcon(WpStoriesCapture)
ENABLED -> clickIcon(CapturePhoto)
HIDDEN -> {
// Do nothing
@@ -547,7 +543,6 @@ class MediaPickerViewModel @Inject constructor(
}
OpenSystemPicker(context, types.toList(), canMultiselect)
}
- is WpStoriesCapture -> OpenCameraForWPStories(canMultiselect)
is CapturePhoto -> OpenCameraForPhotos
is SwitchSource -> {
SwitchMediaPicker(
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/AccountDataSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/AccountDataSource.kt
deleted file mode 100644
index 3b1d69d6099a..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/AccountDataSource.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.wordpress.android.ui.mysite
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MediatorLiveData
-import androidx.lifecycle.MutableLiveData
-import kotlinx.coroutines.CoroutineScope
-import org.wordpress.android.fluxc.store.AccountStore
-import org.wordpress.android.ui.mysite.MySiteSource.SiteIndependentSource
-import org.wordpress.android.ui.mysite.MySiteUiState.PartialState.AccountData
-import javax.inject.Inject
-
-class AccountDataSource @Inject constructor(
- private val accountStore: AccountStore
-) : SiteIndependentSource {
- override val refresh = MutableLiveData(false)
-
- override fun build(coroutineScope: CoroutineScope): LiveData {
- val result = MediatorLiveData()
- result.addSource(refresh) { result.refreshData(refresh.value) }
- refresh()
- return result
- }
-
- private fun MediatorLiveData.refreshData(
- isRefresh: Boolean? = null
- ) {
- when (isRefresh) {
- null, true -> {
- val url = accountStore.account?.avatarUrl.orEmpty()
- val name =
- accountStore.account?.displayName?.ifEmpty { accountStore.account?.userName.orEmpty() }.orEmpty()
- setState(AccountData(url,name))
- }
- false -> Unit // Do nothing
- }
- }
-}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/AccountDataViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/AccountDataViewModelSlice.kt
new file mode 100644
index 000000000000..555bb411a5d2
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/AccountDataViewModelSlice.kt
@@ -0,0 +1,61 @@
+package org.wordpress.android.ui.mysite
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import org.wordpress.android.fluxc.store.AccountStore
+import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper
+import org.wordpress.android.ui.mysite.MySiteUiState.PartialState.AccountData
+import org.wordpress.android.util.BuildConfigWrapper
+import javax.inject.Inject
+
+class AccountDataViewModelSlice @Inject constructor(
+ private val accountStore: AccountStore,
+ private val buildConfigWrapper: BuildConfigWrapper,
+ private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper
+) {
+ private lateinit var scope: CoroutineScope
+
+ private val _isRefreshing = MutableLiveData()
+ val isRefreshing: LiveData = _isRefreshing
+
+ private val _uiModel = MutableLiveData()
+ val uiModel: LiveData = _uiModel
+
+ fun initialize(scope: CoroutineScope) {
+ this.scope = scope
+ }
+
+ fun onResume() {
+ scope.launch {
+ if(!shouldBuildCard()) _uiModel.postValue(null)
+ _isRefreshing.postValue(true)
+ val account = accountStore.account
+ account?.let {
+ val url = account.avatarUrl.orEmpty()
+ val name =account.displayName?.ifEmpty {
+ account.userName.orEmpty()
+ }.orEmpty()
+ _uiModel.postValue(AccountData(url, name))
+ }?: {
+ _uiModel.postValue(null)
+ }
+ _isRefreshing.postValue(false)
+ }
+ }
+
+ fun onRefresh() {
+ onResume()
+ }
+
+ fun onCleared() {
+ scope.cancel()
+ }
+
+ private fun shouldBuildCard(): Boolean {
+ return (!buildConfigWrapper.isJetpackApp
+ && jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures())
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/BlazeCardViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/BlazeCardViewModelSlice.kt
index 2686d691ecb7..a6241b2e2e5a 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/BlazeCardViewModelSlice.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/BlazeCardViewModelSlice.kt
@@ -1,41 +1,120 @@
package org.wordpress.android.ui.mysite
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.distinctUntilChanged
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import org.wordpress.android.Result
import org.wordpress.android.analytics.AnalyticsTracker
+import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.model.blaze.BlazeCampaignModel
+import org.wordpress.android.modules.BG_THREAD
import org.wordpress.android.ui.blaze.BlazeFeatureUtils
import org.wordpress.android.ui.blaze.BlazeFlowSource
import org.wordpress.android.ui.blaze.blazecampaigns.campaigndetail.CampaignDetailPageSource
import org.wordpress.android.ui.blaze.blazecampaigns.campaignlisting.CampaignListingPageSource
+import org.wordpress.android.ui.blaze.blazecampaigns.campaignlisting.FetchCampaignListUseCase
import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.BlazeCardBuilderParams
import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.BlazeCardBuilderParams.CampaignWithBlazeCardBuilderParams
import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.BlazeCardBuilderParams.PromoteWithBlazeCardBuilderParams
-import org.wordpress.android.ui.mysite.MySiteUiState.PartialState.BlazeCardUpdate
+import org.wordpress.android.ui.mysite.cards.blaze.BlazeCardBuilder
+import org.wordpress.android.ui.mysite.cards.blaze.MostRecentCampaignUseCase
import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker
+import org.wordpress.android.util.NetworkUtilsWrapper
import org.wordpress.android.viewmodel.Event
import javax.inject.Inject
+import javax.inject.Named
import javax.inject.Singleton
@Singleton
class BlazeCardViewModelSlice @Inject constructor(
+ @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher,
private val blazeFeatureUtils: BlazeFeatureUtils,
private val selectedSiteRepository: SelectedSiteRepository,
- private val cardsTracker: CardsTracker
+ private val cardsTracker: CardsTracker,
+ private val blazeCardBuilder: BlazeCardBuilder,
+ private val networkUtilsWrapper: NetworkUtilsWrapper,
+ private val fetchCampaignListUseCase: FetchCampaignListUseCase,
+ private val mostRecentCampaignUseCase: MostRecentCampaignUseCase,
) {
+ private lateinit var scope: CoroutineScope
+
private val _onNavigation = MutableLiveData>()
val onNavigation = _onNavigation
- private val _refresh = MutableLiveData>()
- val refresh = _refresh
+ private val _isRefreshing = MutableLiveData()
+ val isRefreshing: LiveData = _isRefreshing
+
+ private val _uiModel = MutableLiveData()
+ val uiModel = _uiModel.distinctUntilChanged()
+
+ fun initialize(scope: CoroutineScope) {
+ this.scope = scope
+ }
+
+ fun buildCard(site: SiteModel) {
+ _isRefreshing.postValue(true)
+ scope.launch(bgDispatcher){
+ if (blazeFeatureUtils.shouldShowBlazeCardEntryPoint(site)) {
+ if (blazeFeatureUtils.shouldShowBlazeCampaigns()) {
+ fetchCampaigns(site)
+ } else {
+ // show blaze promo card if campaign feature is not available
+ showPromoteWithBlazeCard()
+ }
+ } else {
+ postState(false)
+ }
+ }
+ }
+
+ private suspend fun fetchCampaigns(site: SiteModel) {
+ if (networkUtilsWrapper.isNetworkAvailable().not()) {
+ getMostRecentCampaignFromDb(site)
+ } else {
+ when (fetchCampaignListUseCase.execute(site = site, offset = 0)) {
+ is Result.Success -> getMostRecentCampaignFromDb(site)
+ // there are no campaigns or if there is an error , show blaze promo card
+ is Result.Failure -> showPromoteWithBlazeCard()
+ }
+ }
+ }
+
+ private suspend fun getMostRecentCampaignFromDb(site: SiteModel) {
+ when(val result = mostRecentCampaignUseCase.execute(site)) {
+ is Result.Success -> postState(true, campaign = result.value)
+ is Result.Failure -> showPromoteWithBlazeCard()
+ }
+ }
+
+ private fun showPromoteWithBlazeCard() {
+ postState(true)
+ }
- fun getBlazeCardBuilderParams(blazeCardUpdate: BlazeCardUpdate?): BlazeCardBuilderParams? {
- return blazeCardUpdate?.let {
- if (it.blazeEligible) {
- it.campaign?.let { campaign ->
+ private fun postState(isBlazeEligible: Boolean, campaign: BlazeCampaignModel? = null) {
+ _isRefreshing.postValue(false)
+ if(isBlazeEligible) {
+ buildBlazeCard(campaign)?.let {
+ _uiModel.postValue(it)
+ }
+ } else {
+ _uiModel.postValue(null)
+ }
+ }
+
+
+ private fun buildBlazeCard(campaign: BlazeCampaignModel? = null): MySiteCardAndItem.Card.BlazeCard? {
+ return getBlazeCardBuilderParams(campaign).let { blazeCardBuilder.build(it) }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun getBlazeCardBuilderParams(campaign: BlazeCampaignModel? = null): BlazeCardBuilderParams {
+ return campaign?.let {
getCampaignWithBlazeCardBuilderParams(campaign)
} ?: getPromoteWithBlazeCardBuilderParams()
- } else null
- }
}
private fun getCampaignWithBlazeCardBuilderParams(campaign: BlazeCampaignModel) =
@@ -89,6 +168,7 @@ class BlazeCardViewModelSlice @Inject constructor(
)
}
+
private fun getPromoteWithBlazeCardBuilderParams() =
PromoteWithBlazeCardBuilderParams(
onClick = this::onPromoteWithBlazeCardClick,
@@ -99,7 +179,6 @@ class BlazeCardViewModelSlice @Inject constructor(
)
)
-
private fun onPromoteCardLearnMoreClick() {
cardsTracker.trackCardMoreMenuItemClicked(
CardsTracker.Type.PROMOTE_WITH_BLAZE.label,
@@ -138,7 +217,7 @@ class BlazeCardViewModelSlice @Inject constructor(
selectedSiteRepository.getSelectedSite()?.let {
blazeFeatureUtils.hideBlazeCard(it.siteId)
}
- _refresh.value = Event(true)
+ _uiModel.postValue(null)
}
private fun onPromoteCardMoreMenuClick() {
@@ -155,7 +234,7 @@ class BlazeCardViewModelSlice @Inject constructor(
Event(SiteNavigationAction.OpenPromoteWithBlazeOverlay(source = BlazeFlowSource.DASHBOARD_CARD))
}
- private fun onCampaignClick(campaignId: Int) {
+ private fun onCampaignClick(campaignId: String) {
_onNavigation.value =
Event(SiteNavigationAction.OpenCampaignDetailPage(campaignId, CampaignDetailPageSource.DASHBOARD_CARD))
}
@@ -170,6 +249,10 @@ class BlazeCardViewModelSlice @Inject constructor(
_onNavigation.value =
Event(SiteNavigationAction.OpenPromoteWithBlazeOverlay(source = BlazeFlowSource.DASHBOARD_CARD))
}
+
+ fun clearValue() {
+ _uiModel.postValue(null)
+ }
}
enum class CampaignCardMenuItem(val label: String) {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/BloggingPromptsCardTrackHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/BloggingPromptsCardTrackHelper.kt
index 08dc088750e3..d0e5d2ca4dfc 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/BloggingPromptsCardTrackHelper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/BloggingPromptsCardTrackHelper.kt
@@ -9,7 +9,6 @@ import org.wordpress.android.modules.BG_THREAD
import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BloggingPromptCard
import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptsCardAnalyticsTracker
import java.util.concurrent.atomic.AtomicBoolean
-import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
import javax.inject.Named
@@ -26,30 +25,9 @@ class BloggingPromptsCardTrackHelper @Inject constructor(
) {
private var dashboardUpdateDebounceJob: Job? = null
- private val latestPromptCardVisible = AtomicReference(null)
- private val waitingToTrack = AtomicBoolean(false)
- private val currentSite = AtomicReference(null)
-
- private fun onDashboardRefreshed(state: MySiteViewModel.State.SiteSelected?) {
- val bloggingPromptCards = state?.dashboardData
- ?.filterIsInstance()
- ?: listOf()
-
- latestPromptCardVisible.get()?.let { isPromptCardVisible ->
- val attribution = bloggingPromptCards.firstOrNull()?.attribution
- if (isPromptCardVisible) tracker.trackMySiteCardViewed(attribution)
- waitingToTrack.set(false)
- } ?: run {
- waitingToTrack.set(true)
- }
- }
-
-
- fun onDashboardCardsUpdated(scope: CoroutineScope, state: MySiteViewModel.State.SiteSelected?) {
- val bloggingPromptCards = state?.dashboardData
- ?.filterIsInstance()
- ?: listOf()
+ private val waitingToTrack = AtomicBoolean(true)
+ fun onDashboardCardsUpdated(scope: CoroutineScope, bloggingPromptCards: List) {
// cancel any existing job (debouncing mechanism)
dashboardUpdateDebounceJob?.cancel()
@@ -58,8 +36,6 @@ class BloggingPromptsCardTrackHelper @Inject constructor(
// add a delay (debouncing mechanism)
delay(PROMPT_CARD_VISIBLE_DEBOUNCE)
-
- latestPromptCardVisible.set(isVisible)
if (isVisible && waitingToTrack.getAndSet(false)) {
val attribution = bloggingPromptCards.firstOrNull()?.attribution
tracker.trackMySiteCardViewed(attribution)
@@ -72,15 +48,8 @@ class BloggingPromptsCardTrackHelper @Inject constructor(
}
}
- fun onResume(state: MySiteViewModel.State.SiteSelected?) {
- onDashboardRefreshed(state)
- }
-
- fun onSiteChanged(siteId: Int?, state: MySiteViewModel.State.SiteSelected?) {
- if (currentSite.getAndSet(siteId) != siteId) {
- latestPromptCardVisible.set(null)
- onDashboardRefreshed(state)
- }
+ fun onSiteChanged() {
+ waitingToTrack.set(true)
}
private val BloggingPromptCard.attribution: String?
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapter.kt
index 98d44e0f55fc..718905d79376 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapter.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapter.kt
@@ -53,7 +53,7 @@ import org.wordpress.android.ui.mysite.cards.nocards.NoCardsMessageViewHolder
import org.wordpress.android.ui.mysite.cards.personalize.PersonalizeCardViewHolder
import org.wordpress.android.ui.mysite.cards.quicklinksitem.QuickLinkRibbonViewHolder
import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartCardViewHolder
-import org.wordpress.android.ui.mysite.cards.siteinfo.SiteInfoHeaderCardViewholder
+import org.wordpress.android.ui.mysite.cards.siteinfo.SiteInfoHeaderCardViewHolder
import org.wordpress.android.ui.mysite.cards.sotw2023.WpSotw2023NudgeCardViewHolder
import org.wordpress.android.ui.mysite.items.categoryheader.MySiteCategoryItemEmptyViewHolder
import org.wordpress.android.ui.mysite.items.categoryheader.MySiteCategoryItemViewHolder
@@ -80,7 +80,7 @@ class MySiteAdapter(
@Suppress("ComplexMethod")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MySiteCardAndItemViewHolder<*> {
return when (viewType) {
- MySiteCardAndItem.Type.SITE_INFO_CARD.ordinal -> SiteInfoHeaderCardViewholder(parent, imageManager)
+ MySiteCardAndItem.Type.SITE_INFO_CARD.ordinal -> SiteInfoHeaderCardViewHolder(parent, imageManager)
MySiteCardAndItem.Type.QUICK_LINK_RIBBON.ordinal -> QuickLinkRibbonViewHolder(parent)
MySiteCardAndItem.Type.DOMAIN_REGISTRATION_CARD.ordinal -> DomainRegistrationViewHolder(parent)
MySiteCardAndItem.Type.QUICK_START_CARD.ordinal -> QuickStartCardViewHolder(parent, uiHelpers)
@@ -136,7 +136,7 @@ class MySiteAdapter(
@Suppress("ComplexMethod")
override fun onBindViewHolder(holder: MySiteCardAndItemViewHolder<*>, position: Int) {
when (holder) {
- is SiteInfoHeaderCardViewholder -> holder.bind(getItem(position) as SiteInfoHeaderCard)
+ is SiteInfoHeaderCardViewHolder -> holder.bind(getItem(position) as SiteInfoHeaderCard)
is QuickLinkRibbonViewHolder -> holder.bind(getItem(position) as QuickLinksItem)
is DomainRegistrationViewHolder -> holder.bind(getItem(position) as DomainRegistrationCard)
is QuickStartCardViewHolder -> holder.bind(getItem(position) as QuickStartCard)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt
index 9c5f226eda9c..60dfe33e0b65 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt
@@ -326,12 +326,12 @@ sealed class MySiteCardAndItem(open val type: Type, open val activeQuickStartIte
val moreMenuOptions: MoreMenuOptions
) : BlazeCard(type = Type.BLAZE_CAMPAIGNS_CARD) {
data class BlazeCampaignsCardItem(
- val id: Int,
+ val id: String,
val title: UiString,
val status: CampaignStatus?,
val featuredImageUrl: String?,
val stats: BlazeCampaignStats?,
- val onClick: (campaignId: Int) -> Unit,
+ val onClick: (campaignId: String) -> Unit,
) {
data class BlazeCampaignStats(
val impressions: UiString,
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItemBuilderParams.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItemBuilderParams.kt
index f5b256074321..d84bec2a64d9 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItemBuilderParams.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItemBuilderParams.kt
@@ -34,21 +34,6 @@ sealed class MySiteCardAndItemBuilderParams {
val isStaleMessagePresent: Boolean
) : MySiteCardAndItemBuilderParams()
- data class QuickLinkRibbonBuilderParams(
- val siteModel: SiteModel,
- val onPagesClick: () -> Unit,
- val onPostsClick: () -> Unit,
- val onMediaClick: () -> Unit,
- val onStatsClick: () -> Unit,
- val onMoreClick: () -> Unit,
- val activeTask: QuickStartTask?,
- ) : MySiteCardAndItemBuilderParams()
-
- data class DomainRegistrationCardBuilderParams(
- val isDomainCreditAvailable: Boolean,
- val domainRegistrationClick: () -> Unit
- ) : MySiteCardAndItemBuilderParams()
-
data class QuickStartCardBuilderParams(
val quickStartCategories: List,
val onQuickStartTaskTypeItemClick: (type: QuickStartTaskType) -> Unit,
@@ -60,20 +45,6 @@ sealed class MySiteCardAndItemBuilderParams {
)
}
- data class DashboardCardsBuilderParams(
- val showErrorCard: Boolean = false,
- val onErrorRetryClick: () -> Unit,
- val todaysStatsCardBuilderParams: TodaysStatsCardBuilderParams,
- val postCardBuilderParams: PostCardBuilderParams,
- val bloganuaryNudgeCardBuilderParams: BloganuaryNudgeCardBuilderParams,
- val bloggingPromptCardBuilderParams: BloggingPromptCardBuilderParams,
- val blazeCardBuilderParams: BlazeCardBuilderParams? = null,
- val dashboardCardPlansBuilderParams: DashboardCardPlansBuilderParams,
- val pagesCardBuilderParams: PagesCardBuilderParams,
- val activityCardBuilderParams: ActivityCardBuilderParams,
- val dynamicCardsBuilderParams: DynamicCardsBuilderParams,
- ) : MySiteCardAndItemBuilderParams()
-
data class TodaysStatsCardBuilderParams(
val todaysStatsCard: TodaysStatsCardModel?,
val onTodaysStatsCardClick: () -> Unit,
@@ -188,7 +159,7 @@ sealed class MySiteCardAndItemBuilderParams {
data class CampaignWithBlazeCardBuilderParams(
val campaign: BlazeCampaignModel,
val onCreateCampaignClick: () -> Unit,
- val onCampaignClick: (campaignId: Int) -> Unit,
+ val onCampaignClick: (campaignId: String) -> Unit,
val onCardClick: () -> Unit,
val moreMenuParams: MoreMenuParams
) : BlazeCardBuilderParams() {
@@ -214,12 +185,6 @@ sealed class MySiteCardAndItemBuilderParams {
val onActionClick: () -> Unit
)
- data class JetpackInstallFullPluginCardBuilderParams(
- val site: SiteModel,
- val onLearnMoreClick: () -> Unit,
- val onHideMenuItemClick: () -> Unit,
- )
-
data class PersonalizeCardBuilderParams(
val onClick: () -> Unit
)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt
index d6f4d88ee399..4b25f1b5f361 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt
@@ -38,8 +38,10 @@ import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureFullScreenOverlayFr
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil
import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginFragment
import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingDialogFragment
-import org.wordpress.android.ui.main.SitePickerActivity
+import org.wordpress.android.ui.main.AddSiteHandler
+import org.wordpress.android.ui.main.ChooseSiteActivity
import org.wordpress.android.ui.main.WPMainActivity
+import org.wordpress.android.ui.main.WPMainActivity.OnScrollToTopListener
import org.wordpress.android.ui.main.jetpack.migration.JetpackMigrationActivity
import org.wordpress.android.ui.main.utils.MeGravatarLoader
import org.wordpress.android.ui.mysite.MySiteViewModel.State
@@ -49,7 +51,7 @@ import org.wordpress.android.ui.pages.SnackbarMessageHolder
import org.wordpress.android.ui.photopicker.MediaPickerConstants
import org.wordpress.android.ui.photopicker.MediaPickerLauncher
import org.wordpress.android.ui.posts.BasicDialogViewModel
-import org.wordpress.android.ui.posts.EditPostActivity
+import org.wordpress.android.ui.posts.EditPostActivityConstants
import org.wordpress.android.ui.posts.PostListType
import org.wordpress.android.ui.posts.PostUtils
import org.wordpress.android.ui.posts.QuickStartPromptDialogFragment
@@ -91,7 +93,8 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
TextInputDialogFragment.Callback,
QuickStartPromptClickInterface,
FullScreenDialogFragment.OnConfirmListener,
- FullScreenDialogFragment.OnDismissListener {
+ FullScreenDialogFragment.OnDismissListener,
+ OnScrollToTopListener {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
@@ -256,9 +259,6 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
}
}
RequestCodes.STORIES_PHOTO_PICKER,
- RequestCodes.PHOTO_PICKER -> if (resultCode == Activity.RESULT_OK) {
- viewModel.handleStoriesPhotoPickerResult(data)
- }
UCrop.REQUEST_CROP -> {
if (resultCode == UCrop.RESULT_ERROR) {
AppLog.e(
@@ -279,14 +279,14 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
val isNewSite = requestCode == RequestCodes.CREATE_SITE ||
data.getBooleanExtra(LoginEpilogueActivity.KEY_SITE_CREATED_FROM_LOGIN_EPILOGUE, false)
viewModel.performFirstStepAfterSiteCreation(
- data.getBooleanExtra(SitePickerActivity.KEY_SITE_TITLE_TASK_COMPLETED, false),
+ data.getBooleanExtra(ChooseSiteActivity.KEY_SITE_TITLE_TASK_COMPLETED, false),
isNewSite = isNewSite
)
}
RequestCodes.SITE_PICKER -> {
if (data.getIntExtra(WPMainActivity.ARG_CREATE_SITE, 0) == RequestCodes.CREATE_SITE) {
viewModel.performFirstStepAfterSiteCreation(
- data.getBooleanExtra(SitePickerActivity.KEY_SITE_TITLE_TASK_COMPLETED, false),
+ data.getBooleanExtra(ChooseSiteActivity.KEY_SITE_TITLE_TASK_COMPLETED, false),
isNewSite = true
)
} else {
@@ -295,9 +295,9 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
}
RequestCodes.EDIT_LANDING_PAGE -> {
viewModel.checkAndStartQuickStart(
- data.getBooleanExtra(SitePickerActivity.KEY_SITE_TITLE_TASK_COMPLETED, false),
+ data.getBooleanExtra(ChooseSiteActivity.KEY_SITE_TITLE_TASK_COMPLETED, false),
isNewSite = data.getBooleanExtra(
- EditPostActivity.EXTRA_IS_LANDING_EDITOR_OPENED_FOR_NEW_SITE, false
+ EditPostActivityConstants.EXTRA_IS_LANDING_EDITOR_OPENED_FOR_NEW_SITE, false
)
)
}
@@ -385,7 +385,6 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
@Suppress("LongMethod")
private fun MySiteFragmentBinding.setupObservers() {
viewModel.uiModel.observe(viewLifecycleOwner) { uiModel ->
- hideRefreshIndicatorIfNeeded()
when (uiModel) {
is State.SiteSelected -> loadData(uiModel)
is State.NoSites -> loadEmptyView(uiModel)
@@ -448,18 +447,16 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
if (quickStartScrollPosition == -1) {
quickStartScrollPosition = 0
}
- if (quickStartScrollPosition > 0) recyclerView.scrollToPosition(quickStartScrollPosition)
+ recyclerView.scrollToPosition(quickStartScrollPosition)
}
wpMainActivityViewModel.mySiteDashboardRefreshRequested.observeEvent(viewLifecycleOwner) {
viewModel.refresh()
}
- }
- private fun MySiteFragmentBinding.hideRefreshIndicatorIfNeeded() {
- swipeRefreshLayout.postDelayed({
- swipeToRefreshHelper.isRefreshing = viewModel.isRefreshing()
- }, CHECK_REFRESH_DELAY)
+ viewModel.isRefreshingOrLoading.observe(viewLifecycleOwner) {
+ swipeToRefreshHelper.isRefreshing = it
+ }
}
private fun showSnackbar(holder: SnackbarMessageHolder) {
@@ -541,15 +538,16 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
if (!noSitesView.actionableEmptyView.isVisible) {
noSitesView.actionableEmptyView.setVisible(true)
- noSitesView.actionableEmptyView.image.setVisible(state.shouldShowImage)
viewModel.onActionableEmptyViewVisible()
- showAvatarSettingsView(state)
}
+ showAvatarSettingsView(state)
siteTitle = getString(R.string.my_site_section_screen_title)
}
private fun MySiteFragmentBinding.showAvatarSettingsView(state: State.NoSites) {
- if (state.shouldShowAccountSettings) {
+ // For a newly created account, avatar may be null
+ if (state.accountName != null || state.avatarUrl != null){
+ noSitesView.actionableEmptyView.image.setVisible(true)
noSitesView.avatarAccountSettings.visibility = View.VISIBLE
noSitesView.meDisplayName.text = state.accountName
if (state.accountName.isNullOrEmpty()) {
@@ -618,21 +616,6 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
ActivityLauncher.viewConnectJetpackForStats(activity, action.site)
is SiteNavigationAction.StartWPComLoginForJetpackStats ->
ActivityLauncher.loginForJetpackStats(this@MySiteFragment)
- is SiteNavigationAction.OpenStories -> ActivityLauncher.viewStories(activity, action.site, action.event)
- is SiteNavigationAction.AddNewStory ->
- ActivityLauncher.addNewStoryForResult(activity, action.site, action.source)
- is SiteNavigationAction.AddNewStoryWithMediaIds -> ActivityLauncher.addNewStoryWithMediaIdsForResult(
- activity,
- action.site,
- action.source,
- action.mediaIds.toLongArray()
- )
- is SiteNavigationAction.AddNewStoryWithMediaUris -> ActivityLauncher.addNewStoryWithMediaUrisForResult(
- activity,
- action.site,
- action.source,
- action.mediaUris.toTypedArray()
- )
is SiteNavigationAction.OpenDomains -> ActivityLauncher.viewDomainsDashboardActivity(
activity,
action.site
@@ -654,7 +637,8 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
DomainRegistrationActivity.DomainRegistrationPurpose.DOMAIN_PURCHASE
)
- is SiteNavigationAction.AddNewSite -> SitePickerActivity.addSite(activity, action.hasAccessToken, action.source)
+ is SiteNavigationAction.AddNewSite ->
+ AddSiteHandler.addSite(requireActivity(), action.hasAccessToken, action.source)
is SiteNavigationAction.ShowQuickStartDialog -> showQuickStartDialog(
action.title,
action.message,
@@ -674,10 +658,10 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
is SiteNavigationAction.EditScheduledPost ->
ActivityLauncher.viewCurrentBlogPostsOfType(requireActivity(), action.site, PostListType.SCHEDULED)
- is SiteNavigationAction.OpenStatsInsights -> ActivityLauncher.viewBlogStatsForTimeframe(
+ is SiteNavigationAction.OpenStatsByDay -> ActivityLauncher.viewBlogStatsForTimeframe(
requireActivity(),
action.site,
- StatsTimeframe.INSIGHTS,
+ StatsTimeframe.DAY,
StatsLaunchedFrom.TODAY_STATS_CARD
)
@@ -869,7 +853,6 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
companion object {
@JvmField
var TAG: String = MySiteFragment::class.java.simpleName
- private const val CHECK_REFRESH_DELAY = 300L
private const val KEY_LIST_STATE = "key_list_state"
private const val KEY_NESTED_LISTS_STATES = "key_nested_lists_states"
private const val TAG_QUICK_START_DIALOG = "TAG_QUICK_START_DIALOG"
@@ -878,4 +861,8 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
return MySiteFragment()
}
}
+
+ override fun onScrollToTop() {
+ binding?.recyclerView?.smoothScrollToPosition(0)
+ }
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteSource.kt
deleted file mode 100644
index 299465034bc9..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteSource.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package org.wordpress.android.ui.mysite
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MediatorLiveData
-import androidx.lifecycle.MutableLiveData
-import kotlinx.coroutines.CoroutineScope
-import org.wordpress.android.ui.mysite.MySiteUiState.PartialState
-
-interface MySiteSource {
- fun build(coroutineScope: CoroutineScope, siteLocalId: Int): LiveData
-
- interface MySiteRefreshSource : MySiteSource {
- val refresh: MutableLiveData
-
- fun refresh() {
- refresh.postValue(true)
- }
-
- fun isRefreshing() = refresh.value
-
- fun getState(value: T): T {
- refresh.value = false
- return value
- }
-
- fun MediatorLiveData.postState(value: T) {
- refresh.postValue(false)
- this@postState.postValue(value)
- }
-
- fun MediatorLiveData.setState(value: T) {
- refresh.value = false
- this@setState.value = value
- }
-
- fun onRefreshedMainThread() {
- refresh.value = false
- }
-
- fun onRefreshedBackgroundThread() {
- refresh.postValue(false)
- }
- }
-
- interface SiteIndependentSource : MySiteRefreshSource {
- fun build(coroutineScope: CoroutineScope): LiveData
- override fun build(
- coroutineScope: CoroutineScope,
- siteLocalId: Int
- ): LiveData = build(coroutineScope)
- }
-}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteSourceManager.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteSourceManager.kt
deleted file mode 100644
index 533c88df1be4..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteSourceManager.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-package org.wordpress.android.ui.mysite
-
-import androidx.lifecycle.LiveData
-import kotlinx.coroutines.CoroutineScope
-import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper
-import org.wordpress.android.ui.mysite.MySiteSource.MySiteRefreshSource
-import org.wordpress.android.ui.mysite.MySiteSource.SiteIndependentSource
-import org.wordpress.android.ui.mysite.MySiteUiState.PartialState
-import org.wordpress.android.ui.mysite.cards.blaze.BlazeCardSource
-import org.wordpress.android.ui.mysite.cards.dashboard.CardsSource
-import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptCardSource
-import org.wordpress.android.ui.mysite.cards.domainregistration.DomainRegistrationSource
-import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartCardSource
-import javax.inject.Inject
-
-class MySiteSourceManager @Inject constructor(
- private val accountDataSource: AccountDataSource,
- private val domainRegistrationSource: DomainRegistrationSource,
- private val quickStartCardSource: QuickStartCardSource,
- private val scanAndBackupSource: ScanAndBackupSource,
- private val selectedSiteSource: SelectedSiteSource,
- cardsSource: CardsSource,
- siteIconProgressSource: SiteIconProgressSource,
- private val bloggingPromptCardSource: BloggingPromptCardSource,
- blazeCardSource: BlazeCardSource,
- private val selectedSiteRepository: SelectedSiteRepository,
- private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper
-) {
- private val mySiteSources: List> = listOf(
- selectedSiteSource,
- siteIconProgressSource,
- quickStartCardSource,
- accountDataSource,
- domainRegistrationSource,
- scanAndBackupSource,
- cardsSource,
- bloggingPromptCardSource,
- blazeCardSource,
- )
-
- private val showDashboardCards: Boolean
- get() = selectedSiteRepository.getSelectedSite()?.isUsingWpComRestApi == true &&
- jetpackFeatureRemovalPhaseHelper.shouldShowDashboard()
-
- private val allSupportedMySiteSources: List>
- get() = if (showDashboardCards) {
- mySiteSources
- } else {
- mySiteSources.filterNot(CardsSource::class.java::isInstance)
- }
-
- private val siteIndependentSources: List>
- get() = mySiteSources.filterIsInstance(SiteIndependentSource::class.java)
-
- fun build(coroutineScope: CoroutineScope, siteLocalId: Int?): List> {
- return if (siteLocalId != null) {
- allSupportedMySiteSources.map { source -> source.build(coroutineScope, siteLocalId) }
- } else {
- siteIndependentSources.map { source -> source.build(coroutineScope) }
- }
- }
-
- fun isRefreshing(): Boolean {
- val source = if (selectedSiteRepository.hasSelectedSite()) {
- allSupportedMySiteSources
- } else {
- siteIndependentSources
- }
- source.filterIsInstance(MySiteRefreshSource::class.java).forEach {
- if (it.isRefreshing() == true) {
- return true
- }
- }
- return false
- }
-
- fun refresh() {
- allSupportedMySiteSources.filterIsInstance(MySiteRefreshSource::class.java).forEach {
- if (it is SiteIndependentSource || selectedSiteRepository.hasSelectedSite()) it.refresh()
- }
- }
-
- fun onResume(isSiteSelected: Boolean) {
- when (isSiteSelected) {
- true -> refreshSubsetOfAllSources()
- false -> refresh()
- }
- }
-
- fun clear() {
- domainRegistrationSource.clear()
- scanAndBackupSource.clear()
- selectedSiteSource.clear()
- }
-
- private fun refreshSubsetOfAllSources() {
- selectedSiteSource.updateSiteSettingsIfNecessary()
- accountDataSource.refresh()
- if (selectedSiteRepository.hasSelectedSite()) quickStartCardSource.refresh()
- }
-
- fun refreshBloggingPrompts(onlyCurrentPrompt: Boolean) {
- if (onlyCurrentPrompt) {
- bloggingPromptCardSource.refreshTodayPrompt()
- } else {
- bloggingPromptCardSource.refresh()
- }
- }
-
- /* QUICK START */
-
- fun refreshQuickStart() {
- quickStartCardSource.refresh()
- }
-}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
index 2c51f7655bc6..6dcea39fc7a2 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
@@ -2,14 +2,11 @@
package org.wordpress.android.ui.mysite
-import android.content.Intent
import android.net.Uri
import androidx.annotation.StringRes
import androidx.lifecycle.LiveData
-import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.distinctUntilChanged
-import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.delay
@@ -20,74 +17,21 @@ import org.wordpress.android.R
import org.wordpress.android.analytics.AnalyticsTracker.Stat
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.model.SiteModel
-import org.wordpress.android.fluxc.model.dashboard.CardModel.DynamicCardsModel
-import org.wordpress.android.fluxc.model.dashboard.CardModel.ActivityCardModel
-import org.wordpress.android.fluxc.model.dashboard.CardModel.PagesCardModel
-import org.wordpress.android.fluxc.model.dashboard.CardModel.PostsCardModel
-import org.wordpress.android.fluxc.model.dashboard.CardModel.TodaysStatsCardModel
import org.wordpress.android.fluxc.store.AccountStore
import org.wordpress.android.fluxc.store.PostStore.OnPostUploaded
-import org.wordpress.android.fluxc.store.QuickStartStore.Companion.QUICK_START_VIEW_SITE_LABEL
-import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartNewSiteTask
import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask
-import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTaskType
-import org.wordpress.android.localcontentmigration.ContentMigrationAnalyticsTracker
-import org.wordpress.android.models.JetpackPoweredScreen
import org.wordpress.android.modules.BG_THREAD
import org.wordpress.android.modules.UI_THREAD
-import org.wordpress.android.ui.PagePostCreationSourcesDetail.STORY_FROM_MY_SITE
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil
-import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil.JetpackFeatureCollectionOverlaySource.FEATURE_CARD
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper
import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginHelper
import org.wordpress.android.ui.jetpackplugininstall.fullplugin.GetShowJetpackFullPluginInstallOnboardingUseCase
-import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DomainRegistrationCard
-import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.JetpackFeatureCard
-import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.JetpackInstallFullPluginCard
-import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickStartCard
-import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.SingleActionCard
-import org.wordpress.android.ui.mysite.MySiteCardAndItem.JetpackBadge
-import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardPlansBuilderParams
-import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardsBuilderParams
-import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DomainRegistrationCardBuilderParams
-import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.InfoItemBuilderParams
-import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.JetpackInstallFullPluginCardBuilderParams
-import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.QuickStartCardBuilderParams
-import org.wordpress.android.ui.mysite.MySiteUiState.PartialState
-import org.wordpress.android.ui.mysite.MySiteUiState.PartialState.BlazeCardUpdate
-import org.wordpress.android.ui.mysite.MySiteUiState.PartialState.BloggingPromptUpdate
-import org.wordpress.android.ui.mysite.MySiteUiState.PartialState.CardsUpdate
import org.wordpress.android.ui.mysite.MySiteViewModel.State.NoSites
import org.wordpress.android.ui.mysite.MySiteViewModel.State.SiteSelected
-import org.wordpress.android.ui.mysite.cards.CardsBuilder
-import org.wordpress.android.ui.mysite.cards.DomainRegistrationCardShownTracker
-import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker
-import org.wordpress.android.ui.mysite.cards.dashboard.activity.ActivityLogCardViewModelSlice
-import org.wordpress.android.ui.mysite.cards.dashboard.bloganuary.BloganuaryNudgeCardViewModelSlice
-import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptCardViewModelSlice
-import org.wordpress.android.ui.mysite.cards.dashboard.pages.PagesCardViewModelSlice
-import org.wordpress.android.ui.mysite.cards.dashboard.plans.PlansCardUtils
-import org.wordpress.android.ui.mysite.cards.dashboard.posts.PostsCardViewModelSlice
-import org.wordpress.android.ui.mysite.cards.dashboard.todaysstats.TodaysStatsViewModelSlice
-import org.wordpress.android.ui.mysite.cards.dynamiccard.DynamicCardsViewModelSlice
-import org.wordpress.android.ui.mysite.cards.jetpackfeature.JetpackFeatureCardHelper
-import org.wordpress.android.ui.mysite.cards.jetpackfeature.JetpackFeatureCardShownTracker
-import org.wordpress.android.ui.mysite.cards.jpfullplugininstall.JetpackInstallFullPluginCardBuilder
-import org.wordpress.android.ui.mysite.cards.jpfullplugininstall.JetpackInstallFullPluginShownTracker
-import org.wordpress.android.ui.mysite.cards.nocards.NoCardsMessageViewModelSlice
-import org.wordpress.android.ui.mysite.cards.personalize.PersonalizeCardBuilder
-import org.wordpress.android.ui.mysite.cards.personalize.PersonalizeCardViewModelSlice
-import org.wordpress.android.ui.mysite.cards.quicklinksitem.QuickLinksItemViewModelSlice
-import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartCardBuilder
-import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartCardType
+import org.wordpress.android.ui.mysite.cards.DashboardCardsViewModelSlice
import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository
-import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository.QuickStartCategory
-import org.wordpress.android.ui.mysite.cards.siteinfo.SiteInfoHeaderCardBuilder
import org.wordpress.android.ui.mysite.cards.siteinfo.SiteInfoHeaderCardViewModelSlice
-import org.wordpress.android.ui.mysite.cards.sotw2023.WpSotw2023NudgeCardViewModelSlice
-import org.wordpress.android.ui.mysite.items.infoitem.MySiteInfoItemBuilder
-import org.wordpress.android.ui.mysite.items.listitem.SiteItemsBuilder
-import org.wordpress.android.ui.mysite.items.listitem.SiteItemsViewModelSlice
+import org.wordpress.android.ui.mysite.items.DashboardItemsViewModelSlice
import org.wordpress.android.ui.pages.SnackbarMessageHolder
import org.wordpress.android.ui.photopicker.PhotoPickerActivity
import org.wordpress.android.ui.posts.BasicDialogViewModel
@@ -95,21 +39,13 @@ import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.ui.quickstart.QuickStartTracker
import org.wordpress.android.ui.quickstart.QuickStartType.NewSiteQuickStartType
import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource
-import org.wordpress.android.ui.utils.ListItemInteraction
-import org.wordpress.android.ui.utils.UiString.UiStringRes
import org.wordpress.android.util.BuildConfigWrapper
-import org.wordpress.android.util.DisplayUtilsWrapper
-import org.wordpress.android.util.JetpackBrandingUtils
import org.wordpress.android.util.QuickStartUtilsWrapper
import org.wordpress.android.util.SnackbarSequencer
import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
import org.wordpress.android.util.config.LandOnTheEditorFeatureConfig
-import org.wordpress.android.util.filter
import org.wordpress.android.util.getEmailValidationMessage
-import org.wordpress.android.util.mapSafe
import org.wordpress.android.util.merge
-import org.wordpress.android.util.publicdata.AppStatus
-import org.wordpress.android.util.publicdata.WordPressPublicData
import org.wordpress.android.viewmodel.Event
import org.wordpress.android.viewmodel.ScopedViewModel
import org.wordpress.android.viewmodel.SingleLiveEvent
@@ -121,60 +57,29 @@ class MySiteViewModel @Inject constructor(
@param:Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher,
@param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher,
private val analyticsTrackerWrapper: AnalyticsTrackerWrapper,
- private val siteItemsBuilder: SiteItemsBuilder,
private val accountStore: AccountStore,
private val selectedSiteRepository: SelectedSiteRepository,
private val siteIconUploadHandler: SiteIconUploadHandler,
- private val siteStoriesHandler: SiteStoriesHandler,
- private val displayUtilsWrapper: DisplayUtilsWrapper,
private val quickStartRepository: QuickStartRepository,
- private val quickStartCardBuilder: QuickStartCardBuilder,
- private val siteInfoHeaderCardBuilder: SiteInfoHeaderCardBuilder,
private val homePageDataLoader: HomePageDataLoader,
private val quickStartUtilsWrapper: QuickStartUtilsWrapper,
private val snackbarSequencer: SnackbarSequencer,
- private val cardsBuilder: CardsBuilder,
private val landOnTheEditorFeatureConfig: LandOnTheEditorFeatureConfig,
- private val mySiteSourceManager: MySiteSourceManager,
- private val cardsTracker: CardsTracker,
- private val domainRegistrationCardShownTracker: DomainRegistrationCardShownTracker,
private val buildConfigWrapper: BuildConfigWrapper,
- private val jetpackBrandingUtils: JetpackBrandingUtils,
private val appPrefsWrapper: AppPrefsWrapper,
private val quickStartTracker: QuickStartTracker,
- private val contentMigrationAnalyticsTracker: ContentMigrationAnalyticsTracker,
private val dispatcher: Dispatcher,
- private val appStatus: AppStatus,
- private val wordPressPublicData: WordPressPublicData,
- private val jetpackFeatureCardShownTracker: JetpackFeatureCardShownTracker,
private val jetpackFeatureRemovalUtils: JetpackFeatureRemovalOverlayUtil,
- private val jetpackFeatureCardHelper: JetpackFeatureCardHelper,
- private val jetpackInstallFullPluginCardBuilder: JetpackInstallFullPluginCardBuilder,
private val getShowJetpackFullPluginInstallOnboardingUseCase: GetShowJetpackFullPluginInstallOnboardingUseCase,
- private val jetpackInstallFullPluginShownTracker: JetpackInstallFullPluginShownTracker,
- private val dashboardCardPlansUtils: PlansCardUtils,
private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper,
private val wpJetpackIndividualPluginHelper: WPJetpackIndividualPluginHelper,
- private val blazeCardViewModelSlice: BlazeCardViewModelSlice,
- private val pagesCardViewModelSlice: PagesCardViewModelSlice,
- private val dynamicCardsViewModelSlice: DynamicCardsViewModelSlice,
- private val todaysStatsViewModelSlice: TodaysStatsViewModelSlice,
- private val postsCardViewModelSlice: PostsCardViewModelSlice,
- private val activityLogCardViewModelSlice: ActivityLogCardViewModelSlice,
- private val siteItemsViewModelSlice: SiteItemsViewModelSlice,
- private val mySiteInfoItemBuilder: MySiteInfoItemBuilder,
- private val personalizeCardViewModelSlice: PersonalizeCardViewModelSlice,
- private val personalizeCardBuilder: PersonalizeCardBuilder,
- private val bloggingPromptCardViewModelSlice: BloggingPromptCardViewModelSlice,
- private val noCardsMessageViewModelSlice: NoCardsMessageViewModelSlice,
private val siteInfoHeaderCardViewModelSlice: SiteInfoHeaderCardViewModelSlice,
- private val quickLinksItemViewModelSlice: QuickLinksItemViewModelSlice,
- private val bloganuaryNudgeCardViewModelSlice: BloganuaryNudgeCardViewModelSlice,
- private val sotw2023NudgeCardViewModelSlice: WpSotw2023NudgeCardViewModelSlice,
+ private val accountDataViewModelSlice: AccountDataViewModelSlice,
+ private val dashboardCardsViewModelSlice: DashboardCardsViewModelSlice,
+ private val dashboardItemsViewModelSlice: DashboardItemsViewModelSlice
) : ScopedViewModel(mainDispatcher) {
private val _onSnackbarMessage = MutableLiveData>()
private val _onNavigation = MutableLiveData>()
- private val _activeTaskPosition = MutableLiveData>()
private val _onOpenJetpackInstallFullPluginOnboarding = SingleLiveEvent>()
private val _onShowJetpackIndividualPluginOverlay = SingleLiveEvent>()
@@ -182,36 +87,14 @@ class MySiteViewModel @Inject constructor(
as they're already built on site select. */
private var isSiteSelected = false
- val quickLinks: LiveData = merge(
- quickLinksItemViewModelSlice.uiState,
- quickStartRepository.quickStartMenuStep
- ) { quickLinks, quickStartMenuStep ->
- if (quickLinks != null &&
- quickStartMenuStep != null
- ) {
- return@merge quickLinksItemViewModelSlice.updateToShowMoreFocusPointIfNeeded(quickLinks, quickStartMenuStep)
- }
- return@merge quickLinks
- }
+ val onScrollTo: MutableLiveData> = MutableLiveData()
- val onScrollTo: LiveData> = merge(
- _activeTaskPosition.distinctUntilChanged(),
- quickStartRepository.activeTask
- ) { pair, activeTask ->
- if (pair != null && activeTask != null && pair.first == activeTask) {
- Event(pair.second)
- } else {
- null
- }
- }
val onSnackbarMessage = merge(
_onSnackbarMessage,
- siteStoriesHandler.onSnackbar,
quickStartRepository.onSnackbar,
- siteItemsViewModelSlice.onSnackbarMessage,
- bloggingPromptCardViewModelSlice.onSnackbarMessage,
+ dashboardItemsViewModelSlice.onSnackbarMessage,
siteInfoHeaderCardViewModelSlice.onSnackbarMessage,
- quickLinksItemViewModelSlice.onSnackbarMessage
+ dashboardCardsViewModelSlice.onSnackbarMessage
)
val onQuickStartMySitePrompts = quickStartRepository.onQuickStartMySitePrompts
@@ -221,493 +104,100 @@ class MySiteViewModel @Inject constructor(
val onNavigation = merge(
_onNavigation,
- siteStoriesHandler.onNavigation,
- blazeCardViewModelSlice.onNavigation,
- pagesCardViewModelSlice.onNavigation,
- todaysStatsViewModelSlice.onNavigation,
- postsCardViewModelSlice.onNavigation,
- activityLogCardViewModelSlice.onNavigation,
- siteItemsViewModelSlice.onNavigation,
- bloggingPromptCardViewModelSlice.onNavigation,
- bloganuaryNudgeCardViewModelSlice.onNavigation,
- personalizeCardViewModelSlice.onNavigation,
siteInfoHeaderCardViewModelSlice.onNavigation,
- quickLinksItemViewModelSlice.navigation,
- sotw2023NudgeCardViewModelSlice.onNavigation,
- dynamicCardsViewModelSlice.onNavigation,
+ dashboardCardsViewModelSlice.onNavigation,
+ dashboardItemsViewModelSlice.onNavigation
)
val onMediaUpload = siteInfoHeaderCardViewModelSlice.onMediaUpload
val onUploadedItem = siteIconUploadHandler.onUploadedItem
- val onOpenJetpackInstallFullPluginOnboarding = _onOpenJetpackInstallFullPluginOnboarding as LiveData>
+
+ val onOpenJetpackInstallFullPluginOnboarding: LiveData> = merge(
+ _onOpenJetpackInstallFullPluginOnboarding,
+ dashboardCardsViewModelSlice.onOpenJetpackInstallFullPluginOnboarding
+ )
+
val onShowJetpackIndividualPluginOverlay = _onShowJetpackIndividualPluginOverlay as LiveData>
val refresh =
merge(
- blazeCardViewModelSlice.refresh,
- pagesCardViewModelSlice.refresh,
- todaysStatsViewModelSlice.refresh,
- postsCardViewModelSlice.refresh,
- activityLogCardViewModelSlice.refresh,
- bloganuaryNudgeCardViewModelSlice.refresh,
- sotw2023NudgeCardViewModelSlice.refresh,
- dynamicCardsViewModelSlice.refresh,
+ dashboardCardsViewModelSlice.refresh
)
- private var shouldMarkUpdateSiteTitleTaskComplete = false
-
- val state: LiveData =
- selectedSiteRepository.siteSelected.switchMap { siteLocalId ->
- isSiteSelected = true
- quickLinksItemViewModelSlice.onSiteChanged()
- resetShownTrackers()
- val result = MediatorLiveData()
- for (newSource in mySiteSourceManager.build(viewModelScope, siteLocalId)) {
- result.addSource(newSource) { partialState ->
- if (partialState != null) {
- result.value = (result.value ?: SiteIdToState(siteLocalId)).update(partialState)
- }
- }
- }
- // We want to filter out the empty state where we have a site ID but site object is missing.
- // Without this check there is an emission of a NoSites state even if we have the site
- result.filter { it.siteId == null || it.state.site != null }.mapSafe { it.state }
- }
-
- val uiModel: LiveData = merge(state, quickLinks) { cards, quickLinks ->
- val nonNullCards = cards ?: return@merge buildNoSiteState(null, null)
- with(nonNullCards) {
- val state = if (site != null) {
- cardsUpdate?.checkAndShowSnackbarError()
- val state = buildSiteSelectedStateAndScroll(
- site,
- showSiteIconProgressBar,
- activeTask,
- isDomainCreditAvailable,
- quickStartCategories,
- backupAvailable,
- scanAvailable,
- cardsUpdate,
- bloggingPromptsUpdate,
- blazeCardUpdate,
- quickLinks
- )
- trackCardsAndItemsShownIfNeeded(state)
-
- bloggingPromptCardViewModelSlice.onDashboardCardsUpdated(
- viewModelScope,
- state as? SiteSelected
- )
- state
- } else {
- buildNoSiteState(currentAvatarUrl, avatarName)
- }
- bloggingPromptCardViewModelSlice.onSiteChanged(site?.id, state as? SiteSelected)
-
- dashboardCardPlansUtils.onSiteChanged(site?.id, state as? SiteSelected)
+ val isRefreshingOrLoading = merge(
+ dashboardCardsViewModelSlice.isRefreshing,
+ dashboardItemsViewModelSlice.isRefreshing,
+ accountDataViewModelSlice.isRefreshing
+ )
- state
- }
- }
+ private var shouldMarkUpdateSiteTitleTaskComplete = false
- private fun CardsUpdate.checkAndShowSnackbarError() {
- if (showSnackbarError) {
- _onSnackbarMessage
- .postValue(Event(SnackbarMessageHolder(UiStringRes(R.string.my_site_dashboard_update_error))))
- }
- }
+ val uiModel: LiveData = merge(
+ siteInfoHeaderCardViewModelSlice.uiModel,
+ accountDataViewModelSlice.uiModel,
+ dashboardCardsViewModelSlice.uiModel,
+ dashboardItemsViewModelSlice.uiModel
+ ) { siteInfoHeaderCard,
+ accountData,
+ dashboardCards,
+ siteItems ->
+ val nonNullSiteInfoHeaderCard =
+ siteInfoHeaderCard ?: return@merge buildNoSiteState(accountData?.url, accountData?.name)
+ return@merge if (!dashboardCards.isNullOrEmpty())
+ SiteSelected(dashboardData = listOf(nonNullSiteInfoHeaderCard) + dashboardCards)
+ else if (!siteItems.isNullOrEmpty())
+ SiteSelected(dashboardData = listOf(nonNullSiteInfoHeaderCard) + siteItems)
+ else
+ SiteSelected(dashboardData = listOf(nonNullSiteInfoHeaderCard))
+ }.distinctUntilChanged()
init {
dispatcher.register(this)
- bloggingPromptCardViewModelSlice.initialize(viewModelScope, mySiteSourceManager)
- bloganuaryNudgeCardViewModelSlice.initialize(viewModelScope)
siteInfoHeaderCardViewModelSlice.initialize(viewModelScope)
- quickLinksItemViewModelSlice.initialization(viewModelScope)
- quickLinksItemViewModelSlice.start()
- sotw2023NudgeCardViewModelSlice.initialize(viewModelScope)
- }
-
- @Suppress("LongParameterList")
- private fun buildSiteSelectedStateAndScroll(
- site: SiteModel,
- showSiteIconProgressBar: Boolean,
- activeTask: QuickStartTask?,
- isDomainCreditAvailable: Boolean,
- quickStartCategories: List,
- backupAvailable: Boolean,
- scanAvailable: Boolean,
- cardsUpdate: CardsUpdate?,
- bloggingPromptUpdate: BloggingPromptUpdate?,
- blazeCardUpdate: BlazeCardUpdate?,
- quickLinks: MySiteCardAndItem.Card.QuickLinksItem? = null
- ): SiteSelected {
- val siteItems = buildSiteSelectedState(
- site,
- activeTask,
- isDomainCreditAvailable,
- quickStartCategories,
- backupAvailable,
- scanAvailable,
- cardsUpdate,
- bloggingPromptUpdate,
- blazeCardUpdate,
- quickLinks
- )
-
- val siteInfo = siteInfoHeaderCardBuilder.buildSiteInfoCard(
- siteInfoHeaderCardViewModelSlice.getParams(
- site,
- activeTask,
- showSiteIconProgressBar
- )
- )
-
- if (activeTask != null) {
- scrollToQuickStartTaskIfNecessary(
- activeTask,
- getPositionOfQuickStartItem(siteItems)
- )
- }
-
- return SiteSelected(
- dashboardData = listOf(siteInfo) + siteItems
- )
- }
-
- private fun getPositionOfQuickStartItem(
- siteItems: List,
- ): Int {
- return siteItems.indexOfFirst { it.activeQuickStartItem }
- }
-
- @Suppress("LongParameterList", "CyclomaticComplexMethod")
- private fun buildSiteSelectedState(
- site: SiteModel,
- activeTask: QuickStartTask?,
- isDomainCreditAvailable: Boolean,
- quickStartCategories: List,
- backupAvailable: Boolean,
- scanAvailable: Boolean,
- cardsUpdate: CardsUpdate?,
- bloggingPromptUpdate: BloggingPromptUpdate?,
- blazeCardUpdate: BlazeCardUpdate?,
- quickLinks: MySiteCardAndItem.Card.QuickLinksItem?
- ): List {
- return if (shouldShowDashboard(site)) buildDashboardCards(
- site,
- isDomainCreditAvailable,
- quickStartCategories,
- cardsUpdate,
- bloggingPromptUpdate,
- blazeCardUpdate,
- quickLinks
- )
- else buildSiteItemsMenu(site, activeTask, backupAvailable, scanAvailable, cardsUpdate)
- }
-
- private fun buildSiteItemsMenu(
- site: SiteModel,
- activeTask: QuickStartTask?,
- backupAvailable: Boolean,
- scanAvailable: Boolean,
- cardsUpdate: CardsUpdate?
- ): List {
- val infoItem = mySiteInfoItemBuilder.build(
- InfoItemBuilderParams(
- isStaleMessagePresent = cardsUpdate?.showStaleMessage ?: false
- )
- )
- val jetpackFeatureCard = getJetpackFeatureCard()
-
- val jetpackSwitchMenu = getJetpackSwitchMenu()
-
- val jetpackBadge = buildJetpackBadgeIfEnabled()
-
- val siteItems = getSiteItems(site, activeTask, backupAvailable, scanAvailable)
-
- val sotw2023Card = sotw2023NudgeCardViewModelSlice.buildCard()
-
- return mutableListOf().apply {
- infoItem?.let { add(infoItem) }
- sotw2023Card?.let { add(it) }
- addAll(siteItems)
- jetpackSwitchMenu?.let { add(jetpackSwitchMenu) }
- if (jetpackFeatureCardHelper.shouldShowFeatureCardAtTop())
- jetpackFeatureCard?.let { add(0, jetpackFeatureCard) }
- else jetpackFeatureCard?.let { add(jetpackFeatureCard) }
- jetpackBadge?.let { add(jetpackBadge) }
- }.toList()
- }
-
- private fun buildDashboardCards(
- site: SiteModel,
- isDomainCreditAvailable: Boolean,
- quickStartCategories: List,
- cardsUpdate: CardsUpdate?,
- bloggingPromptUpdate: BloggingPromptUpdate?,
- blazeCardUpdate: BlazeCardUpdate?,
- quickLinks: MySiteCardAndItem.Card.QuickLinksItem?
- ): List {
- val infoItem = mySiteInfoItemBuilder.build(
- InfoItemBuilderParams(
- isStaleMessagePresent = cardsUpdate?.showStaleMessage ?: false
- )
- )
-
- val migrationSuccessCard = getJetpackMigrationSuccessCard()
-
- val jetpackInstallFullPluginCardParams = JetpackInstallFullPluginCardBuilderParams(
- site = site,
- onLearnMoreClick = ::onJetpackInstallFullPluginLearnMoreClick,
- onHideMenuItemClick = ::onJetpackInstallFullPluginHideMenuItemClick,
- )
- val jetpackInstallFullPluginCard = jetpackInstallFullPluginCardBuilder.build(jetpackInstallFullPluginCardParams)
-
- val cardsResult = cardsBuilder.build(
- DomainRegistrationCardBuilderParams(
- isDomainCreditAvailable = isDomainCreditAvailable,
- domainRegistrationClick = this::domainRegistrationClick
- ),
- QuickStartCardBuilderParams(
- quickStartCategories = quickStartCategories,
- moreMenuClickParams = QuickStartCardBuilderParams.MoreMenuParams(
- onMoreMenuClick = this::onQuickStartMoreMenuClick,
- onHideThisMenuItemClick = this::onQuickStartHideThisMenuItemClick
- ),
- onQuickStartTaskTypeItemClick = this::onQuickStartTaskTypeItemClick
- ),
- DashboardCardsBuilderParams(
- showErrorCard = cardsUpdate?.showErrorCard == true,
- onErrorRetryClick = this::onDashboardErrorRetry,
- todaysStatsCardBuilderParams = todaysStatsViewModelSlice.getTodaysStatsBuilderParams(
- cardsUpdate?.cards?.firstOrNull { it is TodaysStatsCardModel } as? TodaysStatsCardModel
- ),
- postCardBuilderParams = postsCardViewModelSlice.getPostsCardBuilderParams(
- cardsUpdate?.cards?.firstOrNull { it is PostsCardModel } as? PostsCardModel
- ),
- bloganuaryNudgeCardBuilderParams = bloganuaryNudgeCardViewModelSlice.getBuilderParams(),
- bloggingPromptCardBuilderParams = bloggingPromptCardViewModelSlice.getBuilderParams(
- bloggingPromptUpdate
- ),
- blazeCardBuilderParams = blazeCardViewModelSlice.getBlazeCardBuilderParams(blazeCardUpdate),
- dashboardCardPlansBuilderParams = DashboardCardPlansBuilderParams(
- isEligible = dashboardCardPlansUtils.shouldShowCard(site),
- onClick = this::onDashboardCardPlansClick,
- onHideMenuItemClick = this::onDashboardCardPlansHideMenuItemClick,
- onMoreMenuClick = this::onDashboardCardPlansMoreMenuClick
- ),
- pagesCardBuilderParams = pagesCardViewModelSlice.getPagesCardBuilderParams(
- cardsUpdate?.cards?.firstOrNull { it is PagesCardModel } as? PagesCardModel,
- ),
- activityCardBuilderParams = activityLogCardViewModelSlice.getActivityLogCardBuilderParams(
- cardsUpdate?.cards?.firstOrNull { it is ActivityCardModel } as? ActivityCardModel
- ),
- dynamicCardsBuilderParams = dynamicCardsViewModelSlice.getBuilderParams(
- cardsUpdate?.cards?.firstOrNull { it is DynamicCardsModel } as? DynamicCardsModel
- )
- ),
- jetpackInstallFullPluginCardParams
- )
-
- val personalizeCard = personalizeCardBuilder.build(personalizeCardViewModelSlice.getBuilderParams())
-
- val noCardsMessage = noCardsMessageViewModelSlice.buildNoCardsMessage(cardsResult)
-
- return mutableListOf().apply {
- infoItem?.let { add(infoItem) }
- migrationSuccessCard?.let { add(migrationSuccessCard) }
- jetpackInstallFullPluginCard?.let { add(jetpackInstallFullPluginCard) }
- quickLinks?.let { add(quickLinks) }
- addAll(cardsResult)
- noCardsMessage?.let { add(noCardsMessage) }
- personalizeCard?.let { add(personalizeCard) }
- }.toList()
+ dashboardCardsViewModelSlice.initialize(viewModelScope)
+ dashboardItemsViewModelSlice.initialize(viewModelScope)
+ accountDataViewModelSlice.initialize(viewModelScope)
}
private fun shouldShowDashboard(site: SiteModel): Boolean {
return buildConfigWrapper.isJetpackApp && site.isUsingWpComRestApi
}
- private fun getSiteItems(
- site: SiteModel,
- activeTask: QuickStartTask?,
- backupAvailable: Boolean,
- scanAvailable: Boolean
- ): List {
- if (shouldShowDashboard(site)) return emptyList()
- return siteItemsBuilder.build(
- siteItemsViewModelSlice.buildItems(
- shouldEnableFocusPoints = false,
- site = site,
- activeTask = activeTask,
- backupAvailable = backupAvailable,
- scanAvailable = scanAvailable
- )
- )
- }
-
- private fun getJetpackMigrationSuccessCard(): SingleActionCard? {
- val isJetpackApp = buildConfigWrapper.isJetpackApp
- val isMigrationCompleted = appPrefsWrapper.isJetpackMigrationCompleted()
- val isWordPressInstalled = appStatus.isAppInstalled(wordPressPublicData.currentPackageId())
- if (isJetpackApp && isMigrationCompleted && isWordPressInstalled) {
- return SingleActionCard(
- textResource = R.string.jp_migration_success_card_message,
- imageResource = R.drawable.ic_wordpress_jetpack_appicon,
- onActionClick = ::onPleaseDeleteWordPressAppCardClick
- )
- }
- return null
- }
-
- private fun getJetpackSwitchMenu(): MySiteCardAndItem.Card.JetpackSwitchMenu? {
- if (!jetpackFeatureCardHelper.shouldShowSwitchToJetpackMenuCard()) return null
- return MySiteCardAndItem.Card.JetpackSwitchMenu(
- onClick = ListItemInteraction.create(this::onJetpackFeatureCardClick),
- onRemindMeLaterItemClick = ListItemInteraction.create(this::onSwitchToJetpackMenuCardRemindMeLaterClick),
- onHideMenuItemClick = ListItemInteraction.create(this::onSwitchToJetpackMenuCardHideMenuItemClick),
- onMoreMenuClick = ListItemInteraction.create(this::onJetpackFeatureCardMoreMenuClick)
- )
- }
-
- private fun getJetpackFeatureCard(): JetpackFeatureCard? {
- if (!jetpackFeatureCardHelper.shouldShowJetpackFeatureCard()) return null
- return JetpackFeatureCard(
- content = jetpackFeatureCardHelper.getCardContent(),
- onClick = ListItemInteraction.create(this::onJetpackFeatureCardClick),
- onHideMenuItemClick = ListItemInteraction.create(this::onJetpackFeatureCardHideMenuItemClick),
- onLearnMoreClick = ListItemInteraction.create(this::onJetpackFeatureCardLearnMoreClick),
- onRemindMeLaterItemClick = ListItemInteraction.create(this::onJetpackFeatureCardRemindMeLaterClick),
- onMoreMenuClick = ListItemInteraction.create(this::onJetpackFeatureCardMoreMenuClick),
- learnMoreUrl = jetpackFeatureCardHelper.getLearnMoreUrl()
- )
- }
-
- private fun buildJetpackBadgeIfEnabled(): JetpackBadge? {
- val screen = JetpackPoweredScreen.WithStaticText.HOME
- return JetpackBadge(
- text = jetpackBrandingUtils.getBrandingTextForScreen(screen),
- onClick = if (jetpackBrandingUtils.shouldShowJetpackPoweredBottomSheet()) {
- ListItemInteraction.create(screen, this::onJetpackBadgeClick)
- } else {
- null
- }
- ).takeIf {
- jetpackBrandingUtils.shouldShowJetpackBrandingInDashboard()
- }
- }
-
- private fun onPleaseDeleteWordPressAppCardClick() {
- contentMigrationAnalyticsTracker.trackPleaseDeleteWordPressCardTapped()
- _onNavigation.value = Event(SiteNavigationAction.OpenJetpackMigrationDeleteWP)
- }
-
- private fun onJetpackBadgeClick(screen: JetpackPoweredScreen) {
- jetpackBrandingUtils.trackBadgeTapped(screen)
- _onNavigation.value = Event(SiteNavigationAction.OpenJetpackPoweredBottomSheet)
- }
-
private fun buildNoSiteState(accountUrl: String?, accountName: String?): NoSites {
- // Hide actionable empty view image when screen height is under specified min height.
- val shouldShowImage = !buildConfigWrapper.isJetpackApp &&
- displayUtilsWrapper.getWindowPixelHeight() >= MIN_DISPLAY_PX_HEIGHT_NO_SITE_IMAGE
-
- val shouldShowAccountSettings = jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures()
return NoSites(
- shouldShowImage = shouldShowImage,
avatarUrl = accountUrl,
accountName = accountName,
- shouldShowAccountSettings = shouldShowAccountSettings
- )
- }
-
- private fun scrollToQuickStartTaskIfNecessary(
- quickStartTask: QuickStartTask,
- position: Int
- ) {
- if (_activeTaskPosition.value?.first != quickStartTask && isValidQuickStartFocusPosition(
- quickStartTask,
- position
- )
- ) {
- _activeTaskPosition.postValue(quickStartTask to position)
- }
- }
-
- private fun isValidQuickStartFocusPosition(quickStartTask: QuickStartTask, position: Int): Boolean {
- return if (position == LIST_INDEX_NO_ACTIVE_QUICK_START_ITEM && isSiteHeaderQuickStartTask(quickStartTask)) {
- true
- } else {
- position >= 0
- }
- }
-
- private fun isSiteHeaderQuickStartTask(quickStartTask: QuickStartTask): Boolean {
- return when (quickStartTask) {
- QuickStartNewSiteTask.UPDATE_SITE_TITLE,
- QuickStartNewSiteTask.UPLOAD_SITE_ICON,
- quickStartRepository.quickStartType.getTaskFromString(QUICK_START_VIEW_SITE_LABEL) -> true
-
- else -> false
- }
- }
-
- private fun onQuickStartMoreMenuClick(quickStartCardType: QuickStartCardType) =
- quickStartTracker.trackMoreMenuClicked(quickStartCardType)
-
- private fun onQuickStartHideThisMenuItemClick(quickStartCardType: QuickStartCardType) {
- quickStartTracker.trackMoreMenuItemClicked(quickStartCardType)
- selectedSiteRepository.getSelectedSite()?.let { selectedSite ->
- when (quickStartCardType) {
- QuickStartCardType.GET_TO_KNOW_THE_APP -> {
- quickStartRepository.onHideShowGetToKnowTheAppCard(selectedSite.siteId)
- }
-
- QuickStartCardType.NEXT_STEPS -> {
- quickStartRepository.onHideNextStepsCard(selectedSite.siteId)
- }
- }
- refresh()
- clearActiveQuickStartTask()
- }
- }
-
- private fun onQuickStartTaskTypeItemClick(type: QuickStartTaskType) {
- clearActiveQuickStartTask()
- cardsTracker.trackQuickStartCardItemClicked(type)
- _onNavigation.value = Event(
- SiteNavigationAction.OpenQuickStartFullScreenDialog(type, quickStartCardBuilder.getTitle(type))
)
}
fun onQuickStartTaskCardClick(task: QuickStartTask) {
+ onScrollTo.postValue(Event(0))
quickStartRepository.setActiveTask(task)
}
fun onQuickStartFullScreenDialogDismiss() {
- mySiteSourceManager.refreshQuickStart()
- }
-
- private fun domainRegistrationClick() {
- val selectedSite = requireNotNull(selectedSiteRepository.getSelectedSite())
- analyticsTrackerWrapper.track(Stat.DOMAIN_CREDIT_REDEMPTION_TAPPED, selectedSite)
- _onNavigation.value = Event(SiteNavigationAction.OpenDomainRegistration(selectedSite))
+// mySiteSourceManager.refreshQuickStart()
}
fun refresh(isPullToRefresh: Boolean = false) {
if (isPullToRefresh) analyticsTrackerWrapper.track(Stat.MY_SITE_PULL_TO_REFRESH)
- mySiteSourceManager.refresh()
- quickLinksItemViewModelSlice.onRefresh()
+ selectedSiteRepository.getSelectedSite()?.let {
+ buildDashboardOrSiteItems(it)
+ } ?: run {
+ accountDataViewModelSlice.onRefresh()
+ }
}
fun onResume() {
- mySiteSourceManager.onResume(isSiteSelected)
isSiteSelected = false
checkAndShowJetpackFullPluginInstallOnboarding()
checkAndShowQuickStartNotice()
- bloggingPromptCardViewModelSlice.onResume(uiModel.value as? SiteSelected)
- dashboardCardPlansUtils.onResume(uiModel.value as? SiteSelected)
- quickLinksItemViewModelSlice.onResume()
+ selectedSiteRepository.updateSiteSettingsIfNecessary()
+ selectedSiteRepository.getSelectedSite()?.let {
+ buildDashboardOrSiteItems(it)
+ } ?: run {
+ accountDataViewModelSlice.onResume()
+ }
}
private fun checkAndShowJetpackFullPluginInstallOnboarding() {
@@ -781,28 +271,27 @@ class MySiteViewModel @Inject constructor(
override fun onCleared() {
siteIconUploadHandler.clear()
- siteStoriesHandler.clear()
quickStartRepository.clear()
- mySiteSourceManager.clear()
dispatcher.unregister(this)
+ siteInfoHeaderCardViewModelSlice.onCleared()
+ dashboardCardsViewModelSlice.onCleared()
+ dashboardItemsViewModelSlice.onCleared()
+ accountDataViewModelSlice.onCleared()
super.onCleared()
}
- fun handleStoriesPhotoPickerResult(data: Intent) {
- selectedSiteRepository.getSelectedSite()?.let {
- siteStoriesHandler.handleStoriesResult(it, data, STORY_FROM_MY_SITE)
- }
- }
-
fun onSitePicked() {
selectedSiteRepository.getSelectedSite()?.let {
val siteLocalId = it.id.toLong()
val lastSelectedQuickStartType = appPrefsWrapper.getLastSelectedQuickStartTypeForSite(siteLocalId)
quickStartRepository.checkAndSetQuickStartType(lastSelectedQuickStartType == NewSiteQuickStartType)
+ onSitePicked(it)
+ } ?: run {
+ accountDataViewModelSlice.onResume()
}
- mySiteSourceManager.refreshQuickStart()
}
+
fun performFirstStepAfterSiteCreation(
isSiteTitleTaskCompleted: Boolean,
isNewSite: Boolean
@@ -846,7 +335,7 @@ class MySiteViewModel @Inject constructor(
quickStartRepository.quickStartType,
quickStartTracker
)
- mySiteSourceManager.refreshQuickStart()
+ quickStartRepository.checkAndShowQuickStartNotice()
}
}
@@ -869,9 +358,12 @@ class MySiteViewModel @Inject constructor(
}
fun startQuickStart() {
- quickStartTracker.track(Stat.QUICK_START_REQUEST_DIALOG_POSITIVE_TAPPED)
- startQuickStart(selectedSiteRepository.getSelectedSiteLocalId(), shouldMarkUpdateSiteTitleTaskComplete)
- shouldMarkUpdateSiteTitleTaskComplete = false
+ selectedSiteRepository.getSelectedSite()?.let {
+ quickStartTracker.track(Stat.QUICK_START_REQUEST_DIALOG_POSITIVE_TAPPED)
+ startQuickStart(selectedSiteRepository.getSelectedSiteLocalId(), shouldMarkUpdateSiteTitleTaskComplete)
+ shouldMarkUpdateSiteTitleTaskComplete = false
+ dashboardCardsViewModelSlice.startQuickStart(it)
+ }
}
fun ignoreQuickStart() {
@@ -879,79 +371,30 @@ class MySiteViewModel @Inject constructor(
quickStartTracker.track(Stat.QUICK_START_REQUEST_DIALOG_NEGATIVE_TAPPED)
}
- private fun onDashboardErrorRetry() {
- mySiteSourceManager.refresh()
- }
-
-
- private fun onJetpackFeatureCardClick() {
- jetpackFeatureCardHelper.track(Stat.REMOVE_FEATURE_CARD_TAPPED)
- _onNavigation.value = Event(SiteNavigationAction.OpenJetpackFeatureOverlay(source = FEATURE_CARD))
- }
-
- private fun onJetpackFeatureCardHideMenuItemClick() {
- jetpackFeatureCardHelper.hideJetpackFeatureCard()
- refresh()
- }
-
- private fun onJetpackFeatureCardLearnMoreClick() {
- jetpackFeatureCardHelper.track(Stat.REMOVE_FEATURE_CARD_LINK_TAPPED)
- _onNavigation.value = Event(SiteNavigationAction.OpenJetpackFeatureOverlay(source = FEATURE_CARD))
- }
-
- private fun onJetpackFeatureCardRemindMeLaterClick() {
- jetpackFeatureCardHelper.setJetpackFeatureCardLastShownTimeStamp(System.currentTimeMillis())
- refresh()
- }
-
- private fun onSwitchToJetpackMenuCardRemindMeLaterClick() {
- jetpackFeatureCardHelper.track(Stat.REMOVE_FEATURE_CARD_REMIND_LATER_TAPPED)
- appPrefsWrapper.setSwitchToJetpackMenuCardLastShownTimestamp(System.currentTimeMillis())
- refresh()
- }
-
- private fun onSwitchToJetpackMenuCardHideMenuItemClick() {
- jetpackFeatureCardHelper.hideSwitchToJetpackMenuCard()
- refresh()
- }
-
- private fun onJetpackFeatureCardMoreMenuClick() {
- jetpackFeatureCardHelper.track(Stat.REMOVE_FEATURE_CARD_MENU_ACCESSED)
- }
-
- private fun onJetpackInstallFullPluginHideMenuItemClick() {
- selectedSiteRepository.getSelectedSite()?.localId()?.value?.let {
- analyticsTrackerWrapper.track(Stat.JETPACK_INSTALL_FULL_PLUGIN_CARD_DISMISSED)
- appPrefsWrapper.setShouldHideJetpackInstallFullPluginCard(it, true)
- refresh()
+ private fun buildDashboardOrSiteItems(site: SiteModel) {
+ siteInfoHeaderCardViewModelSlice.buildCard(site)
+ if (shouldShowDashboard(site)) {
+ dashboardCardsViewModelSlice.buildCards(site)
+ dashboardItemsViewModelSlice.clearValue()
+ } else {
+ dashboardItemsViewModelSlice.buildItems(site)
+ dashboardCardsViewModelSlice.clearValue()
}
}
- private fun onJetpackInstallFullPluginLearnMoreClick() {
- analyticsTrackerWrapper.track(Stat.JETPACK_INSTALL_FULL_PLUGIN_CARD_TAPPED)
- _onOpenJetpackInstallFullPluginOnboarding.postValue(Event(Unit))
- }
-
- private fun onDashboardCardPlansClick() {
- val selectedSite = requireNotNull(selectedSiteRepository.getSelectedSite())
- dashboardCardPlansUtils.trackCardTapped(uiModel.value as? SiteSelected)
- _onNavigation.value = Event(SiteNavigationAction.OpenFreeDomainSearch(selectedSite))
- }
-
- private fun onDashboardCardPlansMoreMenuClick() {
- dashboardCardPlansUtils.trackCardMoreMenuTapped(uiModel.value as? SiteSelected)
- }
-
- private fun onDashboardCardPlansHideMenuItemClick() {
- dashboardCardPlansUtils.trackCardHiddenByUser(uiModel.value as? SiteSelected)
- selectedSiteRepository.getSelectedSite()?.let {
- dashboardCardPlansUtils.hideCard(it.siteId)
+ private fun onSitePicked(site: SiteModel) {
+ siteInfoHeaderCardViewModelSlice.buildCard(site)
+ dashboardItemsViewModelSlice.clearValue()
+ dashboardCardsViewModelSlice.clearValue()
+ dashboardCardsViewModelSlice.resetShownTracker()
+ dashboardItemsViewModelSlice.resetShownTracker()
+ if (shouldShowDashboard(site)) {
+ dashboardCardsViewModelSlice.buildCards(site)
+ } else {
+ dashboardItemsViewModelSlice.buildItems(site)
}
- refresh()
}
- fun isRefreshing() = mySiteSourceManager.isRefreshing()
-
fun onActionableEmptyViewVisible() {
analyticsTrackerWrapper.track(Stat.MY_SITE_NO_SITES_VIEW_DISPLAYED)
checkJetpackIndividualPluginOverlayShouldShow()
@@ -974,40 +417,6 @@ class MySiteViewModel @Inject constructor(
_onNavigation.postValue(Event(BloggingPromptCardNavigationAction.LearnMore))
}
- private fun trackCardsAndItemsShownIfNeeded(siteSelected: SiteSelected) {
- siteSelected.dashboardData.filterIsInstance()
- .forEach { domainRegistrationCardShownTracker.trackShown(it.type) }
- siteSelected.dashboardData.filterIsInstance()
- .let { cardsTracker.trackShown(it) }
- siteSelected.dashboardData.filterIsInstance()
- .firstOrNull()?.let { quickStartTracker.trackShown(it.type) }
- siteSelected.dashboardData.filterIsInstance()
- .firstOrNull()?.let { cardsTracker.trackQuickStartCardShown(quickStartRepository.quickStartType) }
- siteSelected.dashboardData.filterIsInstance()
- .forEach { jetpackFeatureCardShownTracker.trackShown(it.type) }
- siteSelected.dashboardData.filterIsInstance()
- .forEach { jetpackInstallFullPluginShownTracker.trackShown(it.type) }
- dashboardCardPlansUtils.trackCardShown(viewModelScope, siteSelected)
- siteSelected.dashboardData.filterIsInstance()
- .forEach { personalizeCardViewModelSlice.trackShown(it.type) }
- siteSelected.dashboardData.filterIsInstance()
- .forEach { noCardsMessageViewModelSlice.trackShown(it.type) }
- siteSelected.dashboardData.filterIsInstance()
- .forEach { _ -> sotw2023NudgeCardViewModelSlice.trackShown() }
- siteSelected.dashboardData.filterIsInstance()
- .forEach { dynamicCardsViewModelSlice.trackShown(it.id) }
- }
-
- private fun resetShownTrackers() {
- domainRegistrationCardShownTracker.resetShown()
- cardsTracker.resetShown()
- quickStartTracker.resetShown()
- jetpackFeatureCardShownTracker.resetShown()
- jetpackInstallFullPluginShownTracker.resetShown()
- personalizeCardViewModelSlice.resetShown()
- sotw2023NudgeCardViewModelSlice.resetShown()
- dynamicCardsViewModelSlice.resetShown()
- }
// FluxC events
@Subscribe(threadMode = MAIN)
@@ -1015,7 +424,7 @@ class MySiteViewModel @Inject constructor(
if (!event.isError) {
event.post?.let {
if (event.post.answeredPromptId > 0 && event.isFirstTimePublish) {
- mySiteSourceManager.refreshBloggingPrompts(true)
+ dashboardCardsViewModelSlice.refreshBloggingPrompt()
}
}
}
@@ -1027,10 +436,8 @@ class MySiteViewModel @Inject constructor(
) : State()
data class NoSites(
- val shouldShowImage: Boolean,
val avatarUrl: String? = null,
val accountName: String? = null,
- val shouldShowAccountSettings: Boolean = false
) : State()
}
@@ -1043,15 +450,7 @@ class MySiteViewModel @Inject constructor(
val isInputEnabled: Boolean
)
- private data class SiteIdToState(val siteId: Int?, val state: MySiteUiState = MySiteUiState()) {
- fun update(partialState: PartialState): SiteIdToState {
- return this.copy(state = state.update(partialState))
- }
- }
-
companion object {
- private const val MIN_DISPLAY_PX_HEIGHT_NO_SITE_IMAGE = 600
- private const val LIST_INDEX_NO_ACTIVE_QUICK_START_ITEM = -1
const val TAG_ADD_SITE_ICON_DIALOG = "TAG_ADD_SITE_ICON_DIALOG"
const val TAG_CHANGE_SITE_ICON_DIALOG = "TAG_CHANGE_SITE_ICON_DIALOG"
const val TAG_REMOVE_NEXT_STEPS_DIALOG = "TAG_REMOVE_NEXT_STEPS_DIALOG"
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/ScanAndBackupSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/ScanAndBackupSource.kt
deleted file mode 100644
index ef9f2eed28f7..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/ScanAndBackupSource.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package org.wordpress.android.ui.mysite
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MediatorLiveData
-import androidx.lifecycle.MutableLiveData
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-import org.wordpress.android.modules.BG_THREAD
-import org.wordpress.android.ui.jetpack.JetpackCapabilitiesUseCase
-import org.wordpress.android.ui.mysite.MySiteSource.MySiteRefreshSource
-import org.wordpress.android.ui.mysite.MySiteUiState.PartialState.JetpackCapabilities
-import org.wordpress.android.util.SiteUtils
-import javax.inject.Inject
-import javax.inject.Named
-
-class ScanAndBackupSource @Inject constructor(
- @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher,
- private val selectedSiteRepository: SelectedSiteRepository,
- private val jetpackCapabilitiesUseCase: JetpackCapabilitiesUseCase
-) : MySiteRefreshSource {
- override val refresh = MutableLiveData(false)
-
- fun clear() {
- jetpackCapabilitiesUseCase.clear()
- }
-
- override fun build(coroutineScope: CoroutineScope, siteLocalId: Int): LiveData {
- val result = MediatorLiveData()
- result.addSource(refresh) { result.refreshData(coroutineScope, siteLocalId, refresh.value) }
- refresh()
- return result
- }
-
- private fun MediatorLiveData.refreshData(
- coroutineScope: CoroutineScope,
- siteLocalId: Int,
- isRefresh: Boolean? = null
- ) {
- when (isRefresh) {
- null, true -> refreshData(coroutineScope, siteLocalId)
- false -> Unit // Do nothing
- }
- }
-
- private fun MediatorLiveData