mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 04:43:54 +01:00
Merge branch 'main' into fix-multiupload
This commit is contained in:
commit
af2d0d8cdd
132 changed files with 8635 additions and 7213 deletions
96
.github/workflows/android-ci-comment.yml
vendored
Normal file
96
.github/workflows/android-ci-comment.yml
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
name: Android CI Comment
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Android CI"]
|
||||||
|
types: [completed]
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment:
|
||||||
|
name: Comment on PR with APK links
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.workflow_run.conclusion == 'success'
|
||||||
|
steps:
|
||||||
|
- name: Checkout base branch
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.workflow_run.head_branch }}
|
||||||
|
- name: Download Run ID Artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: run-id
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
|
||||||
|
- name: Read Run ID
|
||||||
|
id: read-run-id
|
||||||
|
run: echo "RUN_ID=$(cat run_id.txt)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Comment on PR with APK download links
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
try {
|
||||||
|
const token = process.env.GH_TOKEN;
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('GITHUB_TOKEN is not set.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const runId = "${{ env.RUN_ID }}";
|
||||||
|
if (!runId) {
|
||||||
|
throw new Error('Run ID not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the PR number from the workflow_run event
|
||||||
|
const prNumber = ${{ github.event.workflow_run.pull_requests[0].number }};
|
||||||
|
if (!prNumber) {
|
||||||
|
console.log('No PR number found in workflow_run event.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: { artifacts } } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: runId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!artifacts || artifacts.length === 0) {
|
||||||
|
console.log('No artifacts found for this workflow run.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const betaArtifact = artifacts.find(artifact => artifact.name === "betaDebugAPK");
|
||||||
|
const prodArtifact = artifacts.find(artifact => artifact.name === "prodDebugAPK");
|
||||||
|
|
||||||
|
if (!betaArtifact || !prodArtifact) {
|
||||||
|
console.log('Could not find both Beta and Prod APK artifacts.');
|
||||||
|
console.log('Available artifacts:', artifacts.map(a => a.name).join(', '));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const betaDownloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/suites/${runId}/artifacts/${betaArtifact.id}`;
|
||||||
|
const prodDownloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/suites/${runId}/artifacts/${prodArtifact.id}`;
|
||||||
|
|
||||||
|
const commentBody = `
|
||||||
|
📱 **APK for pull request is ready to see the changes** 📱
|
||||||
|
- [Download Beta APK](${betaDownloadUrl})
|
||||||
|
- [Download Prod APK](${prodDownloadUrl})
|
||||||
|
`;
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: commentBody
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Successfully posted comment with APK download links');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in PR comment creation:', error);
|
||||||
|
core.setFailed(`Workflow failed: ${error.message}`);
|
||||||
|
}
|
||||||
21
.github/workflows/android.yml
vendored
21
.github/workflows/android.yml
vendored
|
|
@ -1,6 +1,10 @@
|
||||||
name: Android CI
|
name: Android CI
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
on: [push, pull_request, workflow_dispatch]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: build-${{ github.event.pull_request.number || github.ref }}
|
group: build-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
|
@ -89,7 +93,7 @@ jobs:
|
||||||
run: bash ./gradlew assembleBetaDebug --stacktrace
|
run: bash ./gradlew assembleBetaDebug --stacktrace
|
||||||
|
|
||||||
- name: Upload betaDebug APK
|
- name: Upload betaDebug APK
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: betaDebugAPK
|
name: betaDebugAPK
|
||||||
path: app/build/outputs/apk/beta/debug/app-*.apk
|
path: app/build/outputs/apk/beta/debug/app-*.apk
|
||||||
|
|
@ -98,7 +102,18 @@ jobs:
|
||||||
run: bash ./gradlew assembleProdDebug --stacktrace
|
run: bash ./gradlew assembleProdDebug --stacktrace
|
||||||
|
|
||||||
- name: Upload prodDebug APK
|
- name: Upload prodDebug APK
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: prodDebugAPK
|
name: prodDebugAPK
|
||||||
path: app/build/outputs/apk/prod/debug/app-*.apk
|
path: app/build/outputs/apk/prod/debug/app-*.apk
|
||||||
|
|
||||||
|
- name: Store Workflow Run ID
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
run: echo "${{ github.run_id }}" > run_id.txt
|
||||||
|
|
||||||
|
- name: Upload Run ID as Artifact
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: run-id
|
||||||
|
path: run_id.txt
|
||||||
|
|
|
||||||
41
.github/workflows/build-beta.yml
vendored
Normal file
41
.github/workflows/build-beta.yml
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
name: Build beta only
|
||||||
|
|
||||||
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: set up JDK 17
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
cache: gradle
|
||||||
|
|
||||||
|
- name: Access test login credentials
|
||||||
|
run: |
|
||||||
|
echo "TEST_USER_NAME=${{ secrets.TEST_USER_NAME }}" >> local.properties
|
||||||
|
echo "TEST_USER_PASSWORD=${{ secrets.TEST_USER_PASSWORD }}" >> local.properties
|
||||||
|
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Set env
|
||||||
|
run: echo "COMMIT_SHA=$(git log -n 1 --format='%h')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Generate betaDebug APK
|
||||||
|
run: ./gradlew assembleBetaDebug --stacktrace
|
||||||
|
|
||||||
|
- name: Rename betaDebug APK
|
||||||
|
run: mv app/build/outputs/apk/beta/debug/app-*.apk app/build/outputs/apk/beta/debug/apps-android-commons-betaDebug-$COMMIT_SHA.apk
|
||||||
|
|
||||||
|
- name: Upload betaDebug APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: apps-android-commons-betaDebugAPK-${{ env.COMMIT_SHA }}
|
||||||
|
path: app/build/outputs/apk/beta/debug/*.apk
|
||||||
|
retention-days: 30
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -46,4 +46,5 @@ captures/*
|
||||||
|
|
||||||
# Test and other output
|
# Test and other output
|
||||||
app/jacoco.exec
|
app/jacoco.exec
|
||||||
app/CommonsContributions
|
app/CommonsContributions
|
||||||
|
app/.*
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Wikimedia Commons Android app
|
# Wikimedia Commons Android app
|
||||||

|

|
||||||
[](https://github.com/commons-app/apps-android-commons/actions?query=branch%3Amaster)
|
[](https://github.com/commons-app/apps-android-commons/actions?query=branch%3Amain)
|
||||||
[](https://appetize.io/app/8ywtpe9f8tb8h6bey11c92vkcw)
|
[](https://appetize.io/app/8ywtpe9f8tb8h6bey11c92vkcw)
|
||||||
[](https://codecov.io/gh/commons-app/apps-android-commons)
|
[](https://codecov.io/gh/commons-app/apps-android-commons)
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ This software is open source, licensed under the [Apache License 2.0][10].
|
||||||
|
|
||||||
[1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
|
[1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
|
||||||
[2]: https://commons-app.github.io/
|
[2]: https://commons-app.github.io/
|
||||||
[3]: https://github.com/commons-app/apps-android-commons/issues
|
[3]: https://github.com/commons-app/apps-android-commons/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+-label%3Adebated+label%3Abug+-label%3A%22low+priority%22+-label%3Aupstream
|
||||||
|
|
||||||
[4]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-android-documentation
|
[4]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-android-documentation
|
||||||
[5]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-user-documentation
|
[5]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-user-documentation
|
||||||
|
|
|
||||||
|
|
@ -175,8 +175,8 @@ dependencies {
|
||||||
testImplementation "androidx.work:work-testing:$work_version"
|
testImplementation "androidx.work:work-testing:$work_version"
|
||||||
|
|
||||||
//Glide
|
//Glide
|
||||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
implementation 'com.github.bumptech.glide:glide:4.16.0'
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
|
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||||
kaptTest "androidx.databinding:databinding-compiler:8.0.2"
|
kaptTest "androidx.databinding:databinding-compiler:8.0.2"
|
||||||
kaptAndroidTest "androidx.databinding:databinding-compiler:8.0.2"
|
kaptAndroidTest "androidx.databinding:databinding-compiler:8.0.2"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -232,12 +232,6 @@
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/provider_bookmarks"
|
android:label="@string/provider_bookmarks"
|
||||||
android:syncable="false" />
|
android:syncable="false" />
|
||||||
<provider
|
|
||||||
android:name=".bookmarks.locations.BookmarkLocationsContentProvider"
|
|
||||||
android:authorities="${applicationId}.bookmarks.locations.contentprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:label="@string/provider_bookmarks_location"
|
|
||||||
android:syncable="false" />
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".bookmarks.items.BookmarkItemsContentProvider"
|
android:name=".bookmarks.items.BookmarkItemsContentProvider"
|
||||||
android:authorities="${applicationId}.bookmarks.items.contentprovider"
|
android:authorities="${applicationId}.bookmarks.items.contentprovider"
|
||||||
|
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import fr.free.nrw.commons.databinding.ActivityAboutBinding;
|
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
|
||||||
import fr.free.nrw.commons.utils.DialogUtil;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents about screen of this app
|
|
||||||
*/
|
|
||||||
public class AboutActivity extends BaseActivity {
|
|
||||||
|
|
||||||
/*
|
|
||||||
This View Binding class is auto-generated for each xml file. The format is usually the name
|
|
||||||
of the file with PascalCasing (The underscore characters will be ignored).
|
|
||||||
More information is available at https://developer.android.com/topic/libraries/view-binding
|
|
||||||
*/
|
|
||||||
private ActivityAboutBinding binding;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method helps in the creation About screen
|
|
||||||
*
|
|
||||||
* @param savedInstanceState Data bundle
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@SuppressLint("StringFormatInvalid")
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
/*
|
|
||||||
Instead of just setting the view with the xml file. We need to use View Binding class.
|
|
||||||
*/
|
|
||||||
binding = ActivityAboutBinding.inflate(getLayoutInflater());
|
|
||||||
final View view = binding.getRoot();
|
|
||||||
setContentView(view);
|
|
||||||
|
|
||||||
setSupportActionBar(binding.toolbarBinding.toolbar);
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
final String aboutText = getString(R.string.about_license);
|
|
||||||
/*
|
|
||||||
We can then access all the views by just using the id names like this.
|
|
||||||
camelCasing is used with underscore characters being ignored.
|
|
||||||
*/
|
|
||||||
binding.aboutLicense.setHtmlText(aboutText);
|
|
||||||
|
|
||||||
@SuppressLint("StringFormatMatches")
|
|
||||||
String improveText = String.format(getString(R.string.about_improve), Urls.NEW_ISSUE_URL);
|
|
||||||
binding.aboutImprove.setHtmlText(improveText);
|
|
||||||
binding.aboutVersion.setText(ConfigUtils.getVersionNameWithSha(getApplicationContext()));
|
|
||||||
|
|
||||||
Utils.setUnderlinedText(binding.aboutFaq, R.string.about_faq, getApplicationContext());
|
|
||||||
Utils.setUnderlinedText(binding.aboutRateUs, R.string.about_rate_us, getApplicationContext());
|
|
||||||
Utils.setUnderlinedText(binding.aboutUserGuide, R.string.user_guide, getApplicationContext());
|
|
||||||
Utils.setUnderlinedText(binding.aboutPrivacyPolicy, R.string.about_privacy_policy, getApplicationContext());
|
|
||||||
Utils.setUnderlinedText(binding.aboutTranslate, R.string.about_translate, getApplicationContext());
|
|
||||||
Utils.setUnderlinedText(binding.aboutCredits, R.string.about_credits, getApplicationContext());
|
|
||||||
|
|
||||||
/*
|
|
||||||
To set listeners, we can create a separate method and use lambda syntax.
|
|
||||||
*/
|
|
||||||
binding.facebookLaunchIcon.setOnClickListener(this::launchFacebook);
|
|
||||||
binding.githubLaunchIcon.setOnClickListener(this::launchGithub);
|
|
||||||
binding.websiteLaunchIcon.setOnClickListener(this::launchWebsite);
|
|
||||||
binding.aboutRateUs.setOnClickListener(this::launchRatings);
|
|
||||||
binding.aboutCredits.setOnClickListener(this::launchCredits);
|
|
||||||
binding.aboutPrivacyPolicy.setOnClickListener(this::launchPrivacyPolicy);
|
|
||||||
binding.aboutUserGuide.setOnClickListener(this::launchUserGuide);
|
|
||||||
binding.aboutFaq.setOnClickListener(this::launchFrequentlyAskedQuesions);
|
|
||||||
binding.aboutTranslate.setOnClickListener(this::launchTranslate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onSupportNavigateUp() {
|
|
||||||
onBackPressed();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchFacebook(View view) {
|
|
||||||
Intent intent;
|
|
||||||
try {
|
|
||||||
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(Urls.FACEBOOK_APP_URL));
|
|
||||||
intent.setPackage(Urls.FACEBOOK_PACKAGE_NAME);
|
|
||||||
startActivity(intent);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.FACEBOOK_WEB_URL));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchGithub(View view) {
|
|
||||||
Intent intent;
|
|
||||||
try {
|
|
||||||
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(Urls.GITHUB_REPO_URL));
|
|
||||||
intent.setPackage(Urls.GITHUB_PACKAGE_NAME);
|
|
||||||
startActivity(intent);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.GITHUB_REPO_URL));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchWebsite(View view) {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.WEBSITE_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchRatings(View view){
|
|
||||||
Utils.rateApp(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchCredits(View view) {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.CREDITS_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchUserGuide(View view) {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.USER_GUIDE_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchPrivacyPolicy(View view) {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchFrequentlyAskedQuesions(View view) {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.FAQ_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
MenuInflater inflater = getMenuInflater();
|
|
||||||
inflater.inflate(R.menu.menu_about, menu);
|
|
||||||
return super.onCreateOptionsMenu(menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.share_app_icon:
|
|
||||||
String shareText = String.format(getString(R.string.share_text), Urls.PLAY_STORE_URL_PREFIX + this.getPackageName());
|
|
||||||
Intent sendIntent = new Intent();
|
|
||||||
sendIntent.setAction(Intent.ACTION_SEND);
|
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, shareText);
|
|
||||||
sendIntent.setType("text/plain");
|
|
||||||
startActivity(Intent.createChooser(sendIntent, getString(R.string.share_via)));
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchTranslate(View view) {
|
|
||||||
@NonNull List<String> sortedLocalizedNamesRef = CommonsApplication.getInstance().getLanguageLookUpTable().getCanonicalNames();
|
|
||||||
Collections.sort(sortedLocalizedNamesRef);
|
|
||||||
final ArrayAdapter<String> languageAdapter = new ArrayAdapter<>(AboutActivity.this,
|
|
||||||
android.R.layout.simple_spinner_dropdown_item, sortedLocalizedNamesRef);
|
|
||||||
final Spinner spinner = new Spinner(AboutActivity.this);
|
|
||||||
spinner.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
|
||||||
spinner.setAdapter(languageAdapter);
|
|
||||||
spinner.setGravity(17);
|
|
||||||
spinner.setPadding(50,0,0,0);
|
|
||||||
|
|
||||||
Runnable positiveButtonRunnable = () -> {
|
|
||||||
String langCode = CommonsApplication.getInstance().getLanguageLookUpTable().getCodes().get(spinner.getSelectedItemPosition());
|
|
||||||
Utils.handleWebUrl(AboutActivity.this, Uri.parse(Urls.TRANSLATE_WIKI_URL + langCode));
|
|
||||||
};
|
|
||||||
DialogUtil.showAlertDialog(this,
|
|
||||||
getString(R.string.about_translate_title),
|
|
||||||
getString(R.string.about_translate_message),
|
|
||||||
getString(R.string.about_translate_proceed),
|
|
||||||
getString(R.string.about_translate_cancel),
|
|
||||||
positiveButtonRunnable,
|
|
||||||
() -> {},
|
|
||||||
spinner
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
209
app/src/main/java/fr/free/nrw/commons/AboutActivity.kt
Normal file
209
app/src/main/java/fr/free/nrw/commons/AboutActivity.kt
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Spinner
|
||||||
|
import fr.free.nrw.commons.CommonsApplication.Companion.instance
|
||||||
|
import fr.free.nrw.commons.databinding.ActivityAboutBinding
|
||||||
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
|
import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha
|
||||||
|
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents about screen of this app
|
||||||
|
*/
|
||||||
|
class AboutActivity : BaseActivity() {
|
||||||
|
/*
|
||||||
|
This View Binding class is auto-generated for each xml file. The format is usually the name
|
||||||
|
of the file with PascalCasing (The underscore characters will be ignored).
|
||||||
|
More information is available at https://developer.android.com/topic/libraries/view-binding
|
||||||
|
*/
|
||||||
|
private var binding: ActivityAboutBinding? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method helps in the creation About screen
|
||||||
|
*
|
||||||
|
* @param savedInstanceState Data bundle
|
||||||
|
*/
|
||||||
|
@SuppressLint("StringFormatInvalid") //TODO:
|
||||||
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Instead of just setting the view with the xml file. We need to use View Binding class.
|
||||||
|
*/
|
||||||
|
binding = ActivityAboutBinding.inflate(layoutInflater)
|
||||||
|
val view: View = binding!!.root
|
||||||
|
setContentView(view)
|
||||||
|
|
||||||
|
setSupportActionBar(binding!!.toolbarBinding.toolbar)
|
||||||
|
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||||
|
val aboutText = getString(R.string.about_license)
|
||||||
|
/*
|
||||||
|
We can then access all the views by just using the id names like this.
|
||||||
|
camelCasing is used with underscore characters being ignored.
|
||||||
|
*/
|
||||||
|
binding!!.aboutLicense.setHtmlText(aboutText)
|
||||||
|
|
||||||
|
@SuppressLint("StringFormatMatches") // TODO:
|
||||||
|
val improveText =
|
||||||
|
String.format(getString(R.string.about_improve), Urls.NEW_ISSUE_URL)
|
||||||
|
binding!!.aboutImprove.setHtmlText(improveText)
|
||||||
|
binding!!.aboutVersion.text = applicationContext.getVersionNameWithSha()
|
||||||
|
|
||||||
|
Utils.setUnderlinedText(
|
||||||
|
binding!!.aboutFaq, R.string.about_faq,
|
||||||
|
applicationContext
|
||||||
|
)
|
||||||
|
Utils.setUnderlinedText(
|
||||||
|
binding!!.aboutRateUs, R.string.about_rate_us,
|
||||||
|
applicationContext
|
||||||
|
)
|
||||||
|
Utils.setUnderlinedText(
|
||||||
|
binding!!.aboutUserGuide, R.string.user_guide,
|
||||||
|
applicationContext
|
||||||
|
)
|
||||||
|
Utils.setUnderlinedText(
|
||||||
|
binding!!.aboutPrivacyPolicy, R.string.about_privacy_policy,
|
||||||
|
applicationContext
|
||||||
|
)
|
||||||
|
Utils.setUnderlinedText(
|
||||||
|
binding!!.aboutTranslate, R.string.about_translate,
|
||||||
|
applicationContext
|
||||||
|
)
|
||||||
|
Utils.setUnderlinedText(
|
||||||
|
binding!!.aboutCredits, R.string.about_credits,
|
||||||
|
applicationContext
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
To set listeners, we can create a separate method and use lambda syntax.
|
||||||
|
*/
|
||||||
|
binding!!.facebookLaunchIcon.setOnClickListener(::launchFacebook)
|
||||||
|
binding!!.githubLaunchIcon.setOnClickListener(::launchGithub)
|
||||||
|
binding!!.websiteLaunchIcon.setOnClickListener(::launchWebsite)
|
||||||
|
binding!!.aboutRateUs.setOnClickListener(::launchRatings)
|
||||||
|
binding!!.aboutCredits.setOnClickListener(::launchCredits)
|
||||||
|
binding!!.aboutPrivacyPolicy.setOnClickListener(::launchPrivacyPolicy)
|
||||||
|
binding!!.aboutUserGuide.setOnClickListener(::launchUserGuide)
|
||||||
|
binding!!.aboutFaq.setOnClickListener(::launchFrequentlyAskedQuesions)
|
||||||
|
binding!!.aboutTranslate.setOnClickListener(::launchTranslate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
|
onBackPressed()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchFacebook(view: View?) {
|
||||||
|
val intent: Intent
|
||||||
|
try {
|
||||||
|
intent = Intent(Intent.ACTION_VIEW, Uri.parse(Urls.FACEBOOK_APP_URL))
|
||||||
|
intent.setPackage(Urls.FACEBOOK_PACKAGE_NAME)
|
||||||
|
startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Utils.handleWebUrl(this, Uri.parse(Urls.FACEBOOK_WEB_URL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchGithub(view: View?) {
|
||||||
|
val intent: Intent
|
||||||
|
try {
|
||||||
|
intent = Intent(Intent.ACTION_VIEW, Uri.parse(Urls.GITHUB_REPO_URL))
|
||||||
|
intent.setPackage(Urls.GITHUB_PACKAGE_NAME)
|
||||||
|
startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Utils.handleWebUrl(this, Uri.parse(Urls.GITHUB_REPO_URL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchWebsite(view: View?) {
|
||||||
|
Utils.handleWebUrl(this, Uri.parse(Urls.WEBSITE_URL))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchRatings(view: View?) {
|
||||||
|
Utils.rateApp(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchCredits(view: View?) {
|
||||||
|
Utils.handleWebUrl(this, Uri.parse(Urls.CREDITS_URL))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchUserGuide(view: View?) {
|
||||||
|
Utils.handleWebUrl(this, Uri.parse(Urls.USER_GUIDE_URL))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchPrivacyPolicy(view: View?) {
|
||||||
|
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchFrequentlyAskedQuesions(view: View?) {
|
||||||
|
Utils.handleWebUrl(this, Uri.parse(Urls.FAQ_URL))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
val inflater = menuInflater
|
||||||
|
inflater.inflate(R.menu.menu_about, menu)
|
||||||
|
return super.onCreateOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.share_app_icon -> {
|
||||||
|
val shareText = String.format(
|
||||||
|
getString(R.string.share_text),
|
||||||
|
Urls.PLAY_STORE_URL_PREFIX + this.packageName
|
||||||
|
)
|
||||||
|
val sendIntent = Intent()
|
||||||
|
sendIntent.setAction(Intent.ACTION_SEND)
|
||||||
|
sendIntent.putExtra(Intent.EXTRA_TEXT, shareText)
|
||||||
|
sendIntent.setType("text/plain")
|
||||||
|
startActivity(Intent.createChooser(sendIntent, getString(R.string.share_via)))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchTranslate(view: View?) {
|
||||||
|
val sortedLocalizedNamesRef = instance.languageLookUpTable!!.getCanonicalNames()
|
||||||
|
Collections.sort(sortedLocalizedNamesRef)
|
||||||
|
val languageAdapter = ArrayAdapter(
|
||||||
|
this@AboutActivity,
|
||||||
|
android.R.layout.simple_spinner_dropdown_item, sortedLocalizedNamesRef
|
||||||
|
)
|
||||||
|
val spinner = Spinner(this@AboutActivity)
|
||||||
|
spinner.layoutParams =
|
||||||
|
LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
spinner.adapter = languageAdapter
|
||||||
|
spinner.gravity = 17
|
||||||
|
spinner.setPadding(50, 0, 0, 0)
|
||||||
|
|
||||||
|
val positiveButtonRunnable = Runnable {
|
||||||
|
val langCode = instance.languageLookUpTable!!.getCodes()[spinner.selectedItemPosition]
|
||||||
|
Utils.handleWebUrl(this@AboutActivity, Uri.parse(Urls.TRANSLATE_WIKI_URL + langCode))
|
||||||
|
}
|
||||||
|
showAlertDialog(
|
||||||
|
this,
|
||||||
|
getString(R.string.about_translate_title),
|
||||||
|
getString(R.string.about_translate_message),
|
||||||
|
getString(R.string.about_translate_proceed),
|
||||||
|
getString(R.string.about_translate_cancel),
|
||||||
|
positiveButtonRunnable,
|
||||||
|
{},
|
||||||
|
spinner
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -247,13 +247,17 @@ class CommonsApplication : MultiDexApplication() {
|
||||||
DBOpenHelper.CONTRIBUTIONS_TABLE
|
DBOpenHelper.CONTRIBUTIONS_TABLE
|
||||||
) //Delete the contributions table in the existing db on older versions
|
) //Delete the contributions table in the existing db on older versions
|
||||||
|
|
||||||
|
dbOpenHelper.deleteTable(
|
||||||
|
db,
|
||||||
|
DBOpenHelper.BOOKMARKS_LOCATIONS
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
contributionDao.deleteAll()
|
contributionDao.deleteAll()
|
||||||
} catch (e: SQLiteException) {
|
} catch (e: SQLiteException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
BookmarkPicturesDao.Table.onDelete(db)
|
BookmarkPicturesDao.Table.onDelete(db)
|
||||||
BookmarkLocationsDao.Table.onDelete(db)
|
|
||||||
BookmarkItemsDao.Table.onDelete(db)
|
BookmarkItemsDao.Table.onDelete(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,41 @@ class Media constructor(
|
||||||
captions = captions,
|
captions = captions,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
captions: Map<String, String>,
|
||||||
|
categories: List<String>?,
|
||||||
|
filename: String?,
|
||||||
|
fallbackDescription: String?,
|
||||||
|
author: String?,
|
||||||
|
user: String?,
|
||||||
|
dateUploaded: Date? = Date(),
|
||||||
|
license: String? = null,
|
||||||
|
licenseUrl: String? = null,
|
||||||
|
imageUrl: String? = null,
|
||||||
|
thumbUrl: String? = null,
|
||||||
|
coordinates: LatLng? = null,
|
||||||
|
descriptions: Map<String, String> = emptyMap(),
|
||||||
|
depictionIds: List<String> = emptyList(),
|
||||||
|
categoriesHiddenStatus: Map<String, Boolean> = emptyMap()
|
||||||
|
) : this(
|
||||||
|
pageId = UUID.randomUUID().toString(),
|
||||||
|
filename = filename,
|
||||||
|
fallbackDescription = fallbackDescription,
|
||||||
|
dateUploaded = dateUploaded,
|
||||||
|
author = author,
|
||||||
|
user = user,
|
||||||
|
categories = categories,
|
||||||
|
captions = captions,
|
||||||
|
license = license,
|
||||||
|
licenseUrl = licenseUrl,
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
thumbUrl = thumbUrl,
|
||||||
|
coordinates = coordinates,
|
||||||
|
descriptions = descriptions,
|
||||||
|
depictionIds = depictionIds,
|
||||||
|
categoriesHiddenStatus = categoriesHiddenStatus
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets media display title
|
* Gets media display title
|
||||||
* @return Media title
|
* @return Media title
|
||||||
|
|
|
||||||
|
|
@ -148,13 +148,27 @@ public class Utils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Util function to handle geo coordinates
|
* Util function to handle geo coordinates. It no longer depends on google maps and any app
|
||||||
* It no longer depends on google maps and any app capable of handling the map intent can handle it
|
* capable of handling the map intent can handle it
|
||||||
* @param context
|
*
|
||||||
* @param latLng
|
* @param context The context for launching intent
|
||||||
|
* @param latLng The latitude and longitude of the location
|
||||||
*/
|
*/
|
||||||
public static void handleGeoCoordinates(Context context, LatLng latLng) {
|
public static void handleGeoCoordinates(final Context context, final LatLng latLng) {
|
||||||
Intent mapIntent = new Intent(Intent.ACTION_VIEW, latLng.getGmmIntentUri());
|
handleGeoCoordinates(context, latLng, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Util function to handle geo coordinates with specified zoom level. It no longer depends on
|
||||||
|
* google maps and any app capable of handling the map intent can handle it
|
||||||
|
*
|
||||||
|
* @param context The context for launching intent
|
||||||
|
* @param latLng The latitude and longitude of the location
|
||||||
|
* @param zoomLevel The zoom level
|
||||||
|
*/
|
||||||
|
public static void handleGeoCoordinates(final Context context, final LatLng latLng,
|
||||||
|
final double zoomLevel) {
|
||||||
|
final Intent mapIntent = new Intent(Intent.ACTION_VIEW, latLng.getGmmIntentUri(zoomLevel));
|
||||||
if (mapIntent.resolveActivity(context.getPackageManager()) != null) {
|
if (mapIntent.resolveActivity(context.getPackageManager()) != null) {
|
||||||
context.startActivity(mapIntent);
|
context.startActivity(mapIntent);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import fr.free.nrw.commons.CommonsApplication
|
||||||
|
import fr.free.nrw.commons.CommonsApplication.ActivityLogoutListener
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.di.ApplicationlessInjection
|
import fr.free.nrw.commons.di.ApplicationlessInjection
|
||||||
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar
|
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar
|
||||||
|
|
@ -85,7 +87,12 @@ class SingleWebViewActivity : ComponentActivity() {
|
||||||
url = url,
|
url = url,
|
||||||
successUrl = successUrl,
|
successUrl = successUrl,
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
// TODO Redirect the user to login screen like we do when the user logout's
|
//Redirect the user to login screen like we do when the user logout's
|
||||||
|
val app = applicationContext as CommonsApplication
|
||||||
|
app.clearApplicationData(
|
||||||
|
applicationContext,
|
||||||
|
ActivityLogoutListener(activity = this, ctx = applicationContext)
|
||||||
|
)
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks.locations;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteQueryBuilder;
|
|
||||||
// We can get uri using java.Net.Uri, but andoid implimentation is faster (but it's forgiving with handling exceptions though)
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_NAME;
|
|
||||||
import static fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.TABLE_NAME;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles private storage for Bookmark locations
|
|
||||||
*/
|
|
||||||
public class BookmarkLocationsContentProvider extends CommonsDaggerContentProvider {
|
|
||||||
|
|
||||||
private static final String BASE_PATH = "bookmarksLocations";
|
|
||||||
public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY + "/" + BASE_PATH);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append bookmark locations name to the base uri
|
|
||||||
*/
|
|
||||||
public static Uri uriForName(String name) {
|
|
||||||
return Uri.parse(BASE_URI.toString() + "/" + name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject DBOpenHelper dbOpenHelper;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getType(@NonNull Uri uri) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queries the SQLite database for the bookmark locations
|
|
||||||
* @param uri : contains the uri for bookmark locations
|
|
||||||
* @param projection
|
|
||||||
* @param selection : handles Where
|
|
||||||
* @param selectionArgs : the condition of Where clause
|
|
||||||
* @param sortOrder : ascending or descending
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
@Override
|
|
||||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
|
|
||||||
String[] selectionArgs, String sortOrder) {
|
|
||||||
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
|
||||||
queryBuilder.setTables(TABLE_NAME);
|
|
||||||
|
|
||||||
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
|
||||||
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
|
|
||||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
|
||||||
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the update query of local SQLite Database
|
|
||||||
* @param uri : contains the uri for bookmark locations
|
|
||||||
* @param contentValues : new values to be entered to db
|
|
||||||
* @param selection : handles Where
|
|
||||||
* @param selectionArgs : the condition of Where clause
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
@Override
|
|
||||||
public int update(@NonNull Uri uri, ContentValues contentValues, String selection,
|
|
||||||
String[] selectionArgs) {
|
|
||||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
|
||||||
int rowsUpdated;
|
|
||||||
if (TextUtils.isEmpty(selection)) {
|
|
||||||
int id = Integer.valueOf(uri.getLastPathSegment());
|
|
||||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
|
||||||
contentValues,
|
|
||||||
COLUMN_NAME + " = ?",
|
|
||||||
new String[]{String.valueOf(id)});
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Parameter `selection` should be empty when updating an ID");
|
|
||||||
}
|
|
||||||
getContext().getContentResolver().notifyChange(uri, null);
|
|
||||||
return rowsUpdated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the insertion of new bookmark locations record to local SQLite Database
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
@Override
|
|
||||||
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
|
||||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
|
||||||
long id = sqlDB.insert(BookmarkLocationsDao.Table.TABLE_NAME, null, contentValues);
|
|
||||||
getContext().getContentResolver().notifyChange(uri, null);
|
|
||||||
return Uri.parse(BASE_URI + "/" + id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
@Override
|
|
||||||
public int delete(@NonNull Uri uri, String s, String[] strings) {
|
|
||||||
int rows;
|
|
||||||
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
|
||||||
Timber.d("Deleting bookmark name %s", uri.getLastPathSegment());
|
|
||||||
rows = db.delete(TABLE_NAME,
|
|
||||||
"location_name = ?",
|
|
||||||
new String[]{uri.getLastPathSegment()}
|
|
||||||
);
|
|
||||||
getContext().getContentResolver().notifyChange(uri, null);
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks.locations;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class BookmarkLocationsController {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
BookmarkLocationsDao bookmarkLocationDao;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public BookmarkLocationsController() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load from DB the bookmarked locations
|
|
||||||
* @return a list of Place objects.
|
|
||||||
*/
|
|
||||||
public List<Place> loadFavoritesLocations() {
|
|
||||||
return bookmarkLocationDao.getAllBookmarksLocations();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.locations
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class BookmarkLocationsController @Inject constructor(
|
||||||
|
private val bookmarkLocationDao: BookmarkLocationsDao
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load bookmarked locations from the database.
|
||||||
|
* @return a list of Place objects.
|
||||||
|
*/
|
||||||
|
suspend fun loadFavoritesLocations(): List<Place> =
|
||||||
|
bookmarkLocationDao.getAllBookmarksLocationsPlace()
|
||||||
|
}
|
||||||
|
|
@ -1,313 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks.locations;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteException;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.nearby.NearbyController;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import javax.inject.Provider;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
|
||||||
import fr.free.nrw.commons.nearby.Label;
|
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
|
||||||
import fr.free.nrw.commons.nearby.Sitelinks;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider.BASE_URI;
|
|
||||||
|
|
||||||
public class BookmarkLocationsDao {
|
|
||||||
|
|
||||||
private final Provider<ContentProviderClient> clientProvider;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public BookmarkLocationsDao(@Named("bookmarksLocation") Provider<ContentProviderClient> clientProvider) {
|
|
||||||
this.clientProvider = clientProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all persisted locations bookmarks on database
|
|
||||||
*
|
|
||||||
* @return list of Place
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public List<Place> getAllBookmarksLocations() {
|
|
||||||
List<Place> items = new ArrayList<>();
|
|
||||||
Cursor cursor = null;
|
|
||||||
ContentProviderClient db = clientProvider.get();
|
|
||||||
try {
|
|
||||||
cursor = db.query(
|
|
||||||
BookmarkLocationsContentProvider.BASE_URI,
|
|
||||||
Table.ALL_FIELDS,
|
|
||||||
null,
|
|
||||||
new String[]{},
|
|
||||||
null);
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
|
||||||
items.add(fromCursor(cursor));
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} finally {
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
db.release();
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Look for a place in bookmarks table in order to insert or delete it
|
|
||||||
*
|
|
||||||
* @param bookmarkLocation : Place object
|
|
||||||
* @return is Place now fav ?
|
|
||||||
*/
|
|
||||||
public boolean updateBookmarkLocation(Place bookmarkLocation) {
|
|
||||||
boolean bookmarkExists = findBookmarkLocation(bookmarkLocation);
|
|
||||||
if (bookmarkExists) {
|
|
||||||
deleteBookmarkLocation(bookmarkLocation);
|
|
||||||
NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, false);
|
|
||||||
} else {
|
|
||||||
addBookmarkLocation(bookmarkLocation);
|
|
||||||
NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, true);
|
|
||||||
}
|
|
||||||
return !bookmarkExists;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a Place to bookmarks table
|
|
||||||
*
|
|
||||||
* @param bookmarkLocation : Place to add
|
|
||||||
*/
|
|
||||||
private void addBookmarkLocation(Place bookmarkLocation) {
|
|
||||||
ContentProviderClient db = clientProvider.get();
|
|
||||||
try {
|
|
||||||
db.insert(BASE_URI, toContentValues(bookmarkLocation));
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} finally {
|
|
||||||
db.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a Place from bookmarks table
|
|
||||||
*
|
|
||||||
* @param bookmarkLocation : Place to delete
|
|
||||||
*/
|
|
||||||
private void deleteBookmarkLocation(Place bookmarkLocation) {
|
|
||||||
ContentProviderClient db = clientProvider.get();
|
|
||||||
try {
|
|
||||||
db.delete(BookmarkLocationsContentProvider.uriForName(bookmarkLocation.name), null, null);
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} finally {
|
|
||||||
db.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a Place from database based on its name
|
|
||||||
*
|
|
||||||
* @param bookmarkLocation : Place to find
|
|
||||||
* @return boolean : is Place in database ?
|
|
||||||
*/
|
|
||||||
public boolean findBookmarkLocation(Place bookmarkLocation) {
|
|
||||||
Cursor cursor = null;
|
|
||||||
ContentProviderClient db = clientProvider.get();
|
|
||||||
try {
|
|
||||||
cursor = db.query(
|
|
||||||
BookmarkLocationsContentProvider.BASE_URI,
|
|
||||||
Table.ALL_FIELDS,
|
|
||||||
Table.COLUMN_NAME + "=?",
|
|
||||||
new String[]{bookmarkLocation.name},
|
|
||||||
null);
|
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
// This feels lazy, but to hell with checked exceptions. :)
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} finally {
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
db.release();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("Range")
|
|
||||||
@NonNull
|
|
||||||
Place fromCursor(final Cursor cursor) {
|
|
||||||
final LatLng location = new LatLng(cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LAT)),
|
|
||||||
cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LONG)), 1F);
|
|
||||||
|
|
||||||
final Sitelinks.Builder builder = new Sitelinks.Builder();
|
|
||||||
builder.setWikipediaLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_WIKIPEDIA_LINK)));
|
|
||||||
builder.setWikidataLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_WIKIDATA_LINK)));
|
|
||||||
builder.setCommonsLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_COMMONS_LINK)));
|
|
||||||
|
|
||||||
return new Place(
|
|
||||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_LANGUAGE)),
|
|
||||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)),
|
|
||||||
Label.fromText((cursor.getString(cursor.getColumnIndex(Table.COLUMN_LABEL_TEXT)))),
|
|
||||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)),
|
|
||||||
location,
|
|
||||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_CATEGORY)),
|
|
||||||
builder.build(),
|
|
||||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_PIC)),
|
|
||||||
Boolean.parseBoolean(cursor.getString(cursor.getColumnIndex(Table.COLUMN_EXISTS)))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ContentValues toContentValues(Place bookmarkLocation) {
|
|
||||||
ContentValues cv = new ContentValues();
|
|
||||||
cv.put(BookmarkLocationsDao.Table.COLUMN_NAME, bookmarkLocation.getName());
|
|
||||||
cv.put(BookmarkLocationsDao.Table.COLUMN_LANGUAGE, bookmarkLocation.getLanguage());
|
|
||||||
cv.put(BookmarkLocationsDao.Table.COLUMN_DESCRIPTION, bookmarkLocation.getLongDescription());
|
|
||||||
cv.put(BookmarkLocationsDao.Table.COLUMN_CATEGORY, bookmarkLocation.getCategory());
|
|
||||||
cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_TEXT, bookmarkLocation.getLabel()!=null ? bookmarkLocation.getLabel().getText() : "");
|
|
||||||
cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_ICON, bookmarkLocation.getLabel()!=null ? bookmarkLocation.getLabel().getIcon() : null);
|
|
||||||
cv.put(BookmarkLocationsDao.Table.COLUMN_WIKIPEDIA_LINK, bookmarkLocation.siteLinks.getWikipediaLink().toString());
|
|
||||||
cv.put(BookmarkLocationsDao.Table.COLUMN_WIKIDATA_LINK, bookmarkLocation.siteLinks.getWikidataLink().toString());
|
|
||||||
cv.put(BookmarkLocationsDao.Table.COLUMN_COMMONS_LINK, bookmarkLocation.siteLinks.getCommonsLink().toString());
|
|
||||||
cv.put(BookmarkLocationsDao.Table.COLUMN_LAT, bookmarkLocation.location.getLatitude());
|
|
||||||
cv.put(BookmarkLocationsDao.Table.COLUMN_LONG, bookmarkLocation.location.getLongitude());
|
|
||||||
cv.put(BookmarkLocationsDao.Table.COLUMN_PIC, bookmarkLocation.pic);
|
|
||||||
cv.put(BookmarkLocationsDao.Table.COLUMN_EXISTS, bookmarkLocation.exists.toString());
|
|
||||||
return cv;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Table {
|
|
||||||
public static final String TABLE_NAME = "bookmarksLocations";
|
|
||||||
|
|
||||||
static final String COLUMN_NAME = "location_name";
|
|
||||||
static final String COLUMN_LANGUAGE = "location_language";
|
|
||||||
static final String COLUMN_DESCRIPTION = "location_description";
|
|
||||||
static final String COLUMN_LAT = "location_lat";
|
|
||||||
static final String COLUMN_LONG = "location_long";
|
|
||||||
static final String COLUMN_CATEGORY = "location_category";
|
|
||||||
static final String COLUMN_LABEL_TEXT = "location_label_text";
|
|
||||||
static final String COLUMN_LABEL_ICON = "location_label_icon";
|
|
||||||
static final String COLUMN_IMAGE_URL = "location_image_url";
|
|
||||||
static final String COLUMN_WIKIPEDIA_LINK = "location_wikipedia_link";
|
|
||||||
static final String COLUMN_WIKIDATA_LINK = "location_wikidata_link";
|
|
||||||
static final String COLUMN_COMMONS_LINK = "location_commons_link";
|
|
||||||
static final String COLUMN_PIC = "location_pic";
|
|
||||||
static final String COLUMN_EXISTS = "location_exists";
|
|
||||||
|
|
||||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
|
||||||
public static final String[] ALL_FIELDS = {
|
|
||||||
COLUMN_NAME,
|
|
||||||
COLUMN_LANGUAGE,
|
|
||||||
COLUMN_DESCRIPTION,
|
|
||||||
COLUMN_CATEGORY,
|
|
||||||
COLUMN_LABEL_TEXT,
|
|
||||||
COLUMN_LABEL_ICON,
|
|
||||||
COLUMN_LAT,
|
|
||||||
COLUMN_LONG,
|
|
||||||
COLUMN_IMAGE_URL,
|
|
||||||
COLUMN_WIKIPEDIA_LINK,
|
|
||||||
COLUMN_WIKIDATA_LINK,
|
|
||||||
COLUMN_COMMONS_LINK,
|
|
||||||
COLUMN_PIC,
|
|
||||||
COLUMN_EXISTS,
|
|
||||||
};
|
|
||||||
|
|
||||||
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
|
||||||
|
|
||||||
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
|
||||||
+ COLUMN_NAME + " STRING PRIMARY KEY,"
|
|
||||||
+ COLUMN_LANGUAGE + " STRING,"
|
|
||||||
+ COLUMN_DESCRIPTION + " STRING,"
|
|
||||||
+ COLUMN_CATEGORY + " STRING,"
|
|
||||||
+ COLUMN_LABEL_TEXT + " STRING,"
|
|
||||||
+ COLUMN_LABEL_ICON + " INTEGER,"
|
|
||||||
+ COLUMN_LAT + " DOUBLE,"
|
|
||||||
+ COLUMN_LONG + " DOUBLE,"
|
|
||||||
+ COLUMN_IMAGE_URL + " STRING,"
|
|
||||||
+ COLUMN_WIKIPEDIA_LINK + " STRING,"
|
|
||||||
+ COLUMN_WIKIDATA_LINK + " STRING,"
|
|
||||||
+ COLUMN_COMMONS_LINK + " STRING,"
|
|
||||||
+ COLUMN_PIC + " STRING,"
|
|
||||||
+ COLUMN_EXISTS + " STRING"
|
|
||||||
+ ");";
|
|
||||||
|
|
||||||
public static void onCreate(SQLiteDatabase db) {
|
|
||||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void onDelete(SQLiteDatabase db) {
|
|
||||||
db.execSQL(DROP_TABLE_STATEMENT);
|
|
||||||
onCreate(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void onUpdate(final SQLiteDatabase db, int from, final int to) {
|
|
||||||
Timber.d("bookmarksLocations db is updated from:"+from+", to:"+to);
|
|
||||||
if (from == to) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (from < 7) {
|
|
||||||
// doesn't exist yet
|
|
||||||
from++;
|
|
||||||
onUpdate(db, from, to);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (from == 7) {
|
|
||||||
// table added in version 8
|
|
||||||
onCreate(db);
|
|
||||||
from++;
|
|
||||||
onUpdate(db, from, to);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (from < 10) {
|
|
||||||
from++;
|
|
||||||
onUpdate(db, from, to);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (from == 10) {
|
|
||||||
//This is safe, and can be called clean, as we/I do not remember the appropriate version for this
|
|
||||||
//We are anyways switching to room, these things won't be necessary then
|
|
||||||
try {
|
|
||||||
db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_pic STRING;");
|
|
||||||
}catch (SQLiteException exception){
|
|
||||||
Timber.e(exception);//
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (from >= 12) {
|
|
||||||
try {
|
|
||||||
db.execSQL(
|
|
||||||
"ALTER TABLE bookmarksLocations ADD COLUMN location_destroyed STRING;");
|
|
||||||
} catch (SQLiteException exception) {
|
|
||||||
Timber.e(exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (from >= 13){
|
|
||||||
try {
|
|
||||||
db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_language STRING;");
|
|
||||||
} catch (SQLiteException exception){
|
|
||||||
Timber.e(exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (from >= 14){
|
|
||||||
try {
|
|
||||||
db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_exists STRING;");
|
|
||||||
} catch (SQLiteException exception){
|
|
||||||
Timber.e(exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.locations
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import fr.free.nrw.commons.nearby.NearbyController
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DAO for managing bookmark locations in the database.
|
||||||
|
*/
|
||||||
|
@Dao
|
||||||
|
abstract class BookmarkLocationsDao {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds or updates a bookmark location in the database.
|
||||||
|
*/
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
abstract suspend fun addBookmarkLocation(bookmarkLocation: BookmarksLocations)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all bookmark locations from the database.
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM bookmarks_locations")
|
||||||
|
abstract suspend fun getAllBookmarksLocations(): List<BookmarksLocations>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a bookmark location exists by name.
|
||||||
|
*/
|
||||||
|
@Query("SELECT EXISTS (SELECT 1 FROM bookmarks_locations WHERE location_name = :name)")
|
||||||
|
abstract suspend fun findBookmarkLocation(name: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a bookmark location from the database.
|
||||||
|
*/
|
||||||
|
@Delete
|
||||||
|
abstract suspend fun deleteBookmarkLocation(bookmarkLocation: BookmarksLocations)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds or removes a bookmark location and updates markers.
|
||||||
|
* @return `true` if added, `false` if removed.
|
||||||
|
*/
|
||||||
|
suspend fun updateBookmarkLocation(bookmarkLocation: Place): Boolean {
|
||||||
|
val exists = findBookmarkLocation(bookmarkLocation.name)
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
deleteBookmarkLocation(bookmarkLocation.toBookmarksLocations())
|
||||||
|
NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, false)
|
||||||
|
} else {
|
||||||
|
addBookmarkLocation(bookmarkLocation.toBookmarksLocations())
|
||||||
|
NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return !exists
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all bookmark locations as `Place` objects.
|
||||||
|
*/
|
||||||
|
suspend fun getAllBookmarksLocationsPlace(): List<Place> {
|
||||||
|
return getAllBookmarksLocations().map { it.toPlace() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks.locations;
|
|
||||||
|
|
||||||
import android.Manifest.permission;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import androidx.activity.result.ActivityResultCallback;
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import dagger.android.support.DaggerFragment;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionController;
|
|
||||||
import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding;
|
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
|
||||||
import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions;
|
|
||||||
import fr.free.nrw.commons.nearby.fragments.PlaceAdapter;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import kotlin.Unit;
|
|
||||||
|
|
||||||
public class BookmarkLocationsFragment extends DaggerFragment {
|
|
||||||
|
|
||||||
public FragmentBookmarksLocationsBinding binding;
|
|
||||||
|
|
||||||
@Inject BookmarkLocationsController controller;
|
|
||||||
@Inject ContributionController contributionController;
|
|
||||||
@Inject BookmarkLocationsDao bookmarkLocationDao;
|
|
||||||
@Inject CommonPlaceClickActions commonPlaceClickActions;
|
|
||||||
private PlaceAdapter adapter;
|
|
||||||
|
|
||||||
private final ActivityResultLauncher<Intent> cameraPickLauncherForResult =
|
|
||||||
registerForActivityResult(new StartActivityForResult(),
|
|
||||||
result -> {
|
|
||||||
contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> {
|
|
||||||
contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
private final ActivityResultLauncher<Intent> galleryPickLauncherForResult =
|
|
||||||
registerForActivityResult(new StartActivityForResult(),
|
|
||||||
result -> {
|
|
||||||
contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> {
|
|
||||||
contributionController.onPictureReturnedFromGallery(result, requireActivity(), callbacks);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() {
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(Map<String, Boolean> result) {
|
|
||||||
boolean areAllGranted = true;
|
|
||||||
for(final boolean b : result.values()) {
|
|
||||||
areAllGranted = areAllGranted && b;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (areAllGranted) {
|
|
||||||
contributionController.locationPermissionCallback.onLocationPermissionGranted();
|
|
||||||
} else {
|
|
||||||
if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
|
|
||||||
contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult);
|
|
||||||
} else {
|
|
||||||
contributionController.locationPermissionCallback.onLocationPermissionDenied(getActivity().getString(R.string.in_app_camera_location_permission_denied));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an instance of the fragment with the right bundle parameters
|
|
||||||
* @return an instance of the fragment
|
|
||||||
*/
|
|
||||||
public static BookmarkLocationsFragment newInstance() {
|
|
||||||
return new BookmarkLocationsFragment();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(
|
|
||||||
@NonNull LayoutInflater inflater,
|
|
||||||
ViewGroup container,
|
|
||||||
Bundle savedInstanceState
|
|
||||||
) {
|
|
||||||
binding = FragmentBookmarksLocationsBinding.inflate(inflater, container, false);
|
|
||||||
return binding.getRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
binding.loadingImagesProgressBar.setVisibility(View.VISIBLE);
|
|
||||||
binding.listView.setLayoutManager(new LinearLayoutManager(getContext()));
|
|
||||||
adapter = new PlaceAdapter(bookmarkLocationDao,
|
|
||||||
place -> Unit.INSTANCE,
|
|
||||||
(place, isBookmarked) -> {
|
|
||||||
adapter.remove(place);
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
},
|
|
||||||
commonPlaceClickActions,
|
|
||||||
inAppCameraLocationPermissionLauncher,
|
|
||||||
galleryPickLauncherForResult,
|
|
||||||
cameraPickLauncherForResult
|
|
||||||
);
|
|
||||||
binding.listView.setAdapter(adapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
initList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the recycler view with bookmarked locations
|
|
||||||
*/
|
|
||||||
private void initList() {
|
|
||||||
List<Place> places = controller.loadFavoritesLocations();
|
|
||||||
adapter.setItems(places);
|
|
||||||
binding.loadingImagesProgressBar.setVisibility(View.GONE);
|
|
||||||
if (places.size() <= 0) {
|
|
||||||
binding.statusMessage.setText(R.string.bookmark_empty);
|
|
||||||
binding.statusMessage.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
binding.statusMessage.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
binding = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.locations
|
||||||
|
|
||||||
|
import android.Manifest.permission
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import dagger.android.support.DaggerFragment
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionController
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding
|
||||||
|
import fr.free.nrw.commons.filepicker.FilePicker
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions
|
||||||
|
import fr.free.nrw.commons.nearby.fragments.PlaceAdapter
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
class BookmarkLocationsFragment : DaggerFragment() {
|
||||||
|
|
||||||
|
private var binding: FragmentBookmarksLocationsBinding? = null
|
||||||
|
|
||||||
|
@Inject lateinit var controller: BookmarkLocationsController
|
||||||
|
@Inject lateinit var contributionController: ContributionController
|
||||||
|
@Inject lateinit var bookmarkLocationDao: BookmarkLocationsDao
|
||||||
|
@Inject lateinit var commonPlaceClickActions: CommonPlaceClickActions
|
||||||
|
|
||||||
|
private lateinit var inAppCameraLocationPermissionLauncher:
|
||||||
|
ActivityResultLauncher<Array<String>>
|
||||||
|
private lateinit var adapter: PlaceAdapter
|
||||||
|
|
||||||
|
private val cameraPickLauncherForResult =
|
||||||
|
registerForActivityResult(StartActivityForResult()) { result ->
|
||||||
|
contributionController.handleActivityResultWithCallback(
|
||||||
|
requireActivity(),
|
||||||
|
object: FilePicker.HandleActivityResult {
|
||||||
|
override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) {
|
||||||
|
contributionController.onPictureReturnedFromCamera(
|
||||||
|
result,
|
||||||
|
requireActivity(),
|
||||||
|
callbacks
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val galleryPickLauncherForResult =
|
||||||
|
registerForActivityResult(StartActivityForResult()) { result ->
|
||||||
|
contributionController.handleActivityResultWithCallback(
|
||||||
|
requireActivity(),
|
||||||
|
object: FilePicker.HandleActivityResult {
|
||||||
|
override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) {
|
||||||
|
contributionController.onPictureReturnedFromGallery(
|
||||||
|
result,
|
||||||
|
requireActivity(),
|
||||||
|
callbacks
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance(): BookmarkLocationsFragment {
|
||||||
|
return BookmarkLocationsFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
binding = FragmentBookmarksLocationsBinding.inflate(inflater, container, false)
|
||||||
|
return binding?.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
binding?.loadingImagesProgressBar?.visibility = View.VISIBLE
|
||||||
|
binding?.listView?.layoutManager = LinearLayoutManager(context)
|
||||||
|
|
||||||
|
inAppCameraLocationPermissionLauncher =
|
||||||
|
registerForActivityResult(RequestMultiplePermissions()) { result ->
|
||||||
|
val areAllGranted = result.values.all { it }
|
||||||
|
|
||||||
|
if (areAllGranted) {
|
||||||
|
contributionController.locationPermissionCallback?.onLocationPermissionGranted()
|
||||||
|
} else {
|
||||||
|
if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
|
||||||
|
contributionController.handleShowRationaleFlowCameraLocation(
|
||||||
|
requireActivity(),
|
||||||
|
inAppCameraLocationPermissionLauncher,
|
||||||
|
cameraPickLauncherForResult
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
contributionController.locationPermissionCallback
|
||||||
|
?.onLocationPermissionDenied(
|
||||||
|
getString(R.string.in_app_camera_location_permission_denied)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter = PlaceAdapter(
|
||||||
|
bookmarkLocationDao,
|
||||||
|
lifecycleScope,
|
||||||
|
{ },
|
||||||
|
{ place, _ ->
|
||||||
|
adapter.remove(place)
|
||||||
|
},
|
||||||
|
commonPlaceClickActions,
|
||||||
|
inAppCameraLocationPermissionLauncher,
|
||||||
|
galleryPickLauncherForResult,
|
||||||
|
cameraPickLauncherForResult
|
||||||
|
)
|
||||||
|
binding?.listView?.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
initList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initList() {
|
||||||
|
var places: List<Place>
|
||||||
|
if(view != null) {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
places = controller.loadFavoritesLocations()
|
||||||
|
updateUIList(places)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateUIList(places: List<Place>) {
|
||||||
|
adapter.items = places
|
||||||
|
binding?.loadingImagesProgressBar?.visibility = View.GONE
|
||||||
|
if (places.isEmpty()) {
|
||||||
|
binding?.statusMessage?.text = getString(R.string.bookmark_empty)
|
||||||
|
binding?.statusMessage?.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding?.statusMessage?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
// Make sure to null out the binding to avoid memory leaks
|
||||||
|
binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.locations
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class BookmarkLocationsViewModel(
|
||||||
|
private val bookmarkLocationsDao: BookmarkLocationsDao
|
||||||
|
): ViewModel() {
|
||||||
|
|
||||||
|
// fun getAllBookmarkLocations(): List<Place> {
|
||||||
|
// return bookmarkLocationsDao.getAllBookmarksLocationsPlace()
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.locations
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import fr.free.nrw.commons.location.LatLng
|
||||||
|
import fr.free.nrw.commons.nearby.Label
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
import fr.free.nrw.commons.nearby.Sitelinks
|
||||||
|
|
||||||
|
@Entity(tableName = "bookmarks_locations")
|
||||||
|
data class BookmarksLocations(
|
||||||
|
@PrimaryKey @ColumnInfo(name = "location_name") val locationName: String,
|
||||||
|
@ColumnInfo(name = "location_language") val locationLanguage: String,
|
||||||
|
@ColumnInfo(name = "location_description") val locationDescription: String,
|
||||||
|
@ColumnInfo(name = "location_lat") val locationLat: Double,
|
||||||
|
@ColumnInfo(name = "location_long") val locationLong: Double,
|
||||||
|
@ColumnInfo(name = "location_category") val locationCategory: String,
|
||||||
|
@ColumnInfo(name = "location_label_text") val locationLabelText: String,
|
||||||
|
@ColumnInfo(name = "location_label_icon") val locationLabelIcon: Int?,
|
||||||
|
@ColumnInfo(name = "location_image_url") val locationImageUrl: String,
|
||||||
|
@ColumnInfo(name = "location_wikipedia_link") val locationWikipediaLink: String,
|
||||||
|
@ColumnInfo(name = "location_wikidata_link") val locationWikidataLink: String,
|
||||||
|
@ColumnInfo(name = "location_commons_link") val locationCommonsLink: String,
|
||||||
|
@ColumnInfo(name = "location_pic") val locationPic: String,
|
||||||
|
@ColumnInfo(name = "location_exists") val locationExists: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
fun BookmarksLocations.toPlace(): Place {
|
||||||
|
val location = LatLng(
|
||||||
|
locationLat,
|
||||||
|
locationLong,
|
||||||
|
1F
|
||||||
|
)
|
||||||
|
|
||||||
|
val builder = Sitelinks.Builder().apply {
|
||||||
|
setWikipediaLink(locationWikipediaLink)
|
||||||
|
setWikidataLink(locationWikidataLink)
|
||||||
|
setCommonsLink(locationCommonsLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Place(
|
||||||
|
locationLanguage,
|
||||||
|
locationName,
|
||||||
|
Label.fromText(locationLabelText),
|
||||||
|
locationDescription,
|
||||||
|
location,
|
||||||
|
locationCategory,
|
||||||
|
builder.build(),
|
||||||
|
locationPic,
|
||||||
|
locationExists
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Place.toBookmarksLocations(): BookmarksLocations {
|
||||||
|
return BookmarksLocations(
|
||||||
|
locationName = name,
|
||||||
|
locationLanguage = language,
|
||||||
|
locationDescription = longDescription,
|
||||||
|
locationCategory = category,
|
||||||
|
locationLat = location.latitude,
|
||||||
|
locationLong = location.longitude,
|
||||||
|
locationLabelText = label?.text ?: "",
|
||||||
|
locationLabelIcon = label?.icon,
|
||||||
|
locationImageUrl = pic,
|
||||||
|
locationWikipediaLink = siteLinks.wikipediaLink.toString(),
|
||||||
|
locationWikidataLink = siteLinks.wikidataLink.toString(),
|
||||||
|
locationCommonsLink = siteLinks.commonsLink.toString(),
|
||||||
|
locationPic = pic,
|
||||||
|
locationExists = exists
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -36,37 +36,35 @@ class CategoriesModel
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
fun isSpammyCategory(item: String): Boolean {
|
fun isSpammyCategory(item: String): Boolean {
|
||||||
// Check for current and previous year to exclude these categories from removal
|
|
||||||
val now = Calendar.getInstance()
|
|
||||||
val curYear = now[Calendar.YEAR]
|
|
||||||
val curYearInString = curYear.toString()
|
|
||||||
val prevYear = curYear - 1
|
|
||||||
val prevYearInString = prevYear.toString()
|
|
||||||
Timber.d("Previous year: %s", prevYearInString)
|
|
||||||
|
|
||||||
val mentionsDecade = item.matches(".*0s.*".toRegex())
|
|
||||||
val recentDecade = item.matches(".*20[0-2]0s.*".toRegex())
|
|
||||||
val spammyCategory =
|
|
||||||
item.matches("(.*)needing(.*)".toRegex()) ||
|
|
||||||
item.matches("(.*)taken on(.*)".toRegex())
|
|
||||||
|
|
||||||
// always skip irrelevant categories such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
// always skip irrelevant categories such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
||||||
|
val spammyCategory = item.matches("(.*)needing(.*)".toRegex())
|
||||||
|
|| item.matches("(.*)taken on(.*)".toRegex())
|
||||||
|
|
||||||
|
// checks for
|
||||||
|
// dd/mm/yyyy or yy
|
||||||
|
// yyyy or yy/mm/dd
|
||||||
|
// yyyy or yy/mm
|
||||||
|
// mm/yyyy or yy
|
||||||
|
// for `yy` it is assumed that 20XX is implicit.
|
||||||
|
// with separators [., /, -]
|
||||||
|
val isIrrelevantCategory =
|
||||||
|
item.contains("""\d{1,2}[-/.]\d{1,2}[-/.]\d{2,4}|\d{2,4}[-/.]\d{1,2}[-/.]\d{1,2}|\d{2,4}[-/.]\d{1,2}|\d{1,2}[-/.]\d{2,4}""".toRegex())
|
||||||
|
|
||||||
|
|
||||||
if (spammyCategory) {
|
if (spammyCategory) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mentionsDecade) {
|
if(isIrrelevantCategory){
|
||||||
// Check if the year in the form of XX(X)0s is recent/relevant, i.e. in the 2000s or 2010s/2020s as stated in Issue #1029
|
return true
|
||||||
// Example: "2020s" is OK, but "1920s" is not (and should be skipped)
|
|
||||||
return !recentDecade
|
|
||||||
} else {
|
|
||||||
// If it is not an year in decade form (e.g. 19xxs/20xxs), then check if item contains a 4-digit year
|
|
||||||
// anywhere within the string (.* is wildcard) (Issue #47)
|
|
||||||
// And that item does not equal the current year or previous year
|
|
||||||
return item.matches(".*(19|20)\\d{2}.*".toRegex()) &&
|
|
||||||
!item.contains(curYearInString) &&
|
|
||||||
!item.contains(prevYearInString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hasYear = item.matches("(.*\\d{4}.*)".toRegex())
|
||||||
|
val validYearsRange = item.matches(".*(20[0-9]{2}).*".toRegex())
|
||||||
|
|
||||||
|
// finally if there's 4 digits year exists in XXXX it should only be in 20XX range.
|
||||||
|
return hasYear && !validYearsRange
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,405 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
|
|
||||||
|
|
||||||
import android.Manifest.permission;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.activity.result.ActivityResult;
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.paging.DataSource.Factory;
|
|
||||||
import androidx.paging.LivePagedListBuilder;
|
|
||||||
import androidx.paging.PagedList;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.filepicker.DefaultCallback;
|
|
||||||
import fr.free.nrw.commons.filepicker.FilePicker;
|
|
||||||
import fr.free.nrw.commons.filepicker.FilePicker.ImageSource;
|
|
||||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
|
||||||
import fr.free.nrw.commons.location.LocationPermissionsHelper;
|
|
||||||
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback;
|
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
|
||||||
import fr.free.nrw.commons.upload.UploadActivity;
|
|
||||||
import fr.free.nrw.commons.utils.DialogUtil;
|
|
||||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class ContributionController {
|
|
||||||
|
|
||||||
public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads";
|
|
||||||
private final JsonKvStore defaultKvStore;
|
|
||||||
private LatLng locationBeforeImageCapture;
|
|
||||||
private boolean isInAppCameraUpload;
|
|
||||||
public LocationPermissionCallback locationPermissionCallback;
|
|
||||||
private LocationPermissionsHelper locationPermissionsHelper;
|
|
||||||
// Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
|
||||||
// LiveData<PagedList<Contribution>> failedAndPendingContributionList;
|
|
||||||
LiveData<PagedList<Contribution>> pendingContributionList;
|
|
||||||
LiveData<PagedList<Contribution>> failedContributionList;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
LocationServiceManager locationManager;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ContributionsRepository repository;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) {
|
|
||||||
this.defaultKvStore = defaultKvStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check for permissions and initiate camera click
|
|
||||||
*/
|
|
||||||
public void initiateCameraPick(Activity activity,
|
|
||||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher,
|
|
||||||
ActivityResultLauncher<Intent> resultLauncher) {
|
|
||||||
boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true);
|
|
||||||
if (!useExtStorage) {
|
|
||||||
initiateCameraUpload(activity, resultLauncher);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PermissionUtils.checkPermissionsAndPerformAction(activity,
|
|
||||||
() -> {
|
|
||||||
if (defaultKvStore.getBoolean("inAppCameraFirstRun")) {
|
|
||||||
defaultKvStore.putBoolean("inAppCameraFirstRun", false);
|
|
||||||
askUserToAllowLocationAccess(activity, inAppCameraLocationPermissionLauncher, resultLauncher);
|
|
||||||
} else if (defaultKvStore.getBoolean("inAppCameraLocationPref")) {
|
|
||||||
createDialogsAndHandleLocationPermissions(activity,
|
|
||||||
inAppCameraLocationPermissionLauncher, resultLauncher);
|
|
||||||
} else {
|
|
||||||
initiateCameraUpload(activity, resultLauncher);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
R.string.storage_permission_title,
|
|
||||||
R.string.write_storage_permission_rationale,
|
|
||||||
PermissionUtils.getPERMISSIONS_STORAGE());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asks users to provide location access
|
|
||||||
*
|
|
||||||
* @param activity
|
|
||||||
*/
|
|
||||||
private void createDialogsAndHandleLocationPermissions(Activity activity,
|
|
||||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher,
|
|
||||||
ActivityResultLauncher<Intent> resultLauncher) {
|
|
||||||
locationPermissionCallback = new LocationPermissionCallback() {
|
|
||||||
@Override
|
|
||||||
public void onLocationPermissionDenied(String toastMessage) {
|
|
||||||
Toast.makeText(
|
|
||||||
activity,
|
|
||||||
toastMessage,
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show();
|
|
||||||
initiateCameraUpload(activity, resultLauncher);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLocationPermissionGranted() {
|
|
||||||
if (!locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) {
|
|
||||||
showLocationOffDialog(activity, R.string.in_app_camera_needs_location,
|
|
||||||
R.string.in_app_camera_location_unavailable, resultLauncher);
|
|
||||||
} else {
|
|
||||||
initiateCameraUpload(activity, resultLauncher);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
locationPermissionsHelper = new LocationPermissionsHelper(
|
|
||||||
activity, locationManager, locationPermissionCallback);
|
|
||||||
if (inAppCameraLocationPermissionLauncher != null) {
|
|
||||||
inAppCameraLocationPermissionLauncher.launch(
|
|
||||||
new String[]{permission.ACCESS_FINE_LOCATION});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a dialog alerting the user about location services being off and asking them to turn it
|
|
||||||
* on
|
|
||||||
* TODO: Add a seperate callback in LocationPermissionsHelper for this.
|
|
||||||
* Ref: https://github.com/commons-app/apps-android-commons/pull/5494/files#r1510553114
|
|
||||||
*
|
|
||||||
* @param activity Activity reference
|
|
||||||
* @param dialogTextResource Resource id of text to be shown in dialog
|
|
||||||
* @param toastTextResource Resource id of text to be shown in toast
|
|
||||||
* @param resultLauncher
|
|
||||||
*/
|
|
||||||
private void showLocationOffDialog(Activity activity, int dialogTextResource,
|
|
||||||
int toastTextResource, ActivityResultLauncher<Intent> resultLauncher) {
|
|
||||||
DialogUtil
|
|
||||||
.showAlertDialog(activity,
|
|
||||||
activity.getString(R.string.ask_to_turn_location_on),
|
|
||||||
activity.getString(dialogTextResource),
|
|
||||||
activity.getString(R.string.title_app_shortcut_setting),
|
|
||||||
activity.getString(R.string.cancel),
|
|
||||||
() -> locationPermissionsHelper.openLocationSettings(activity),
|
|
||||||
() -> {
|
|
||||||
Toast.makeText(activity, activity.getString(toastTextResource),
|
|
||||||
Toast.LENGTH_LONG).show();
|
|
||||||
initiateCameraUpload(activity, resultLauncher);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleShowRationaleFlowCameraLocation(Activity activity,
|
|
||||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher,
|
|
||||||
ActivityResultLauncher<Intent> resultLauncher) {
|
|
||||||
DialogUtil.showAlertDialog(activity, activity.getString(R.string.location_permission_title),
|
|
||||||
activity.getString(R.string.in_app_camera_location_permission_rationale),
|
|
||||||
activity.getString(android.R.string.ok),
|
|
||||||
activity.getString(android.R.string.cancel),
|
|
||||||
() -> {
|
|
||||||
createDialogsAndHandleLocationPermissions(activity,
|
|
||||||
inAppCameraLocationPermissionLauncher, resultLauncher);
|
|
||||||
},
|
|
||||||
() -> locationPermissionCallback.onLocationPermissionDenied(
|
|
||||||
activity.getString(R.string.in_app_camera_location_permission_denied)),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Suggest user to attach location information with pictures. If the user selects "Yes", then:
|
|
||||||
* <p>
|
|
||||||
* Location is taken from the EXIF if the default camera application does not redact location
|
|
||||||
* tags.
|
|
||||||
* <p>
|
|
||||||
* Otherwise, if the EXIF metadata does not have location information, then location captured by
|
|
||||||
* the app is used
|
|
||||||
*
|
|
||||||
* @param activity
|
|
||||||
*/
|
|
||||||
private void askUserToAllowLocationAccess(Activity activity,
|
|
||||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher,
|
|
||||||
ActivityResultLauncher<Intent> resultLauncher) {
|
|
||||||
DialogUtil.showAlertDialog(activity,
|
|
||||||
activity.getString(R.string.in_app_camera_location_permission_title),
|
|
||||||
activity.getString(R.string.in_app_camera_location_access_explanation),
|
|
||||||
activity.getString(R.string.option_allow),
|
|
||||||
activity.getString(R.string.option_dismiss),
|
|
||||||
() -> {
|
|
||||||
defaultKvStore.putBoolean("inAppCameraLocationPref", true);
|
|
||||||
createDialogsAndHandleLocationPermissions(activity,
|
|
||||||
inAppCameraLocationPermissionLauncher, resultLauncher);
|
|
||||||
},
|
|
||||||
() -> {
|
|
||||||
ViewUtil.showLongToast(activity, R.string.in_app_camera_location_permission_denied);
|
|
||||||
defaultKvStore.putBoolean("inAppCameraLocationPref", false);
|
|
||||||
initiateCameraUpload(activity, resultLauncher);
|
|
||||||
},
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiate gallery picker
|
|
||||||
*/
|
|
||||||
public void initiateGalleryPick(final Activity activity, ActivityResultLauncher<Intent> resultLauncher, final boolean allowMultipleUploads) {
|
|
||||||
initiateGalleryUpload(activity, resultLauncher, allowMultipleUploads);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiate gallery picker with permission
|
|
||||||
*/
|
|
||||||
public void initiateCustomGalleryPickWithPermission(final Activity activity, ActivityResultLauncher<Intent> resultLauncher) {
|
|
||||||
setPickerConfiguration(activity, true);
|
|
||||||
|
|
||||||
PermissionUtils.checkPermissionsAndPerformAction(activity,
|
|
||||||
() -> FilePicker.openCustomSelector(activity, resultLauncher, 0),
|
|
||||||
R.string.storage_permission_title,
|
|
||||||
R.string.write_storage_permission_rationale,
|
|
||||||
PermissionUtils.getPERMISSIONS_STORAGE());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open chooser for gallery uploads
|
|
||||||
*/
|
|
||||||
private void initiateGalleryUpload(final Activity activity, ActivityResultLauncher<Intent> resultLauncher,
|
|
||||||
final boolean allowMultipleUploads) {
|
|
||||||
setPickerConfiguration(activity, allowMultipleUploads);
|
|
||||||
FilePicker.openGallery(activity, resultLauncher, 0, isDocumentPhotoPickerPreferred());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets configuration for file picker
|
|
||||||
*/
|
|
||||||
private void setPickerConfiguration(Activity activity,
|
|
||||||
boolean allowMultipleUploads) {
|
|
||||||
boolean copyToExternalStorage = defaultKvStore.getBoolean("useExternalStorage", true);
|
|
||||||
FilePicker.configuration(activity)
|
|
||||||
.setCopyTakenPhotosToPublicGalleryAppFolder(copyToExternalStorage)
|
|
||||||
.setAllowMultiplePickInGallery(allowMultipleUploads);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiate camera upload by opening camera
|
|
||||||
*/
|
|
||||||
private void initiateCameraUpload(Activity activity, ActivityResultLauncher<Intent> resultLauncher) {
|
|
||||||
setPickerConfiguration(activity, false);
|
|
||||||
if (defaultKvStore.getBoolean("inAppCameraLocationPref", false)) {
|
|
||||||
locationBeforeImageCapture = locationManager.getLastLocation();
|
|
||||||
}
|
|
||||||
isInAppCameraUpload = true;
|
|
||||||
FilePicker.openCameraForImage(activity, resultLauncher, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDocumentPhotoPickerPreferred(){
|
|
||||||
return defaultKvStore.getBoolean(
|
|
||||||
"openDocumentPhotoPickerPref", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onPictureReturnedFromGallery(ActivityResult result, Activity activity, FilePicker.Callbacks callbacks){
|
|
||||||
|
|
||||||
if(isDocumentPhotoPickerPreferred()){
|
|
||||||
FilePicker.onPictureReturnedFromDocuments(result, activity, callbacks);
|
|
||||||
} else {
|
|
||||||
FilePicker.onPictureReturnedFromGallery(result, activity, callbacks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onPictureReturnedFromCustomSelector(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) {
|
|
||||||
FilePicker.onPictureReturnedFromCustomSelector(result, activity, callbacks);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onPictureReturnedFromCamera(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) {
|
|
||||||
FilePicker.onPictureReturnedFromCamera(result, activity, callbacks);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attaches callback for file picker.
|
|
||||||
*/
|
|
||||||
public void handleActivityResultWithCallback(Activity activity, FilePicker.HandleActivityResult handleActivityResult) {
|
|
||||||
|
|
||||||
handleActivityResult.onHandleActivityResult(new DefaultCallback() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCanceled(final ImageSource source, final int type) {
|
|
||||||
super.onCanceled(source, type);
|
|
||||||
defaultKvStore.remove(PLACE_OBJECT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onImagePickerError(Exception e, FilePicker.ImageSource source,
|
|
||||||
int type) {
|
|
||||||
ViewUtil.showShortToast(activity, R.string.error_occurred_in_picking_images);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onImagesPicked(@NonNull List<UploadableFile> imagesFiles,
|
|
||||||
FilePicker.ImageSource source, int type) {
|
|
||||||
Intent intent = handleImagesPicked(activity, imagesFiles);
|
|
||||||
activity.startActivity(intent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<UploadableFile> handleExternalImagesPicked(Activity activity,
|
|
||||||
Intent data) {
|
|
||||||
return FilePicker.handleExternalImagesPicked(data, activity);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns intent to be passed to upload activity Attaches place object for nearby uploads and
|
|
||||||
* location before image capture if in-app camera is used
|
|
||||||
*/
|
|
||||||
private Intent handleImagesPicked(Context context,
|
|
||||||
List<UploadableFile> imagesFiles) {
|
|
||||||
Intent shareIntent = new Intent(context, UploadActivity.class);
|
|
||||||
shareIntent.setAction(ACTION_INTERNAL_UPLOADS);
|
|
||||||
shareIntent
|
|
||||||
.putParcelableArrayListExtra(UploadActivity.EXTRA_FILES, new ArrayList<>(imagesFiles));
|
|
||||||
Place place = defaultKvStore.getJson(PLACE_OBJECT, Place.class);
|
|
||||||
|
|
||||||
if (place != null) {
|
|
||||||
shareIntent.putExtra(PLACE_OBJECT, place);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (locationBeforeImageCapture != null) {
|
|
||||||
shareIntent.putExtra(
|
|
||||||
UploadActivity.LOCATION_BEFORE_IMAGE_CAPTURE,
|
|
||||||
locationBeforeImageCapture);
|
|
||||||
}
|
|
||||||
|
|
||||||
shareIntent.putExtra(
|
|
||||||
UploadActivity.IN_APP_CAMERA_UPLOAD,
|
|
||||||
isInAppCameraUpload
|
|
||||||
);
|
|
||||||
isInAppCameraUpload = false; // reset the flag for next use
|
|
||||||
return shareIntent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the contributions with the state "IN_PROGRESS", "QUEUED" and "PAUSED" and then it
|
|
||||||
* populates the `pendingContributionList`.
|
|
||||||
**/
|
|
||||||
void getPendingContributions() {
|
|
||||||
final PagedList.Config pagedListConfig =
|
|
||||||
(new PagedList.Config.Builder())
|
|
||||||
.setPrefetchDistance(50)
|
|
||||||
.setPageSize(10).build();
|
|
||||||
Factory<Integer, Contribution> factory;
|
|
||||||
factory = repository.fetchContributionsWithStates(
|
|
||||||
Arrays.asList(Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED,
|
|
||||||
Contribution.STATE_PAUSED));
|
|
||||||
|
|
||||||
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
|
|
||||||
pagedListConfig);
|
|
||||||
pendingContributionList = livePagedListBuilder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the contributions with the state "FAILED" and populates the
|
|
||||||
* `failedContributionList`.
|
|
||||||
**/
|
|
||||||
void getFailedContributions() {
|
|
||||||
final PagedList.Config pagedListConfig =
|
|
||||||
(new PagedList.Config.Builder())
|
|
||||||
.setPrefetchDistance(50)
|
|
||||||
.setPageSize(10).build();
|
|
||||||
Factory<Integer, Contribution> factory;
|
|
||||||
factory = repository.fetchContributionsWithStates(
|
|
||||||
Collections.singletonList(Contribution.STATE_FAILED));
|
|
||||||
|
|
||||||
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
|
|
||||||
pagedListConfig);
|
|
||||||
failedContributionList = livePagedListBuilder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
|
||||||
* Fetches the contributions with the state "IN_PROGRESS", "QUEUED", "PAUSED" and "FAILED" and
|
|
||||||
* then it populates the `failedAndPendingContributionList`.
|
|
||||||
**/
|
|
||||||
// void getFailedAndPendingContributions() {
|
|
||||||
// final PagedList.Config pagedListConfig =
|
|
||||||
// (new PagedList.Config.Builder())
|
|
||||||
// .setPrefetchDistance(50)
|
|
||||||
// .setPageSize(10).build();
|
|
||||||
// Factory<Integer, Contribution> factory;
|
|
||||||
// factory = repository.fetchContributionsWithStates(
|
|
||||||
// Arrays.asList(Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED,
|
|
||||||
// Contribution.STATE_PAUSED, Contribution.STATE_FAILED));
|
|
||||||
//
|
|
||||||
// LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
|
|
||||||
// pagedListConfig);
|
|
||||||
// failedAndPendingContributionList = livePagedListBuilder.build();
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,474 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import android.Manifest.permission
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.paging.LivePagedListBuilder
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.filepicker.DefaultCallback
|
||||||
|
import fr.free.nrw.commons.filepicker.FilePicker
|
||||||
|
import fr.free.nrw.commons.filepicker.FilePicker.HandleActivityResult
|
||||||
|
import fr.free.nrw.commons.filepicker.FilePicker.configuration
|
||||||
|
import fr.free.nrw.commons.filepicker.FilePicker.handleExternalImagesPicked
|
||||||
|
import fr.free.nrw.commons.filepicker.FilePicker.onPictureReturnedFromDocuments
|
||||||
|
import fr.free.nrw.commons.filepicker.FilePicker.openCameraForImage
|
||||||
|
import fr.free.nrw.commons.filepicker.FilePicker.openCustomSelector
|
||||||
|
import fr.free.nrw.commons.filepicker.FilePicker.openGallery
|
||||||
|
import fr.free.nrw.commons.filepicker.UploadableFile
|
||||||
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
|
import fr.free.nrw.commons.location.LatLng
|
||||||
|
import fr.free.nrw.commons.location.LocationPermissionsHelper
|
||||||
|
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback
|
||||||
|
import fr.free.nrw.commons.location.LocationServiceManager
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
import fr.free.nrw.commons.upload.UploadActivity
|
||||||
|
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||||
|
import fr.free.nrw.commons.utils.PermissionUtils.PERMISSIONS_STORAGE
|
||||||
|
import fr.free.nrw.commons.utils.PermissionUtils.checkPermissionsAndPerformAction
|
||||||
|
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
|
||||||
|
import fr.free.nrw.commons.utils.ViewUtil.showShortToast
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT
|
||||||
|
import java.util.Arrays
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class ContributionController @Inject constructor(@param:Named("default_preferences") private val defaultKvStore: JsonKvStore) {
|
||||||
|
private var locationBeforeImageCapture: LatLng? = null
|
||||||
|
private var isInAppCameraUpload = false
|
||||||
|
@JvmField
|
||||||
|
var locationPermissionCallback: LocationPermissionCallback? = null
|
||||||
|
private var locationPermissionsHelper: LocationPermissionsHelper? = null
|
||||||
|
|
||||||
|
// Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
||||||
|
// LiveData<PagedList<Contribution>> failedAndPendingContributionList;
|
||||||
|
@JvmField
|
||||||
|
var pendingContributionList: LiveData<PagedList<Contribution>>? = null
|
||||||
|
@JvmField
|
||||||
|
var failedContributionList: LiveData<PagedList<Contribution>>? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var locationManager: LocationServiceManager? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var repository: ContributionsRepository? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for permissions and initiate camera click
|
||||||
|
*/
|
||||||
|
fun initiateCameraPick(
|
||||||
|
activity: Activity,
|
||||||
|
inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>
|
||||||
|
) {
|
||||||
|
val useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true)
|
||||||
|
if (!useExtStorage) {
|
||||||
|
initiateCameraUpload(activity, resultLauncher)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPermissionsAndPerformAction(
|
||||||
|
activity,
|
||||||
|
{
|
||||||
|
if (defaultKvStore.getBoolean("inAppCameraFirstRun")) {
|
||||||
|
defaultKvStore.putBoolean("inAppCameraFirstRun", false)
|
||||||
|
askUserToAllowLocationAccess(
|
||||||
|
activity,
|
||||||
|
inAppCameraLocationPermissionLauncher,
|
||||||
|
resultLauncher
|
||||||
|
)
|
||||||
|
} else if (defaultKvStore.getBoolean("inAppCameraLocationPref")) {
|
||||||
|
createDialogsAndHandleLocationPermissions(
|
||||||
|
activity,
|
||||||
|
inAppCameraLocationPermissionLauncher, resultLauncher
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
initiateCameraUpload(activity, resultLauncher)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
R.string.storage_permission_title,
|
||||||
|
R.string.write_storage_permission_rationale,
|
||||||
|
*PERMISSIONS_STORAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asks users to provide location access
|
||||||
|
*
|
||||||
|
* @param activity
|
||||||
|
*/
|
||||||
|
private fun createDialogsAndHandleLocationPermissions(
|
||||||
|
activity: Activity,
|
||||||
|
inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>?,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>
|
||||||
|
) {
|
||||||
|
locationPermissionCallback = object : LocationPermissionCallback {
|
||||||
|
override fun onLocationPermissionDenied(toastMessage: String) {
|
||||||
|
Toast.makeText(
|
||||||
|
activity,
|
||||||
|
toastMessage,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
initiateCameraUpload(activity, resultLauncher)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLocationPermissionGranted() {
|
||||||
|
if (!locationPermissionsHelper!!.isLocationAccessToAppsTurnedOn()) {
|
||||||
|
showLocationOffDialog(
|
||||||
|
activity, R.string.in_app_camera_needs_location,
|
||||||
|
R.string.in_app_camera_location_unavailable, resultLauncher
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
initiateCameraUpload(activity, resultLauncher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locationPermissionsHelper = LocationPermissionsHelper(
|
||||||
|
activity, locationManager!!, locationPermissionCallback
|
||||||
|
)
|
||||||
|
inAppCameraLocationPermissionLauncher?.launch(
|
||||||
|
arrayOf(permission.ACCESS_FINE_LOCATION)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a dialog alerting the user about location services being off and asking them to turn it
|
||||||
|
* on
|
||||||
|
* TODO: Add a seperate callback in LocationPermissionsHelper for this.
|
||||||
|
* Ref: https://github.com/commons-app/apps-android-commons/pull/5494/files#r1510553114
|
||||||
|
*
|
||||||
|
* @param activity Activity reference
|
||||||
|
* @param dialogTextResource Resource id of text to be shown in dialog
|
||||||
|
* @param toastTextResource Resource id of text to be shown in toast
|
||||||
|
* @param resultLauncher
|
||||||
|
*/
|
||||||
|
private fun showLocationOffDialog(
|
||||||
|
activity: Activity, dialogTextResource: Int,
|
||||||
|
toastTextResource: Int, resultLauncher: ActivityResultLauncher<Intent>
|
||||||
|
) {
|
||||||
|
showAlertDialog(activity,
|
||||||
|
activity.getString(R.string.ask_to_turn_location_on),
|
||||||
|
activity.getString(dialogTextResource),
|
||||||
|
activity.getString(R.string.title_app_shortcut_setting),
|
||||||
|
activity.getString(R.string.cancel),
|
||||||
|
{ locationPermissionsHelper!!.openLocationSettings(activity) },
|
||||||
|
{
|
||||||
|
Toast.makeText(
|
||||||
|
activity, activity.getString(toastTextResource),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
initiateCameraUpload(activity, resultLauncher)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleShowRationaleFlowCameraLocation(
|
||||||
|
activity: Activity,
|
||||||
|
inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>?,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>
|
||||||
|
) {
|
||||||
|
showAlertDialog(
|
||||||
|
activity, activity.getString(R.string.location_permission_title),
|
||||||
|
activity.getString(R.string.in_app_camera_location_permission_rationale),
|
||||||
|
activity.getString(android.R.string.ok),
|
||||||
|
activity.getString(android.R.string.cancel),
|
||||||
|
{
|
||||||
|
createDialogsAndHandleLocationPermissions(
|
||||||
|
activity,
|
||||||
|
inAppCameraLocationPermissionLauncher, resultLauncher
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
locationPermissionCallback!!.onLocationPermissionDenied(
|
||||||
|
activity.getString(R.string.in_app_camera_location_permission_denied)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suggest user to attach location information with pictures. If the user selects "Yes", then:
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Location is taken from the EXIF if the default camera application does not redact location
|
||||||
|
* tags.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Otherwise, if the EXIF metadata does not have location information, then location captured by
|
||||||
|
* the app is used
|
||||||
|
*
|
||||||
|
* @param activity
|
||||||
|
*/
|
||||||
|
private fun askUserToAllowLocationAccess(
|
||||||
|
activity: Activity,
|
||||||
|
inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>
|
||||||
|
) {
|
||||||
|
showAlertDialog(
|
||||||
|
activity,
|
||||||
|
activity.getString(R.string.in_app_camera_location_permission_title),
|
||||||
|
activity.getString(R.string.in_app_camera_location_access_explanation),
|
||||||
|
activity.getString(R.string.option_allow),
|
||||||
|
activity.getString(R.string.option_dismiss),
|
||||||
|
{
|
||||||
|
defaultKvStore.putBoolean("inAppCameraLocationPref", true)
|
||||||
|
createDialogsAndHandleLocationPermissions(
|
||||||
|
activity,
|
||||||
|
inAppCameraLocationPermissionLauncher, resultLauncher
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
showLongToast(activity, R.string.in_app_camera_location_permission_denied)
|
||||||
|
defaultKvStore.putBoolean("inAppCameraLocationPref", false)
|
||||||
|
initiateCameraUpload(activity, resultLauncher)
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate gallery picker
|
||||||
|
*/
|
||||||
|
fun initiateGalleryPick(
|
||||||
|
activity: Activity,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>,
|
||||||
|
allowMultipleUploads: Boolean
|
||||||
|
) {
|
||||||
|
initiateGalleryUpload(activity, resultLauncher, allowMultipleUploads)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate gallery picker with permission
|
||||||
|
*/
|
||||||
|
fun initiateCustomGalleryPickWithPermission(
|
||||||
|
activity: Activity,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>
|
||||||
|
) {
|
||||||
|
setPickerConfiguration(activity, true)
|
||||||
|
|
||||||
|
checkPermissionsAndPerformAction(
|
||||||
|
activity,
|
||||||
|
{ openCustomSelector(activity, resultLauncher, 0) },
|
||||||
|
R.string.storage_permission_title,
|
||||||
|
R.string.write_storage_permission_rationale,
|
||||||
|
*PERMISSIONS_STORAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open chooser for gallery uploads
|
||||||
|
*/
|
||||||
|
private fun initiateGalleryUpload(
|
||||||
|
activity: Activity, resultLauncher: ActivityResultLauncher<Intent>,
|
||||||
|
allowMultipleUploads: Boolean
|
||||||
|
) {
|
||||||
|
setPickerConfiguration(activity, allowMultipleUploads)
|
||||||
|
openGallery(activity, resultLauncher, 0, isDocumentPhotoPickerPreferred)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets configuration for file picker
|
||||||
|
*/
|
||||||
|
private fun setPickerConfiguration(
|
||||||
|
activity: Activity,
|
||||||
|
allowMultipleUploads: Boolean
|
||||||
|
) {
|
||||||
|
val copyToExternalStorage = defaultKvStore.getBoolean("useExternalStorage", true)
|
||||||
|
configuration(activity)
|
||||||
|
.setCopyTakenPhotosToPublicGalleryAppFolder(copyToExternalStorage)
|
||||||
|
.setAllowMultiplePickInGallery(allowMultipleUploads)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate camera upload by opening camera
|
||||||
|
*/
|
||||||
|
private fun initiateCameraUpload(
|
||||||
|
activity: Activity,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>
|
||||||
|
) {
|
||||||
|
setPickerConfiguration(activity, false)
|
||||||
|
if (defaultKvStore.getBoolean("inAppCameraLocationPref", false)) {
|
||||||
|
locationBeforeImageCapture = locationManager!!.getLastLocation()
|
||||||
|
}
|
||||||
|
isInAppCameraUpload = true
|
||||||
|
openCameraForImage(activity, resultLauncher, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isDocumentPhotoPickerPreferred: Boolean
|
||||||
|
get() = defaultKvStore.getBoolean(
|
||||||
|
"openDocumentPhotoPickerPref", true
|
||||||
|
)
|
||||||
|
|
||||||
|
fun onPictureReturnedFromGallery(
|
||||||
|
result: ActivityResult,
|
||||||
|
activity: Activity,
|
||||||
|
callbacks: FilePicker.Callbacks
|
||||||
|
) {
|
||||||
|
if (isDocumentPhotoPickerPreferred) {
|
||||||
|
onPictureReturnedFromDocuments(result, activity, callbacks)
|
||||||
|
} else {
|
||||||
|
FilePicker.onPictureReturnedFromGallery(result, activity, callbacks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPictureReturnedFromCustomSelector(
|
||||||
|
result: ActivityResult,
|
||||||
|
activity: Activity,
|
||||||
|
callbacks: FilePicker.Callbacks
|
||||||
|
) {
|
||||||
|
FilePicker.onPictureReturnedFromCustomSelector(result, activity, callbacks)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPictureReturnedFromCamera(
|
||||||
|
result: ActivityResult,
|
||||||
|
activity: Activity,
|
||||||
|
callbacks: FilePicker.Callbacks
|
||||||
|
) {
|
||||||
|
FilePicker.onPictureReturnedFromCamera(result, activity, callbacks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches callback for file picker.
|
||||||
|
*/
|
||||||
|
fun handleActivityResultWithCallback(
|
||||||
|
activity: Activity,
|
||||||
|
handleActivityResult: HandleActivityResult
|
||||||
|
) {
|
||||||
|
handleActivityResult.onHandleActivityResult(object : DefaultCallback() {
|
||||||
|
override fun onCanceled(source: FilePicker.ImageSource, type: Int) {
|
||||||
|
super.onCanceled(source, type)
|
||||||
|
defaultKvStore.remove(PLACE_OBJECT)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onImagePickerError(
|
||||||
|
e: Exception, source: FilePicker.ImageSource,
|
||||||
|
type: Int
|
||||||
|
) {
|
||||||
|
showShortToast(activity, R.string.error_occurred_in_picking_images)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onImagesPicked(
|
||||||
|
imagesFiles: List<UploadableFile>,
|
||||||
|
source: FilePicker.ImageSource, type: Int
|
||||||
|
) {
|
||||||
|
val intent = handleImagesPicked(activity, imagesFiles)
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleExternalImagesPicked(
|
||||||
|
activity: Activity,
|
||||||
|
data: Intent?
|
||||||
|
): List<UploadableFile> {
|
||||||
|
return handleExternalImagesPicked(data, activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns intent to be passed to upload activity Attaches place object for nearby uploads and
|
||||||
|
* location before image capture if in-app camera is used
|
||||||
|
*/
|
||||||
|
private fun handleImagesPicked(
|
||||||
|
context: Context,
|
||||||
|
imagesFiles: List<UploadableFile>
|
||||||
|
): Intent {
|
||||||
|
val shareIntent = Intent(context, UploadActivity::class.java)
|
||||||
|
shareIntent.setAction(ACTION_INTERNAL_UPLOADS)
|
||||||
|
shareIntent
|
||||||
|
.putParcelableArrayListExtra(UploadActivity.EXTRA_FILES, ArrayList(imagesFiles))
|
||||||
|
val place = defaultKvStore.getJson<Place>(PLACE_OBJECT, Place::class.java)
|
||||||
|
|
||||||
|
if (place != null) {
|
||||||
|
shareIntent.putExtra(PLACE_OBJECT, place)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locationBeforeImageCapture != null) {
|
||||||
|
shareIntent.putExtra(
|
||||||
|
UploadActivity.LOCATION_BEFORE_IMAGE_CAPTURE,
|
||||||
|
locationBeforeImageCapture
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
shareIntent.putExtra(
|
||||||
|
UploadActivity.IN_APP_CAMERA_UPLOAD,
|
||||||
|
isInAppCameraUpload
|
||||||
|
)
|
||||||
|
isInAppCameraUpload = false // reset the flag for next use
|
||||||
|
return shareIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
val pendingContributions: Unit
|
||||||
|
/**
|
||||||
|
* Fetches the contributions with the state "IN_PROGRESS", "QUEUED" and "PAUSED" and then it
|
||||||
|
* populates the `pendingContributionList`.
|
||||||
|
*/
|
||||||
|
get() {
|
||||||
|
val pagedListConfig =
|
||||||
|
(PagedList.Config.Builder())
|
||||||
|
.setPrefetchDistance(50)
|
||||||
|
.setPageSize(10).build()
|
||||||
|
val factory = repository!!.fetchContributionsWithStates(
|
||||||
|
Arrays.asList(
|
||||||
|
Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED,
|
||||||
|
Contribution.STATE_PAUSED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val livePagedListBuilder = LivePagedListBuilder(factory, pagedListConfig)
|
||||||
|
pendingContributionList = livePagedListBuilder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
val failedContributions: Unit
|
||||||
|
/**
|
||||||
|
* Fetches the contributions with the state "FAILED" and populates the
|
||||||
|
* `failedContributionList`.
|
||||||
|
*/
|
||||||
|
get() {
|
||||||
|
val pagedListConfig =
|
||||||
|
(PagedList.Config.Builder())
|
||||||
|
.setPrefetchDistance(50)
|
||||||
|
.setPageSize(10).build()
|
||||||
|
val factory = repository!!.fetchContributionsWithStates(
|
||||||
|
listOf(Contribution.STATE_FAILED)
|
||||||
|
)
|
||||||
|
|
||||||
|
val livePagedListBuilder = LivePagedListBuilder(factory, pagedListConfig)
|
||||||
|
failedContributionList = livePagedListBuilder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
||||||
|
* Fetches the contributions with the state "IN_PROGRESS", "QUEUED", "PAUSED" and "FAILED" and
|
||||||
|
* then it populates the `failedAndPendingContributionList`.
|
||||||
|
*/
|
||||||
|
// void getFailedAndPendingContributions() {
|
||||||
|
// final PagedList.Config pagedListConfig =
|
||||||
|
// (new PagedList.Config.Builder())
|
||||||
|
// .setPrefetchDistance(50)
|
||||||
|
// .setPageSize(10).build();
|
||||||
|
// Factory<Integer, Contribution> factory;
|
||||||
|
// factory = repository.fetchContributionsWithStates(
|
||||||
|
// Arrays.asList(Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED,
|
||||||
|
// Contribution.STATE_PAUSED, Contribution.STATE_FAILED));
|
||||||
|
//
|
||||||
|
// LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
|
||||||
|
// pagedListConfig);
|
||||||
|
// failedAndPendingContributionList = livePagedListBuilder.build();
|
||||||
|
// }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_INTERNAL_UPLOADS: String = "internalImageUploads"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import android.database.sqlite.SQLiteException;
|
|
||||||
import androidx.paging.DataSource;
|
|
||||||
import androidx.room.Dao;
|
|
||||||
import androidx.room.Delete;
|
|
||||||
import androidx.room.Insert;
|
|
||||||
import androidx.room.OnConflictStrategy;
|
|
||||||
import androidx.room.Query;
|
|
||||||
import androidx.room.Transaction;
|
|
||||||
import androidx.room.Update;
|
|
||||||
import io.reactivex.Completable;
|
|
||||||
import io.reactivex.Single;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.List;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
public abstract class ContributionDao {
|
|
||||||
|
|
||||||
@Query("SELECT * FROM contribution order by media_dateUploaded DESC")
|
|
||||||
abstract DataSource.Factory<Integer, Contribution> fetchContributions();
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
public abstract void saveSynchronous(Contribution contribution);
|
|
||||||
|
|
||||||
public Completable save(final Contribution contribution) {
|
|
||||||
return Completable
|
|
||||||
.fromAction(() -> {
|
|
||||||
contribution.setDateModified(Calendar.getInstance().getTime());
|
|
||||||
if (contribution.getDateUploadStarted() == null) {
|
|
||||||
contribution.setDateUploadStarted(Calendar.getInstance().getTime());
|
|
||||||
}
|
|
||||||
saveSynchronous(contribution);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
public void deleteAndSaveContribution(final Contribution oldContribution,
|
|
||||||
final Contribution newContribution) {
|
|
||||||
deleteSynchronous(oldContribution);
|
|
||||||
saveSynchronous(newContribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
public abstract Single<List<Long>> save(List<Contribution> contribution);
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
public abstract void deleteSynchronous(Contribution contribution);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes contributions with specific states from the database.
|
|
||||||
*
|
|
||||||
* @param states The states of the contributions to delete.
|
|
||||||
* @throws SQLiteException If an SQLite error occurs.
|
|
||||||
*/
|
|
||||||
@Query("DELETE FROM contribution WHERE state IN (:states)")
|
|
||||||
public abstract void deleteContributionsWithStatesSynchronous(List<Integer> states)
|
|
||||||
throws SQLiteException;
|
|
||||||
|
|
||||||
public Completable delete(final Contribution contribution) {
|
|
||||||
return Completable
|
|
||||||
.fromAction(() -> deleteSynchronous(contribution));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes contributions with specific states from the database.
|
|
||||||
*
|
|
||||||
* @param states The states of the contributions to delete.
|
|
||||||
* @return A Completable indicating the result of the operation.
|
|
||||||
*/
|
|
||||||
public Completable deleteContributionsWithStates(List<Integer> states) {
|
|
||||||
return Completable
|
|
||||||
.fromAction(() -> deleteContributionsWithStatesSynchronous(states));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Query("SELECT * from contribution WHERE media_filename=:fileName")
|
|
||||||
public abstract List<Contribution> getContributionWithTitle(String fileName);
|
|
||||||
|
|
||||||
@Query("SELECT * from contribution WHERE pageId=:pageId")
|
|
||||||
public abstract Contribution getContribution(String pageId);
|
|
||||||
|
|
||||||
@Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
|
|
||||||
public abstract Single<List<Contribution>> getContribution(List<Integer> states);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets contributions with specific states in descending order by the date they were uploaded.
|
|
||||||
*
|
|
||||||
* @param states The states of the contributions to fetch.
|
|
||||||
* @return A DataSource factory for paginated contributions with the specified states.
|
|
||||||
*/
|
|
||||||
@Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
|
|
||||||
public abstract DataSource.Factory<Integer, Contribution> getContributions(
|
|
||||||
List<Integer> states);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets contributions with specific states in ascending order by the date the upload started.
|
|
||||||
*
|
|
||||||
* @param states The states of the contributions to fetch.
|
|
||||||
* @return A DataSource factory for paginated contributions with the specified states.
|
|
||||||
*/
|
|
||||||
@Query("SELECT * from contribution WHERE state IN (:states) order by dateUploadStarted ASC")
|
|
||||||
public abstract DataSource.Factory<Integer, Contribution> getContributionsSortedByDateUploadStarted(
|
|
||||||
List<Integer> states);
|
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)")
|
|
||||||
public abstract Single<Integer> getPendingUploads(int[] toUpdateStates);
|
|
||||||
|
|
||||||
@Query("Delete FROM contribution")
|
|
||||||
public abstract void deleteAll() throws SQLiteException;
|
|
||||||
|
|
||||||
@Update
|
|
||||||
public abstract void updateSynchronous(Contribution contribution);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the state of contributions with specific states.
|
|
||||||
*
|
|
||||||
* @param states The current states of the contributions to update.
|
|
||||||
* @param newState The new state to set.
|
|
||||||
*/
|
|
||||||
@Query("UPDATE contribution SET state = :newState WHERE state IN (:states)")
|
|
||||||
public abstract void updateContributionsState(List<Integer> states, int newState);
|
|
||||||
|
|
||||||
public Completable update(final Contribution contribution) {
|
|
||||||
return Completable
|
|
||||||
.fromAction(() -> {
|
|
||||||
contribution.setDateModified(Calendar.getInstance().getTime());
|
|
||||||
updateSynchronous(contribution);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the state of contributions with specific states asynchronously.
|
|
||||||
*
|
|
||||||
* @param states The current states of the contributions to update.
|
|
||||||
* @param newState The new state to set.
|
|
||||||
* @return A Completable indicating the result of the operation.
|
|
||||||
*/
|
|
||||||
public Completable updateContributionsWithStates(List<Integer> states, int newState) {
|
|
||||||
return Completable
|
|
||||||
.fromAction(() -> {
|
|
||||||
updateContributionsState(states, newState);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteException
|
||||||
|
import androidx.paging.DataSource
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
|
import io.reactivex.Completable
|
||||||
|
import io.reactivex.Single
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class ContributionDao {
|
||||||
|
@Query("SELECT * FROM contribution order by media_dateUploaded DESC")
|
||||||
|
abstract fun fetchContributions(): DataSource.Factory<Int, Contribution>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
abstract fun saveSynchronous(contribution: Contribution)
|
||||||
|
|
||||||
|
fun save(contribution: Contribution): Completable {
|
||||||
|
return Completable
|
||||||
|
.fromAction {
|
||||||
|
contribution.dateModified = Calendar.getInstance().time
|
||||||
|
if (contribution.dateUploadStarted == null) {
|
||||||
|
contribution.dateUploadStarted = Calendar.getInstance().time
|
||||||
|
}
|
||||||
|
saveSynchronous(contribution)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun deleteAndSaveContribution(
|
||||||
|
oldContribution: Contribution,
|
||||||
|
newContribution: Contribution
|
||||||
|
) {
|
||||||
|
deleteSynchronous(oldContribution)
|
||||||
|
saveSynchronous(newContribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
abstract fun save(contribution: List<Contribution>): Single<List<Long>>
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
abstract fun deleteSynchronous(contribution: Contribution)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes contributions with specific states from the database.
|
||||||
|
*
|
||||||
|
* @param states The states of the contributions to delete.
|
||||||
|
* @throws SQLiteException If an SQLite error occurs.
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM contribution WHERE state IN (:states)")
|
||||||
|
@Throws(SQLiteException::class)
|
||||||
|
abstract fun deleteContributionsWithStatesSynchronous(states: List<Int>)
|
||||||
|
|
||||||
|
fun delete(contribution: Contribution): Completable {
|
||||||
|
return Completable
|
||||||
|
.fromAction { deleteSynchronous(contribution) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes contributions with specific states from the database.
|
||||||
|
*
|
||||||
|
* @param states The states of the contributions to delete.
|
||||||
|
* @return A Completable indicating the result of the operation.
|
||||||
|
*/
|
||||||
|
fun deleteContributionsWithStates(states: List<Int>): Completable {
|
||||||
|
return Completable
|
||||||
|
.fromAction { deleteContributionsWithStatesSynchronous(states) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("SELECT * from contribution WHERE media_filename=:fileName")
|
||||||
|
abstract fun getContributionWithTitle(fileName: String): List<Contribution>
|
||||||
|
|
||||||
|
@Query("SELECT * from contribution WHERE pageId=:pageId")
|
||||||
|
abstract fun getContribution(pageId: String): Contribution
|
||||||
|
|
||||||
|
@Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
|
||||||
|
abstract fun getContribution(states: List<Int>): Single<List<Contribution>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets contributions with specific states in descending order by the date they were uploaded.
|
||||||
|
*
|
||||||
|
* @param states The states of the contributions to fetch.
|
||||||
|
* @return A DataSource factory for paginated contributions with the specified states.
|
||||||
|
*/
|
||||||
|
@Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
|
||||||
|
abstract fun getContributions(
|
||||||
|
states: List<Int>
|
||||||
|
): DataSource.Factory<Int, Contribution>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets contributions with specific states in ascending order by the date the upload started.
|
||||||
|
*
|
||||||
|
* @param states The states of the contributions to fetch.
|
||||||
|
* @return A DataSource factory for paginated contributions with the specified states.
|
||||||
|
*/
|
||||||
|
@Query("SELECT * from contribution WHERE state IN (:states) order by dateUploadStarted ASC")
|
||||||
|
abstract fun getContributionsSortedByDateUploadStarted(
|
||||||
|
states: List<Int>
|
||||||
|
): DataSource.Factory<Int, Contribution>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)")
|
||||||
|
abstract fun getPendingUploads(toUpdateStates: IntArray): Single<Int>
|
||||||
|
|
||||||
|
@Query("Delete FROM contribution")
|
||||||
|
@Throws(SQLiteException::class)
|
||||||
|
abstract fun deleteAll()
|
||||||
|
|
||||||
|
@Update
|
||||||
|
abstract fun updateSynchronous(contribution: Contribution)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the state of contributions with specific states.
|
||||||
|
*
|
||||||
|
* @param states The current states of the contributions to update.
|
||||||
|
* @param newState The new state to set.
|
||||||
|
*/
|
||||||
|
@Query("UPDATE contribution SET state = :newState WHERE state IN (:states)")
|
||||||
|
abstract fun updateContributionsState(states: List<Int>, newState: Int)
|
||||||
|
|
||||||
|
fun update(contribution: Contribution): Completable {
|
||||||
|
return Completable.fromAction {
|
||||||
|
contribution.dateModified = Calendar.getInstance().time
|
||||||
|
updateSynchronous(contribution)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the state of contributions with specific states asynchronously.
|
||||||
|
*
|
||||||
|
* @param states The current states of the contributions to update.
|
||||||
|
* @param newState The new state to set.
|
||||||
|
* @return A Completable indicating the result of the operation.
|
||||||
|
*/
|
||||||
|
fun updateContributionsWithStates(states: List<Int>, newState: Int): Completable {
|
||||||
|
return Completable
|
||||||
|
.fromAction {
|
||||||
|
updateContributionsState(states, newState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.View;
|
|
||||||
import android.webkit.URLUtil;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.app.AlertDialog.Builder;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import com.facebook.drawee.view.SimpleDraweeView;
|
|
||||||
import com.facebook.imagepipeline.request.ImageRequest;
|
|
||||||
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
|
|
||||||
import fr.free.nrw.commons.databinding.LayoutContributionBinding;
|
|
||||||
import fr.free.nrw.commons.media.MediaClient;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
private final Callback callback;
|
|
||||||
|
|
||||||
LayoutContributionBinding binding;
|
|
||||||
|
|
||||||
private int position;
|
|
||||||
private Contribution contribution;
|
|
||||||
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
|
|
||||||
private final MediaClient mediaClient;
|
|
||||||
private boolean isWikipediaButtonDisplayed;
|
|
||||||
private AlertDialog pausingPopUp;
|
|
||||||
private View parent;
|
|
||||||
private ImageRequest imageRequest;
|
|
||||||
|
|
||||||
ContributionViewHolder(final View parent, final Callback callback,
|
|
||||||
final MediaClient mediaClient) {
|
|
||||||
super(parent);
|
|
||||||
this.parent = parent;
|
|
||||||
this.mediaClient = mediaClient;
|
|
||||||
this.callback = callback;
|
|
||||||
|
|
||||||
binding = LayoutContributionBinding.bind(parent);
|
|
||||||
|
|
||||||
binding.contributionImage.setOnClickListener(v -> imageClicked());
|
|
||||||
binding.wikipediaButton.setOnClickListener(v -> wikipediaButtonClicked());
|
|
||||||
|
|
||||||
/* Set a dialog indicating that the upload is being paused. This is needed because pausing
|
|
||||||
an upload might take a dozen seconds. */
|
|
||||||
AlertDialog.Builder builder = new Builder(parent.getContext());
|
|
||||||
builder.setCancelable(false);
|
|
||||||
builder.setView(R.layout.progress_dialog);
|
|
||||||
pausingPopUp = builder.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void init(final int position, final Contribution contribution) {
|
|
||||||
|
|
||||||
//handling crashes when the contribution is null.
|
|
||||||
if (null == contribution) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.contribution = contribution;
|
|
||||||
this.position = position;
|
|
||||||
binding.contributionTitle.setText(contribution.getMedia().getMostRelevantCaption());
|
|
||||||
binding.authorView.setText(contribution.getMedia().getAuthor());
|
|
||||||
|
|
||||||
//Removes flicker of loading image.
|
|
||||||
binding.contributionImage.getHierarchy().setFadeDuration(0);
|
|
||||||
|
|
||||||
binding.contributionImage.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
|
|
||||||
binding.contributionImage.getHierarchy().setFailureImage(R.drawable.image_placeholder);
|
|
||||||
|
|
||||||
final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(),
|
|
||||||
contribution.getLocalUri());
|
|
||||||
if (!TextUtils.isEmpty(imageSource)) {
|
|
||||||
if (URLUtil.isHttpsUrl(imageSource)) {
|
|
||||||
imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
|
|
||||||
.setProgressiveRenderingEnabled(true)
|
|
||||||
.build();
|
|
||||||
} else if (URLUtil.isFileUrl(imageSource)) {
|
|
||||||
imageRequest = ImageRequest.fromUri(Uri.parse(imageSource));
|
|
||||||
} else if (imageSource != null) {
|
|
||||||
final File file = new File(imageSource);
|
|
||||||
imageRequest = ImageRequest.fromFile(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageRequest != null) {
|
|
||||||
binding.contributionImage.setImageRequest(imageRequest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.contributionSequenceNumber.setText(String.valueOf(position + 1));
|
|
||||||
binding.contributionSequenceNumber.setVisibility(View.VISIBLE);
|
|
||||||
binding.wikipediaButton.setVisibility(View.GONE);
|
|
||||||
binding.contributionState.setVisibility(View.GONE);
|
|
||||||
binding.contributionProgress.setVisibility(View.GONE);
|
|
||||||
binding.imageOptions.setVisibility(View.GONE);
|
|
||||||
binding.contributionState.setText("");
|
|
||||||
checkIfMediaExistsOnWikipediaPage(contribution);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a media exists on the corresponding Wikipedia article Currently the check is made
|
|
||||||
* for the device's current language Wikipedia
|
|
||||||
*
|
|
||||||
* @param contribution
|
|
||||||
*/
|
|
||||||
private void checkIfMediaExistsOnWikipediaPage(final Contribution contribution) {
|
|
||||||
if (contribution.getWikidataPlace() == null
|
|
||||||
|| contribution.getWikidataPlace().getWikipediaArticle() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final String wikipediaArticle = contribution.getWikidataPlace().getWikipediaPageTitle();
|
|
||||||
compositeDisposable.add(mediaClient.doesPageContainMedia(wikipediaArticle)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(mediaExists -> {
|
|
||||||
displayWikipediaButton(mediaExists);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle action buttons visibility if the corresponding wikipedia page doesn't contain any
|
|
||||||
* media. This method needs to control the state of just the scenario where media does not
|
|
||||||
* exists as other scenarios are already handled in the init method.
|
|
||||||
*
|
|
||||||
* @param mediaExists
|
|
||||||
*/
|
|
||||||
private void displayWikipediaButton(Boolean mediaExists) {
|
|
||||||
if (!mediaExists) {
|
|
||||||
binding.wikipediaButton.setVisibility(View.VISIBLE);
|
|
||||||
isWikipediaButtonDisplayed = true;
|
|
||||||
binding.imageOptions.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the image source for the image view, first preference is given to thumbUrl if that is
|
|
||||||
* null, moves to local uri and if both are null return null
|
|
||||||
*
|
|
||||||
* @param thumbUrl
|
|
||||||
* @param localUri
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
private String chooseImageSource(final String thumbUrl, final Uri localUri) {
|
|
||||||
return !TextUtils.isEmpty(thumbUrl) ? thumbUrl :
|
|
||||||
localUri != null ? localUri.toString() :
|
|
||||||
null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void imageClicked() {
|
|
||||||
callback.openMediaDetail(position, isWikipediaButtonDisplayed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void wikipediaButtonClicked() {
|
|
||||||
callback.addImageToWikipedia(contribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ImageRequest getImageRequest() {
|
|
||||||
return imageRequest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.View
|
||||||
|
import android.webkit.URLUtil
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.facebook.imagepipeline.request.ImageRequest
|
||||||
|
import com.facebook.imagepipeline.request.ImageRequestBuilder
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.databinding.LayoutContributionBinding
|
||||||
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class ContributionViewHolder internal constructor(
|
||||||
|
private val parent: View, private val callback: ContributionsListAdapter.Callback,
|
||||||
|
private val mediaClient: MediaClient
|
||||||
|
) : RecyclerView.ViewHolder(parent) {
|
||||||
|
var binding: LayoutContributionBinding = LayoutContributionBinding.bind(parent)
|
||||||
|
|
||||||
|
private var position = 0
|
||||||
|
private var contribution: Contribution? = null
|
||||||
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
private var isWikipediaButtonDisplayed = false
|
||||||
|
private val pausingPopUp: AlertDialog
|
||||||
|
var imageRequest: ImageRequest? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
init {
|
||||||
|
binding.contributionImage.setOnClickListener { v: View? -> imageClicked() }
|
||||||
|
binding.wikipediaButton.setOnClickListener { v: View? -> wikipediaButtonClicked() }
|
||||||
|
|
||||||
|
/* Set a dialog indicating that the upload is being paused. This is needed because pausing
|
||||||
|
an upload might take a dozen seconds. */
|
||||||
|
val builder = AlertDialog.Builder(
|
||||||
|
parent.context
|
||||||
|
)
|
||||||
|
builder.setCancelable(false)
|
||||||
|
builder.setView(R.layout.progress_dialog)
|
||||||
|
pausingPopUp = builder.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init(position: Int, contribution: Contribution?) {
|
||||||
|
//handling crashes when the contribution is null.
|
||||||
|
|
||||||
|
if (null == contribution) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contribution = contribution
|
||||||
|
this.position = position
|
||||||
|
binding.contributionTitle.text = contribution.media.mostRelevantCaption
|
||||||
|
binding.authorView.text = contribution.media.author
|
||||||
|
|
||||||
|
//Removes flicker of loading image.
|
||||||
|
binding.contributionImage.hierarchy.fadeDuration = 0
|
||||||
|
|
||||||
|
binding.contributionImage.hierarchy.setPlaceholderImage(R.drawable.image_placeholder)
|
||||||
|
binding.contributionImage.hierarchy.setFailureImage(R.drawable.image_placeholder)
|
||||||
|
|
||||||
|
val imageSource = chooseImageSource(
|
||||||
|
contribution.media.thumbUrl,
|
||||||
|
contribution.localUri
|
||||||
|
)
|
||||||
|
if (!TextUtils.isEmpty(imageSource)) {
|
||||||
|
if (URLUtil.isHttpsUrl(imageSource)) {
|
||||||
|
imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
|
||||||
|
.setProgressiveRenderingEnabled(true)
|
||||||
|
.build()
|
||||||
|
} else if (URLUtil.isFileUrl(imageSource)) {
|
||||||
|
imageRequest = ImageRequest.fromUri(Uri.parse(imageSource))
|
||||||
|
} else if (imageSource != null) {
|
||||||
|
val file = File(imageSource)
|
||||||
|
imageRequest = ImageRequest.fromFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageRequest != null) {
|
||||||
|
binding.contributionImage.setImageRequest(imageRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.contributionSequenceNumber.text = (position + 1).toString()
|
||||||
|
binding.contributionSequenceNumber.visibility = View.VISIBLE
|
||||||
|
binding.wikipediaButton.visibility = View.GONE
|
||||||
|
binding.contributionState.visibility = View.GONE
|
||||||
|
binding.contributionProgress.visibility = View.GONE
|
||||||
|
binding.imageOptions.visibility = View.GONE
|
||||||
|
binding.contributionState.text = ""
|
||||||
|
checkIfMediaExistsOnWikipediaPage(contribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a media exists on the corresponding Wikipedia article Currently the check is made
|
||||||
|
* for the device's current language Wikipedia
|
||||||
|
*
|
||||||
|
* @param contribution
|
||||||
|
*/
|
||||||
|
private fun checkIfMediaExistsOnWikipediaPage(contribution: Contribution) {
|
||||||
|
if (contribution.wikidataPlace == null
|
||||||
|
|| contribution.wikidataPlace!!.wikipediaArticle == null
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val wikipediaArticle = contribution.wikidataPlace!!.getWikipediaPageTitle()
|
||||||
|
compositeDisposable.add(
|
||||||
|
mediaClient.doesPageContainMedia(wikipediaArticle)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { mediaExists: Boolean ->
|
||||||
|
displayWikipediaButton(mediaExists)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle action buttons visibility if the corresponding wikipedia page doesn't contain any
|
||||||
|
* media. This method needs to control the state of just the scenario where media does not
|
||||||
|
* exists as other scenarios are already handled in the init method.
|
||||||
|
*
|
||||||
|
* @param mediaExists
|
||||||
|
*/
|
||||||
|
private fun displayWikipediaButton(mediaExists: Boolean) {
|
||||||
|
if (!mediaExists) {
|
||||||
|
binding.wikipediaButton.visibility = View.VISIBLE
|
||||||
|
isWikipediaButtonDisplayed = true
|
||||||
|
binding.imageOptions.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the image source for the image view, first preference is given to thumbUrl if that is
|
||||||
|
* null, moves to local uri and if both are null return null
|
||||||
|
*
|
||||||
|
* @param thumbUrl
|
||||||
|
* @param localUri
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private fun chooseImageSource(thumbUrl: String?, localUri: Uri?): String? {
|
||||||
|
return if (!TextUtils.isEmpty(thumbUrl)) thumbUrl else localUri?.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun imageClicked() {
|
||||||
|
callback.openMediaDetail(position, isWikipediaButtonDisplayed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun wikipediaButtonClicked() {
|
||||||
|
callback.addImageToWikipedia(contribution)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import fr.free.nrw.commons.BasePresenter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The contract for Contributions View & Presenter
|
|
||||||
*/
|
|
||||||
public class ContributionsContract {
|
|
||||||
|
|
||||||
public interface View {
|
|
||||||
|
|
||||||
void showMessage(String localizedMessage);
|
|
||||||
|
|
||||||
Context getContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface UserActionListener extends BasePresenter<ContributionsContract.View> {
|
|
||||||
|
|
||||||
Contribution getContributionsWithTitle(String uri);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import fr.free.nrw.commons.BasePresenter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contract for Contributions View & Presenter
|
||||||
|
*/
|
||||||
|
interface ContributionsContract {
|
||||||
|
|
||||||
|
interface View {
|
||||||
|
fun showMessage(localizedMessage: String)
|
||||||
|
fun getContext(): Context
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserActionListener : BasePresenter<View> {
|
||||||
|
fun getContributionsWithTitle(uri: String): Contribution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,940 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import static android.content.Context.SENSOR_SERVICE;
|
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
|
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED;
|
|
||||||
import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL;
|
|
||||||
import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME;
|
|
||||||
import static fr.free.nrw.commons.utils.LengthUtils.computeBearing;
|
|
||||||
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.Manifest.permission;
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.hardware.Sensor;
|
|
||||||
import android.hardware.SensorEvent;
|
|
||||||
import android.hardware.SensorEventListener;
|
|
||||||
import android.hardware.SensorManager;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.activity.result.ActivityResultCallback;
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
|
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
|
||||||
import fr.free.nrw.commons.Utils;
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
|
||||||
import fr.free.nrw.commons.databinding.FragmentContributionsBinding;
|
|
||||||
import fr.free.nrw.commons.notification.models.Notification;
|
|
||||||
import fr.free.nrw.commons.notification.NotificationController;
|
|
||||||
import fr.free.nrw.commons.profile.ProfileActivity;
|
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
|
||||||
import fr.free.nrw.commons.upload.UploadProgressActivity;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
import fr.free.nrw.commons.Media;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.campaigns.models.Campaign;
|
|
||||||
import fr.free.nrw.commons.campaigns.CampaignView;
|
|
||||||
import fr.free.nrw.commons.campaigns.CampaignsPresenter;
|
|
||||||
import fr.free.nrw.commons.campaigns.ICampaignsView;
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionsListFragment.Callback;
|
|
||||||
import fr.free.nrw.commons.contributions.MainActivity.ActiveFragment;
|
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
|
||||||
import fr.free.nrw.commons.location.LocationUpdateListener;
|
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider;
|
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
|
||||||
import fr.free.nrw.commons.nearby.NearbyController;
|
|
||||||
import fr.free.nrw.commons.nearby.NearbyNotificationCardView;
|
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
|
||||||
import fr.free.nrw.commons.notification.NotificationActivity;
|
|
||||||
import fr.free.nrw.commons.upload.worker.UploadWorker;
|
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
|
||||||
import fr.free.nrw.commons.utils.DialogUtil;
|
|
||||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
|
||||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
|
||||||
import io.reactivex.Observable;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class ContributionsFragment
|
|
||||||
extends CommonsDaggerSupportFragment
|
|
||||||
implements
|
|
||||||
OnBackStackChangedListener,
|
|
||||||
LocationUpdateListener,
|
|
||||||
MediaDetailProvider,
|
|
||||||
SensorEventListener,
|
|
||||||
ICampaignsView, ContributionsContract.View, Callback {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@Named("default_preferences")
|
|
||||||
JsonKvStore store;
|
|
||||||
@Inject
|
|
||||||
NearbyController nearbyController;
|
|
||||||
@Inject
|
|
||||||
OkHttpJsonApiClient okHttpJsonApiClient;
|
|
||||||
@Inject
|
|
||||||
CampaignsPresenter presenter;
|
|
||||||
@Inject
|
|
||||||
LocationServiceManager locationManager;
|
|
||||||
@Inject
|
|
||||||
NotificationController notificationController;
|
|
||||||
@Inject
|
|
||||||
ContributionController contributionController;
|
|
||||||
|
|
||||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
|
||||||
|
|
||||||
private ContributionsListFragment contributionsListFragment;
|
|
||||||
private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
|
|
||||||
private MediaDetailPagerFragment mediaDetailPagerFragment;
|
|
||||||
static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
|
|
||||||
private static final int MAX_RETRIES = 10;
|
|
||||||
|
|
||||||
public FragmentContributionsBinding binding;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ContributionsPresenter contributionsPresenter;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
SessionManager sessionManager;
|
|
||||||
|
|
||||||
private LatLng currentLatLng;
|
|
||||||
|
|
||||||
private boolean isFragmentAttachedBefore = false;
|
|
||||||
private View checkBoxView;
|
|
||||||
private CheckBox checkBox;
|
|
||||||
|
|
||||||
public TextView notificationCount;
|
|
||||||
|
|
||||||
public TextView pendingUploadsCountTextView;
|
|
||||||
|
|
||||||
public TextView uploadsErrorTextView;
|
|
||||||
|
|
||||||
public ImageView pendingUploadsImageView;
|
|
||||||
|
|
||||||
private Campaign wlmCampaign;
|
|
||||||
|
|
||||||
String userName;
|
|
||||||
private boolean isUserProfile;
|
|
||||||
|
|
||||||
private SensorManager mSensorManager;
|
|
||||||
private Sensor mLight;
|
|
||||||
private float direction;
|
|
||||||
private ActivityResultLauncher<String[]> nearbyLocationPermissionLauncher = registerForActivityResult(
|
|
||||||
new ActivityResultContracts.RequestMultiplePermissions(),
|
|
||||||
new ActivityResultCallback<Map<String, Boolean>>() {
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(Map<String, Boolean> result) {
|
|
||||||
boolean areAllGranted = true;
|
|
||||||
for (final boolean b : result.values()) {
|
|
||||||
areAllGranted = areAllGranted && b;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (areAllGranted) {
|
|
||||||
onLocationPermissionGranted();
|
|
||||||
} else {
|
|
||||||
if (shouldShowRequestPermissionRationale(
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION)
|
|
||||||
&& store.getBoolean("displayLocationPermissionForCardView", true)
|
|
||||||
&& !store.getBoolean("doNotAskForLocationPermission", false)
|
|
||||||
&& (((MainActivity) getActivity()).activeFragment
|
|
||||||
== ActiveFragment.CONTRIBUTIONS)) {
|
|
||||||
binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION;
|
|
||||||
} else {
|
|
||||||
displayYouWontSeeNearbyMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static ContributionsFragment newInstance() {
|
|
||||||
ContributionsFragment fragment = new ContributionsFragment();
|
|
||||||
fragment.setRetainInstance(true);
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldShowMediaDetailsFragment;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
if (getArguments() != null && getArguments().getString(KEY_USERNAME) != null) {
|
|
||||||
userName = getArguments().getString(KEY_USERNAME);
|
|
||||||
isUserProfile = true;
|
|
||||||
}
|
|
||||||
mSensorManager = (SensorManager) getActivity().getSystemService(SENSOR_SERVICE);
|
|
||||||
mLight = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
|
||||||
@Nullable Bundle savedInstanceState) {
|
|
||||||
|
|
||||||
binding = FragmentContributionsBinding.inflate(inflater, container, false);
|
|
||||||
|
|
||||||
initWLMCampaign();
|
|
||||||
presenter.onAttachView(this);
|
|
||||||
contributionsPresenter.onAttachView(this);
|
|
||||||
binding.campaignsView.setVisibility(View.GONE);
|
|
||||||
checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
|
|
||||||
checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
|
|
||||||
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
|
||||||
if (isChecked) {
|
|
||||||
// Do not ask for permission on activity start again
|
|
||||||
store.putBoolean("displayLocationPermissionForCardView", false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager()
|
|
||||||
.findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
|
|
||||||
contributionsListFragment = (ContributionsListFragment) getChildFragmentManager()
|
|
||||||
.findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG);
|
|
||||||
shouldShowMediaDetailsFragment = savedInstanceState.getBoolean("mediaDetailsVisible");
|
|
||||||
}
|
|
||||||
|
|
||||||
initFragments();
|
|
||||||
if (!isUserProfile) {
|
|
||||||
upDateUploadCount();
|
|
||||||
}
|
|
||||||
if (shouldShowMediaDetailsFragment) {
|
|
||||||
showMediaDetailPagerFragment();
|
|
||||||
} else {
|
|
||||||
if (mediaDetailPagerFragment != null) {
|
|
||||||
removeFragment(mediaDetailPagerFragment);
|
|
||||||
}
|
|
||||||
showContributionsListFragment();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ConfigUtils.isBetaFlavour() && sessionManager.isUserLoggedIn()
|
|
||||||
&& sessionManager.getCurrentAccount() != null && !isUserProfile) {
|
|
||||||
setUploadCount();
|
|
||||||
}
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
return binding.getRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialise the campaign object for WML
|
|
||||||
*/
|
|
||||||
private void initWLMCampaign() {
|
|
||||||
wlmCampaign = new Campaign(getString(R.string.wlm_campaign_title),
|
|
||||||
getString(R.string.wlm_campaign_description), Utils.getWLMStartDate().toString(),
|
|
||||||
Utils.getWLMEndDate().toString(), WLM_URL, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
|
||||||
@NonNull final MenuInflater inflater) {
|
|
||||||
|
|
||||||
// Removing contributions menu items for ProfileActivity
|
|
||||||
if (getActivity() instanceof ProfileActivity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inflater.inflate(R.menu.contribution_activity_notification_menu, menu);
|
|
||||||
|
|
||||||
MenuItem notificationsMenuItem = menu.findItem(R.id.notifications);
|
|
||||||
final View notification = notificationsMenuItem.getActionView();
|
|
||||||
notificationCount = notification.findViewById(R.id.notification_count_badge);
|
|
||||||
MenuItem uploadMenuItem = menu.findItem(R.id.upload_tab);
|
|
||||||
final View uploadMenuItemActionView = uploadMenuItem.getActionView();
|
|
||||||
pendingUploadsCountTextView = uploadMenuItemActionView.findViewById(
|
|
||||||
R.id.pending_uploads_count_badge);
|
|
||||||
uploadsErrorTextView = uploadMenuItemActionView.findViewById(
|
|
||||||
R.id.uploads_error_count_badge);
|
|
||||||
pendingUploadsImageView = uploadMenuItemActionView.findViewById(
|
|
||||||
R.id.pending_uploads_image_view);
|
|
||||||
if (pendingUploadsImageView != null) {
|
|
||||||
pendingUploadsImageView.setOnClickListener(view -> {
|
|
||||||
startActivity(new Intent(getContext(), UploadProgressActivity.class));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (pendingUploadsCountTextView != null) {
|
|
||||||
pendingUploadsCountTextView.setOnClickListener(view -> {
|
|
||||||
startActivity(new Intent(getContext(), UploadProgressActivity.class));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (uploadsErrorTextView != null) {
|
|
||||||
uploadsErrorTextView.setOnClickListener(view -> {
|
|
||||||
startActivity(new Intent(getContext(), UploadProgressActivity.class));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
notification.setOnClickListener(view -> {
|
|
||||||
NotificationActivity.Companion.startYourself(getContext(), "unread");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
|
||||||
public void setNotificationCount() {
|
|
||||||
compositeDisposable.add(notificationController.getNotifications(false)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(this::initNotificationViews,
|
|
||||||
throwable -> Timber.e(throwable, "Error occurred while loading notifications")));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
|
||||||
* Sets the visibility of the upload icon based on the number of failed and pending
|
|
||||||
* contributions.
|
|
||||||
*/
|
|
||||||
// public void setUploadIconVisibility() {
|
|
||||||
// contributionController.getFailedAndPendingContributions();
|
|
||||||
// contributionController.failedAndPendingContributionList.observe(getViewLifecycleOwner(),
|
|
||||||
// list -> {
|
|
||||||
// updateUploadIcon(list.size());
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the count for the upload icon based on the number of pending and failed contributions.
|
|
||||||
*/
|
|
||||||
public void setUploadIconCount() {
|
|
||||||
contributionController.getPendingContributions();
|
|
||||||
contributionController.pendingContributionList.observe(getViewLifecycleOwner(),
|
|
||||||
list -> {
|
|
||||||
updatePendingIcon(list.size());
|
|
||||||
});
|
|
||||||
contributionController.getFailedContributions();
|
|
||||||
contributionController.failedContributionList.observe(getViewLifecycleOwner(), list -> {
|
|
||||||
updateErrorIcon(list.size());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void scrollToTop() {
|
|
||||||
if (contributionsListFragment != null) {
|
|
||||||
contributionsListFragment.scrollToTop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initNotificationViews(List<Notification> notificationList) {
|
|
||||||
Timber.d("Number of notifications is %d", notificationList.size());
|
|
||||||
if (notificationList.isEmpty()) {
|
|
||||||
notificationCount.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
notificationCount.setVisibility(View.VISIBLE);
|
|
||||||
notificationCount.setText(String.valueOf(notificationList.size()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(Context context) {
|
|
||||||
super.onAttach(context);
|
|
||||||
/*
|
|
||||||
- There are some operations we need auth, so we need to make sure isAuthCookieAcquired.
|
|
||||||
- And since we use same retained fragment doesn't want to make all network operations
|
|
||||||
all over again on same fragment attached to recreated activity, we do this network
|
|
||||||
operations on first time fragment attached to an activity. Then they will be retained
|
|
||||||
until fragment life time ends.
|
|
||||||
*/
|
|
||||||
if (!isFragmentAttachedBefore && getActivity() != null) {
|
|
||||||
isFragmentAttachedBefore = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace FrameLayout with ContributionsListFragment, user will see contributions list. Creates
|
|
||||||
* new one if null.
|
|
||||||
*/
|
|
||||||
private void showContributionsListFragment() {
|
|
||||||
// show nearby card view on contributions list is visible
|
|
||||||
if (binding.cardViewNearby != null && !isUserProfile) {
|
|
||||||
if (store.getBoolean("displayNearbyCardView", true)) {
|
|
||||||
if (binding.cardViewNearby.cardViewVisibilityState
|
|
||||||
== NearbyNotificationCardView.CardViewVisibilityState.READY) {
|
|
||||||
binding.cardViewNearby.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.cardViewNearby.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG,
|
|
||||||
mediaDetailPagerFragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showMediaDetailPagerFragment() {
|
|
||||||
// hide nearby card view on media detail is visible
|
|
||||||
setupViewForMediaDetails();
|
|
||||||
showFragment(mediaDetailPagerFragment, MEDIA_DETAIL_PAGER_FRAGMENT_TAG,
|
|
||||||
contributionsListFragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupViewForMediaDetails() {
|
|
||||||
if (binding != null) {
|
|
||||||
binding.campaignsView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackStackChanged() {
|
|
||||||
fetchCampaigns();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initFragments() {
|
|
||||||
if (null == contributionsListFragment) {
|
|
||||||
contributionsListFragment = new ContributionsListFragment();
|
|
||||||
Bundle contributionsListBundle = new Bundle();
|
|
||||||
contributionsListBundle.putString(KEY_USERNAME, userName);
|
|
||||||
contributionsListFragment.setArguments(contributionsListBundle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldShowMediaDetailsFragment) {
|
|
||||||
showMediaDetailPagerFragment();
|
|
||||||
} else {
|
|
||||||
showContributionsListFragment();
|
|
||||||
}
|
|
||||||
|
|
||||||
showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG,
|
|
||||||
mediaDetailPagerFragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces the root frame layout with the given fragment
|
|
||||||
*
|
|
||||||
* @param fragment
|
|
||||||
* @param tag
|
|
||||||
* @param otherFragment
|
|
||||||
*/
|
|
||||||
private void showFragment(Fragment fragment, String tag, Fragment otherFragment) {
|
|
||||||
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
|
|
||||||
if (fragment.isAdded() && otherFragment != null) {
|
|
||||||
transaction.hide(otherFragment);
|
|
||||||
transaction.show(fragment);
|
|
||||||
transaction.addToBackStack(tag);
|
|
||||||
transaction.commit();
|
|
||||||
getChildFragmentManager().executePendingTransactions();
|
|
||||||
} else if (fragment.isAdded() && otherFragment == null) {
|
|
||||||
transaction.show(fragment);
|
|
||||||
transaction.addToBackStack(tag);
|
|
||||||
transaction.commit();
|
|
||||||
getChildFragmentManager().executePendingTransactions();
|
|
||||||
} else if (!fragment.isAdded() && otherFragment != null) {
|
|
||||||
transaction.hide(otherFragment);
|
|
||||||
transaction.add(R.id.root_frame, fragment, tag);
|
|
||||||
transaction.addToBackStack(tag);
|
|
||||||
transaction.commit();
|
|
||||||
getChildFragmentManager().executePendingTransactions();
|
|
||||||
} else if (!fragment.isAdded()) {
|
|
||||||
transaction.replace(R.id.root_frame, fragment, tag);
|
|
||||||
transaction.addToBackStack(tag);
|
|
||||||
transaction.commit();
|
|
||||||
getChildFragmentManager().executePendingTransactions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeFragment(Fragment fragment) {
|
|
||||||
getChildFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.remove(fragment)
|
|
||||||
.commit();
|
|
||||||
getChildFragmentManager().executePendingTransactions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
private void setUploadCount() {
|
|
||||||
compositeDisposable.add(okHttpJsonApiClient
|
|
||||||
.getUploadCount(((MainActivity) getActivity()).sessionManager.getCurrentAccount().name)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(this::displayUploadCount,
|
|
||||||
t -> Timber.e(t, "Fetching upload count failed")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void displayUploadCount(Integer uploadCount) {
|
|
||||||
if (getActivity().isFinishing()
|
|
||||||
|| getResources() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
((MainActivity) getActivity()).setNumOfUploads(uploadCount);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
locationManager.removeLocationListener(this);
|
|
||||||
locationManager.unregisterLocationManager();
|
|
||||||
mSensorManager.unregisterListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
contributionsPresenter.onAttachView(this);
|
|
||||||
locationManager.addLocationListener(this);
|
|
||||||
|
|
||||||
if (binding == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.cardViewNearby.permissionRequestButton.setOnClickListener(v -> {
|
|
||||||
showNearbyCardPermissionRationale();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Notification cards should only be seen on contributions list, not in media details
|
|
||||||
if (mediaDetailPagerFragment == null && !isUserProfile) {
|
|
||||||
if (store.getBoolean("displayNearbyCardView", true)) {
|
|
||||||
checkPermissionsAndShowNearbyCardView();
|
|
||||||
|
|
||||||
// Calling nearby card to keep showing it even when user clicks on it and comes back
|
|
||||||
try {
|
|
||||||
updateClosestNearbyCardViewInfo();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.e(e);
|
|
||||||
}
|
|
||||||
if (binding.cardViewNearby.cardViewVisibilityState
|
|
||||||
== NearbyNotificationCardView.CardViewVisibilityState.READY) {
|
|
||||||
binding.cardViewNearby.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Hide nearby notification card view if related shared preferences is false
|
|
||||||
binding.cardViewNearby.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notification Count and Campaigns should not be set, if it is used in User Profile
|
|
||||||
if (!isUserProfile) {
|
|
||||||
setNotificationCount();
|
|
||||||
fetchCampaigns();
|
|
||||||
// Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
|
||||||
// setUploadIconVisibility();
|
|
||||||
setUploadIconCount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mSensorManager.registerListener(this, mLight, SensorManager.SENSOR_DELAY_UI);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkPermissionsAndShowNearbyCardView() {
|
|
||||||
if (PermissionUtils.hasPermission(getActivity(),
|
|
||||||
new String[]{Manifest.permission.ACCESS_FINE_LOCATION})) {
|
|
||||||
onLocationPermissionGranted();
|
|
||||||
} else if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)
|
|
||||||
&& store.getBoolean("displayLocationPermissionForCardView", true)
|
|
||||||
&& !store.getBoolean("doNotAskForLocationPermission", false)
|
|
||||||
&& (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) {
|
|
||||||
binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION;
|
|
||||||
showNearbyCardPermissionRationale();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void requestLocationPermission() {
|
|
||||||
nearbyLocationPermissionLauncher.launch(new String[]{permission.ACCESS_FINE_LOCATION});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onLocationPermissionGranted() {
|
|
||||||
binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.NO_PERMISSION_NEEDED;
|
|
||||||
locationManager.registerLocationManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showNearbyCardPermissionRationale() {
|
|
||||||
DialogUtil.showAlertDialog(getActivity(),
|
|
||||||
getString(R.string.nearby_card_permission_title),
|
|
||||||
getString(R.string.nearby_card_permission_explanation),
|
|
||||||
this::requestLocationPermission,
|
|
||||||
this::displayYouWontSeeNearbyMessage,
|
|
||||||
checkBoxView
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void displayYouWontSeeNearbyMessage() {
|
|
||||||
ViewUtil.showLongToast(getActivity(),
|
|
||||||
getResources().getString(R.string.unable_to_display_nearest_place));
|
|
||||||
// Set to true as the user doesn't want the app to ask for location permission anymore
|
|
||||||
store.putBoolean("doNotAskForLocationPermission", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void updateClosestNearbyCardViewInfo() {
|
|
||||||
currentLatLng = locationManager.getLastLocation();
|
|
||||||
compositeDisposable.add(Observable.fromCallable(() -> nearbyController
|
|
||||||
.loadAttractionsFromLocation(currentLatLng, currentLatLng, true,
|
|
||||||
false)) // thanks to boolean, it will only return closest result
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(this::updateNearbyNotification,
|
|
||||||
throwable -> {
|
|
||||||
Timber.d(throwable);
|
|
||||||
updateNearbyNotification(null);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateNearbyNotification(
|
|
||||||
@Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
|
|
||||||
if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null
|
|
||||||
&& nearbyPlacesInfo.placeList.size() > 0) {
|
|
||||||
Place closestNearbyPlace = null;
|
|
||||||
// Find the first nearby place that has no image and exists
|
|
||||||
for (Place place : nearbyPlacesInfo.placeList) {
|
|
||||||
if (place.pic.equals("") && place.exists) {
|
|
||||||
closestNearbyPlace = place;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closestNearbyPlace == null) {
|
|
||||||
binding.cardViewNearby.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
String distance = formatDistanceBetween(currentLatLng, closestNearbyPlace.location);
|
|
||||||
closestNearbyPlace.setDistance(distance);
|
|
||||||
direction = (float) computeBearing(currentLatLng, closestNearbyPlace.location);
|
|
||||||
binding.cardViewNearby.updateContent(closestNearbyPlace);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Means that no close nearby place is found
|
|
||||||
binding.cardViewNearby.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent Nearby banner from appearing in Media Details, fixing bug https://github.com/commons-app/apps-android-commons/issues/4731
|
|
||||||
if (mediaDetailPagerFragment != null && !contributionsListFragment.isVisible()) {
|
|
||||||
binding.cardViewNearby.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
try {
|
|
||||||
compositeDisposable.clear();
|
|
||||||
getChildFragmentManager().removeOnBackStackChangedListener(this);
|
|
||||||
locationManager.unregisterLocationManager();
|
|
||||||
locationManager.removeLocationListener(this);
|
|
||||||
super.onDestroy();
|
|
||||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
|
||||||
Timber.e(exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLocationChangedSignificantly(LatLng latLng) {
|
|
||||||
// Will be called if location changed more than 1000 meter
|
|
||||||
updateClosestNearbyCardViewInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLocationChangedSlightly(LatLng latLng) {
|
|
||||||
/* Update closest nearby notification card onLocationChangedSlightly
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
updateClosestNearbyCardViewInfo();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.e(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLocationChangedMedium(LatLng latLng) {
|
|
||||||
// Update closest nearby card view if location changed more than 500 meters
|
|
||||||
updateClosestNearbyCardViewInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(@NonNull View view,
|
|
||||||
@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* As the home screen has limited space, we have choosen to show either campaigns or WLM card.
|
|
||||||
* The WLM Card gets the priority over monuments, so if the WLM is going on we show that instead
|
|
||||||
* of campaigns on the campaigns card
|
|
||||||
*/
|
|
||||||
private void fetchCampaigns() {
|
|
||||||
if (Utils.isMonumentsEnabled(new Date())) {
|
|
||||||
if (binding != null) {
|
|
||||||
binding.campaignsView.setCampaign(wlmCampaign);
|
|
||||||
binding.campaignsView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
} else if (store.getBoolean(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE, true)) {
|
|
||||||
presenter.getCampaigns();
|
|
||||||
} else {
|
|
||||||
if (binding != null) {
|
|
||||||
binding.campaignsView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showMessage(String message) {
|
|
||||||
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showCampaigns(Campaign campaign) {
|
|
||||||
if (campaign != null && !isUserProfile) {
|
|
||||||
if (binding != null) {
|
|
||||||
binding.campaignsView.setCampaign(campaign);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
super.onDestroyView();
|
|
||||||
presenter.onDetachView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void notifyDataSetChanged() {
|
|
||||||
if (mediaDetailPagerFragment != null) {
|
|
||||||
mediaDetailPagerFragment.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notify the viewpager that number of items have changed.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void viewPagerNotifyDataSetChanged() {
|
|
||||||
if (mediaDetailPagerFragment != null) {
|
|
||||||
mediaDetailPagerFragment.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the visibility and text of the pending uploads count TextView based on the given
|
|
||||||
* count.
|
|
||||||
*
|
|
||||||
* @param pendingCount The number of pending uploads.
|
|
||||||
*/
|
|
||||||
public void updatePendingIcon(int pendingCount) {
|
|
||||||
if (pendingUploadsCountTextView != null) {
|
|
||||||
if (pendingCount != 0) {
|
|
||||||
pendingUploadsCountTextView.setVisibility(View.VISIBLE);
|
|
||||||
pendingUploadsCountTextView.setText(String.valueOf(pendingCount));
|
|
||||||
} else {
|
|
||||||
pendingUploadsCountTextView.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the visibility and text of the error uploads TextView based on the given count.
|
|
||||||
*
|
|
||||||
* @param errorCount The number of error uploads.
|
|
||||||
*/
|
|
||||||
public void updateErrorIcon(int errorCount) {
|
|
||||||
if (uploadsErrorTextView != null) {
|
|
||||||
if (errorCount != 0) {
|
|
||||||
uploadsErrorTextView.setVisibility(View.VISIBLE);
|
|
||||||
uploadsErrorTextView.setText(String.valueOf(errorCount));
|
|
||||||
} else {
|
|
||||||
uploadsErrorTextView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
|
||||||
* @param count The number of pending uploads.
|
|
||||||
*/
|
|
||||||
// public void updateUploadIcon(int count) {
|
|
||||||
// if (pendingUploadsImageView != null) {
|
|
||||||
// if (count != 0) {
|
|
||||||
// pendingUploadsImageView.setVisibility(View.VISIBLE);
|
|
||||||
// } else {
|
|
||||||
// pendingUploadsImageView.setVisibility(View.GONE);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace whatever is in the current contributionsFragmentContainer view with
|
|
||||||
* mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects
|
|
||||||
* a contribution.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void showDetail(int position, boolean isWikipediaButtonDisplayed) {
|
|
||||||
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
|
|
||||||
mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true);
|
|
||||||
if (isUserProfile) {
|
|
||||||
((ProfileActivity) getActivity()).setScroll(false);
|
|
||||||
}
|
|
||||||
showMediaDetailPagerFragment();
|
|
||||||
}
|
|
||||||
mediaDetailPagerFragment.showImage(position, isWikipediaButtonDisplayed);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Media getMediaAtPosition(int i) {
|
|
||||||
return contributionsListFragment.getMediaAtPosition(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getTotalMediaCount() {
|
|
||||||
return contributionsListFragment.getTotalMediaCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer getContributionStateAt(int position) {
|
|
||||||
return contributionsListFragment.getContributionStateAt(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean backButtonClicked() {
|
|
||||||
if (mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible()) {
|
|
||||||
if (store.getBoolean("displayNearbyCardView", true) && !isUserProfile) {
|
|
||||||
if (binding.cardViewNearby.cardViewVisibilityState
|
|
||||||
== NearbyNotificationCardView.CardViewVisibilityState.READY) {
|
|
||||||
binding.cardViewNearby.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.cardViewNearby.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
removeFragment(mediaDetailPagerFragment);
|
|
||||||
showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG,
|
|
||||||
mediaDetailPagerFragment);
|
|
||||||
if (isUserProfile) {
|
|
||||||
// Fragment is associated with ProfileActivity
|
|
||||||
// Enable ParentViewPager Scroll
|
|
||||||
((ProfileActivity) getActivity()).setScroll(true);
|
|
||||||
} else {
|
|
||||||
fetchCampaigns();
|
|
||||||
}
|
|
||||||
if (getActivity() instanceof MainActivity) {
|
|
||||||
// Fragment is associated with MainActivity
|
|
||||||
((BaseActivity) getActivity()).getSupportActionBar()
|
|
||||||
.setDisplayHomeAsUpEnabled(false);
|
|
||||||
((MainActivity) getActivity()).showTabs();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getter for mediaDetailPagerFragment
|
|
||||||
public MediaDetailPagerFragment getMediaDetailPagerFragment() {
|
|
||||||
return mediaDetailPagerFragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* this function updates the number of contributions
|
|
||||||
*/
|
|
||||||
void upDateUploadCount() {
|
|
||||||
WorkManager.getInstance(getContext())
|
|
||||||
.getWorkInfosForUniqueWorkLiveData(UploadWorker.class.getSimpleName()).observe(
|
|
||||||
getViewLifecycleOwner(), workInfos -> {
|
|
||||||
if (workInfos.size() > 0) {
|
|
||||||
setUploadCount();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restarts the upload process for a contribution
|
|
||||||
*
|
|
||||||
* @param contribution
|
|
||||||
*/
|
|
||||||
public void restartUpload(Contribution contribution) {
|
|
||||||
contribution.setDateUploadStarted(Calendar.getInstance().getTime());
|
|
||||||
if (contribution.getState() == Contribution.STATE_FAILED) {
|
|
||||||
if (contribution.getErrorInfo() == null) {
|
|
||||||
contribution.setChunkInfo(null);
|
|
||||||
contribution.setTransferred(0);
|
|
||||||
}
|
|
||||||
contributionsPresenter.checkDuplicateImageAndRestartContribution(contribution);
|
|
||||||
} else {
|
|
||||||
contribution.setState(Contribution.STATE_QUEUED);
|
|
||||||
contributionsPresenter.saveContribution(contribution);
|
|
||||||
Timber.d("Restarting for %s", contribution.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retry upload when it is failed
|
|
||||||
*
|
|
||||||
* @param contribution contribution to be retried
|
|
||||||
*/
|
|
||||||
public void retryUpload(Contribution contribution) {
|
|
||||||
if (NetworkUtils.isInternetConnectionEstablished(getContext())) {
|
|
||||||
if (contribution.getState() == STATE_PAUSED) {
|
|
||||||
restartUpload(contribution);
|
|
||||||
} else if (contribution.getState() == STATE_FAILED) {
|
|
||||||
int retries = contribution.getRetries();
|
|
||||||
// TODO: Improve UX. Additional details: https://github.com/commons-app/apps-android-commons/pull/5257#discussion_r1304662562
|
|
||||||
/* Limit the number of retries for a failed upload
|
|
||||||
to handle cases like invalid filename as such uploads
|
|
||||||
will never be successful */
|
|
||||||
if (retries < MAX_RETRIES) {
|
|
||||||
contribution.setRetries(retries + 1);
|
|
||||||
Timber.d("Retried uploading %s %d times", contribution.getMedia().getFilename(),
|
|
||||||
retries + 1);
|
|
||||||
restartUpload(contribution);
|
|
||||||
} else {
|
|
||||||
// TODO: Show the exact reason for failure
|
|
||||||
Toast.makeText(getContext(),
|
|
||||||
R.string.retry_limit_reached, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.d("Skipping re-upload for non-failed %s", contribution.toString());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ViewUtil.showLongToast(getContext(), R.string.this_function_needs_network_connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reload media detail fragment once media is nominated
|
|
||||||
*
|
|
||||||
* @param index item position that has been nominated
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void refreshNominatedMedia(int index) {
|
|
||||||
if (mediaDetailPagerFragment != null && !contributionsListFragment.isVisible()) {
|
|
||||||
removeFragment(mediaDetailPagerFragment);
|
|
||||||
mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true);
|
|
||||||
mediaDetailPagerFragment.showImage(index);
|
|
||||||
showMediaDetailPagerFragment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When the device rotates, rotate the Nearby banner's compass arrow in tandem.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onSensorChanged(SensorEvent event) {
|
|
||||||
float rotateDegree = Math.round(event.values[0]);
|
|
||||||
binding.cardViewNearby.rotateCompass(rotateDegree, direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
|
||||||
// Nothing to do.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,998 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import android.Manifest.permission
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.hardware.Sensor
|
||||||
|
import android.hardware.SensorEvent
|
||||||
|
import android.hardware.SensorEventListener
|
||||||
|
import android.hardware.SensorManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CheckBox
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultCallback
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import androidx.work.WorkInfo
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import fr.free.nrw.commons.MapController.NearbyPlacesInfo
|
||||||
|
import fr.free.nrw.commons.Media
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.Utils
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
|
import fr.free.nrw.commons.campaigns.CampaignView
|
||||||
|
import fr.free.nrw.commons.campaigns.CampaignsPresenter
|
||||||
|
import fr.free.nrw.commons.campaigns.ICampaignsView
|
||||||
|
import fr.free.nrw.commons.campaigns.models.Campaign
|
||||||
|
import fr.free.nrw.commons.contributions.MainActivity.ActiveFragment
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentContributionsBinding
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||||
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
|
import fr.free.nrw.commons.location.LatLng
|
||||||
|
import fr.free.nrw.commons.location.LocationServiceManager
|
||||||
|
import fr.free.nrw.commons.location.LocationUpdateListener
|
||||||
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment
|
||||||
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider
|
||||||
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||||
|
import fr.free.nrw.commons.nearby.NearbyController
|
||||||
|
import fr.free.nrw.commons.nearby.NearbyNotificationCardView
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment
|
||||||
|
import fr.free.nrw.commons.notification.NotificationActivity.Companion.startYourself
|
||||||
|
import fr.free.nrw.commons.notification.NotificationController
|
||||||
|
import fr.free.nrw.commons.notification.models.Notification
|
||||||
|
import fr.free.nrw.commons.profile.ProfileActivity
|
||||||
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
|
import fr.free.nrw.commons.upload.UploadProgressActivity
|
||||||
|
import fr.free.nrw.commons.upload.worker.UploadWorker
|
||||||
|
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
|
||||||
|
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||||
|
import fr.free.nrw.commons.utils.LengthUtils.computeBearing
|
||||||
|
import fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween
|
||||||
|
import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
|
||||||
|
import fr.free.nrw.commons.utils.PermissionUtils.hasPermission
|
||||||
|
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
class ContributionsFragment
|
||||||
|
|
||||||
|
: CommonsDaggerSupportFragment(), FragmentManager.OnBackStackChangedListener,
|
||||||
|
LocationUpdateListener, MediaDetailProvider, SensorEventListener, ICampaignsView,
|
||||||
|
ContributionsContract.View,
|
||||||
|
ContributionsListFragment.Callback {
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
@Named("default_preferences")
|
||||||
|
var store: JsonKvStore? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var nearbyController: NearbyController? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var okHttpJsonApiClient: OkHttpJsonApiClient? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var presenter: CampaignsPresenter? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var locationManager: LocationServiceManager? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var notificationController: NotificationController? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var contributionController: ContributionController? = null
|
||||||
|
|
||||||
|
override var compositeDisposable: CompositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
|
private var contributionsListFragment: ContributionsListFragment? = null
|
||||||
|
|
||||||
|
// Getter for mediaDetailPagerFragment
|
||||||
|
var mediaDetailPagerFragment: MediaDetailPagerFragment? = null
|
||||||
|
private set
|
||||||
|
var binding: FragmentContributionsBinding? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var contributionsPresenter: ContributionsPresenter? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var sessionManager: SessionManager? = null
|
||||||
|
|
||||||
|
private var currentLatLng: LatLng? = null
|
||||||
|
|
||||||
|
private var isFragmentAttachedBefore = false
|
||||||
|
private var checkBoxView: View? = null
|
||||||
|
private var checkBox: CheckBox? = null
|
||||||
|
|
||||||
|
var notificationCount: TextView? = null
|
||||||
|
|
||||||
|
var pendingUploadsCountTextView: TextView? = null
|
||||||
|
|
||||||
|
var uploadsErrorTextView: TextView? = null
|
||||||
|
|
||||||
|
var pendingUploadsImageView: ImageView? = null
|
||||||
|
|
||||||
|
private var wlmCampaign: Campaign? = null
|
||||||
|
|
||||||
|
var userName: String? = null
|
||||||
|
private var isUserProfile = false
|
||||||
|
|
||||||
|
private var mSensorManager: SensorManager? = null
|
||||||
|
private var mLight: Sensor? = null
|
||||||
|
private var direction = 0f
|
||||||
|
private val nearbyLocationPermissionLauncher =
|
||||||
|
registerForActivityResult<Array<String>, Map<String, Boolean>>(
|
||||||
|
ActivityResultContracts.RequestMultiplePermissions(),
|
||||||
|
object : ActivityResultCallback<Map<String, Boolean>> {
|
||||||
|
override fun onActivityResult(result: Map<String, Boolean>) {
|
||||||
|
var areAllGranted = true
|
||||||
|
for (b in result.values) {
|
||||||
|
areAllGranted = areAllGranted && b
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areAllGranted) {
|
||||||
|
onLocationPermissionGranted()
|
||||||
|
} else {
|
||||||
|
if (shouldShowRequestPermissionRationale(
|
||||||
|
permission.ACCESS_FINE_LOCATION
|
||||||
|
)
|
||||||
|
&& store!!.getBoolean("displayLocationPermissionForCardView", true)
|
||||||
|
&& !store!!.getBoolean("doNotAskForLocationPermission", false)
|
||||||
|
&& ((activity as MainActivity).activeFragment
|
||||||
|
== ActiveFragment.CONTRIBUTIONS)
|
||||||
|
) {
|
||||||
|
binding!!.cardViewNearby.permissionType =
|
||||||
|
NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION
|
||||||
|
} else {
|
||||||
|
displayYouWontSeeNearbyMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
private var shouldShowMediaDetailsFragment = false
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
if (arguments != null && requireArguments().getString(ProfileActivity.KEY_USERNAME) != null) {
|
||||||
|
userName = requireArguments().getString(ProfileActivity.KEY_USERNAME)
|
||||||
|
isUserProfile = true
|
||||||
|
}
|
||||||
|
mSensorManager = requireActivity().getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||||
|
mLight = mSensorManager!!.getDefaultSensor(Sensor.TYPE_ORIENTATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
binding = FragmentContributionsBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
initWLMCampaign()
|
||||||
|
presenter!!.onAttachView(this)
|
||||||
|
contributionsPresenter!!.onAttachView(this)
|
||||||
|
binding!!.campaignsView.visibility = View.GONE
|
||||||
|
checkBoxView = View.inflate(activity, R.layout.nearby_permission_dialog, null)
|
||||||
|
checkBox = checkBoxView?.findViewById<View>(R.id.never_ask_again) as CheckBox
|
||||||
|
checkBox!!.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean ->
|
||||||
|
if (isChecked) {
|
||||||
|
// Do not ask for permission on activity start again
|
||||||
|
store!!.putBoolean("displayLocationPermissionForCardView", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
mediaDetailPagerFragment = childFragmentManager
|
||||||
|
.findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG) as MediaDetailPagerFragment?
|
||||||
|
contributionsListFragment = childFragmentManager
|
||||||
|
.findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG) as ContributionsListFragment?
|
||||||
|
shouldShowMediaDetailsFragment = savedInstanceState.getBoolean("mediaDetailsVisible")
|
||||||
|
}
|
||||||
|
|
||||||
|
initFragments()
|
||||||
|
if (!isUserProfile) {
|
||||||
|
upDateUploadCount()
|
||||||
|
}
|
||||||
|
if (shouldShowMediaDetailsFragment) {
|
||||||
|
showMediaDetailPagerFragment()
|
||||||
|
} else {
|
||||||
|
if (mediaDetailPagerFragment != null) {
|
||||||
|
removeFragment(mediaDetailPagerFragment!!)
|
||||||
|
}
|
||||||
|
showContributionsListFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isBetaFlavour && sessionManager!!.isUserLoggedIn
|
||||||
|
&& sessionManager!!.currentAccount != null && !isUserProfile
|
||||||
|
) {
|
||||||
|
setUploadCount()
|
||||||
|
}
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
return binding!!.root
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise the campaign object for WML
|
||||||
|
*/
|
||||||
|
private fun initWLMCampaign() {
|
||||||
|
wlmCampaign = Campaign(
|
||||||
|
getString(R.string.wlm_campaign_title),
|
||||||
|
getString(R.string.wlm_campaign_description), Utils.getWLMStartDate().toString(),
|
||||||
|
Utils.getWLMEndDate().toString(), NearbyParentFragment.WLM_URL, true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(
|
||||||
|
menu: Menu,
|
||||||
|
inflater: MenuInflater
|
||||||
|
) {
|
||||||
|
// Removing contributions menu items for ProfileActivity
|
||||||
|
|
||||||
|
if (activity is ProfileActivity) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inflater.inflate(R.menu.contribution_activity_notification_menu, menu)
|
||||||
|
|
||||||
|
val notificationsMenuItem = menu.findItem(R.id.notifications)
|
||||||
|
val notification = notificationsMenuItem.actionView
|
||||||
|
notificationCount = notification!!.findViewById(R.id.notification_count_badge)
|
||||||
|
val uploadMenuItem = menu.findItem(R.id.upload_tab)
|
||||||
|
val uploadMenuItemActionView = uploadMenuItem.actionView
|
||||||
|
pendingUploadsCountTextView = uploadMenuItemActionView!!.findViewById(
|
||||||
|
R.id.pending_uploads_count_badge
|
||||||
|
)
|
||||||
|
uploadsErrorTextView = uploadMenuItemActionView.findViewById(
|
||||||
|
R.id.uploads_error_count_badge
|
||||||
|
)
|
||||||
|
pendingUploadsImageView = uploadMenuItemActionView.findViewById(
|
||||||
|
R.id.pending_uploads_image_view
|
||||||
|
)
|
||||||
|
if (pendingUploadsImageView != null) {
|
||||||
|
pendingUploadsImageView!!.setOnClickListener { view: View? ->
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
context,
|
||||||
|
UploadProgressActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pendingUploadsCountTextView != null) {
|
||||||
|
pendingUploadsCountTextView!!.setOnClickListener { view: View? ->
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
context,
|
||||||
|
UploadProgressActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uploadsErrorTextView != null) {
|
||||||
|
uploadsErrorTextView!!.setOnClickListener { view: View? ->
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
context,
|
||||||
|
UploadProgressActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notification.setOnClickListener { view: View? ->
|
||||||
|
startYourself(
|
||||||
|
context, "unread"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
fun setNotificationCount() {
|
||||||
|
compositeDisposable.add(
|
||||||
|
notificationController!!.getNotifications(false)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ notificationList: List<Notification> ->
|
||||||
|
this.initNotificationViews(
|
||||||
|
notificationList
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ throwable: Throwable? ->
|
||||||
|
Timber.e(
|
||||||
|
throwable,
|
||||||
|
"Error occurred while loading notifications"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
||||||
|
* Sets the visibility of the upload icon based on the number of failed and pending
|
||||||
|
* contributions.
|
||||||
|
*/
|
||||||
|
// public void setUploadIconVisibility() {
|
||||||
|
// contributionController.getFailedAndPendingContributions();
|
||||||
|
// contributionController.failedAndPendingContributionList.observe(getViewLifecycleOwner(),
|
||||||
|
// list -> {
|
||||||
|
// updateUploadIcon(list.size());
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
/**
|
||||||
|
* Sets the count for the upload icon based on the number of pending and failed contributions.
|
||||||
|
*/
|
||||||
|
fun setUploadIconCount() {
|
||||||
|
contributionController!!.pendingContributions
|
||||||
|
contributionController!!.pendingContributionList!!.observe(
|
||||||
|
viewLifecycleOwner,
|
||||||
|
Observer<PagedList<Contribution>> { list: PagedList<Contribution> ->
|
||||||
|
updatePendingIcon(list.size)
|
||||||
|
})
|
||||||
|
contributionController!!.failedContributions
|
||||||
|
contributionController!!.failedContributionList!!.observe(
|
||||||
|
viewLifecycleOwner,
|
||||||
|
Observer<PagedList<Contribution>> { list: PagedList<Contribution> ->
|
||||||
|
updateErrorIcon(list.size)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scrollToTop() {
|
||||||
|
if (contributionsListFragment != null) {
|
||||||
|
contributionsListFragment!!.scrollToTop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNotificationViews(notificationList: List<Notification>) {
|
||||||
|
Timber.d("Number of notifications is %d", notificationList.size)
|
||||||
|
if (notificationList.isEmpty()) {
|
||||||
|
notificationCount!!.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
notificationCount!!.visibility = View.VISIBLE
|
||||||
|
notificationCount!!.text = notificationList.size.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
/*
|
||||||
|
- There are some operations we need auth, so we need to make sure isAuthCookieAcquired.
|
||||||
|
- And since we use same retained fragment doesn't want to make all network operations
|
||||||
|
all over again on same fragment attached to recreated activity, we do this network
|
||||||
|
operations on first time fragment attached to an activity. Then they will be retained
|
||||||
|
until fragment life time ends.
|
||||||
|
*/
|
||||||
|
if (!isFragmentAttachedBefore && activity != null) {
|
||||||
|
isFragmentAttachedBefore = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace FrameLayout with ContributionsListFragment, user will see contributions list. Creates
|
||||||
|
* new one if null.
|
||||||
|
*/
|
||||||
|
private fun showContributionsListFragment() {
|
||||||
|
// show nearby card view on contributions list is visible
|
||||||
|
if (binding!!.cardViewNearby != null && !isUserProfile) {
|
||||||
|
if (store!!.getBoolean("displayNearbyCardView", true)) {
|
||||||
|
if (binding!!.cardViewNearby.cardViewVisibilityState
|
||||||
|
== NearbyNotificationCardView.CardViewVisibilityState.READY
|
||||||
|
) {
|
||||||
|
binding!!.cardViewNearby.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding!!.cardViewNearby.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showFragment(
|
||||||
|
contributionsListFragment!!, CONTRIBUTION_LIST_FRAGMENT_TAG,
|
||||||
|
mediaDetailPagerFragment
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showMediaDetailPagerFragment() {
|
||||||
|
// hide nearby card view on media detail is visible
|
||||||
|
setupViewForMediaDetails()
|
||||||
|
showFragment(
|
||||||
|
mediaDetailPagerFragment!!, MEDIA_DETAIL_PAGER_FRAGMENT_TAG,
|
||||||
|
contributionsListFragment
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupViewForMediaDetails() {
|
||||||
|
if (binding != null) {
|
||||||
|
binding!!.campaignsView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackStackChanged() {
|
||||||
|
fetchCampaigns()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initFragments() {
|
||||||
|
if (null == contributionsListFragment) {
|
||||||
|
contributionsListFragment = ContributionsListFragment()
|
||||||
|
val contributionsListBundle = Bundle()
|
||||||
|
contributionsListBundle.putString(ProfileActivity.KEY_USERNAME, userName)
|
||||||
|
contributionsListFragment!!.arguments = contributionsListBundle
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldShowMediaDetailsFragment) {
|
||||||
|
showMediaDetailPagerFragment()
|
||||||
|
} else {
|
||||||
|
showContributionsListFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
showFragment(
|
||||||
|
contributionsListFragment!!, CONTRIBUTION_LIST_FRAGMENT_TAG,
|
||||||
|
mediaDetailPagerFragment
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the root frame layout with the given fragment
|
||||||
|
*
|
||||||
|
* @param fragment
|
||||||
|
* @param tag
|
||||||
|
* @param otherFragment
|
||||||
|
*/
|
||||||
|
private fun showFragment(fragment: Fragment, tag: String, otherFragment: Fragment?) {
|
||||||
|
val transaction = childFragmentManager.beginTransaction()
|
||||||
|
if (fragment.isAdded && otherFragment != null) {
|
||||||
|
transaction.hide(otherFragment)
|
||||||
|
transaction.show(fragment)
|
||||||
|
transaction.addToBackStack(tag)
|
||||||
|
transaction.commit()
|
||||||
|
childFragmentManager.executePendingTransactions()
|
||||||
|
} else if (fragment.isAdded && otherFragment == null) {
|
||||||
|
transaction.show(fragment)
|
||||||
|
transaction.addToBackStack(tag)
|
||||||
|
transaction.commit()
|
||||||
|
childFragmentManager.executePendingTransactions()
|
||||||
|
} else if (!fragment.isAdded && otherFragment != null) {
|
||||||
|
transaction.hide(otherFragment)
|
||||||
|
transaction.add(R.id.root_frame, fragment, tag)
|
||||||
|
transaction.addToBackStack(tag)
|
||||||
|
transaction.commit()
|
||||||
|
childFragmentManager.executePendingTransactions()
|
||||||
|
} else if (!fragment.isAdded) {
|
||||||
|
transaction.replace(R.id.root_frame, fragment, tag)
|
||||||
|
transaction.addToBackStack(tag)
|
||||||
|
transaction.commit()
|
||||||
|
childFragmentManager.executePendingTransactions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeFragment(fragment: Fragment) {
|
||||||
|
childFragmentManager
|
||||||
|
.beginTransaction()
|
||||||
|
.remove(fragment)
|
||||||
|
.commit()
|
||||||
|
childFragmentManager.executePendingTransactions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUploadCount() {
|
||||||
|
okHttpJsonApiClient
|
||||||
|
?.getUploadCount((activity as MainActivity).sessionManager?.currentAccount!!.name)
|
||||||
|
?.subscribeOn(Schedulers.io())
|
||||||
|
?.observeOn(AndroidSchedulers.mainThread())?.let {
|
||||||
|
compositeDisposable.add(
|
||||||
|
it
|
||||||
|
.subscribe(
|
||||||
|
{ uploadCount: Int -> this.displayUploadCount(uploadCount) },
|
||||||
|
{ t: Throwable? -> Timber.e(t, "Fetching upload count failed") }
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayUploadCount(uploadCount: Int) {
|
||||||
|
if (requireActivity().isFinishing
|
||||||
|
|| resources == null
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
(activity as MainActivity).setNumOfUploads(uploadCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
locationManager!!.removeLocationListener(this)
|
||||||
|
locationManager!!.unregisterLocationManager()
|
||||||
|
mSensorManager!!.unregisterListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
contributionsPresenter!!.onAttachView(this)
|
||||||
|
locationManager!!.addLocationListener(this)
|
||||||
|
|
||||||
|
if (binding == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding!!.cardViewNearby.permissionRequestButton.setOnClickListener { v: View? ->
|
||||||
|
showNearbyCardPermissionRationale()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification cards should only be seen on contributions list, not in media details
|
||||||
|
if (mediaDetailPagerFragment == null && !isUserProfile) {
|
||||||
|
if (store!!.getBoolean("displayNearbyCardView", true)) {
|
||||||
|
checkPermissionsAndShowNearbyCardView()
|
||||||
|
|
||||||
|
// Calling nearby card to keep showing it even when user clicks on it and comes back
|
||||||
|
try {
|
||||||
|
updateClosestNearbyCardViewInfo()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
if (binding!!.cardViewNearby.cardViewVisibilityState
|
||||||
|
== NearbyNotificationCardView.CardViewVisibilityState.READY
|
||||||
|
) {
|
||||||
|
binding!!.cardViewNearby.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Hide nearby notification card view if related shared preferences is false
|
||||||
|
binding!!.cardViewNearby.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification Count and Campaigns should not be set, if it is used in User Profile
|
||||||
|
if (!isUserProfile) {
|
||||||
|
setNotificationCount()
|
||||||
|
fetchCampaigns()
|
||||||
|
// Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
||||||
|
// setUploadIconVisibility();
|
||||||
|
setUploadIconCount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mSensorManager!!.registerListener(this, mLight, SensorManager.SENSOR_DELAY_UI)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkPermissionsAndShowNearbyCardView() {
|
||||||
|
if (hasPermission(
|
||||||
|
requireActivity(),
|
||||||
|
arrayOf(permission.ACCESS_FINE_LOCATION)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
onLocationPermissionGranted()
|
||||||
|
} else if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)
|
||||||
|
&& store!!.getBoolean("displayLocationPermissionForCardView", true)
|
||||||
|
&& !store!!.getBoolean("doNotAskForLocationPermission", false)
|
||||||
|
&& ((activity as MainActivity).activeFragment == ActiveFragment.CONTRIBUTIONS)
|
||||||
|
) {
|
||||||
|
binding!!.cardViewNearby.permissionType =
|
||||||
|
NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION
|
||||||
|
showNearbyCardPermissionRationale()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestLocationPermission() {
|
||||||
|
nearbyLocationPermissionLauncher.launch(arrayOf(permission.ACCESS_FINE_LOCATION))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onLocationPermissionGranted() {
|
||||||
|
binding!!.cardViewNearby.permissionType =
|
||||||
|
NearbyNotificationCardView.PermissionType.NO_PERMISSION_NEEDED
|
||||||
|
locationManager!!.registerLocationManager()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNearbyCardPermissionRationale() {
|
||||||
|
showAlertDialog(
|
||||||
|
requireActivity(),
|
||||||
|
getString(R.string.nearby_card_permission_title),
|
||||||
|
getString(R.string.nearby_card_permission_explanation),
|
||||||
|
{ this.requestLocationPermission() },
|
||||||
|
{ this.displayYouWontSeeNearbyMessage() },
|
||||||
|
checkBoxView
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayYouWontSeeNearbyMessage() {
|
||||||
|
showLongToast(
|
||||||
|
requireActivity(),
|
||||||
|
resources.getString(R.string.unable_to_display_nearest_place)
|
||||||
|
)
|
||||||
|
// Set to true as the user doesn't want the app to ask for location permission anymore
|
||||||
|
store!!.putBoolean("doNotAskForLocationPermission", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun updateClosestNearbyCardViewInfo() {
|
||||||
|
currentLatLng = locationManager!!.getLastLocation()
|
||||||
|
compositeDisposable.add(Observable.fromCallable {
|
||||||
|
nearbyController?.loadAttractionsFromLocation(
|
||||||
|
currentLatLng, currentLatLng, true,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
} // thanks to boolean, it will only return closest result
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ nearbyPlacesInfo: NearbyPlacesInfo? ->
|
||||||
|
this.updateNearbyNotification(
|
||||||
|
nearbyPlacesInfo
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ throwable: Throwable? ->
|
||||||
|
Timber.d(throwable)
|
||||||
|
updateNearbyNotification(null)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateNearbyNotification(
|
||||||
|
nearbyPlacesInfo: NearbyPlacesInfo?
|
||||||
|
) {
|
||||||
|
if (nearbyPlacesInfo?.placeList != null && nearbyPlacesInfo.placeList.size > 0) {
|
||||||
|
var closestNearbyPlace: Place? = null
|
||||||
|
// Find the first nearby place that has no image and exists
|
||||||
|
for (place in nearbyPlacesInfo.placeList) {
|
||||||
|
if (place.pic == "" && place.exists) {
|
||||||
|
closestNearbyPlace = place
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closestNearbyPlace == null) {
|
||||||
|
binding!!.cardViewNearby.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
val distance = formatDistanceBetween(currentLatLng, closestNearbyPlace.location)
|
||||||
|
closestNearbyPlace.setDistance(distance)
|
||||||
|
direction = computeBearing(currentLatLng!!, closestNearbyPlace.location).toFloat()
|
||||||
|
binding!!.cardViewNearby.updateContent(closestNearbyPlace)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Means that no close nearby place is found
|
||||||
|
binding!!.cardViewNearby.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent Nearby banner from appearing in Media Details, fixing bug https://github.com/commons-app/apps-android-commons/issues/4731
|
||||||
|
if (mediaDetailPagerFragment != null && !contributionsListFragment!!.isVisible) {
|
||||||
|
binding!!.cardViewNearby.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
try {
|
||||||
|
compositeDisposable.clear()
|
||||||
|
childFragmentManager.removeOnBackStackChangedListener(this)
|
||||||
|
locationManager!!.unregisterLocationManager()
|
||||||
|
locationManager!!.removeLocationListener(this)
|
||||||
|
super.onDestroy()
|
||||||
|
} catch (exception: IllegalArgumentException) {
|
||||||
|
Timber.e(exception)
|
||||||
|
} catch (exception: IllegalStateException) {
|
||||||
|
Timber.e(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLocationChangedSignificantly(latLng: LatLng) {
|
||||||
|
// Will be called if location changed more than 1000 meter
|
||||||
|
updateClosestNearbyCardViewInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLocationChangedSlightly(latLng: LatLng) {
|
||||||
|
/* Update closest nearby notification card onLocationChangedSlightly
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
updateClosestNearbyCardViewInfo()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLocationChangedMedium(latLng: LatLng) {
|
||||||
|
// Update closest nearby card view if location changed more than 500 meters
|
||||||
|
updateClosestNearbyCardViewInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(
|
||||||
|
view: View,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As the home screen has limited space, we have choosen to show either campaigns or WLM card.
|
||||||
|
* The WLM Card gets the priority over monuments, so if the WLM is going on we show that instead
|
||||||
|
* of campaigns on the campaigns card
|
||||||
|
*/
|
||||||
|
private fun fetchCampaigns() {
|
||||||
|
if (Utils.isMonumentsEnabled(Date())) {
|
||||||
|
if (binding != null) {
|
||||||
|
binding!!.campaignsView.setCampaign(wlmCampaign)
|
||||||
|
binding!!.campaignsView.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
} else if (store!!.getBoolean(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE, true)) {
|
||||||
|
presenter!!.getCampaigns()
|
||||||
|
} else {
|
||||||
|
if (binding != null) {
|
||||||
|
binding!!.campaignsView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showMessage(message: String) {
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showCampaigns(campaign: Campaign?) {
|
||||||
|
if (campaign != null && !isUserProfile) {
|
||||||
|
if (binding != null) {
|
||||||
|
binding!!.campaignsView.setCampaign(campaign)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
presenter!!.onDetachView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notifyDataSetChanged() {
|
||||||
|
if (mediaDetailPagerFragment != null) {
|
||||||
|
mediaDetailPagerFragment!!.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify the viewpager that number of items have changed.
|
||||||
|
*/
|
||||||
|
override fun viewPagerNotifyDataSetChanged() {
|
||||||
|
if (mediaDetailPagerFragment != null) {
|
||||||
|
mediaDetailPagerFragment!!.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the visibility and text of the pending uploads count TextView based on the given
|
||||||
|
* count.
|
||||||
|
*
|
||||||
|
* @param pendingCount The number of pending uploads.
|
||||||
|
*/
|
||||||
|
fun updatePendingIcon(pendingCount: Int) {
|
||||||
|
if (pendingUploadsCountTextView != null) {
|
||||||
|
if (pendingCount != 0) {
|
||||||
|
pendingUploadsCountTextView!!.visibility = View.VISIBLE
|
||||||
|
pendingUploadsCountTextView!!.text = pendingCount.toString()
|
||||||
|
} else {
|
||||||
|
pendingUploadsCountTextView!!.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the visibility and text of the error uploads TextView based on the given count.
|
||||||
|
*
|
||||||
|
* @param errorCount The number of error uploads.
|
||||||
|
*/
|
||||||
|
fun updateErrorIcon(errorCount: Int) {
|
||||||
|
if (uploadsErrorTextView != null) {
|
||||||
|
if (errorCount != 0) {
|
||||||
|
uploadsErrorTextView!!.visibility = View.VISIBLE
|
||||||
|
uploadsErrorTextView!!.text = errorCount.toString()
|
||||||
|
} else {
|
||||||
|
uploadsErrorTextView!!.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
||||||
|
* @param count The number of pending uploads.
|
||||||
|
*/
|
||||||
|
// public void updateUploadIcon(int count) {
|
||||||
|
// if (pendingUploadsImageView != null) {
|
||||||
|
// if (count != 0) {
|
||||||
|
// pendingUploadsImageView.setVisibility(View.VISIBLE);
|
||||||
|
// } else {
|
||||||
|
// pendingUploadsImageView.setVisibility(View.GONE);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
/**
|
||||||
|
* Replace whatever is in the current contributionsFragmentContainer view with
|
||||||
|
* mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects
|
||||||
|
* a contribution.
|
||||||
|
*/
|
||||||
|
override fun showDetail(position: Int, isWikipediaButtonDisplayed: Boolean) {
|
||||||
|
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment!!.isVisible) {
|
||||||
|
mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true)
|
||||||
|
if (isUserProfile) {
|
||||||
|
(activity as ProfileActivity).setScroll(false)
|
||||||
|
}
|
||||||
|
showMediaDetailPagerFragment()
|
||||||
|
}
|
||||||
|
mediaDetailPagerFragment!!.showImage(position, isWikipediaButtonDisplayed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMediaAtPosition(i: Int): Media? {
|
||||||
|
return contributionsListFragment!!.getMediaAtPosition(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTotalMediaCount(): Int {
|
||||||
|
return contributionsListFragment!!.totalMediaCount
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getContributionStateAt(position: Int): Int {
|
||||||
|
return contributionsListFragment!!.getContributionStateAt(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun backButtonClicked(): Boolean {
|
||||||
|
if (mediaDetailPagerFragment != null && mediaDetailPagerFragment!!.isVisible) {
|
||||||
|
if (store!!.getBoolean("displayNearbyCardView", true) && !isUserProfile) {
|
||||||
|
if (binding!!.cardViewNearby.cardViewVisibilityState
|
||||||
|
== NearbyNotificationCardView.CardViewVisibilityState.READY
|
||||||
|
) {
|
||||||
|
binding!!.cardViewNearby.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding!!.cardViewNearby.visibility = View.GONE
|
||||||
|
}
|
||||||
|
removeFragment(mediaDetailPagerFragment!!)
|
||||||
|
showFragment(
|
||||||
|
contributionsListFragment!!, CONTRIBUTION_LIST_FRAGMENT_TAG,
|
||||||
|
mediaDetailPagerFragment
|
||||||
|
)
|
||||||
|
if (isUserProfile) {
|
||||||
|
// Fragment is associated with ProfileActivity
|
||||||
|
// Enable ParentViewPager Scroll
|
||||||
|
(activity as ProfileActivity).setScroll(true)
|
||||||
|
} else {
|
||||||
|
fetchCampaigns()
|
||||||
|
}
|
||||||
|
if (activity is MainActivity) {
|
||||||
|
// Fragment is associated with MainActivity
|
||||||
|
(activity as BaseActivity).supportActionBar
|
||||||
|
?.setDisplayHomeAsUpEnabled(false)
|
||||||
|
(activity as MainActivity).showTabs()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this function updates the number of contributions
|
||||||
|
*/
|
||||||
|
fun upDateUploadCount() {
|
||||||
|
WorkManager.getInstance(context)
|
||||||
|
.getWorkInfosForUniqueWorkLiveData(UploadWorker::class.java.simpleName).observe(
|
||||||
|
viewLifecycleOwner
|
||||||
|
) { workInfos: List<WorkInfo?> ->
|
||||||
|
if (workInfos.size > 0) {
|
||||||
|
setUploadCount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restarts the upload process for a contribution
|
||||||
|
*
|
||||||
|
* @param contribution
|
||||||
|
*/
|
||||||
|
fun restartUpload(contribution: Contribution) {
|
||||||
|
contribution.dateUploadStarted = Calendar.getInstance().time
|
||||||
|
if (contribution.state == Contribution.STATE_FAILED) {
|
||||||
|
if (contribution.errorInfo == null) {
|
||||||
|
contribution.chunkInfo = null
|
||||||
|
contribution.transferred = 0
|
||||||
|
}
|
||||||
|
contributionsPresenter!!.checkDuplicateImageAndRestartContribution(contribution)
|
||||||
|
} else {
|
||||||
|
contribution.state = Contribution.STATE_QUEUED
|
||||||
|
contributionsPresenter!!.saveContribution(contribution)
|
||||||
|
Timber.d("Restarting for %s", contribution.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry upload when it is failed
|
||||||
|
*
|
||||||
|
* @param contribution contribution to be retried
|
||||||
|
*/
|
||||||
|
fun retryUpload(contribution: Contribution) {
|
||||||
|
if (isInternetConnectionEstablished(context)) {
|
||||||
|
if (contribution.state == Contribution.STATE_PAUSED) {
|
||||||
|
restartUpload(contribution)
|
||||||
|
} else if (contribution.state == Contribution.STATE_FAILED) {
|
||||||
|
val retries = contribution.retries
|
||||||
|
// TODO: Improve UX. Additional details: https://github.com/commons-app/apps-android-commons/pull/5257#discussion_r1304662562
|
||||||
|
/* Limit the number of retries for a failed upload
|
||||||
|
to handle cases like invalid filename as such uploads
|
||||||
|
will never be successful */
|
||||||
|
if (retries < MAX_RETRIES) {
|
||||||
|
contribution.retries = retries + 1
|
||||||
|
Timber.d(
|
||||||
|
"Retried uploading %s %d times", contribution.media.filename,
|
||||||
|
retries + 1
|
||||||
|
)
|
||||||
|
restartUpload(contribution)
|
||||||
|
} else {
|
||||||
|
// TODO: Show the exact reason for failure
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
R.string.retry_limit_reached, Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.d("Skipping re-upload for non-failed %s", contribution.toString())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showLongToast(context, R.string.this_function_needs_network_connection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload media detail fragment once media is nominated
|
||||||
|
*
|
||||||
|
* @param index item position that has been nominated
|
||||||
|
*/
|
||||||
|
override fun refreshNominatedMedia(index: Int) {
|
||||||
|
if (mediaDetailPagerFragment != null && !contributionsListFragment!!.isVisible) {
|
||||||
|
removeFragment(mediaDetailPagerFragment!!)
|
||||||
|
mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true)
|
||||||
|
mediaDetailPagerFragment?.showImage(index)
|
||||||
|
showMediaDetailPagerFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the device rotates, rotate the Nearby banner's compass arrow in tandem.
|
||||||
|
*/
|
||||||
|
override fun onSensorChanged(event: SensorEvent) {
|
||||||
|
val rotateDegree = Math.round(event.values[0]).toFloat()
|
||||||
|
binding!!.cardViewNearby.rotateCompass(rotateDegree, direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
|
||||||
|
// Nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag"
|
||||||
|
const val MEDIA_DETAIL_PAGER_FRAGMENT_TAG: String = "MediaDetailFragmentTag"
|
||||||
|
private const val MAX_RETRIES = 10
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun newInstance(): ContributionsFragment {
|
||||||
|
val fragment = ContributionsFragment()
|
||||||
|
fragment.retainInstance = true
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.paging.PagedListAdapter;
|
|
||||||
import androidx.recyclerview.widget.DiffUtil;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.media.MediaClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents The View Adapter for the List of Contributions
|
|
||||||
*/
|
|
||||||
public class ContributionsListAdapter extends
|
|
||||||
PagedListAdapter<Contribution, ContributionViewHolder> {
|
|
||||||
|
|
||||||
private final Callback callback;
|
|
||||||
private final MediaClient mediaClient;
|
|
||||||
|
|
||||||
ContributionsListAdapter(final Callback callback,
|
|
||||||
final MediaClient mediaClient) {
|
|
||||||
super(DIFF_CALLBACK);
|
|
||||||
this.callback = callback;
|
|
||||||
this.mediaClient = mediaClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uses DiffUtil to calculate the changes in the list
|
|
||||||
* It has methods that check ID and the content of the items to determine if its a new item
|
|
||||||
*/
|
|
||||||
private static final DiffUtil.ItemCallback<Contribution> DIFF_CALLBACK =
|
|
||||||
new DiffUtil.ItemCallback<Contribution>() {
|
|
||||||
@Override
|
|
||||||
public boolean areItemsTheSame(final Contribution oldContribution, final Contribution newContribution) {
|
|
||||||
return oldContribution.getPageId().equals(newContribution.getPageId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean areContentsTheSame(final Contribution oldContribution, final Contribution newContribution) {
|
|
||||||
return oldContribution.equals(newContribution);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the view holder with contribution data
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) {
|
|
||||||
holder.init(position, getItem(position));
|
|
||||||
}
|
|
||||||
|
|
||||||
Contribution getContributionForPosition(final int position) {
|
|
||||||
return getItem(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the new View Holder which will be used to display items(contributions) using the
|
|
||||||
* onBindViewHolder(viewHolder,position)
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ContributionViewHolder onCreateViewHolder(@NonNull final ViewGroup parent,
|
|
||||||
final int viewType) {
|
|
||||||
final ContributionViewHolder viewHolder = new ContributionViewHolder(
|
|
||||||
LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.layout_contribution, parent, false),
|
|
||||||
callback, mediaClient);
|
|
||||||
return viewHolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Callback {
|
|
||||||
|
|
||||||
void openMediaDetail(int contribution, boolean isWikipediaPageExists);
|
|
||||||
|
|
||||||
void addImageToWikipedia(Contribution contribution);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.paging.PagedListAdapter
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents The View Adapter for the List of Contributions
|
||||||
|
*/
|
||||||
|
class ContributionsListAdapter internal constructor(
|
||||||
|
private val callback: Callback,
|
||||||
|
private val mediaClient: MediaClient
|
||||||
|
) : PagedListAdapter<Contribution, ContributionViewHolder>(DIFF_CALLBACK) {
|
||||||
|
/**
|
||||||
|
* Initializes the view holder with contribution data
|
||||||
|
*/
|
||||||
|
override fun onBindViewHolder(holder: ContributionViewHolder, position: Int) {
|
||||||
|
holder.init(position, getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getContributionForPosition(position: Int): Contribution? {
|
||||||
|
return getItem(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the new View Holder which will be used to display items(contributions) using the
|
||||||
|
* onBindViewHolder(viewHolder,position)
|
||||||
|
*/
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): ContributionViewHolder {
|
||||||
|
val viewHolder = ContributionViewHolder(
|
||||||
|
LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.layout_contribution, parent, false),
|
||||||
|
callback, mediaClient
|
||||||
|
)
|
||||||
|
return viewHolder
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun openMediaDetail(contribution: Int, isWikipediaPageExists: Boolean)
|
||||||
|
|
||||||
|
fun addImageToWikipedia(contribution: Contribution?)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Uses DiffUtil to calculate the changes in the list
|
||||||
|
* It has methods that check ID and the content of the items to determine if its a new item
|
||||||
|
*/
|
||||||
|
private val DIFF_CALLBACK: DiffUtil.ItemCallback<Contribution> =
|
||||||
|
object : DiffUtil.ItemCallback<Contribution>() {
|
||||||
|
override fun areItemsTheSame(
|
||||||
|
oldContribution: Contribution,
|
||||||
|
newContribution: Contribution
|
||||||
|
): Boolean {
|
||||||
|
return oldContribution.pageId == newContribution.pageId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(
|
||||||
|
oldContribution: Contribution,
|
||||||
|
newContribution: Contribution
|
||||||
|
): Boolean {
|
||||||
|
return oldContribution == newContribution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|
||||||
import fr.free.nrw.commons.BasePresenter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The contract for Contributions list View & Presenter
|
|
||||||
*/
|
|
||||||
public class ContributionsListContract {
|
|
||||||
|
|
||||||
public interface View {
|
|
||||||
|
|
||||||
void showWelcomeTip(boolean numberOfUploads);
|
|
||||||
|
|
||||||
void showProgress(boolean shouldShow);
|
|
||||||
|
|
||||||
void showNoContributionsUI(boolean shouldShow);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface UserActionListener extends BasePresenter<View> {
|
|
||||||
|
|
||||||
void refreshList(SwipeRefreshLayout swipeRefreshLayout);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import fr.free.nrw.commons.BasePresenter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contract for Contributions list View & Presenter
|
||||||
|
*/
|
||||||
|
class ContributionsListContract {
|
||||||
|
interface View {
|
||||||
|
fun showWelcomeTip(numberOfUploads: Boolean)
|
||||||
|
|
||||||
|
fun showProgress(shouldShow: Boolean)
|
||||||
|
|
||||||
|
fun showNoContributionsUI(shouldShow: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserActionListener : BasePresenter<View?> {
|
||||||
|
fun refreshList(swipeRefreshLayout: SwipeRefreshLayout?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,534 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import static android.view.View.GONE;
|
|
||||||
import static android.view.View.VISIBLE;
|
|
||||||
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE;
|
|
||||||
|
|
||||||
import android.Manifest.permission;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.animation.Animation;
|
|
||||||
import android.view.animation.AnimationUtils;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import androidx.activity.result.ActivityResultCallback;
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.ItemAnimator;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
|
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator;
|
|
||||||
import fr.free.nrw.commons.Media;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.Utils;
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
|
|
||||||
import fr.free.nrw.commons.databinding.FragmentContributionsListBinding;
|
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
|
||||||
import fr.free.nrw.commons.media.MediaClient;
|
|
||||||
import fr.free.nrw.commons.profile.ProfileActivity;
|
|
||||||
import fr.free.nrw.commons.utils.DialogUtil;
|
|
||||||
import fr.free.nrw.commons.utils.SystemThemeUtils;
|
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import fr.free.nrw.commons.wikidata.model.WikiSite;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by root on 01.06.2018.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class ContributionsListFragment extends CommonsDaggerSupportFragment implements
|
|
||||||
ContributionsListContract.View, Callback,
|
|
||||||
WikipediaInstructionsDialogFragment.Callback {
|
|
||||||
|
|
||||||
private static final String RV_STATE = "rv_scroll_state";
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
SystemThemeUtils systemThemeUtils;
|
|
||||||
@Inject
|
|
||||||
ContributionController controller;
|
|
||||||
@Inject
|
|
||||||
MediaClient mediaClient;
|
|
||||||
@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
|
|
||||||
@Inject
|
|
||||||
WikiSite languageWikipediaSite;
|
|
||||||
@Inject
|
|
||||||
ContributionsListPresenter contributionsListPresenter;
|
|
||||||
@Inject
|
|
||||||
SessionManager sessionManager;
|
|
||||||
|
|
||||||
private FragmentContributionsListBinding binding;
|
|
||||||
private Animation fab_close;
|
|
||||||
private Animation fab_open;
|
|
||||||
private Animation rotate_forward;
|
|
||||||
private Animation rotate_backward;
|
|
||||||
private boolean isFabOpen;
|
|
||||||
@VisibleForTesting
|
|
||||||
protected RecyclerView rvContributionsList;
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
protected ContributionsListAdapter adapter;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@VisibleForTesting
|
|
||||||
protected Callback callback;
|
|
||||||
|
|
||||||
private final int SPAN_COUNT_LANDSCAPE = 3;
|
|
||||||
private final int SPAN_COUNT_PORTRAIT = 1;
|
|
||||||
|
|
||||||
private int contributionsSize;
|
|
||||||
private String userName;
|
|
||||||
|
|
||||||
private final ActivityResultLauncher<Intent> galleryPickLauncherForResult =
|
|
||||||
registerForActivityResult(new StartActivityForResult(),
|
|
||||||
result -> {
|
|
||||||
controller.handleActivityResultWithCallback(requireActivity(), callbacks -> {
|
|
||||||
controller.onPictureReturnedFromGallery(result, requireActivity(), callbacks);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
private final ActivityResultLauncher<Intent> customSelectorLauncherForResult =
|
|
||||||
registerForActivityResult(new StartActivityForResult(),
|
|
||||||
result -> {
|
|
||||||
controller.handleActivityResultWithCallback(requireActivity(), callbacks -> {
|
|
||||||
controller.onPictureReturnedFromCustomSelector(result, requireActivity(), callbacks);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
private final ActivityResultLauncher<Intent> cameraPickLauncherForResult =
|
|
||||||
registerForActivityResult(new StartActivityForResult(),
|
|
||||||
result -> {
|
|
||||||
controller.handleActivityResultWithCallback(requireActivity(), callbacks -> {
|
|
||||||
controller.onPictureReturnedFromCamera(result, requireActivity(), callbacks);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(
|
|
||||||
new RequestMultiplePermissions(),
|
|
||||||
new ActivityResultCallback<Map<String, Boolean>>() {
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(Map<String, Boolean> result) {
|
|
||||||
boolean areAllGranted = true;
|
|
||||||
for (final boolean b : result.values()) {
|
|
||||||
areAllGranted = areAllGranted && b;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (areAllGranted) {
|
|
||||||
controller.locationPermissionCallback.onLocationPermissionGranted();
|
|
||||||
} else {
|
|
||||||
if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
|
|
||||||
controller.handleShowRationaleFlowCameraLocation(getActivity(),
|
|
||||||
inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult);
|
|
||||||
} else {
|
|
||||||
controller.locationPermissionCallback.onLocationPermissionDenied(
|
|
||||||
getActivity().getString(
|
|
||||||
R.string.in_app_camera_location_permission_denied));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(
|
|
||||||
@Nullable @org.jetbrains.annotations.Nullable final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
//Now that we are allowing this fragment to be started for
|
|
||||||
// any userName- we expect it to be passed as an argument
|
|
||||||
if (getArguments() != null) {
|
|
||||||
userName = getArguments().getString(ProfileActivity.KEY_USERNAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.isEmpty(userName)) {
|
|
||||||
userName = sessionManager.getUserName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(
|
|
||||||
final LayoutInflater inflater, @Nullable final ViewGroup container,
|
|
||||||
@Nullable final Bundle savedInstanceState) {
|
|
||||||
binding = FragmentContributionsListBinding.inflate(
|
|
||||||
inflater, container, false
|
|
||||||
);
|
|
||||||
rvContributionsList = binding.contributionsList;
|
|
||||||
|
|
||||||
contributionsListPresenter.onAttachView(this);
|
|
||||||
binding.fabCustomGallery.setOnClickListener(v -> launchCustomSelector());
|
|
||||||
binding.fabCustomGallery.setOnLongClickListener(view -> {
|
|
||||||
ViewUtil.showShortToast(getContext(), R.string.custom_selector_title);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Objects.equals(sessionManager.getUserName(), userName)) {
|
|
||||||
binding.tvContributionsOfUser.setVisibility(GONE);
|
|
||||||
binding.fabLayout.setVisibility(VISIBLE);
|
|
||||||
} else {
|
|
||||||
binding.tvContributionsOfUser.setVisibility(VISIBLE);
|
|
||||||
binding.tvContributionsOfUser.setText(
|
|
||||||
getString(R.string.contributions_of_user, userName));
|
|
||||||
binding.fabLayout.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
initAdapter();
|
|
||||||
|
|
||||||
// pull down to refresh only enabled for self user.
|
|
||||||
if(Objects.equals(sessionManager.getUserName(), userName)){
|
|
||||||
binding.swipeRefreshLayout.setOnRefreshListener(() -> {
|
|
||||||
contributionsListPresenter.refreshList(binding.swipeRefreshLayout);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
binding.swipeRefreshLayout.setEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding.getRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
binding = null;
|
|
||||||
super.onDestroyView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(Context context) {
|
|
||||||
super.onAttach(context);
|
|
||||||
if (getParentFragment() != null && getParentFragment() instanceof ContributionsFragment) {
|
|
||||||
callback = ((ContributionsFragment) getParentFragment());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetach() {
|
|
||||||
super.onDetach();
|
|
||||||
callback = null;//To avoid possible memory leak
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initAdapter() {
|
|
||||||
adapter = new ContributionsListAdapter(this, mediaClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
initRecyclerView();
|
|
||||||
initializeAnimations();
|
|
||||||
setListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initRecyclerView() {
|
|
||||||
final GridLayoutManager layoutManager = new GridLayoutManager(getContext(),
|
|
||||||
getSpanCount(getResources().getConfiguration().orientation));
|
|
||||||
rvContributionsList.setLayoutManager(layoutManager);
|
|
||||||
|
|
||||||
//Setting flicker animation of recycler view to false.
|
|
||||||
final ItemAnimator animator = rvContributionsList.getItemAnimator();
|
|
||||||
if (animator instanceof SimpleItemAnimator) {
|
|
||||||
((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
contributionsListPresenter.setup(userName,
|
|
||||||
Objects.equals(sessionManager.getUserName(), userName));
|
|
||||||
contributionsListPresenter.contributionList.observe(getViewLifecycleOwner(), list -> {
|
|
||||||
contributionsSize = list.size();
|
|
||||||
adapter.submitList(list);
|
|
||||||
if (callback != null) {
|
|
||||||
callback.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
rvContributionsList.setAdapter(adapter);
|
|
||||||
adapter.registerAdapterDataObserver(new AdapterDataObserver() {
|
|
||||||
@Override
|
|
||||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
|
||||||
super.onItemRangeInserted(positionStart, itemCount);
|
|
||||||
contributionsSize = adapter.getItemCount();
|
|
||||||
if (callback != null) {
|
|
||||||
callback.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
if (itemCount > 0 && positionStart == 0) {
|
|
||||||
if (adapter.getContributionForPosition(positionStart) != null) {
|
|
||||||
rvContributionsList
|
|
||||||
.scrollToPosition(0);//Newly upload items are always added to the top
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called whenever items in the list have changed
|
|
||||||
* Calls viewPagerNotifyDataSetChanged() that will notify the viewpager
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onItemRangeChanged(final int positionStart, final int itemCount) {
|
|
||||||
super.onItemRangeChanged(positionStart, itemCount);
|
|
||||||
if (callback != null) {
|
|
||||||
callback.viewPagerNotifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//Fab close on touch outside (Scrolling or taping on item triggers this action).
|
|
||||||
rvContributionsList.addOnItemTouchListener(new OnItemTouchListener() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Silently observe and/or take over touch events sent to the RecyclerView before
|
|
||||||
* they are handled by either the RecyclerView itself or its child views.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
|
||||||
if (e.getAction() == MotionEvent.ACTION_DOWN) {
|
|
||||||
if (isFabOpen) {
|
|
||||||
animateFAB(isFabOpen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process a touch event as part of a gesture that was claimed by returning true
|
|
||||||
* from a previous call to {@link #onInterceptTouchEvent}.
|
|
||||||
*
|
|
||||||
* @param rv
|
|
||||||
* @param e MotionEvent describing the touch event. All coordinates are in the
|
|
||||||
* RecyclerView's coordinate system.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
|
||||||
//required abstract method DO NOT DELETE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a child of RecyclerView does not want RecyclerView and its ancestors
|
|
||||||
* to intercept touch events with {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
|
|
||||||
*
|
|
||||||
* @param disallowIntercept True if the child does not want the parent to intercept
|
|
||||||
* touch events.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
|
|
||||||
//required abstract method DO NOT DELETE
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getSpanCount(final int orientation) {
|
|
||||||
return orientation == Configuration.ORIENTATION_LANDSCAPE ?
|
|
||||||
SPAN_COUNT_LANDSCAPE : SPAN_COUNT_PORTRAIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConfigurationChanged(final Configuration newConfig) {
|
|
||||||
super.onConfigurationChanged(newConfig);
|
|
||||||
// check orientation
|
|
||||||
binding.fabLayout.setOrientation(
|
|
||||||
newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
|
|
||||||
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
|
|
||||||
rvContributionsList
|
|
||||||
.setLayoutManager(
|
|
||||||
new GridLayoutManager(getContext(), getSpanCount(newConfig.orientation)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeAnimations() {
|
|
||||||
fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open);
|
|
||||||
fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close);
|
|
||||||
rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward);
|
|
||||||
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setListeners() {
|
|
||||||
binding.fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
|
|
||||||
binding.fabCamera.setOnClickListener(view -> {
|
|
||||||
controller.initiateCameraPick(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult);
|
|
||||||
animateFAB(isFabOpen);
|
|
||||||
});
|
|
||||||
binding.fabCamera.setOnLongClickListener(view -> {
|
|
||||||
ViewUtil.showShortToast(getContext(), R.string.add_contribution_from_camera);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
binding.fabGallery.setOnClickListener(view -> {
|
|
||||||
controller.initiateGalleryPick(getActivity(), galleryPickLauncherForResult, true);
|
|
||||||
animateFAB(isFabOpen);
|
|
||||||
});
|
|
||||||
binding.fabGallery.setOnLongClickListener(view -> {
|
|
||||||
ViewUtil.showShortToast(getContext(), R.string.menu_from_gallery);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch Custom Selector.
|
|
||||||
*/
|
|
||||||
protected void launchCustomSelector() {
|
|
||||||
controller.initiateCustomGalleryPickWithPermission(getActivity(), customSelectorLauncherForResult);
|
|
||||||
animateFAB(isFabOpen);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void scrollToTop() {
|
|
||||||
rvContributionsList.smoothScrollToPosition(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateFAB(final boolean isFabOpen) {
|
|
||||||
this.isFabOpen = !isFabOpen;
|
|
||||||
if (binding.fabPlus.isShown()) {
|
|
||||||
if (isFabOpen) {
|
|
||||||
binding.fabPlus.startAnimation(rotate_backward);
|
|
||||||
binding.fabCamera.startAnimation(fab_close);
|
|
||||||
binding.fabGallery.startAnimation(fab_close);
|
|
||||||
binding.fabCustomGallery.startAnimation(fab_close);
|
|
||||||
binding.fabCamera.hide();
|
|
||||||
binding.fabGallery.hide();
|
|
||||||
binding.fabCustomGallery.hide();
|
|
||||||
} else {
|
|
||||||
binding.fabPlus.startAnimation(rotate_forward);
|
|
||||||
binding.fabCamera.startAnimation(fab_open);
|
|
||||||
binding.fabGallery.startAnimation(fab_open);
|
|
||||||
binding.fabCustomGallery.startAnimation(fab_open);
|
|
||||||
binding.fabCamera.show();
|
|
||||||
binding.fabGallery.show();
|
|
||||||
binding.fabCustomGallery.show();
|
|
||||||
}
|
|
||||||
this.isFabOpen = !isFabOpen;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows welcome message if user has no contributions yet i.e. new user.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void showWelcomeTip(final boolean shouldShow) {
|
|
||||||
binding.noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Responsible to set progress bar invisible and visible
|
|
||||||
*
|
|
||||||
* @param shouldShow True when contributions list should be hidden.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void showProgress(final boolean shouldShow) {
|
|
||||||
binding.loadingContributionsProgressBar.setVisibility(shouldShow ? VISIBLE : GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showNoContributionsUI(final boolean shouldShow) {
|
|
||||||
binding.noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
final GridLayoutManager layoutManager = (GridLayoutManager) rvContributionsList
|
|
||||||
.getLayoutManager();
|
|
||||||
outState.putParcelable(RV_STATE, layoutManager.onSaveInstanceState());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onViewStateRestored(savedInstanceState);
|
|
||||||
if (null != savedInstanceState) {
|
|
||||||
final Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(RV_STATE);
|
|
||||||
rvContributionsList.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void openMediaDetail(final int position, boolean isWikipediaButtonDisplayed) {
|
|
||||||
if (null != callback) {//Just being safe, ideally they won't be called when detached
|
|
||||||
callback.showDetail(position, isWikipediaButtonDisplayed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle callback for wikipedia icon clicked
|
|
||||||
*
|
|
||||||
* @param contribution
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void addImageToWikipedia(Contribution contribution) {
|
|
||||||
DialogUtil.showAlertDialog(getActivity(),
|
|
||||||
getString(R.string.add_picture_to_wikipedia_article_title),
|
|
||||||
getString(R.string.add_picture_to_wikipedia_article_desc),
|
|
||||||
() -> {
|
|
||||||
showAddImageToWikipediaInstructions(contribution);
|
|
||||||
}, () -> {
|
|
||||||
// do nothing
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display confirmation dialog with instructions when the user tries to add image to wikipedia
|
|
||||||
*
|
|
||||||
* @param contribution
|
|
||||||
*/
|
|
||||||
private void showAddImageToWikipediaInstructions(Contribution contribution) {
|
|
||||||
FragmentManager fragmentManager = getFragmentManager();
|
|
||||||
WikipediaInstructionsDialogFragment fragment = WikipediaInstructionsDialogFragment
|
|
||||||
.newInstance(contribution);
|
|
||||||
fragment.setCallback(this::onConfirmClicked);
|
|
||||||
fragment.show(fragmentManager, "WikimediaFragment");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Media getMediaAtPosition(final int i) {
|
|
||||||
if (adapter.getContributionForPosition(i) != null) {
|
|
||||||
return adapter.getContributionForPosition(i).getMedia();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalMediaCount() {
|
|
||||||
return contributionsSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the editor for the language Wikipedia
|
|
||||||
*
|
|
||||||
* @param contribution
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onConfirmClicked(@Nullable Contribution contribution, boolean copyWikicode) {
|
|
||||||
if (copyWikicode) {
|
|
||||||
String wikicode = contribution.getMedia().getWikiCode();
|
|
||||||
Utils.copy("wikicode", wikicode, getContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
final String url =
|
|
||||||
languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace()
|
|
||||||
.getWikipediaPageTitle();
|
|
||||||
Utils.handleWebUrl(getContext(), Uri.parse(url));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getContributionStateAt(int position) {
|
|
||||||
return adapter.getContributionForPosition(position).getState();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Callback {
|
|
||||||
|
|
||||||
void notifyDataSetChanged();
|
|
||||||
|
|
||||||
void showDetail(int position, boolean isWikipediaButtonDisplayed);
|
|
||||||
|
|
||||||
// Notify the viewpager that number of items have changed.
|
|
||||||
void viewPagerNotifyDataSetChanged();
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,551 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import android.Manifest.permission
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.AnimationUtils
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import fr.free.nrw.commons.Media
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.Utils
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
|
import fr.free.nrw.commons.contributions.WikipediaInstructionsDialogFragment.Companion.newInstance
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentContributionsListBinding
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||||
|
import fr.free.nrw.commons.di.NetworkingModule
|
||||||
|
import fr.free.nrw.commons.filepicker.FilePicker
|
||||||
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
|
import fr.free.nrw.commons.profile.ProfileActivity
|
||||||
|
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||||
|
import fr.free.nrw.commons.utils.SystemThemeUtils
|
||||||
|
import fr.free.nrw.commons.utils.ViewUtil.showShortToast
|
||||||
|
import fr.free.nrw.commons.wikidata.model.WikiSite
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by root on 01.06.2018.
|
||||||
|
*/
|
||||||
|
class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsListContract.View,
|
||||||
|
ContributionsListAdapter.Callback, WikipediaInstructionsDialogFragment.Callback {
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var systemThemeUtils: SystemThemeUtils? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var controller: ContributionController? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var mediaClient: MediaClient? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Named(NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
|
||||||
|
@Inject
|
||||||
|
var languageWikipediaSite: WikiSite? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var contributionsListPresenter: ContributionsListPresenter? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var sessionManager: SessionManager? = null
|
||||||
|
|
||||||
|
private var binding: FragmentContributionsListBinding? = null
|
||||||
|
private var fab_close: Animation? = null
|
||||||
|
private var fab_open: Animation? = null
|
||||||
|
private var rotate_forward: Animation? = null
|
||||||
|
private var rotate_backward: Animation? = null
|
||||||
|
private var isFabOpen = false
|
||||||
|
|
||||||
|
private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
var rvContributionsList: RecyclerView? = null
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
var adapter: ContributionsListAdapter? = null
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
|
private val SPAN_COUNT_LANDSCAPE = 3
|
||||||
|
private val SPAN_COUNT_PORTRAIT = 1
|
||||||
|
|
||||||
|
private var contributionsSize = 0
|
||||||
|
private var userName: String? = null
|
||||||
|
|
||||||
|
private val galleryPickLauncherForResult = registerForActivityResult<Intent, ActivityResult>(
|
||||||
|
StartActivityForResult()
|
||||||
|
) { result: ActivityResult? ->
|
||||||
|
controller!!.handleActivityResultWithCallback(requireActivity()
|
||||||
|
) { callbacks: FilePicker.Callbacks? ->
|
||||||
|
controller!!.onPictureReturnedFromGallery(
|
||||||
|
result!!, requireActivity(), callbacks!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val customSelectorLauncherForResult = registerForActivityResult<Intent, ActivityResult>(
|
||||||
|
StartActivityForResult()
|
||||||
|
) { result: ActivityResult? ->
|
||||||
|
controller!!.handleActivityResultWithCallback(requireActivity()
|
||||||
|
) { callbacks: FilePicker.Callbacks? ->
|
||||||
|
controller!!.onPictureReturnedFromCustomSelector(
|
||||||
|
result!!, requireActivity(), callbacks!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cameraPickLauncherForResult = registerForActivityResult<Intent, ActivityResult>(
|
||||||
|
StartActivityForResult()
|
||||||
|
) { result: ActivityResult? ->
|
||||||
|
controller!!.handleActivityResultWithCallback(requireActivity()
|
||||||
|
) { callbacks: FilePicker.Callbacks? ->
|
||||||
|
controller!!.onPictureReturnedFromCamera(
|
||||||
|
result!!, requireActivity(), callbacks!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
override fun onCreate(
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
//Now that we are allowing this fragment to be started for
|
||||||
|
// any userName- we expect it to be passed as an argument
|
||||||
|
if (arguments != null) {
|
||||||
|
userName = requireArguments().getString(ProfileActivity.KEY_USERNAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isEmpty(userName)) {
|
||||||
|
userName = sessionManager!!.userName
|
||||||
|
}
|
||||||
|
inAppCameraLocationPermissionLauncher =
|
||||||
|
registerForActivityResult(RequestMultiplePermissions()) { result ->
|
||||||
|
val areAllGranted = result.values.all { it }
|
||||||
|
|
||||||
|
if (areAllGranted) {
|
||||||
|
controller?.locationPermissionCallback?.onLocationPermissionGranted()
|
||||||
|
} else {
|
||||||
|
activity?.let { currentActivity ->
|
||||||
|
if (currentActivity.shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
|
||||||
|
controller?.handleShowRationaleFlowCameraLocation(
|
||||||
|
currentActivity,
|
||||||
|
inAppCameraLocationPermissionLauncher, // Pass launcher
|
||||||
|
cameraPickLauncherForResult
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
controller?.locationPermissionCallback?.onLocationPermissionDenied(
|
||||||
|
currentActivity.getString(R.string.in_app_camera_location_permission_denied)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
binding = FragmentContributionsListBinding.inflate(
|
||||||
|
inflater, container, false
|
||||||
|
)
|
||||||
|
rvContributionsList = binding!!.contributionsList
|
||||||
|
|
||||||
|
contributionsListPresenter!!.onAttachView(this)
|
||||||
|
binding!!.fabCustomGallery.setOnClickListener { v: View? -> launchCustomSelector() }
|
||||||
|
binding!!.fabCustomGallery.setOnLongClickListener { view: View? ->
|
||||||
|
showShortToast(context, fr.free.nrw.commons.R.string.custom_selector_title)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionManager!!.userName == userName) {
|
||||||
|
binding!!.tvContributionsOfUser.visibility = View.GONE
|
||||||
|
binding!!.fabLayout.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding!!.tvContributionsOfUser.visibility = View.VISIBLE
|
||||||
|
binding!!.tvContributionsOfUser.text =
|
||||||
|
getString(fr.free.nrw.commons.R.string.contributions_of_user, userName)
|
||||||
|
binding!!.fabLayout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
initAdapter()
|
||||||
|
|
||||||
|
// pull down to refresh only enabled for self user.
|
||||||
|
if (sessionManager!!.userName == userName) {
|
||||||
|
binding!!.swipeRefreshLayout.setOnRefreshListener {
|
||||||
|
contributionsListPresenter!!.refreshList(
|
||||||
|
binding!!.swipeRefreshLayout
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding!!.swipeRefreshLayout.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return binding!!.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
binding = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
if (parentFragment != null && parentFragment is ContributionsFragment) {
|
||||||
|
callback = (parentFragment as ContributionsFragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
super.onDetach()
|
||||||
|
callback = null //To avoid possible memory leak
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initAdapter() {
|
||||||
|
adapter = ContributionsListAdapter(this, mediaClient!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
initRecyclerView()
|
||||||
|
initializeAnimations()
|
||||||
|
setListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initRecyclerView() {
|
||||||
|
val layoutManager = GridLayoutManager(
|
||||||
|
context,
|
||||||
|
getSpanCount(resources.configuration.orientation)
|
||||||
|
)
|
||||||
|
rvContributionsList!!.layoutManager = layoutManager
|
||||||
|
|
||||||
|
//Setting flicker animation of recycler view to false.
|
||||||
|
val animator = rvContributionsList!!.itemAnimator
|
||||||
|
if (animator is SimpleItemAnimator) {
|
||||||
|
animator.supportsChangeAnimations = false
|
||||||
|
}
|
||||||
|
|
||||||
|
contributionsListPresenter!!.setup(
|
||||||
|
userName,
|
||||||
|
sessionManager!!.userName == userName
|
||||||
|
)
|
||||||
|
contributionsListPresenter!!.contributionList?.observe(
|
||||||
|
viewLifecycleOwner
|
||||||
|
) { list: PagedList<Contribution>? ->
|
||||||
|
if (list != null) {
|
||||||
|
contributionsSize = list.size
|
||||||
|
}
|
||||||
|
adapter!!.submitList(list)
|
||||||
|
if (callback != null) {
|
||||||
|
callback!!.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rvContributionsList!!.adapter = adapter
|
||||||
|
adapter!!.registerAdapterDataObserver(object : AdapterDataObserver() {
|
||||||
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
|
super.onItemRangeInserted(positionStart, itemCount)
|
||||||
|
contributionsSize = adapter!!.itemCount
|
||||||
|
if (callback != null) {
|
||||||
|
callback!!.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
if (itemCount > 0 && positionStart == 0) {
|
||||||
|
if (adapter!!.getContributionForPosition(positionStart) != null) {
|
||||||
|
rvContributionsList!!
|
||||||
|
.scrollToPosition(0) //Newly upload items are always added to the top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever items in the list have changed
|
||||||
|
* Calls viewPagerNotifyDataSetChanged() that will notify the viewpager
|
||||||
|
*/
|
||||||
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
||||||
|
super.onItemRangeChanged(positionStart, itemCount)
|
||||||
|
if (callback != null) {
|
||||||
|
callback!!.viewPagerNotifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//Fab close on touch outside (Scrolling or taping on item triggers this action).
|
||||||
|
rvContributionsList!!.addOnItemTouchListener(object : OnItemTouchListener {
|
||||||
|
/**
|
||||||
|
* Silently observe and/or take over touch events sent to the RecyclerView before
|
||||||
|
* they are handled by either the RecyclerView itself or its child views.
|
||||||
|
*/
|
||||||
|
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
|
||||||
|
if (e.action == MotionEvent.ACTION_DOWN) {
|
||||||
|
if (isFabOpen) {
|
||||||
|
animateFAB(isFabOpen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a touch event as part of a gesture that was claimed by returning true
|
||||||
|
* from a previous call to [.onInterceptTouchEvent].
|
||||||
|
*
|
||||||
|
* @param rv
|
||||||
|
* @param e MotionEvent describing the touch event. All coordinates are in the
|
||||||
|
* RecyclerView's coordinate system.
|
||||||
|
*/
|
||||||
|
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {
|
||||||
|
//required abstract method DO NOT DELETE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a child of RecyclerView does not want RecyclerView and its ancestors
|
||||||
|
* to intercept touch events with [ViewGroup.onInterceptTouchEvent].
|
||||||
|
*
|
||||||
|
* @param disallowIntercept True if the child does not want the parent to intercept
|
||||||
|
* touch events.
|
||||||
|
*/
|
||||||
|
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
||||||
|
//required abstract method DO NOT DELETE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSpanCount(orientation: Int): Int {
|
||||||
|
return if (orientation == Configuration.ORIENTATION_LANDSCAPE) SPAN_COUNT_LANDSCAPE else SPAN_COUNT_PORTRAIT
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
// check orientation
|
||||||
|
binding!!.fabLayout.orientation =
|
||||||
|
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL
|
||||||
|
rvContributionsList
|
||||||
|
?.setLayoutManager(
|
||||||
|
GridLayoutManager(context, getSpanCount(newConfig.orientation))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeAnimations() {
|
||||||
|
fab_open = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.fab_open)
|
||||||
|
fab_close = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.fab_close)
|
||||||
|
rotate_forward = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.rotate_forward)
|
||||||
|
rotate_backward = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.rotate_backward)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setListeners() {
|
||||||
|
binding!!.fabPlus.setOnClickListener { view: View? -> animateFAB(isFabOpen) }
|
||||||
|
binding!!.fabCamera.setOnClickListener { view: View? ->
|
||||||
|
controller!!.initiateCameraPick(
|
||||||
|
requireActivity(),
|
||||||
|
inAppCameraLocationPermissionLauncher,
|
||||||
|
cameraPickLauncherForResult
|
||||||
|
)
|
||||||
|
animateFAB(isFabOpen)
|
||||||
|
}
|
||||||
|
binding!!.fabCamera.setOnLongClickListener { view: View? ->
|
||||||
|
showShortToast(
|
||||||
|
context,
|
||||||
|
fr.free.nrw.commons.R.string.add_contribution_from_camera
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
binding!!.fabGallery.setOnClickListener { view: View? ->
|
||||||
|
controller!!.initiateGalleryPick(requireActivity(), galleryPickLauncherForResult, true)
|
||||||
|
animateFAB(isFabOpen)
|
||||||
|
}
|
||||||
|
binding!!.fabGallery.setOnLongClickListener { view: View? ->
|
||||||
|
showShortToast(context, fr.free.nrw.commons.R.string.menu_from_gallery)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch Custom Selector.
|
||||||
|
*/
|
||||||
|
protected fun launchCustomSelector() {
|
||||||
|
controller!!.initiateCustomGalleryPickWithPermission(
|
||||||
|
requireActivity(),
|
||||||
|
customSelectorLauncherForResult
|
||||||
|
)
|
||||||
|
animateFAB(isFabOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scrollToTop() {
|
||||||
|
rvContributionsList!!.smoothScrollToPosition(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateFAB(isFabOpen: Boolean) {
|
||||||
|
this.isFabOpen = !isFabOpen
|
||||||
|
if (binding!!.fabPlus.isShown) {
|
||||||
|
if (isFabOpen) {
|
||||||
|
binding!!.fabPlus.startAnimation(rotate_backward)
|
||||||
|
binding!!.fabCamera.startAnimation(fab_close)
|
||||||
|
binding!!.fabGallery.startAnimation(fab_close)
|
||||||
|
binding!!.fabCustomGallery.startAnimation(fab_close)
|
||||||
|
binding!!.fabCamera.hide()
|
||||||
|
binding!!.fabGallery.hide()
|
||||||
|
binding!!.fabCustomGallery.hide()
|
||||||
|
} else {
|
||||||
|
binding!!.fabPlus.startAnimation(rotate_forward)
|
||||||
|
binding!!.fabCamera.startAnimation(fab_open)
|
||||||
|
binding!!.fabGallery.startAnimation(fab_open)
|
||||||
|
binding!!.fabCustomGallery.startAnimation(fab_open)
|
||||||
|
binding!!.fabCamera.show()
|
||||||
|
binding!!.fabGallery.show()
|
||||||
|
binding!!.fabCustomGallery.show()
|
||||||
|
}
|
||||||
|
this.isFabOpen = !isFabOpen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows welcome message if user has no contributions yet i.e. new user.
|
||||||
|
*/
|
||||||
|
override fun showWelcomeTip(shouldShow: Boolean) {
|
||||||
|
binding!!.noContributionsYet.visibility =
|
||||||
|
if (shouldShow) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible to set progress bar invisible and visible
|
||||||
|
*
|
||||||
|
* @param shouldShow True when contributions list should be hidden.
|
||||||
|
*/
|
||||||
|
override fun showProgress(shouldShow: Boolean) {
|
||||||
|
binding!!.loadingContributionsProgressBar.visibility =
|
||||||
|
if (shouldShow) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showNoContributionsUI(shouldShow: Boolean) {
|
||||||
|
binding!!.noContributionsYet.visibility =
|
||||||
|
if (shouldShow) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
val layoutManager = rvContributionsList
|
||||||
|
?.getLayoutManager() as GridLayoutManager?
|
||||||
|
outState.putParcelable(RV_STATE, layoutManager!!.onSaveInstanceState())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||||
|
super.onViewStateRestored(savedInstanceState)
|
||||||
|
if (null != savedInstanceState) {
|
||||||
|
val savedRecyclerLayoutState = savedInstanceState.getParcelable<Parcelable>(RV_STATE)
|
||||||
|
rvContributionsList!!.layoutManager!!.onRestoreInstanceState(savedRecyclerLayoutState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openMediaDetail(position: Int, isWikipediaButtonDisplayed: Boolean) {
|
||||||
|
if (null != callback) { //Just being safe, ideally they won't be called when detached
|
||||||
|
callback!!.showDetail(position, isWikipediaButtonDisplayed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle callback for wikipedia icon clicked
|
||||||
|
*
|
||||||
|
* @param contribution
|
||||||
|
*/
|
||||||
|
override fun addImageToWikipedia(contribution: Contribution?) {
|
||||||
|
showAlertDialog(
|
||||||
|
requireActivity(),
|
||||||
|
getString(fr.free.nrw.commons.R.string.add_picture_to_wikipedia_article_title),
|
||||||
|
getString(fr.free.nrw.commons.R.string.add_picture_to_wikipedia_article_desc),
|
||||||
|
{
|
||||||
|
if (contribution != null) {
|
||||||
|
showAddImageToWikipediaInstructions(contribution)
|
||||||
|
}
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display confirmation dialog with instructions when the user tries to add image to wikipedia
|
||||||
|
*
|
||||||
|
* @param contribution
|
||||||
|
*/
|
||||||
|
private fun showAddImageToWikipediaInstructions(contribution: Contribution) {
|
||||||
|
val fragmentManager = fragmentManager
|
||||||
|
val fragment = newInstance(contribution)
|
||||||
|
fragment.callback =
|
||||||
|
WikipediaInstructionsDialogFragment.Callback { contribution: Contribution?, copyWikicode: Boolean ->
|
||||||
|
this.onConfirmClicked(
|
||||||
|
contribution,
|
||||||
|
copyWikicode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fragment.show(fragmentManager!!, "WikimediaFragment")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getMediaAtPosition(i: Int): Media? {
|
||||||
|
if (adapter!!.getContributionForPosition(i) != null) {
|
||||||
|
return adapter!!.getContributionForPosition(i)!!.media
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val totalMediaCount: Int
|
||||||
|
get() = contributionsSize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the editor for the language Wikipedia
|
||||||
|
*
|
||||||
|
* @param contribution
|
||||||
|
*/
|
||||||
|
override fun onConfirmClicked(contribution: Contribution?, copyWikicode: Boolean) {
|
||||||
|
if (copyWikicode) {
|
||||||
|
val wikicode = contribution!!.media.wikiCode
|
||||||
|
Utils.copy("wikicode", wikicode, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
val url =
|
||||||
|
languageWikipediaSite!!.mobileUrl() + "/wiki/" + (contribution!!.wikidataPlace
|
||||||
|
?.getWikipediaPageTitle())
|
||||||
|
Utils.handleWebUrl(context, Uri.parse(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getContributionStateAt(position: Int): Int {
|
||||||
|
return adapter!!.getContributionForPosition(position)!!.state
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun notifyDataSetChanged()
|
||||||
|
|
||||||
|
fun showDetail(position: Int, isWikipediaButtonDisplayed: Boolean)
|
||||||
|
|
||||||
|
// Notify the viewpager that number of items have changed.
|
||||||
|
fun viewPagerNotifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val RV_STATE = "rv_scroll_state"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.paging.DataSource;
|
|
||||||
import androidx.paging.DataSource.Factory;
|
|
||||||
import androidx.paging.LivePagedListBuilder;
|
|
||||||
import androidx.paging.PagedList;
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionsListContract.UserActionListener;
|
|
||||||
import io.reactivex.Scheduler;
|
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
|
||||||
import java.util.Collections;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import kotlin.Unit;
|
|
||||||
import kotlin.jvm.functions.Function0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The presenter class for Contributions
|
|
||||||
*/
|
|
||||||
public class ContributionsListPresenter implements UserActionListener {
|
|
||||||
|
|
||||||
private final ContributionBoundaryCallback contributionBoundaryCallback;
|
|
||||||
private final ContributionsRepository repository;
|
|
||||||
private final Scheduler ioThreadScheduler;
|
|
||||||
|
|
||||||
private final CompositeDisposable compositeDisposable;
|
|
||||||
private final ContributionsRemoteDataSource contributionsRemoteDataSource;
|
|
||||||
|
|
||||||
LiveData<PagedList<Contribution>> contributionList;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ContributionsListPresenter(
|
|
||||||
final ContributionBoundaryCallback contributionBoundaryCallback,
|
|
||||||
final ContributionsRemoteDataSource contributionsRemoteDataSource,
|
|
||||||
final ContributionsRepository repository,
|
|
||||||
@Named(IO_THREAD) final Scheduler ioThreadScheduler) {
|
|
||||||
this.contributionBoundaryCallback = contributionBoundaryCallback;
|
|
||||||
this.repository = repository;
|
|
||||||
this.ioThreadScheduler = ioThreadScheduler;
|
|
||||||
this.contributionsRemoteDataSource = contributionsRemoteDataSource;
|
|
||||||
compositeDisposable = new CompositeDisposable();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttachView(final ContributionsListContract.View view) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup the paged list. This method sets the configuration for paged list and ties it up with
|
|
||||||
* the live data object. This method can be tweaked to update the lazy loading behavior of the
|
|
||||||
* contributions list
|
|
||||||
*/
|
|
||||||
void setup(String userName, boolean isSelf) {
|
|
||||||
final PagedList.Config pagedListConfig =
|
|
||||||
(new PagedList.Config.Builder())
|
|
||||||
.setPrefetchDistance(50)
|
|
||||||
.setPageSize(10).build();
|
|
||||||
Factory<Integer, Contribution> factory;
|
|
||||||
boolean shouldSetBoundaryCallback;
|
|
||||||
if (!isSelf) {
|
|
||||||
//We don't want to persist contributions for other user's, therefore
|
|
||||||
// creating a new DataSource for them
|
|
||||||
contributionsRemoteDataSource.setUserName(userName);
|
|
||||||
factory = new Factory<Integer, Contribution>() {
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public DataSource<Integer, Contribution> create() {
|
|
||||||
return contributionsRemoteDataSource;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
shouldSetBoundaryCallback = false;
|
|
||||||
} else {
|
|
||||||
contributionBoundaryCallback.setUserName(userName);
|
|
||||||
shouldSetBoundaryCallback = true;
|
|
||||||
factory = repository.fetchContributionsWithStates(
|
|
||||||
Collections.singletonList(Contribution.STATE_COMPLETED));
|
|
||||||
}
|
|
||||||
|
|
||||||
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
|
|
||||||
pagedListConfig);
|
|
||||||
if (shouldSetBoundaryCallback) {
|
|
||||||
livePagedListBuilder.setBoundaryCallback(contributionBoundaryCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
contributionList = livePagedListBuilder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetachView() {
|
|
||||||
compositeDisposable.clear();
|
|
||||||
contributionsRemoteDataSource.dispose();
|
|
||||||
contributionBoundaryCallback.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It is used to refresh list.
|
|
||||||
*
|
|
||||||
* @param swipeRefreshLayout used to stop refresh animation when
|
|
||||||
* refresh finishes.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void refreshList(final SwipeRefreshLayout swipeRefreshLayout) {
|
|
||||||
contributionBoundaryCallback.refreshList(() -> {
|
|
||||||
swipeRefreshLayout.setRefreshing(false);
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.paging.DataSource
|
||||||
|
import androidx.paging.LivePagedListBuilder
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import fr.free.nrw.commons.di.CommonsApplicationModule
|
||||||
|
import io.reactivex.Scheduler
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The presenter class for Contributions
|
||||||
|
*/
|
||||||
|
class ContributionsListPresenter @Inject internal constructor(
|
||||||
|
private val contributionBoundaryCallback: ContributionBoundaryCallback,
|
||||||
|
private val contributionsRemoteDataSource: ContributionsRemoteDataSource,
|
||||||
|
private val repository: ContributionsRepository,
|
||||||
|
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler
|
||||||
|
) : ContributionsListContract.UserActionListener {
|
||||||
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
|
var contributionList: LiveData<PagedList<Contribution>>? = null
|
||||||
|
|
||||||
|
override fun onAttachView(view: ContributionsListContract.View) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the paged list. This method sets the configuration for paged list and ties it up with
|
||||||
|
* the live data object. This method can be tweaked to update the lazy loading behavior of the
|
||||||
|
* contributions list
|
||||||
|
*/
|
||||||
|
fun setup(userName: String?, isSelf: Boolean) {
|
||||||
|
val pagedListConfig =
|
||||||
|
(PagedList.Config.Builder())
|
||||||
|
.setPrefetchDistance(50)
|
||||||
|
.setPageSize(10).build()
|
||||||
|
val factory: DataSource.Factory<Int, Contribution>
|
||||||
|
val shouldSetBoundaryCallback: Boolean
|
||||||
|
if (!isSelf) {
|
||||||
|
//We don't want to persist contributions for other user's, therefore
|
||||||
|
// creating a new DataSource for them
|
||||||
|
contributionsRemoteDataSource.userName = userName
|
||||||
|
factory = object : DataSource.Factory<Int, Contribution>() {
|
||||||
|
override fun create(): DataSource<Int, Contribution> {
|
||||||
|
return contributionsRemoteDataSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shouldSetBoundaryCallback = false
|
||||||
|
} else {
|
||||||
|
contributionBoundaryCallback.userName = userName
|
||||||
|
shouldSetBoundaryCallback = true
|
||||||
|
factory = repository.fetchContributionsWithStates(
|
||||||
|
listOf(Contribution.STATE_COMPLETED)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val livePagedListBuilder: LivePagedListBuilder<Int, Contribution> = LivePagedListBuilder(
|
||||||
|
factory,
|
||||||
|
pagedListConfig
|
||||||
|
)
|
||||||
|
if (shouldSetBoundaryCallback) {
|
||||||
|
livePagedListBuilder.setBoundaryCallback(contributionBoundaryCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
contributionList = livePagedListBuilder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachView() {
|
||||||
|
compositeDisposable.clear()
|
||||||
|
contributionsRemoteDataSource.dispose()
|
||||||
|
contributionBoundaryCallback.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is used to refresh list.
|
||||||
|
*
|
||||||
|
* @param swipeRefreshLayout used to stop refresh animation when
|
||||||
|
* refresh finishes.
|
||||||
|
*/
|
||||||
|
override fun refreshList(swipeRefreshLayout: SwipeRefreshLayout?) {
|
||||||
|
contributionBoundaryCallback.refreshList {
|
||||||
|
if (swipeRefreshLayout != null) {
|
||||||
|
swipeRefreshLayout.isRefreshing = false
|
||||||
|
}
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import androidx.paging.DataSource.Factory;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import io.reactivex.Completable;
|
|
||||||
import io.reactivex.Single;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The LocalDataSource class for Contributions
|
|
||||||
*/
|
|
||||||
class ContributionsLocalDataSource {
|
|
||||||
|
|
||||||
private final ContributionDao contributionDao;
|
|
||||||
private final JsonKvStore defaultKVStore;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public ContributionsLocalDataSource(
|
|
||||||
@Named("default_preferences") final JsonKvStore defaultKVStore,
|
|
||||||
final ContributionDao contributionDao) {
|
|
||||||
this.defaultKVStore = defaultKVStore;
|
|
||||||
this.contributionDao = contributionDao;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch default number of contributions to be show, based on user preferences
|
|
||||||
*/
|
|
||||||
public String getString(final String key) {
|
|
||||||
return defaultKVStore.getString(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch default number of contributions to be show, based on user preferences
|
|
||||||
*/
|
|
||||||
public long getLong(final String key) {
|
|
||||||
return defaultKVStore.getLong(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get contribution object from cursor
|
|
||||||
*
|
|
||||||
* @param uri
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public Contribution getContributionWithFileName(final String uri) {
|
|
||||||
final List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle(
|
|
||||||
uri);
|
|
||||||
if (!contributionWithUri.isEmpty()) {
|
|
||||||
return contributionWithUri.get(0);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a contribution from the contributions table
|
|
||||||
*
|
|
||||||
* @param contribution
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public Completable deleteContribution(final Contribution contribution) {
|
|
||||||
return contributionDao.delete(contribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes contributions with specific states.
|
|
||||||
*
|
|
||||||
* @param states The states of the contributions to delete.
|
|
||||||
* @return A Completable indicating the result of the operation.
|
|
||||||
*/
|
|
||||||
public Completable deleteContributionsWithStates(List<Integer> states) {
|
|
||||||
return contributionDao.deleteContributionsWithStates(states);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Factory<Integer, Contribution> getContributions() {
|
|
||||||
return contributionDao.fetchContributions();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches contributions with specific states.
|
|
||||||
*
|
|
||||||
* @param states The states of the contributions to fetch.
|
|
||||||
* @return A DataSource factory for paginated contributions with the specified states.
|
|
||||||
*/
|
|
||||||
public Factory<Integer, Contribution> getContributionsWithStates(List<Integer> states) {
|
|
||||||
return contributionDao.getContributions(states);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches contributions with specific states sorted by the date the upload started.
|
|
||||||
*
|
|
||||||
* @param states The states of the contributions to fetch.
|
|
||||||
* @return A DataSource factory for paginated contributions with the specified states sorted by
|
|
||||||
* date upload started.
|
|
||||||
*/
|
|
||||||
public Factory<Integer, Contribution> getContributionsWithStatesSortedByDateUploadStarted(
|
|
||||||
List<Integer> states) {
|
|
||||||
return contributionDao.getContributionsSortedByDateUploadStarted(states);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Single<List<Long>> saveContributions(final List<Contribution> contributions) {
|
|
||||||
final List<Contribution> contributionList = new ArrayList<>();
|
|
||||||
for (final Contribution contribution : contributions) {
|
|
||||||
final Contribution oldContribution = contributionDao.getContribution(
|
|
||||||
contribution.getPageId());
|
|
||||||
if (oldContribution != null) {
|
|
||||||
contribution.setWikidataPlace(oldContribution.getWikidataPlace());
|
|
||||||
}
|
|
||||||
contributionList.add(contribution);
|
|
||||||
}
|
|
||||||
return contributionDao.save(contributionList);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable saveContributions(Contribution contribution) {
|
|
||||||
return contributionDao.save(contribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void set(final String key, final long value) {
|
|
||||||
defaultKVStore.putLong(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable updateContribution(final Contribution contribution) {
|
|
||||||
return contributionDao.update(contribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable updateContributionsWithStates(List<Integer> states, int newState) {
|
|
||||||
return contributionDao.updateContributionsWithStates(states, newState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import androidx.paging.DataSource
|
||||||
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
|
import io.reactivex.Completable
|
||||||
|
import io.reactivex.Single
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The LocalDataSource class for Contributions
|
||||||
|
*/
|
||||||
|
class ContributionsLocalDataSource @Inject constructor(
|
||||||
|
@param:Named("default_preferences") private val defaultKVStore: JsonKvStore,
|
||||||
|
private val contributionDao: ContributionDao
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Fetch default number of contributions to be show, based on user preferences
|
||||||
|
*/
|
||||||
|
fun getString(key: String): String? {
|
||||||
|
return defaultKVStore.getString(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch default number of contributions to be show, based on user preferences
|
||||||
|
*/
|
||||||
|
fun getLong(key: String): Long {
|
||||||
|
return defaultKVStore.getLong(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get contribution object from cursor
|
||||||
|
*
|
||||||
|
* @param uri
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
fun getContributionWithFileName(uri: String): Contribution {
|
||||||
|
val contributionWithUri = contributionDao.getContributionWithTitle(uri)
|
||||||
|
if (contributionWithUri.isNotEmpty()) {
|
||||||
|
return contributionWithUri[0]
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Contribution not found for URI: $uri")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a contribution from the contributions table
|
||||||
|
*
|
||||||
|
* @param contribution
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
fun deleteContribution(contribution: Contribution): Completable {
|
||||||
|
return contributionDao.delete(contribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes contributions with specific states.
|
||||||
|
*
|
||||||
|
* @param states The states of the contributions to delete.
|
||||||
|
* @return A Completable indicating the result of the operation.
|
||||||
|
*/
|
||||||
|
fun deleteContributionsWithStates(states: List<Int>): Completable {
|
||||||
|
return contributionDao.deleteContributionsWithStates(states)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getContributions(): DataSource.Factory<Int, Contribution> {
|
||||||
|
return contributionDao.fetchContributions()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches contributions with specific states.
|
||||||
|
*
|
||||||
|
* @param states The states of the contributions to fetch.
|
||||||
|
* @return A DataSource factory for paginated contributions with the specified states.
|
||||||
|
*/
|
||||||
|
fun getContributionsWithStates(states: List<Int>): DataSource.Factory<Int, Contribution> {
|
||||||
|
return contributionDao.getContributions(states)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches contributions with specific states sorted by the date the upload started.
|
||||||
|
*
|
||||||
|
* @param states The states of the contributions to fetch.
|
||||||
|
* @return A DataSource factory for paginated contributions with the specified states sorted by
|
||||||
|
* date upload started.
|
||||||
|
*/
|
||||||
|
fun getContributionsWithStatesSortedByDateUploadStarted(
|
||||||
|
states: List<Int>
|
||||||
|
): DataSource.Factory<Int, Contribution> {
|
||||||
|
return contributionDao.getContributionsSortedByDateUploadStarted(states)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveContributions(contributions: List<Contribution>): Single<List<Long>> {
|
||||||
|
val contributionList: MutableList<Contribution> = ArrayList()
|
||||||
|
for (contribution in contributions) {
|
||||||
|
val oldContribution = contributionDao.getContribution(
|
||||||
|
contribution.pageId
|
||||||
|
)
|
||||||
|
if (oldContribution != null) {
|
||||||
|
contribution.wikidataPlace = oldContribution.wikidataPlace
|
||||||
|
}
|
||||||
|
contributionList.add(contribution)
|
||||||
|
}
|
||||||
|
return contributionDao.save(contributionList)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveContributions(contribution: Contribution): Completable {
|
||||||
|
return contributionDao.save(contribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun set(key: String, value: Long) {
|
||||||
|
defaultKVStore.putLong(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateContribution(contribution: Contribution): Completable {
|
||||||
|
return contributionDao.update(contribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateContributionsWithStates(states: List<Int>, newState: Int): Completable {
|
||||||
|
return contributionDao.updateContributionsWithStates(states, newState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import dagger.Binds;
|
|
||||||
import dagger.Module;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Dagger Module for contributions related presenters and (some other objects maybe in future)
|
|
||||||
*/
|
|
||||||
@Module
|
|
||||||
public abstract class ContributionsModule {
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
public abstract ContributionsContract.UserActionListener bindsContibutionsPresenter(
|
|
||||||
ContributionsPresenter presenter);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Dagger Module for contributions-related presenters and other dependencies
|
||||||
|
*/
|
||||||
|
@Module
|
||||||
|
abstract class ContributionsModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindsContributionsPresenter(
|
||||||
|
presenter: ContributionsPresenter?
|
||||||
|
): ContributionsContract.UserActionListener?
|
||||||
|
}
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
|
|
||||||
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
|
|
||||||
|
|
||||||
import androidx.work.ExistingWorkPolicy;
|
|
||||||
import fr.free.nrw.commons.MediaDataExtractor;
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
|
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
|
||||||
import fr.free.nrw.commons.repository.UploadRepository;
|
|
||||||
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
|
|
||||||
import io.reactivex.Scheduler;
|
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The presenter class for Contributions
|
|
||||||
*/
|
|
||||||
public class ContributionsPresenter implements UserActionListener {
|
|
||||||
|
|
||||||
private final ContributionsRepository contributionsRepository;
|
|
||||||
private final UploadRepository uploadRepository;
|
|
||||||
private final Scheduler ioThreadScheduler;
|
|
||||||
private CompositeDisposable compositeDisposable;
|
|
||||||
private ContributionsContract.View view;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
MediaDataExtractor mediaDataExtractor;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ContributionsPresenter(ContributionsRepository repository,
|
|
||||||
UploadRepository uploadRepository,
|
|
||||||
@Named(IO_THREAD) Scheduler ioThreadScheduler) {
|
|
||||||
this.contributionsRepository = repository;
|
|
||||||
this.uploadRepository = uploadRepository;
|
|
||||||
this.ioThreadScheduler = ioThreadScheduler;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttachView(ContributionsContract.View view) {
|
|
||||||
this.view = view;
|
|
||||||
compositeDisposable = new CompositeDisposable();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetachView() {
|
|
||||||
this.view = null;
|
|
||||||
compositeDisposable.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Contribution getContributionsWithTitle(String title) {
|
|
||||||
return contributionsRepository.getContributionWithFileName(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a contribution is a duplicate and restarts the contribution process if it is not.
|
|
||||||
*
|
|
||||||
* @param contribution The contribution to check and potentially restart.
|
|
||||||
*/
|
|
||||||
public void checkDuplicateImageAndRestartContribution(Contribution contribution) {
|
|
||||||
compositeDisposable.add(uploadRepository
|
|
||||||
.checkDuplicateImage(contribution.getLocalUriPath().getPath())
|
|
||||||
.subscribeOn(ioThreadScheduler)
|
|
||||||
.subscribe(imageCheckResult -> {
|
|
||||||
if (imageCheckResult == IMAGE_OK) {
|
|
||||||
contribution.setState(Contribution.STATE_QUEUED);
|
|
||||||
saveContribution(contribution);
|
|
||||||
} else {
|
|
||||||
Timber.e("Contribution already exists");
|
|
||||||
compositeDisposable.add(contributionsRepository
|
|
||||||
.deleteContributionFromDB(contribution)
|
|
||||||
.subscribeOn(ioThreadScheduler)
|
|
||||||
.subscribe());
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the contribution's state in the databse, upon completion, trigger the workmanager to
|
|
||||||
* process this contribution
|
|
||||||
*
|
|
||||||
* @param contribution
|
|
||||||
*/
|
|
||||||
public void saveContribution(Contribution contribution) {
|
|
||||||
compositeDisposable.add(contributionsRepository
|
|
||||||
.save(contribution)
|
|
||||||
.subscribeOn(ioThreadScheduler)
|
|
||||||
.subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest(
|
|
||||||
view.getContext().getApplicationContext(), ExistingWorkPolicy.KEEP)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import fr.free.nrw.commons.MediaDataExtractor
|
||||||
|
import fr.free.nrw.commons.di.CommonsApplicationModule
|
||||||
|
import fr.free.nrw.commons.repository.UploadRepository
|
||||||
|
import fr.free.nrw.commons.upload.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest
|
||||||
|
import fr.free.nrw.commons.utils.ImageUtils
|
||||||
|
import io.reactivex.Scheduler
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The presenter class for Contributions
|
||||||
|
*/
|
||||||
|
class ContributionsPresenter @Inject internal constructor(
|
||||||
|
private val contributionsRepository: ContributionsRepository,
|
||||||
|
private val uploadRepository: UploadRepository,
|
||||||
|
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler
|
||||||
|
) : ContributionsContract.UserActionListener {
|
||||||
|
private var compositeDisposable: CompositeDisposable? = null
|
||||||
|
private var view: ContributionsContract.View? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var mediaDataExtractor: MediaDataExtractor? = null
|
||||||
|
|
||||||
|
override fun onAttachView(view: ContributionsContract.View) {
|
||||||
|
this.view = view
|
||||||
|
compositeDisposable = CompositeDisposable()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachView() {
|
||||||
|
this.view = null
|
||||||
|
compositeDisposable!!.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getContributionsWithTitle(title: String): Contribution {
|
||||||
|
return contributionsRepository.getContributionWithFileName(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a contribution is a duplicate and restarts the contribution process if it is not.
|
||||||
|
*
|
||||||
|
* @param contribution The contribution to check and potentially restart.
|
||||||
|
*/
|
||||||
|
fun checkDuplicateImageAndRestartContribution(contribution: Contribution) {
|
||||||
|
compositeDisposable!!.add(
|
||||||
|
uploadRepository
|
||||||
|
.checkDuplicateImage(
|
||||||
|
contribution.contentUri,
|
||||||
|
contribution.localUri)
|
||||||
|
.subscribeOn(ioThreadScheduler)
|
||||||
|
.subscribe { imageCheckResult: Int ->
|
||||||
|
if (imageCheckResult == ImageUtils.IMAGE_OK) {
|
||||||
|
contribution.state = Contribution.STATE_QUEUED
|
||||||
|
saveContribution(contribution)
|
||||||
|
} else {
|
||||||
|
Timber.e("Contribution already exists")
|
||||||
|
compositeDisposable!!.add(
|
||||||
|
contributionsRepository
|
||||||
|
.deleteContributionFromDB(contribution)
|
||||||
|
.subscribeOn(ioThreadScheduler)
|
||||||
|
.subscribe()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the contribution's state in the databse, upon completion, trigger the workmanager to
|
||||||
|
* process this contribution
|
||||||
|
*
|
||||||
|
* @param contribution
|
||||||
|
*/
|
||||||
|
fun saveContribution(contribution: Contribution) {
|
||||||
|
compositeDisposable!!.add(contributionsRepository
|
||||||
|
.save(contribution)
|
||||||
|
.subscribeOn(ioThreadScheduler)
|
||||||
|
.subscribe {
|
||||||
|
makeOneTimeWorkRequest(
|
||||||
|
view!!.getContext().applicationContext, ExistingWorkPolicy.KEEP
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
|
import fr.free.nrw.commons.wikidata.model.WikiSite
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Dagger Module for contributions-related providers
|
||||||
|
*/
|
||||||
|
@Module
|
||||||
|
class ContributionsProvidesModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun providesApplicationKvStore(
|
||||||
|
@Named("default_preferences") kvStore: JsonKvStore
|
||||||
|
): JsonKvStore {
|
||||||
|
return kvStore
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun providesLanguageWikipediaSite(
|
||||||
|
@Named("language-wikipedia-wikisite") languageWikipediaSite: WikiSite
|
||||||
|
): WikiSite {
|
||||||
|
return languageWikipediaSite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import androidx.paging.DataSource.Factory;
|
|
||||||
import io.reactivex.Completable;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import io.reactivex.Single;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The repository class for contributions
|
|
||||||
*/
|
|
||||||
public class ContributionsRepository {
|
|
||||||
|
|
||||||
private ContributionsLocalDataSource localDataSource;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public ContributionsRepository(ContributionsLocalDataSource localDataSource) {
|
|
||||||
this.localDataSource = localDataSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch default number of contributions to be show, based on user preferences
|
|
||||||
*/
|
|
||||||
public String getString(String key) {
|
|
||||||
return localDataSource.getString(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a failed upload from DB
|
|
||||||
*
|
|
||||||
* @param contribution
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public Completable deleteContributionFromDB(Contribution contribution) {
|
|
||||||
return localDataSource.deleteContribution(contribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes contributions from the database with specific states.
|
|
||||||
*
|
|
||||||
* @param states The states of the contributions to delete.
|
|
||||||
* @return A Completable indicating the result of the operation.
|
|
||||||
*/
|
|
||||||
public Completable deleteContributionsFromDBWithStates(List<Integer> states) {
|
|
||||||
return localDataSource.deleteContributionsWithStates(states);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get contribution object with title
|
|
||||||
*
|
|
||||||
* @param fileName
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public Contribution getContributionWithFileName(String fileName) {
|
|
||||||
return localDataSource.getContributionWithFileName(fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Factory<Integer, Contribution> fetchContributions() {
|
|
||||||
return localDataSource.getContributions();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches contributions with specific states.
|
|
||||||
*
|
|
||||||
* @param states The states of the contributions to fetch.
|
|
||||||
* @return A DataSource factory for paginated contributions with the specified states.
|
|
||||||
*/
|
|
||||||
public Factory<Integer, Contribution> fetchContributionsWithStates(List<Integer> states) {
|
|
||||||
return localDataSource.getContributionsWithStates(states);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches contributions with specific states sorted by the date the upload started.
|
|
||||||
*
|
|
||||||
* @param states The states of the contributions to fetch.
|
|
||||||
* @return A DataSource factory for paginated contributions with the specified states sorted by
|
|
||||||
* date upload started.
|
|
||||||
*/
|
|
||||||
public Factory<Integer, Contribution> fetchContributionsWithStatesSortedByDateUploadStarted(
|
|
||||||
List<Integer> states) {
|
|
||||||
return localDataSource.getContributionsWithStatesSortedByDateUploadStarted(states);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Single<List<Long>> save(List<Contribution> contributions) {
|
|
||||||
return localDataSource.saveContributions(contributions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable save(Contribution contributions) {
|
|
||||||
return localDataSource.saveContributions(contributions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void set(String key, long value) {
|
|
||||||
localDataSource.set(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable updateContribution(Contribution contribution) {
|
|
||||||
return localDataSource.updateContribution(contribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the state of contributions with specific states.
|
|
||||||
*
|
|
||||||
* @param states The current states of the contributions to update.
|
|
||||||
* @param newState The new state to set.
|
|
||||||
* @return A Completable indicating the result of the operation.
|
|
||||||
*/
|
|
||||||
public Completable updateContributionsWithStates(List<Integer> states, int newState) {
|
|
||||||
return localDataSource.updateContributionsWithStates(states, newState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import androidx.paging.DataSource
|
||||||
|
import io.reactivex.Completable
|
||||||
|
import io.reactivex.Single
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The repository class for contributions
|
||||||
|
*/
|
||||||
|
class ContributionsRepository @Inject constructor(private val localDataSource: ContributionsLocalDataSource) {
|
||||||
|
/**
|
||||||
|
* Fetch default number of contributions to be show, based on user preferences
|
||||||
|
*/
|
||||||
|
fun getString(key: String): String? {
|
||||||
|
return localDataSource.getString(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a failed upload from DB
|
||||||
|
*
|
||||||
|
* @param contribution
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
fun deleteContributionFromDB(contribution: Contribution): Completable {
|
||||||
|
return localDataSource.deleteContribution(contribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes contributions from the database with specific states.
|
||||||
|
*
|
||||||
|
* @param states The states of the contributions to delete.
|
||||||
|
* @return A Completable indicating the result of the operation.
|
||||||
|
*/
|
||||||
|
fun deleteContributionsFromDBWithStates(states: List<Int>): Completable {
|
||||||
|
return localDataSource.deleteContributionsWithStates(states)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get contribution object with title
|
||||||
|
*
|
||||||
|
* @param fileName
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
fun getContributionWithFileName(fileName: String): Contribution {
|
||||||
|
return localDataSource.getContributionWithFileName(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchContributions(): DataSource.Factory<Int, Contribution> {
|
||||||
|
return localDataSource.getContributions()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches contributions with specific states.
|
||||||
|
*
|
||||||
|
* @param states The states of the contributions to fetch.
|
||||||
|
* @return A DataSource factory for paginated contributions with the specified states.
|
||||||
|
*/
|
||||||
|
fun fetchContributionsWithStates(states: List<Int>): DataSource.Factory<Int, Contribution> {
|
||||||
|
return localDataSource.getContributionsWithStates(states)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches contributions with specific states sorted by the date the upload started.
|
||||||
|
*
|
||||||
|
* @param states The states of the contributions to fetch.
|
||||||
|
* @return A DataSource factory for paginated contributions with the specified states sorted by
|
||||||
|
* date upload started.
|
||||||
|
*/
|
||||||
|
fun fetchContributionsWithStatesSortedByDateUploadStarted(
|
||||||
|
states: List<Int>
|
||||||
|
): DataSource.Factory<Int, Contribution> {
|
||||||
|
return localDataSource.getContributionsWithStatesSortedByDateUploadStarted(states)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(contributions: List<Contribution>): Single<List<Long>> {
|
||||||
|
return localDataSource.saveContributions(contributions)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(contributions: Contribution): Completable {
|
||||||
|
return localDataSource.saveContributions(contributions)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun set(key: String, value: Long) {
|
||||||
|
localDataSource.set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateContribution(contribution: Contribution): Completable {
|
||||||
|
return localDataSource.updateContribution(contribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the state of contributions with specific states.
|
||||||
|
*
|
||||||
|
* @param states The current states of the contributions to update.
|
||||||
|
* @param newState The new state to set.
|
||||||
|
* @return A Completable indicating the result of the operation.
|
||||||
|
*/
|
||||||
|
fun updateContributionsWithStates(states: List<Int>, newState: Int): Completable {
|
||||||
|
return localDataSource.updateContributionsWithStates(states, newState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,486 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.work.ExistingWorkPolicy;
|
|
||||||
import fr.free.nrw.commons.databinding.MainBinding;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.WelcomeActivity;
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
|
||||||
import fr.free.nrw.commons.bookmarks.BookmarkFragment;
|
|
||||||
import fr.free.nrw.commons.explore.ExploreFragment;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
|
||||||
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment;
|
|
||||||
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment;
|
|
||||||
import fr.free.nrw.commons.navtab.NavTab;
|
|
||||||
import fr.free.nrw.commons.navtab.NavTabLayout;
|
|
||||||
import fr.free.nrw.commons.navtab.NavTabLoggedOut;
|
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
|
||||||
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment;
|
|
||||||
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.NearbyParentFragmentInstanceReadyCallback;
|
|
||||||
import fr.free.nrw.commons.notification.NotificationActivity;
|
|
||||||
import fr.free.nrw.commons.notification.NotificationController;
|
|
||||||
import fr.free.nrw.commons.quiz.QuizChecker;
|
|
||||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
|
||||||
import fr.free.nrw.commons.upload.UploadProgressActivity;
|
|
||||||
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
|
|
||||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
|
||||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
|
||||||
import io.reactivex.Completable;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class MainActivity extends BaseActivity
|
|
||||||
implements FragmentManager.OnBackStackChangedListener {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
SessionManager sessionManager;
|
|
||||||
@Inject
|
|
||||||
ContributionController controller;
|
|
||||||
@Inject
|
|
||||||
ContributionDao contributionDao;
|
|
||||||
|
|
||||||
private ContributionsFragment contributionsFragment;
|
|
||||||
private NearbyParentFragment nearbyParentFragment;
|
|
||||||
private ExploreFragment exploreFragment;
|
|
||||||
private BookmarkFragment bookmarkFragment;
|
|
||||||
public ActiveFragment activeFragment;
|
|
||||||
private MediaDetailPagerFragment mediaDetailPagerFragment;
|
|
||||||
private NavTabLayout.OnNavigationItemSelectedListener navListener;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public LocationServiceManager locationManager;
|
|
||||||
@Inject
|
|
||||||
NotificationController notificationController;
|
|
||||||
@Inject
|
|
||||||
QuizChecker quizChecker;
|
|
||||||
@Inject
|
|
||||||
@Named("default_preferences")
|
|
||||||
public
|
|
||||||
JsonKvStore applicationKvStore;
|
|
||||||
@Inject
|
|
||||||
ViewUtilWrapper viewUtilWrapper;
|
|
||||||
|
|
||||||
public Menu menu;
|
|
||||||
|
|
||||||
public MainBinding binding;
|
|
||||||
|
|
||||||
NavTabLayout tabLayout;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consumers should be simply using this method to use this activity.
|
|
||||||
*
|
|
||||||
* @param context A Context of the application package implementing this class.
|
|
||||||
*/
|
|
||||||
public static void startYourself(Context context) {
|
|
||||||
Intent intent = new Intent(context, MainActivity.class);
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
|
||||||
context.startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onSupportNavigateUp() {
|
|
||||||
if (activeFragment == ActiveFragment.CONTRIBUTIONS) {
|
|
||||||
if (!contributionsFragment.backButtonClicked()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onBackPressed();
|
|
||||||
showTabs();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
binding = MainBinding.inflate(getLayoutInflater());
|
|
||||||
setContentView(binding.getRoot());
|
|
||||||
setSupportActionBar(binding.toolbarBinding.toolbar);
|
|
||||||
tabLayout = binding.fragmentMainNavTabLayout;
|
|
||||||
loadLocale();
|
|
||||||
|
|
||||||
binding.toolbarBinding.toolbar.setNavigationOnClickListener(view -> {
|
|
||||||
onSupportNavigateUp();
|
|
||||||
});
|
|
||||||
/*
|
|
||||||
"first_edit_depict" is a key for getting information about opening the depiction editor
|
|
||||||
screen for the first time after opening the app.
|
|
||||||
|
|
||||||
Getting true by the key means the depiction editor screen is opened for the first time
|
|
||||||
after opening the app.
|
|
||||||
Getting false by the key means the depiction editor screen is not opened for the first time
|
|
||||||
after opening the app.
|
|
||||||
*/
|
|
||||||
applicationKvStore.putBoolean("first_edit_depict", true);
|
|
||||||
if (applicationKvStore.getBoolean("login_skipped") == true) {
|
|
||||||
setTitle(getString(R.string.navigation_item_explore));
|
|
||||||
setUpLoggedOutPager();
|
|
||||||
} else {
|
|
||||||
if (applicationKvStore.getBoolean("firstrun", true)) {
|
|
||||||
applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false);
|
|
||||||
applicationKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", false);
|
|
||||||
}
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
//starting a fresh fragment.
|
|
||||||
// Open Last opened screen if it is Contributions or Nearby, otherwise Contributions
|
|
||||||
if (applicationKvStore.getBoolean("last_opened_nearby")) {
|
|
||||||
setTitle(getString(R.string.nearby_fragment));
|
|
||||||
showNearby();
|
|
||||||
loadFragment(NearbyParentFragment.newInstance(), false);
|
|
||||||
} else {
|
|
||||||
setTitle(getString(R.string.contributions_fragment));
|
|
||||||
loadFragment(ContributionsFragment.newInstance(), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setUpPager();
|
|
||||||
/**
|
|
||||||
* Ask the user for media location access just after login
|
|
||||||
* so that location in the EXIF metadata of the images shared by the user
|
|
||||||
* is retained on devices running Android 10 or above
|
|
||||||
*/
|
|
||||||
// if (VERSION.SDK_INT >= VERSION_CODES.Q) {
|
|
||||||
// ActivityCompat.requestPermissions(this,
|
|
||||||
// new String[]{Manifest.permission.ACCESS_MEDIA_LOCATION}, 0);
|
|
||||||
// PermissionUtils.checkPermissionsAndPerformAction(
|
|
||||||
// this,
|
|
||||||
// () -> {},
|
|
||||||
// R.string.media_location_permission_denied,
|
|
||||||
// R.string.add_location_manually,
|
|
||||||
// permission.ACCESS_MEDIA_LOCATION);
|
|
||||||
// }
|
|
||||||
checkAndResumeStuckUploads();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSelectedItemId(int id) {
|
|
||||||
binding.fragmentMainNavTabLayout.setSelectedItemId(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setUpPager() {
|
|
||||||
binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(
|
|
||||||
navListener = (item) -> {
|
|
||||||
if (!item.getTitle().equals(getString(R.string.more))) {
|
|
||||||
// do not change title for more fragment
|
|
||||||
setTitle(item.getTitle());
|
|
||||||
}
|
|
||||||
// set last_opened_nearby true if item is nearby screen else set false
|
|
||||||
applicationKvStore.putBoolean("last_opened_nearby",
|
|
||||||
item.getTitle().equals(getString(R.string.nearby_fragment)));
|
|
||||||
final Fragment fragment = NavTab.of(item.getOrder()).newInstance();
|
|
||||||
return loadFragment(fragment, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setUpLoggedOutPager() {
|
|
||||||
loadFragment(ExploreFragment.newInstance(), false);
|
|
||||||
binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(item -> {
|
|
||||||
if (!item.getTitle().equals(getString(R.string.more))) {
|
|
||||||
// do not change title for more fragment
|
|
||||||
setTitle(item.getTitle());
|
|
||||||
}
|
|
||||||
Fragment fragment = NavTabLoggedOut.of(item.getOrder()).newInstance();
|
|
||||||
return loadFragment(fragment, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean loadFragment(Fragment fragment, boolean showBottom) {
|
|
||||||
//showBottom so that we do not show the bottom tray again when constructing
|
|
||||||
//from the saved instance state.
|
|
||||||
if (fragment instanceof ContributionsFragment) {
|
|
||||||
if (activeFragment == ActiveFragment.CONTRIBUTIONS) {
|
|
||||||
// scroll to top if already on the Contributions tab
|
|
||||||
contributionsFragment.scrollToTop();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
contributionsFragment = (ContributionsFragment) fragment;
|
|
||||||
activeFragment = ActiveFragment.CONTRIBUTIONS;
|
|
||||||
} else if (fragment instanceof NearbyParentFragment) {
|
|
||||||
if (activeFragment == ActiveFragment.NEARBY) { // Do nothing if same tab
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
nearbyParentFragment = (NearbyParentFragment) fragment;
|
|
||||||
activeFragment = ActiveFragment.NEARBY;
|
|
||||||
} else if (fragment instanceof ExploreFragment) {
|
|
||||||
if (activeFragment == ActiveFragment.EXPLORE) { // Do nothing if same tab
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
exploreFragment = (ExploreFragment) fragment;
|
|
||||||
activeFragment = ActiveFragment.EXPLORE;
|
|
||||||
} else if (fragment instanceof BookmarkFragment) {
|
|
||||||
if (activeFragment == ActiveFragment.BOOKMARK) { // Do nothing if same tab
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
bookmarkFragment = (BookmarkFragment) fragment;
|
|
||||||
activeFragment = ActiveFragment.BOOKMARK;
|
|
||||||
} else if (fragment == null && showBottom) {
|
|
||||||
if (applicationKvStore.getBoolean("login_skipped")
|
|
||||||
== true) { // If logged out, more sheet is different
|
|
||||||
MoreBottomSheetLoggedOutFragment bottomSheet = new MoreBottomSheetLoggedOutFragment();
|
|
||||||
bottomSheet.show(getSupportFragmentManager(),
|
|
||||||
"MoreBottomSheetLoggedOut");
|
|
||||||
} else {
|
|
||||||
MoreBottomSheetFragment bottomSheet = new MoreBottomSheetFragment();
|
|
||||||
bottomSheet.show(getSupportFragmentManager(),
|
|
||||||
"MoreBottomSheet");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fragment != null) {
|
|
||||||
getSupportFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.replace(R.id.fragmentContainer, fragment)
|
|
||||||
.commit();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void hideTabs() {
|
|
||||||
binding.fragmentMainNavTabLayout.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showTabs() {
|
|
||||||
binding.fragmentMainNavTabLayout.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds number of uploads next to tab text "Contributions" then it will look like "Contributions
|
|
||||||
* (NUMBER)"
|
|
||||||
*
|
|
||||||
* @param uploadCount
|
|
||||||
*/
|
|
||||||
public void setNumOfUploads(int uploadCount) {
|
|
||||||
if (activeFragment == ActiveFragment.CONTRIBUTIONS) {
|
|
||||||
setTitle(getResources().getString(R.string.contributions_fragment) + " " + (
|
|
||||||
!(uploadCount == 0) ?
|
|
||||||
getResources()
|
|
||||||
.getQuantityString(R.plurals.contributions_subtitle,
|
|
||||||
uploadCount, uploadCount)
|
|
||||||
: getString(R.string.contributions_subtitle_zero)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resume the uploads that got stuck because of the app being killed or the device being
|
|
||||||
* rebooted.
|
|
||||||
* <p>
|
|
||||||
* When the app is terminated or the device is restarted, contributions remain in the
|
|
||||||
* 'STATE_IN_PROGRESS' state. This status persists and doesn't change during these events. So,
|
|
||||||
* retrieving contributions labeled as 'STATE_IN_PROGRESS' from the database will provide the
|
|
||||||
* list of uploads that appear as stuck on opening the app again
|
|
||||||
*/
|
|
||||||
@SuppressLint("CheckResult")
|
|
||||||
private void checkAndResumeStuckUploads() {
|
|
||||||
List<Contribution> stuckUploads = contributionDao.getContribution(
|
|
||||||
Collections.singletonList(Contribution.STATE_IN_PROGRESS))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.blockingGet();
|
|
||||||
Timber.d("Resuming " + stuckUploads.size() + " uploads...");
|
|
||||||
if (!stuckUploads.isEmpty()) {
|
|
||||||
for (Contribution contribution : stuckUploads) {
|
|
||||||
contribution.setState(Contribution.STATE_QUEUED);
|
|
||||||
contribution.setDateUploadStarted(Calendar.getInstance().getTime());
|
|
||||||
Completable.fromAction(() -> contributionDao.saveSynchronous(contribution))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.subscribe();
|
|
||||||
}
|
|
||||||
WorkRequestHelper.Companion.makeOneTimeWorkRequest(
|
|
||||||
this, ExistingWorkPolicy.APPEND_OR_REPLACE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onPostCreate(savedInstanceState);
|
|
||||||
//quizChecker.initQuizCheck(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putInt("viewPagerCurrentItem", binding.pager.getCurrentItem());
|
|
||||||
outState.putString("activeFragment", activeFragment.name());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
|
||||||
super.onRestoreInstanceState(savedInstanceState);
|
|
||||||
String activeFragmentName = savedInstanceState.getString("activeFragment");
|
|
||||||
if (activeFragmentName != null) {
|
|
||||||
restoreActiveFragment(activeFragmentName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void restoreActiveFragment(@NonNull String fragmentName) {
|
|
||||||
if (fragmentName.equals(ActiveFragment.CONTRIBUTIONS.name())) {
|
|
||||||
setTitle(getString(R.string.contributions_fragment));
|
|
||||||
loadFragment(ContributionsFragment.newInstance(), false);
|
|
||||||
} else if (fragmentName.equals(ActiveFragment.NEARBY.name())) {
|
|
||||||
setTitle(getString(R.string.nearby_fragment));
|
|
||||||
loadFragment(NearbyParentFragment.newInstance(), false);
|
|
||||||
} else if (fragmentName.equals(ActiveFragment.EXPLORE.name())) {
|
|
||||||
setTitle(getString(R.string.navigation_item_explore));
|
|
||||||
loadFragment(ExploreFragment.newInstance(), false);
|
|
||||||
} else if (fragmentName.equals(ActiveFragment.BOOKMARK.name())) {
|
|
||||||
setTitle(getString(R.string.bookmarks));
|
|
||||||
loadFragment(BookmarkFragment.newInstance(), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
if (contributionsFragment != null && activeFragment == ActiveFragment.CONTRIBUTIONS) {
|
|
||||||
// Means that contribution fragment is visible
|
|
||||||
if (!contributionsFragment.backButtonClicked()) {//If this one does not wan't to handle
|
|
||||||
// the back press, let the activity do so
|
|
||||||
super.onBackPressed();
|
|
||||||
}
|
|
||||||
} else if (nearbyParentFragment != null && activeFragment == ActiveFragment.NEARBY) {
|
|
||||||
// Means that nearby fragment is visible
|
|
||||||
/* If function nearbyParentFragment.backButtonClick() returns false, it means that the bottomsheet is
|
|
||||||
not expanded. So if the back button is pressed, then go back to the Contributions tab */
|
|
||||||
if (!nearbyParentFragment.backButtonClicked()) {
|
|
||||||
getSupportFragmentManager().beginTransaction().remove(nearbyParentFragment)
|
|
||||||
.commit();
|
|
||||||
setSelectedItemId(NavTab.CONTRIBUTIONS.code());
|
|
||||||
}
|
|
||||||
} else if (exploreFragment != null && activeFragment == ActiveFragment.EXPLORE) {
|
|
||||||
// Means that explore fragment is visible
|
|
||||||
if (!exploreFragment.onBackPressed()) {
|
|
||||||
if (applicationKvStore.getBoolean("login_skipped")) {
|
|
||||||
super.onBackPressed();
|
|
||||||
} else {
|
|
||||||
setSelectedItemId(NavTab.CONTRIBUTIONS.code());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (bookmarkFragment != null && activeFragment == ActiveFragment.BOOKMARK) {
|
|
||||||
// Means that bookmark fragment is visible
|
|
||||||
bookmarkFragment.onBackPressed();
|
|
||||||
} else {
|
|
||||||
super.onBackPressed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackStackChanged() {
|
|
||||||
//initBackButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retry all failed uploads as soon as the user returns to the app
|
|
||||||
*/
|
|
||||||
@SuppressLint("CheckResult")
|
|
||||||
private void retryAllFailedUploads() {
|
|
||||||
contributionDao.
|
|
||||||
getContribution(Collections.singletonList(Contribution.STATE_FAILED))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.subscribe(failedUploads -> {
|
|
||||||
for (Contribution contribution : failedUploads) {
|
|
||||||
contributionsFragment.retryUpload(contribution);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles item selection in the options menu. This method is called when a user interacts with
|
|
||||||
* the options menu in the Top Bar.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.upload_tab:
|
|
||||||
startActivity(new Intent(this, UploadProgressActivity.class));
|
|
||||||
return true;
|
|
||||||
case R.id.notifications:
|
|
||||||
// Starts notification activity on click to notification icon
|
|
||||||
NotificationActivity.Companion.startYourself(this, "unread");
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void centerMapToPlace(Place place) {
|
|
||||||
setSelectedItemId(NavTab.NEARBY.code());
|
|
||||||
nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback(
|
|
||||||
new NearbyParentFragmentInstanceReadyCallback() {
|
|
||||||
@Override
|
|
||||||
public void onReady() {
|
|
||||||
nearbyParentFragment.centerMapToPlace(place);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
if ((applicationKvStore.getBoolean("firstrun", true)) &&
|
|
||||||
(!applicationKvStore.getBoolean("login_skipped"))) {
|
|
||||||
defaultKvStore.putBoolean("inAppCameraFirstRun", true);
|
|
||||||
WelcomeActivity.startYourself(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
retryAllFailedUploads();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
quizChecker.cleanup();
|
|
||||||
locationManager.unregisterLocationManager();
|
|
||||||
// Remove ourself from hashmap to prevent memory leaks
|
|
||||||
locationManager = null;
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Public method to show nearby from the reference of this.
|
|
||||||
*/
|
|
||||||
public void showNearby() {
|
|
||||||
binding.fragmentMainNavTabLayout.setSelectedItemId(NavTab.NEARBY.code());
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ActiveFragment {
|
|
||||||
CONTRIBUTIONS,
|
|
||||||
NEARBY,
|
|
||||||
EXPLORE,
|
|
||||||
BOOKMARK,
|
|
||||||
MORE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load default language in onCreate from SharedPreferences
|
|
||||||
*/
|
|
||||||
private void loadLocale() {
|
|
||||||
final SharedPreferences preferences = getSharedPreferences("Settings",
|
|
||||||
Activity.MODE_PRIVATE);
|
|
||||||
final String language = preferences.getString("language", "");
|
|
||||||
final SettingsFragment settingsFragment = new SettingsFragment();
|
|
||||||
settingsFragment.setLocale(this, language);
|
|
||||||
}
|
|
||||||
|
|
||||||
public NavTabLayout.OnNavigationItemSelectedListener getNavListener() {
|
|
||||||
return navListener;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,569 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.WelcomeActivity
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
|
import fr.free.nrw.commons.bookmarks.BookmarkFragment
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionsFragment.Companion.newInstance
|
||||||
|
import fr.free.nrw.commons.databinding.MainBinding
|
||||||
|
import fr.free.nrw.commons.explore.ExploreFragment
|
||||||
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
|
import fr.free.nrw.commons.location.LocationServiceManager
|
||||||
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment
|
||||||
|
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment
|
||||||
|
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment
|
||||||
|
import fr.free.nrw.commons.navtab.NavTab
|
||||||
|
import fr.free.nrw.commons.navtab.NavTabLayout
|
||||||
|
import fr.free.nrw.commons.navtab.NavTabLoggedOut
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment
|
||||||
|
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.NearbyParentFragmentInstanceReadyCallback
|
||||||
|
import fr.free.nrw.commons.notification.NotificationActivity.Companion.startYourself
|
||||||
|
import fr.free.nrw.commons.notification.NotificationController
|
||||||
|
import fr.free.nrw.commons.quiz.QuizChecker
|
||||||
|
import fr.free.nrw.commons.settings.SettingsFragment
|
||||||
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
|
import fr.free.nrw.commons.upload.UploadProgressActivity
|
||||||
|
import fr.free.nrw.commons.upload.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest
|
||||||
|
import fr.free.nrw.commons.utils.ViewUtilWrapper
|
||||||
|
import io.reactivex.Completable
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.Calendar
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
|
||||||
|
class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener {
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var sessionManager: SessionManager? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var controller: ContributionController? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var contributionDao: ContributionDao? = null
|
||||||
|
|
||||||
|
private var contributionsFragment: ContributionsFragment? = null
|
||||||
|
private var nearbyParentFragment: NearbyParentFragment? = null
|
||||||
|
private var exploreFragment: ExploreFragment? = null
|
||||||
|
private var bookmarkFragment: BookmarkFragment? = null
|
||||||
|
@JvmField
|
||||||
|
var activeFragment: ActiveFragment? = null
|
||||||
|
private val mediaDetailPagerFragment: MediaDetailPagerFragment? = null
|
||||||
|
var navListener: BottomNavigationView.OnNavigationItemSelectedListener? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var locationManager: LocationServiceManager? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var notificationController: NotificationController? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var quizChecker: QuizChecker? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
@Named("default_preferences")
|
||||||
|
var applicationKvStore: JsonKvStore? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var viewUtilWrapper: ViewUtilWrapper? = null
|
||||||
|
|
||||||
|
var menu: Menu? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var binding: MainBinding? = null
|
||||||
|
|
||||||
|
var tabLayout: NavTabLayout? = null
|
||||||
|
|
||||||
|
|
||||||
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
|
if (activeFragment == ActiveFragment.CONTRIBUTIONS) {
|
||||||
|
if (!contributionsFragment!!.backButtonClicked()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onBackPressed()
|
||||||
|
showTabs()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = MainBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding!!.root)
|
||||||
|
setSupportActionBar(binding!!.toolbarBinding.toolbar)
|
||||||
|
tabLayout = binding!!.fragmentMainNavTabLayout
|
||||||
|
loadLocale()
|
||||||
|
|
||||||
|
binding!!.toolbarBinding.toolbar.setNavigationOnClickListener { view: View? ->
|
||||||
|
onSupportNavigateUp()
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
"first_edit_depict" is a key for getting information about opening the depiction editor
|
||||||
|
screen for the first time after opening the app.
|
||||||
|
|
||||||
|
Getting true by the key means the depiction editor screen is opened for the first time
|
||||||
|
after opening the app.
|
||||||
|
Getting false by the key means the depiction editor screen is not opened for the first time
|
||||||
|
after opening the app.
|
||||||
|
*/
|
||||||
|
applicationKvStore!!.putBoolean("first_edit_depict", true)
|
||||||
|
if (applicationKvStore!!.getBoolean("login_skipped") == true) {
|
||||||
|
title = getString(R.string.navigation_item_explore)
|
||||||
|
setUpLoggedOutPager()
|
||||||
|
} else {
|
||||||
|
if (applicationKvStore!!.getBoolean("firstrun", true)) {
|
||||||
|
applicationKvStore!!.putBoolean("hasAlreadyLaunchedBigMultiupload", false)
|
||||||
|
applicationKvStore!!.putBoolean("hasAlreadyLaunchedCategoriesDialog", false)
|
||||||
|
}
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
//starting a fresh fragment.
|
||||||
|
// Open Last opened screen if it is Contributions or Nearby, otherwise Contributions
|
||||||
|
if (applicationKvStore!!.getBoolean("last_opened_nearby")) {
|
||||||
|
title = getString(R.string.nearby_fragment)
|
||||||
|
showNearby()
|
||||||
|
loadFragment(NearbyParentFragment.newInstance(), false)
|
||||||
|
} else {
|
||||||
|
title = getString(R.string.contributions_fragment)
|
||||||
|
loadFragment(newInstance(), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setUpPager()
|
||||||
|
/**
|
||||||
|
* Ask the user for media location access just after login
|
||||||
|
* so that location in the EXIF metadata of the images shared by the user
|
||||||
|
* is retained on devices running Android 10 or above
|
||||||
|
*/
|
||||||
|
// if (VERSION.SDK_INT >= VERSION_CODES.Q) {
|
||||||
|
// ActivityCompat.requestPermissions(this,
|
||||||
|
// new String[]{Manifest.permission.ACCESS_MEDIA_LOCATION}, 0);
|
||||||
|
// PermissionUtils.checkPermissionsAndPerformAction(
|
||||||
|
// this,
|
||||||
|
// () -> {},
|
||||||
|
// R.string.media_location_permission_denied,
|
||||||
|
// R.string.add_location_manually,
|
||||||
|
// permission.ACCESS_MEDIA_LOCATION);
|
||||||
|
// }
|
||||||
|
checkAndResumeStuckUploads()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSelectedItemId(id: Int) {
|
||||||
|
binding!!.fragmentMainNavTabLayout.selectedItemId = id
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUpPager() {
|
||||||
|
binding!!.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(
|
||||||
|
BottomNavigationView.OnNavigationItemSelectedListener { item: MenuItem ->
|
||||||
|
if (item.title != getString(R.string.more)) {
|
||||||
|
// do not change title for more fragment
|
||||||
|
title = item.title
|
||||||
|
}
|
||||||
|
// set last_opened_nearby true if item is nearby screen else set false
|
||||||
|
applicationKvStore!!.putBoolean(
|
||||||
|
"last_opened_nearby",
|
||||||
|
item.title == getString(R.string.nearby_fragment)
|
||||||
|
)
|
||||||
|
val fragment = NavTab.of(item.order).newInstance()
|
||||||
|
loadFragment(fragment, true)
|
||||||
|
}.also { navListener = it })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUpLoggedOutPager() {
|
||||||
|
loadFragment(ExploreFragment.newInstance(), false)
|
||||||
|
binding!!.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener { item: MenuItem ->
|
||||||
|
if (item.title != getString(R.string.more)) {
|
||||||
|
// do not change title for more fragment
|
||||||
|
title = item.title
|
||||||
|
}
|
||||||
|
val fragment =
|
||||||
|
NavTabLoggedOut.of(item.order).newInstance()
|
||||||
|
loadFragment(fragment, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadFragment(fragment: Fragment?, showBottom: Boolean): Boolean {
|
||||||
|
//showBottom so that we do not show the bottom tray again when constructing
|
||||||
|
//from the saved instance state.
|
||||||
|
|
||||||
|
freeUpFragments();
|
||||||
|
|
||||||
|
if (fragment is ContributionsFragment) {
|
||||||
|
if (activeFragment == ActiveFragment.CONTRIBUTIONS) {
|
||||||
|
// scroll to top if already on the Contributions tab
|
||||||
|
contributionsFragment!!.scrollToTop()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
contributionsFragment = fragment
|
||||||
|
activeFragment = ActiveFragment.CONTRIBUTIONS
|
||||||
|
} else if (fragment is NearbyParentFragment) {
|
||||||
|
if (activeFragment == ActiveFragment.NEARBY) { // Do nothing if same tab
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
nearbyParentFragment = fragment
|
||||||
|
activeFragment = ActiveFragment.NEARBY
|
||||||
|
} else if (fragment is ExploreFragment) {
|
||||||
|
if (activeFragment == ActiveFragment.EXPLORE) { // Do nothing if same tab
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
exploreFragment = fragment
|
||||||
|
activeFragment = ActiveFragment.EXPLORE
|
||||||
|
} else if (fragment is BookmarkFragment) {
|
||||||
|
if (activeFragment == ActiveFragment.BOOKMARK) { // Do nothing if same tab
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
bookmarkFragment = fragment
|
||||||
|
activeFragment = ActiveFragment.BOOKMARK
|
||||||
|
} else if (fragment == null && showBottom) {
|
||||||
|
if (applicationKvStore!!.getBoolean("login_skipped")
|
||||||
|
== true
|
||||||
|
) { // If logged out, more sheet is different
|
||||||
|
val bottomSheet = MoreBottomSheetLoggedOutFragment()
|
||||||
|
bottomSheet.show(
|
||||||
|
supportFragmentManager,
|
||||||
|
"MoreBottomSheetLoggedOut"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val bottomSheet = MoreBottomSheetFragment()
|
||||||
|
bottomSheet.show(
|
||||||
|
supportFragmentManager,
|
||||||
|
"MoreBottomSheet"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fragment != null) {
|
||||||
|
supportFragmentManager
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(R.id.fragmentContainer, fragment)
|
||||||
|
.commit()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loadFragment() overload that supports passing extras to fragments
|
||||||
|
*/
|
||||||
|
private fun loadFragment(fragment: Fragment?, showBottom: Boolean, args: Bundle?): Boolean {
|
||||||
|
if (fragment != null && args != null) {
|
||||||
|
fragment.arguments = args
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadFragment(fragment, showBottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Old implementation of loadFragment() was causing memory leaks, due to MainActivity holding
|
||||||
|
* references to cleared fragments. This function frees up all fragment references.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Called in loadFragment() before doing the actual loading.
|
||||||
|
*/
|
||||||
|
fun freeUpFragments() {
|
||||||
|
// free all fragments except contributionsFragment because several tests depend on it.
|
||||||
|
// hence, contributionsFragment is probably still a leak
|
||||||
|
nearbyParentFragment = null
|
||||||
|
exploreFragment = null
|
||||||
|
bookmarkFragment = null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun hideTabs() {
|
||||||
|
binding!!.fragmentMainNavTabLayout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showTabs() {
|
||||||
|
binding!!.fragmentMainNavTabLayout.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds number of uploads next to tab text "Contributions" then it will look like "Contributions
|
||||||
|
* (NUMBER)"
|
||||||
|
*
|
||||||
|
* @param uploadCount
|
||||||
|
*/
|
||||||
|
fun setNumOfUploads(uploadCount: Int) {
|
||||||
|
if (activeFragment == ActiveFragment.CONTRIBUTIONS) {
|
||||||
|
title =
|
||||||
|
resources.getString(R.string.contributions_fragment) + " " + (if (uploadCount != 0)
|
||||||
|
resources
|
||||||
|
.getQuantityString(
|
||||||
|
R.plurals.contributions_subtitle,
|
||||||
|
uploadCount, uploadCount
|
||||||
|
)
|
||||||
|
else
|
||||||
|
getString(R.string.contributions_subtitle_zero))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume the uploads that got stuck because of the app being killed or the device being
|
||||||
|
* rebooted.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* When the app is terminated or the device is restarted, contributions remain in the
|
||||||
|
* 'STATE_IN_PROGRESS' state. This status persists and doesn't change during these events. So,
|
||||||
|
* retrieving contributions labeled as 'STATE_IN_PROGRESS' from the database will provide the
|
||||||
|
* list of uploads that appear as stuck on opening the app again
|
||||||
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
private fun checkAndResumeStuckUploads() {
|
||||||
|
val stuckUploads = contributionDao!!.getContribution(
|
||||||
|
listOf(Contribution.STATE_IN_PROGRESS)
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.blockingGet()
|
||||||
|
Timber.d("Resuming " + stuckUploads.size + " uploads...")
|
||||||
|
if (!stuckUploads.isEmpty()) {
|
||||||
|
for (contribution in stuckUploads) {
|
||||||
|
contribution.state = Contribution.STATE_QUEUED
|
||||||
|
contribution.dateUploadStarted = Calendar.getInstance().time
|
||||||
|
Completable.fromAction { contributionDao!!.saveSynchronous(contribution) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
makeOneTimeWorkRequest(
|
||||||
|
this, ExistingWorkPolicy.APPEND_OR_REPLACE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onPostCreate(savedInstanceState)
|
||||||
|
//quizChecker.initQuizCheck(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putInt("viewPagerCurrentItem", binding!!.pager.currentItem)
|
||||||
|
outState.putString("activeFragment", activeFragment!!.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
|
val activeFragmentName = savedInstanceState.getString("activeFragment")
|
||||||
|
if (activeFragmentName != null) {
|
||||||
|
restoreActiveFragment(activeFragmentName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreActiveFragment(fragmentName: String) {
|
||||||
|
if (fragmentName == ActiveFragment.CONTRIBUTIONS.name) {
|
||||||
|
title = getString(R.string.contributions_fragment)
|
||||||
|
loadFragment(newInstance(), false)
|
||||||
|
} else if (fragmentName == ActiveFragment.NEARBY.name) {
|
||||||
|
title = getString(R.string.nearby_fragment)
|
||||||
|
loadFragment(NearbyParentFragment.newInstance(), false)
|
||||||
|
} else if (fragmentName == ActiveFragment.EXPLORE.name) {
|
||||||
|
title = getString(R.string.navigation_item_explore)
|
||||||
|
loadFragment(ExploreFragment.newInstance(), false)
|
||||||
|
} else if (fragmentName == ActiveFragment.BOOKMARK.name) {
|
||||||
|
title = getString(R.string.bookmarks)
|
||||||
|
loadFragment(BookmarkFragment.newInstance(), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (contributionsFragment != null && activeFragment == ActiveFragment.CONTRIBUTIONS) {
|
||||||
|
// Means that contribution fragment is visible
|
||||||
|
if (!contributionsFragment!!.backButtonClicked()) { //If this one does not wan't to handle
|
||||||
|
// the back press, let the activity do so
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
} else if (nearbyParentFragment != null && activeFragment == ActiveFragment.NEARBY) {
|
||||||
|
// Means that nearby fragment is visible
|
||||||
|
/* If function nearbyParentFragment.backButtonClick() returns false, it means that the bottomsheet is
|
||||||
|
not expanded. So if the back button is pressed, then go back to the Contributions tab */
|
||||||
|
if (!nearbyParentFragment!!.backButtonClicked()) {
|
||||||
|
supportFragmentManager.beginTransaction().remove(nearbyParentFragment!!)
|
||||||
|
.commit()
|
||||||
|
setSelectedItemId(NavTab.CONTRIBUTIONS.code())
|
||||||
|
}
|
||||||
|
} else if (exploreFragment != null && activeFragment == ActiveFragment.EXPLORE) {
|
||||||
|
// Means that explore fragment is visible
|
||||||
|
if (!exploreFragment!!.onBackPressed()) {
|
||||||
|
if (applicationKvStore!!.getBoolean("login_skipped")) {
|
||||||
|
super.onBackPressed()
|
||||||
|
} else {
|
||||||
|
setSelectedItemId(NavTab.CONTRIBUTIONS.code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (bookmarkFragment != null && activeFragment == ActiveFragment.BOOKMARK) {
|
||||||
|
// Means that bookmark fragment is visible
|
||||||
|
bookmarkFragment!!.onBackPressed()
|
||||||
|
} else {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackStackChanged() {
|
||||||
|
//initBackButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry all failed uploads as soon as the user returns to the app
|
||||||
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
private fun retryAllFailedUploads() {
|
||||||
|
contributionDao
|
||||||
|
?.getContribution(listOf(Contribution.STATE_FAILED))
|
||||||
|
?.subscribeOn(Schedulers.io())
|
||||||
|
?.subscribe { failedUploads ->
|
||||||
|
failedUploads.forEach { contribution ->
|
||||||
|
contributionsFragment?.retryUpload(contribution)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles item selection in the options menu. This method is called when a user interacts with
|
||||||
|
* the options menu in the Top Bar.
|
||||||
|
*/
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.upload_tab -> {
|
||||||
|
startActivity(Intent(this, UploadProgressActivity::class.java))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.notifications -> {
|
||||||
|
// Starts notification activity on click to notification icon
|
||||||
|
startYourself(this, "unread")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun centerMapToPlace(place: Place?) {
|
||||||
|
setSelectedItemId(NavTab.NEARBY.code())
|
||||||
|
nearbyParentFragment!!.setNearbyParentFragmentInstanceReadyCallback(
|
||||||
|
object : NearbyParentFragmentInstanceReadyCallback {
|
||||||
|
override fun onReady() {
|
||||||
|
nearbyParentFragment!!.centerMapToPlace(place)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the Explore fragment from Nearby fragment. This method is called when a user clicks
|
||||||
|
* the 'Show in Explore' option in the 3-dots menu in Nearby.
|
||||||
|
*
|
||||||
|
* @param zoom current zoom of Nearby map
|
||||||
|
* @param latitude current latitude of Nearby map
|
||||||
|
* @param longitude current longitude of Nearby map
|
||||||
|
*/
|
||||||
|
fun loadExploreMapFromNearby(zoom: Double, latitude: Double, longitude: Double) {
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putDouble("prev_zoom", zoom)
|
||||||
|
bundle.putDouble("prev_latitude", latitude)
|
||||||
|
bundle.putDouble("prev_longitude", longitude)
|
||||||
|
|
||||||
|
loadFragment(ExploreFragment.newInstance(), false, bundle)
|
||||||
|
setSelectedItemId(NavTab.EXPLORE.code())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the Nearby fragment from Explore fragment. This method is called when a user clicks
|
||||||
|
* the 'Show in Nearby' option in the 3-dots menu in Explore.
|
||||||
|
*
|
||||||
|
* @param zoom current zoom of Explore map
|
||||||
|
* @param latitude current latitude of Explore map
|
||||||
|
* @param longitude current longitude of Explore map
|
||||||
|
*/
|
||||||
|
fun loadNearbyMapFromExplore(zoom: Double, latitude: Double, longitude: Double) {
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putDouble("prev_zoom", zoom)
|
||||||
|
bundle.putDouble("prev_latitude", latitude)
|
||||||
|
bundle.putDouble("prev_longitude", longitude)
|
||||||
|
|
||||||
|
loadFragment(NearbyParentFragment.newInstance(), false, bundle)
|
||||||
|
setSelectedItemId(NavTab.NEARBY.code())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
if ((applicationKvStore!!.getBoolean("firstrun", true)) &&
|
||||||
|
(!applicationKvStore!!.getBoolean("login_skipped"))
|
||||||
|
) {
|
||||||
|
defaultKvStore.putBoolean("inAppCameraFirstRun", true)
|
||||||
|
WelcomeActivity.startYourself(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
retryAllFailedUploads()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
quizChecker!!.cleanup()
|
||||||
|
locationManager!!.unregisterLocationManager()
|
||||||
|
// Remove ourself from hashmap to prevent memory leaks
|
||||||
|
locationManager = null
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public method to show nearby from the reference of this.
|
||||||
|
*/
|
||||||
|
fun showNearby() {
|
||||||
|
binding!!.fragmentMainNavTabLayout.selectedItemId = NavTab.NEARBY.code()
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ActiveFragment {
|
||||||
|
CONTRIBUTIONS,
|
||||||
|
NEARBY,
|
||||||
|
EXPLORE,
|
||||||
|
BOOKMARK,
|
||||||
|
MORE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load default language in onCreate from SharedPreferences
|
||||||
|
*/
|
||||||
|
private fun loadLocale() {
|
||||||
|
val preferences = getSharedPreferences(
|
||||||
|
"Settings",
|
||||||
|
MODE_PRIVATE
|
||||||
|
)
|
||||||
|
val language = preferences.getString("language", "")!!
|
||||||
|
val settingsFragment = SettingsFragment()
|
||||||
|
settingsFragment.setLocale(this, language)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Consumers should be simply using this method to use this activity.
|
||||||
|
*
|
||||||
|
* @param context A Context of the application package implementing this class.
|
||||||
|
*/
|
||||||
|
fun startYourself(context: Context) {
|
||||||
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.app.WallpaperManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.work.Worker;
|
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
import com.facebook.common.executors.CallerThreadExecutor;
|
|
||||||
import com.facebook.common.references.CloseableReference;
|
|
||||||
import com.facebook.datasource.DataSource;
|
|
||||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
|
||||||
import com.facebook.imagepipeline.core.ImagePipeline;
|
|
||||||
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
|
|
||||||
import com.facebook.imagepipeline.image.CloseableImage;
|
|
||||||
import com.facebook.imagepipeline.request.ImageRequest;
|
|
||||||
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class SetWallpaperWorker extends Worker {
|
|
||||||
|
|
||||||
private static final String NOTIFICATION_CHANNEL_ID = "set_wallpaper_channel";
|
|
||||||
private static final int NOTIFICATION_ID = 1;
|
|
||||||
|
|
||||||
public SetWallpaperWorker(@NonNull Context context, @NonNull WorkerParameters params) {
|
|
||||||
super(context, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Result doWork() {
|
|
||||||
Context context = getApplicationContext();
|
|
||||||
createNotificationChannel(context);
|
|
||||||
showProgressNotification(context);
|
|
||||||
|
|
||||||
String imageUrl = getInputData().getString("imageUrl");
|
|
||||||
if (imageUrl == null) {
|
|
||||||
return Result.failure();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageRequest imageRequest = ImageRequestBuilder
|
|
||||||
.newBuilderWithSource(Uri.parse(imageUrl))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
ImagePipeline imagePipeline = Fresco.getImagePipeline();
|
|
||||||
final DataSource<CloseableReference<CloseableImage>>
|
|
||||||
dataSource = imagePipeline.fetchDecodedImage(imageRequest, context);
|
|
||||||
|
|
||||||
dataSource.subscribe(new BaseBitmapDataSubscriber() {
|
|
||||||
@Override
|
|
||||||
public void onNewResultImpl(@Nullable Bitmap bitmap) {
|
|
||||||
if (dataSource.isFinished() && bitmap != null) {
|
|
||||||
Timber.d("Bitmap loaded from url %s", imageUrl.toString());
|
|
||||||
setWallpaper(context, Bitmap.createBitmap(bitmap));
|
|
||||||
dataSource.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailureImpl(DataSource dataSource) {
|
|
||||||
Timber.d("Error getting bitmap from image url %s", imageUrl.toString());
|
|
||||||
showNotification(context, "Setting Wallpaper Failed", "Failed to download image.");
|
|
||||||
if (dataSource != null) {
|
|
||||||
dataSource.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, CallerThreadExecutor.getInstance());
|
|
||||||
|
|
||||||
return Result.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setWallpaper(Context context, Bitmap bitmap) {
|
|
||||||
WallpaperManager wallpaperManager = WallpaperManager.getInstance(context);
|
|
||||||
|
|
||||||
try {
|
|
||||||
wallpaperManager.setBitmap(bitmap);
|
|
||||||
showNotification(context, "Wallpaper Set", "Wallpaper has been updated successfully.");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.e(e, "Error setting wallpaper");
|
|
||||||
showNotification(context, "Setting Wallpaper Failed", " "+e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showProgressNotification(Context context) {
|
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.commons_logo)
|
|
||||||
.setContentTitle("Setting Wallpaper")
|
|
||||||
.setContentText("Please wait...")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
||||||
.setOngoing(true)
|
|
||||||
.setProgress(0, 0, true);
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showNotification(Context context, String title, String content) {
|
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.commons_logo)
|
|
||||||
.setContentTitle(title)
|
|
||||||
.setContentText(content)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
||||||
.setOngoing(false);
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createNotificationChannel(Context context) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
CharSequence name = "Wallpaper Setting";
|
|
||||||
String description = "Notifications for wallpaper setting progress";
|
|
||||||
int importance = NotificationManager.IMPORTANCE_HIGH;
|
|
||||||
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance);
|
|
||||||
channel.setDescription(description);
|
|
||||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
|
||||||
notificationManager.createNotificationChannel(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.WallpaperManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.facebook.common.executors.CallerThreadExecutor
|
||||||
|
import com.facebook.common.references.CloseableReference
|
||||||
|
import com.facebook.datasource.DataSource
|
||||||
|
import com.facebook.drawee.backends.pipeline.Fresco
|
||||||
|
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
|
||||||
|
import com.facebook.imagepipeline.image.CloseableImage
|
||||||
|
import com.facebook.imagepipeline.request.ImageRequestBuilder
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class SetWallpaperWorker(context: Context, params: WorkerParameters) :
|
||||||
|
Worker(context, params) {
|
||||||
|
override fun doWork(): Result {
|
||||||
|
val context = applicationContext
|
||||||
|
createNotificationChannel(context)
|
||||||
|
showProgressNotification(context)
|
||||||
|
|
||||||
|
val imageUrl = inputData.getString("imageUrl") ?: return Result.failure()
|
||||||
|
|
||||||
|
val imageRequest = ImageRequestBuilder
|
||||||
|
.newBuilderWithSource(Uri.parse(imageUrl))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val imagePipeline = Fresco.getImagePipeline()
|
||||||
|
val dataSource = imagePipeline.fetchDecodedImage(imageRequest, context)
|
||||||
|
|
||||||
|
dataSource.subscribe(object : BaseBitmapDataSubscriber() {
|
||||||
|
public override fun onNewResultImpl(bitmap: Bitmap?) {
|
||||||
|
if (dataSource.isFinished && bitmap != null) {
|
||||||
|
Timber.d("Bitmap loaded from url %s", imageUrl.toString())
|
||||||
|
setWallpaper(context, Bitmap.createBitmap(bitmap))
|
||||||
|
dataSource.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>?) {
|
||||||
|
Timber.d("Error getting bitmap from image url %s", imageUrl.toString())
|
||||||
|
showNotification(context, "Setting Wallpaper Failed", "Failed to download image.")
|
||||||
|
dataSource?.close()
|
||||||
|
}
|
||||||
|
}, CallerThreadExecutor.getInstance())
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setWallpaper(context: Context, bitmap: Bitmap) {
|
||||||
|
val wallpaperManager = WallpaperManager.getInstance(context)
|
||||||
|
|
||||||
|
try {
|
||||||
|
wallpaperManager.setBitmap(bitmap)
|
||||||
|
showNotification(context, "Wallpaper Set", "Wallpaper has been updated successfully.")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Error setting wallpaper")
|
||||||
|
showNotification(context, "Setting Wallpaper Failed", " " + e.localizedMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showProgressNotification(context: Context) {
|
||||||
|
val notificationManager =
|
||||||
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.commons_logo)
|
||||||
|
.setContentTitle("Setting Wallpaper")
|
||||||
|
.setContentText("Please wait...")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setProgress(0, 0, true)
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNotification(context: Context, title: String, content: String) {
|
||||||
|
val notificationManager =
|
||||||
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.commons_logo)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(content)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setOngoing(false)
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannel(context: Context) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val name: CharSequence = "Wallpaper Setting"
|
||||||
|
val description = "Notifications for wallpaper setting progress"
|
||||||
|
val importance = NotificationManager.IMPORTANCE_HIGH
|
||||||
|
val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance)
|
||||||
|
channel.description = description
|
||||||
|
val notificationManager = context.getSystemService(
|
||||||
|
NotificationManager::class.java
|
||||||
|
)
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NOTIFICATION_CHANNEL_ID = "set_wallpaper_channel"
|
||||||
|
private const val NOTIFICATION_ID = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.viewpager.widget.ViewPager;
|
|
||||||
|
|
||||||
public class UnswipableViewPager extends ViewPager{
|
|
||||||
public UnswipableViewPager(@NonNull Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public UnswipableViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onInterceptTouchEvent(MotionEvent event) {
|
|
||||||
// Unswipable
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
|
||||||
// Unswipable
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import androidx.viewpager.widget.ViewPager
|
||||||
|
|
||||||
|
class UnswipableViewPager : ViewPager {
|
||||||
|
constructor(context: Context) : super(context)
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
|
|
||||||
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
// Unswipable
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
// Unswipable
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,7 +43,7 @@ class WikipediaInstructionsDialogFragment : DialogFragment() {
|
||||||
/**
|
/**
|
||||||
* Callback for handling confirm button clicked
|
* Callback for handling confirm button clicked
|
||||||
*/
|
*/
|
||||||
interface Callback {
|
fun interface Callback {
|
||||||
fun onConfirmClicked(
|
fun onConfirmClicked(
|
||||||
contribution: Contribution?,
|
contribution: Contribution?,
|
||||||
copyWikicode: Boolean,
|
copyWikicode: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@ class DBOpenHelper(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val DATABASE_NAME = "commons.db"
|
private const val DATABASE_NAME = "commons.db"
|
||||||
private const val DATABASE_VERSION = 21
|
private const val DATABASE_VERSION = 22
|
||||||
const val CONTRIBUTIONS_TABLE = "contributions"
|
const val CONTRIBUTIONS_TABLE = "contributions"
|
||||||
|
const val BOOKMARKS_LOCATIONS = "bookmarksLocations"
|
||||||
private const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS %s"
|
private const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS %s"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,7 +31,6 @@ class DBOpenHelper(
|
||||||
override fun onCreate(db: SQLiteDatabase) {
|
override fun onCreate(db: SQLiteDatabase) {
|
||||||
CategoryDao.Table.onCreate(db)
|
CategoryDao.Table.onCreate(db)
|
||||||
BookmarkPicturesDao.Table.onCreate(db)
|
BookmarkPicturesDao.Table.onCreate(db)
|
||||||
BookmarkLocationsDao.Table.onCreate(db)
|
|
||||||
BookmarkItemsDao.Table.onCreate(db)
|
BookmarkItemsDao.Table.onCreate(db)
|
||||||
RecentSearchesDao.Table.onCreate(db)
|
RecentSearchesDao.Table.onCreate(db)
|
||||||
RecentLanguagesDao.Table.onCreate(db)
|
RecentLanguagesDao.Table.onCreate(db)
|
||||||
|
|
@ -39,11 +39,11 @@ class DBOpenHelper(
|
||||||
override fun onUpgrade(db: SQLiteDatabase, from: Int, to: Int) {
|
override fun onUpgrade(db: SQLiteDatabase, from: Int, to: Int) {
|
||||||
CategoryDao.Table.onUpdate(db, from, to)
|
CategoryDao.Table.onUpdate(db, from, to)
|
||||||
BookmarkPicturesDao.Table.onUpdate(db, from, to)
|
BookmarkPicturesDao.Table.onUpdate(db, from, to)
|
||||||
BookmarkLocationsDao.Table.onUpdate(db, from, to)
|
|
||||||
BookmarkItemsDao.Table.onUpdate(db, from, to)
|
BookmarkItemsDao.Table.onUpdate(db, from, to)
|
||||||
RecentSearchesDao.Table.onUpdate(db, from, to)
|
RecentSearchesDao.Table.onUpdate(db, from, to)
|
||||||
RecentLanguagesDao.Table.onUpdate(db, from, to)
|
RecentLanguagesDao.Table.onUpdate(db, from, to)
|
||||||
deleteTable(db, CONTRIBUTIONS_TABLE)
|
deleteTable(db, CONTRIBUTIONS_TABLE)
|
||||||
|
deleteTable(db, BOOKMARKS_LOCATIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
package fr.free.nrw.commons.db
|
package fr.free.nrw.commons.db
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao
|
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao
|
||||||
import fr.free.nrw.commons.bookmarks.category.BookmarksCategoryModal
|
import fr.free.nrw.commons.bookmarks.category.BookmarksCategoryModal
|
||||||
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
|
||||||
|
import fr.free.nrw.commons.bookmarks.locations.BookmarksLocations
|
||||||
import fr.free.nrw.commons.contributions.Contribution
|
import fr.free.nrw.commons.contributions.Contribution
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao
|
import fr.free.nrw.commons.contributions.ContributionDao
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
||||||
|
|
@ -23,8 +29,8 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class, BookmarksCategoryModal::class],
|
entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class, BookmarksCategoryModal::class, BookmarksLocations::class],
|
||||||
version = 19,
|
version = 20,
|
||||||
exportSchema = false,
|
exportSchema = false,
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
@ -42,4 +48,6 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun ReviewDao(): ReviewDao
|
abstract fun ReviewDao(): ReviewDao
|
||||||
|
|
||||||
abstract fun bookmarkCategoriesDao(): BookmarkCategoriesDao
|
abstract fun bookmarkCategoriesDao(): BookmarkCategoriesDao
|
||||||
|
|
||||||
|
abstract fun bookmarkLocationsDao(): BookmarkLocationsDao
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import fr.free.nrw.commons.CommonsApplication
|
||||||
import fr.free.nrw.commons.activity.SingleWebViewActivity
|
import fr.free.nrw.commons.activity.SingleWebViewActivity
|
||||||
import fr.free.nrw.commons.auth.LoginActivity
|
import fr.free.nrw.commons.auth.LoginActivity
|
||||||
import fr.free.nrw.commons.contributions.ContributionsModule
|
import fr.free.nrw.commons.contributions.ContributionsModule
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionsProvidesModule
|
||||||
import fr.free.nrw.commons.explore.SearchModule
|
import fr.free.nrw.commons.explore.SearchModule
|
||||||
import fr.free.nrw.commons.explore.categories.CategoriesModule
|
import fr.free.nrw.commons.explore.categories.CategoriesModule
|
||||||
import fr.free.nrw.commons.explore.depictions.DepictionModule
|
import fr.free.nrw.commons.explore.depictions.DepictionModule
|
||||||
|
|
@ -40,6 +41,7 @@ import javax.inject.Singleton
|
||||||
ContentProviderBuilderModule::class,
|
ContentProviderBuilderModule::class,
|
||||||
UploadModule::class,
|
UploadModule::class,
|
||||||
ContributionsModule::class,
|
ContributionsModule::class,
|
||||||
|
ContributionsProvidesModule::class,
|
||||||
SearchModule::class,
|
SearchModule::class,
|
||||||
DepictionModule::class,
|
DepictionModule::class,
|
||||||
CategoriesModule::class
|
CategoriesModule::class
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.app.Activity
|
||||||
import android.content.ContentProviderClient
|
import android.content.ContentProviderClient
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import androidx.collection.LruCache
|
import androidx.collection.LruCache
|
||||||
import androidx.room.Room.databaseBuilder
|
import androidx.room.Room.databaseBuilder
|
||||||
|
|
@ -16,6 +17,7 @@ import fr.free.nrw.commons.BuildConfig
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.auth.SessionManager
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao
|
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao
|
||||||
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao
|
import fr.free.nrw.commons.contributions.ContributionDao
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||||
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
||||||
|
|
@ -36,6 +38,7 @@ import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl
|
||||||
import io.reactivex.Scheduler
|
import io.reactivex.Scheduler
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import timber.log.Timber
|
||||||
import java.util.Objects
|
import java.util.Objects
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
@ -49,6 +52,11 @@ import javax.inject.Singleton
|
||||||
@Module
|
@Module
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
open class CommonsApplicationModule(private val applicationContext: Context) {
|
open class CommonsApplicationModule(private val applicationContext: Context) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
appContext = applicationContext
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun providesImageFileLoader(context: Context): ImageFileLoader =
|
fun providesImageFileLoader(context: Context): ImageFileLoader =
|
||||||
ImageFileLoader(context)
|
ImageFileLoader(context)
|
||||||
|
|
@ -110,11 +118,6 @@ open class CommonsApplicationModule(private val applicationContext: Context) {
|
||||||
fun provideBookmarkContentProviderClient(context: Context): ContentProviderClient? =
|
fun provideBookmarkContentProviderClient(context: Context): ContentProviderClient? =
|
||||||
context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_AUTHORITY)
|
context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_AUTHORITY)
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Named("bookmarksLocation")
|
|
||||||
fun provideBookmarkLocationContentProviderClient(context: Context): ContentProviderClient? =
|
|
||||||
context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY)
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Named("bookmarksItem")
|
@Named("bookmarksItem")
|
||||||
fun provideBookmarkItemContentProviderClient(context: Context): ContentProviderClient? =
|
fun provideBookmarkItemContentProviderClient(context: Context): ContentProviderClient? =
|
||||||
|
|
@ -196,7 +199,10 @@ open class CommonsApplicationModule(private val applicationContext: Context) {
|
||||||
applicationContext,
|
applicationContext,
|
||||||
AppDatabase::class.java,
|
AppDatabase::class.java,
|
||||||
"commons_room.db"
|
"commons_room.db"
|
||||||
).addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build()
|
).addMigrations(
|
||||||
|
MIGRATION_1_2,
|
||||||
|
MIGRATION_19_TO_20
|
||||||
|
).fallbackToDestructiveMigration().build()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun providesContributionsDao(appDatabase: AppDatabase): ContributionDao =
|
fun providesContributionsDao(appDatabase: AppDatabase): ContributionDao =
|
||||||
|
|
@ -206,6 +212,10 @@ open class CommonsApplicationModule(private val applicationContext: Context) {
|
||||||
fun providesPlaceDao(appDatabase: AppDatabase): PlaceDao =
|
fun providesPlaceDao(appDatabase: AppDatabase): PlaceDao =
|
||||||
appDatabase.PlaceDao()
|
appDatabase.PlaceDao()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun providesBookmarkLocationsDao(appDatabase: AppDatabase): BookmarkLocationsDao =
|
||||||
|
appDatabase.bookmarkLocationsDao()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun providesDepictDao(appDatabase: AppDatabase): DepictsDao =
|
fun providesDepictDao(appDatabase: AppDatabase): DepictsDao =
|
||||||
appDatabase.DepictsDao()
|
appDatabase.DepictsDao()
|
||||||
|
|
@ -239,6 +249,9 @@ open class CommonsApplicationModule(private val applicationContext: Context) {
|
||||||
const val IO_THREAD: String = "io_thread"
|
const val IO_THREAD: String = "io_thread"
|
||||||
const val MAIN_THREAD: String = "main_thread"
|
const val MAIN_THREAD: String = "main_thread"
|
||||||
|
|
||||||
|
lateinit var appContext: Context
|
||||||
|
private set
|
||||||
|
|
||||||
val MIGRATION_1_2: Migration = object : Migration(1, 2) {
|
val MIGRATION_1_2: Migration = object : Migration(1, 2) {
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
db.execSQL(
|
db.execSQL(
|
||||||
|
|
@ -246,5 +259,101 @@ open class CommonsApplicationModule(private val applicationContext: Context) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val MIGRATION_19_TO_20 = object : Migration(19, 20) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS bookmarks_locations (
|
||||||
|
location_name TEXT NOT NULL PRIMARY KEY,
|
||||||
|
location_language TEXT NOT NULL,
|
||||||
|
location_description TEXT NOT NULL,
|
||||||
|
location_lat REAL NOT NULL,
|
||||||
|
location_long REAL NOT NULL,
|
||||||
|
location_category TEXT NOT NULL,
|
||||||
|
location_label_text TEXT NOT NULL,
|
||||||
|
location_label_icon INTEGER,
|
||||||
|
location_image_url TEXT NOT NULL DEFAULT '',
|
||||||
|
location_wikipedia_link TEXT NOT NULL,
|
||||||
|
location_wikidata_link TEXT NOT NULL,
|
||||||
|
location_commons_link TEXT NOT NULL,
|
||||||
|
location_pic TEXT NOT NULL,
|
||||||
|
location_exists INTEGER NOT NULL CHECK(location_exists IN (0, 1))
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
val oldDbPath = appContext.getDatabasePath("commons.db").path
|
||||||
|
val oldDb = SQLiteDatabase
|
||||||
|
.openDatabase(oldDbPath, null, SQLiteDatabase.OPEN_READONLY)
|
||||||
|
|
||||||
|
val cursor = oldDb.rawQuery("SELECT * FROM bookmarksLocations", null)
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val locationName =
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow("location_name"))
|
||||||
|
val locationLanguage =
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow("location_language"))
|
||||||
|
val locationDescription =
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow("location_description"))
|
||||||
|
val locationCategory =
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow("location_category"))
|
||||||
|
val locationLabelText =
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow("location_label_text"))
|
||||||
|
val locationLabelIcon =
|
||||||
|
cursor.getInt(cursor.getColumnIndexOrThrow("location_label_icon"))
|
||||||
|
val locationLat =
|
||||||
|
cursor.getDouble(cursor.getColumnIndexOrThrow("location_lat"))
|
||||||
|
val locationLong =
|
||||||
|
cursor.getDouble(cursor.getColumnIndexOrThrow("location_long"))
|
||||||
|
|
||||||
|
// Handle NULL values safely
|
||||||
|
val locationImageUrl =
|
||||||
|
cursor.getString(
|
||||||
|
cursor.getColumnIndexOrThrow("location_image_url")
|
||||||
|
) ?: ""
|
||||||
|
val locationWikipediaLink =
|
||||||
|
cursor.getString(
|
||||||
|
cursor.getColumnIndexOrThrow("location_wikipedia_link")
|
||||||
|
) ?: ""
|
||||||
|
val locationWikidataLink =
|
||||||
|
cursor.getString(
|
||||||
|
cursor.getColumnIndexOrThrow("location_wikidata_link")
|
||||||
|
) ?: ""
|
||||||
|
val locationCommonsLink =
|
||||||
|
cursor.getString(
|
||||||
|
cursor.getColumnIndexOrThrow("location_commons_link")
|
||||||
|
) ?: ""
|
||||||
|
val locationPic =
|
||||||
|
cursor.getString(
|
||||||
|
cursor.getColumnIndexOrThrow("location_pic")
|
||||||
|
) ?: ""
|
||||||
|
val locationExists =
|
||||||
|
cursor.getInt(
|
||||||
|
cursor.getColumnIndexOrThrow("location_exists")
|
||||||
|
)
|
||||||
|
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT OR REPLACE INTO bookmarks_locations (
|
||||||
|
location_name, location_language, location_description, location_category,
|
||||||
|
location_label_text, location_label_icon, location_lat, location_long,
|
||||||
|
location_image_url, location_wikipedia_link, location_wikidata_link,
|
||||||
|
location_commons_link, location_pic, location_exists
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
arrayOf(
|
||||||
|
locationName, locationLanguage, locationDescription, locationCategory,
|
||||||
|
locationLabelText, locationLabelIcon, locationLat, locationLong,
|
||||||
|
locationImageUrl, locationWikipediaLink, locationWikidataLink,
|
||||||
|
locationCommonsLink, locationPic, locationExists
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
oldDb.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ abstract class CommonsDaggerSupportFragment : Fragment(), HasSupportFragmentInje
|
||||||
@Inject @JvmField
|
@Inject @JvmField
|
||||||
var childFragmentInjector: DispatchingAndroidInjector<Fragment>? = null
|
var childFragmentInjector: DispatchingAndroidInjector<Fragment>? = null
|
||||||
|
|
||||||
@JvmField
|
// Removed @JvmField to allow overriding
|
||||||
protected var compositeDisposable: CompositeDisposable = CompositeDisposable()
|
protected open var compositeDisposable: CompositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
inject()
|
inject()
|
||||||
|
|
@ -63,4 +63,9 @@ abstract class CommonsDaggerSupportFragment : Fragment(), HasSupportFragmentInje
|
||||||
|
|
||||||
return getInstance(activity.applicationContext)
|
return getInstance(activity.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure getContext() returns a non-null Context
|
||||||
|
override fun getContext(): Context {
|
||||||
|
return super.getContext() ?: throw IllegalStateException("Context is null")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons.di
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider
|
|
||||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider
|
||||||
import fr.free.nrw.commons.category.CategoryContentProvider
|
import fr.free.nrw.commons.category.CategoryContentProvider
|
||||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider
|
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider
|
||||||
|
|
@ -26,9 +25,6 @@ abstract class ContentProviderBuilderModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun bindBookmarkContentProvider(): BookmarkPicturesContentProvider
|
abstract fun bindBookmarkContentProvider(): BookmarkPicturesContentProvider
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
|
||||||
abstract fun bindBookmarkLocationContentProvider(): BookmarkLocationsContentProvider
|
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun bindBookmarkItemContentProvider(): BookmarkItemsContentProvider
|
abstract fun bindBookmarkItemContentProvider(): BookmarkItemsContentProvider
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package fr.free.nrw.commons.explore;
|
package fr.free.nrw.commons.explore;
|
||||||
|
|
||||||
|
import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
|
|
@ -42,9 +44,13 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
||||||
@Named("default_preferences")
|
@Named("default_preferences")
|
||||||
public JsonKvStore applicationKvStore;
|
public JsonKvStore applicationKvStore;
|
||||||
|
|
||||||
public void setScroll(boolean canScroll){
|
// Nearby map state (for if we came from Nearby fragment)
|
||||||
if (binding != null)
|
private double prevZoom;
|
||||||
{
|
private double prevLatitude;
|
||||||
|
private double prevLongitude;
|
||||||
|
|
||||||
|
public void setScroll(boolean canScroll) {
|
||||||
|
if (binding != null) {
|
||||||
binding.viewPager.setCanScroll(canScroll);
|
binding.viewPager.setCanScroll(canScroll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -60,6 +66,7 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
@Nullable Bundle savedInstanceState) {
|
@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
loadNearbyMapData();
|
||||||
binding = FragmentExploreBinding.inflate(inflater, container, false);
|
binding = FragmentExploreBinding.inflate(inflater, container, false);
|
||||||
|
|
||||||
viewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager());
|
viewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager());
|
||||||
|
|
@ -89,6 +96,11 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
||||||
});
|
});
|
||||||
setTabs();
|
setTabs();
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
|
// if we came from 'Show in Explore' in Nearby, jump to Map tab
|
||||||
|
if (isCameFromNearbyMap()) {
|
||||||
|
binding.viewPager.setCurrentItem(2);
|
||||||
|
}
|
||||||
return binding.getRoot();
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,6 +120,13 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
||||||
Bundle mapArguments = new Bundle();
|
Bundle mapArguments = new Bundle();
|
||||||
mapArguments.putString("categoryName", EXPLORE_MAP);
|
mapArguments.putString("categoryName", EXPLORE_MAP);
|
||||||
|
|
||||||
|
// if we came from 'Show in Explore' in Nearby, pass on zoom and center to Explore map root
|
||||||
|
if (isCameFromNearbyMap()) {
|
||||||
|
mapArguments.putDouble("prev_zoom", prevZoom);
|
||||||
|
mapArguments.putDouble("prev_latitude", prevLatitude);
|
||||||
|
mapArguments.putDouble("prev_longitude", prevLongitude);
|
||||||
|
}
|
||||||
|
|
||||||
featuredRootFragment = new ExploreListRootFragment(featuredArguments);
|
featuredRootFragment = new ExploreListRootFragment(featuredArguments);
|
||||||
mobileRootFragment = new ExploreListRootFragment(mobileArguments);
|
mobileRootFragment = new ExploreListRootFragment(mobileArguments);
|
||||||
mapRootFragment = new ExploreMapRootFragment(mapArguments);
|
mapRootFragment = new ExploreMapRootFragment(mapArguments);
|
||||||
|
|
@ -120,13 +139,35 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
||||||
fragmentList.add(mapRootFragment);
|
fragmentList.add(mapRootFragment);
|
||||||
titleList.add(getString(R.string.explore_tab_title_map).toUpperCase(Locale.ROOT));
|
titleList.add(getString(R.string.explore_tab_title_map).toUpperCase(Locale.ROOT));
|
||||||
|
|
||||||
((MainActivity)getActivity()).showTabs();
|
((MainActivity) getActivity()).showTabs();
|
||||||
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||||
|
|
||||||
viewPagerAdapter.setTabData(fragmentList, titleList);
|
viewPagerAdapter.setTabData(fragmentList, titleList);
|
||||||
viewPagerAdapter.notifyDataSetChanged();
|
viewPagerAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Nearby map camera data from fragment arguments if any.
|
||||||
|
*/
|
||||||
|
public void loadNearbyMapData() {
|
||||||
|
// get fragment arguments
|
||||||
|
if (getArguments() != null) {
|
||||||
|
prevZoom = getArguments().getDouble("prev_zoom");
|
||||||
|
prevLatitude = getArguments().getDouble("prev_latitude");
|
||||||
|
prevLongitude = getArguments().getDouble("prev_longitude");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if fragment arguments contain data from Nearby map. if present, then the user
|
||||||
|
* navigated from Nearby using 'Show in Explore'.
|
||||||
|
*
|
||||||
|
* @return true if user navigated from Nearby map
|
||||||
|
**/
|
||||||
|
public boolean isCameFromNearbyMap() {
|
||||||
|
return prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean onBackPressed() {
|
public boolean onBackPressed() {
|
||||||
if (binding.tabLayout.getSelectedTabPosition() == 0) {
|
if (binding.tabLayout.getSelectedTabPosition() == 0) {
|
||||||
if (featuredRootFragment.backPressed()) {
|
if (featuredRootFragment.backPressed()) {
|
||||||
|
|
@ -155,7 +196,38 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
inflater.inflate(R.menu.menu_search, menu);
|
// if logged in 'Show in Nearby' menu item is visible
|
||||||
|
if (applicationKvStore.getBoolean("login_skipped") == false) {
|
||||||
|
inflater.inflate(R.menu.explore_fragment_menu, menu);
|
||||||
|
|
||||||
|
MenuItem others = menu.findItem(R.id.list_item_show_in_nearby);
|
||||||
|
|
||||||
|
if (binding.viewPager.getCurrentItem() == 2) {
|
||||||
|
others.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if on Map tab, show all menu options, else only show search
|
||||||
|
binding.viewPager.addOnPageChangeListener(new OnPageChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onPageScrolled(int position, float positionOffset,
|
||||||
|
int positionOffsetPixels) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageSelected(int position) {
|
||||||
|
others.setVisible((position == 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageScrollStateChanged(int state) {
|
||||||
|
if (state == SCROLL_STATE_IDLE && binding.viewPager.getCurrentItem() == 2) {
|
||||||
|
onPageSelected(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
inflater.inflate(R.menu.menu_search, menu);
|
||||||
|
}
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,6 +243,9 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
||||||
case R.id.action_search:
|
case R.id.action_search:
|
||||||
ActivityUtils.startActivityWithFlags(getActivity(), SearchActivity.class);
|
ActivityUtils.startActivityWithFlags(getActivity(), SearchActivity.class);
|
||||||
return true;
|
return true;
|
||||||
|
case R.id.list_item_show_in_nearby:
|
||||||
|
mapRootFragment.loadNearbyMapFromExplore();
|
||||||
|
return true;
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,22 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExploreMapRootFragment(Bundle bundle) {
|
public ExploreMapRootFragment(Bundle bundle) {
|
||||||
|
// get fragment arguments
|
||||||
String title = bundle.getString("categoryName");
|
String title = bundle.getString("categoryName");
|
||||||
|
double zoom = bundle.getDouble("prev_zoom");
|
||||||
|
double latitude = bundle.getDouble("prev_latitude");
|
||||||
|
double longitude = bundle.getDouble("prev_longitude");
|
||||||
|
|
||||||
mapFragment = new ExploreMapFragment();
|
mapFragment = new ExploreMapFragment();
|
||||||
Bundle featuredArguments = new Bundle();
|
Bundle featuredArguments = new Bundle();
|
||||||
featuredArguments.putString("categoryName", title);
|
featuredArguments.putString("categoryName", title);
|
||||||
|
|
||||||
|
// if we came from 'Show in Explore' in Nearby, pass on zoom and center
|
||||||
|
if (zoom != 0.0 || latitude != 0.0 || longitude != 0.0) {
|
||||||
|
featuredArguments.putDouble("prev_zoom", zoom);
|
||||||
|
featuredArguments.putDouble("prev_latitude", latitude);
|
||||||
|
featuredArguments.putDouble("prev_longitude", longitude);
|
||||||
|
}
|
||||||
mapFragment.setArguments(featuredArguments);
|
mapFragment.setArguments(featuredArguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,7 +210,8 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
|
||||||
((MainActivity) getActivity()).showTabs();
|
((MainActivity) getActivity()).showTabs();
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} if (mapFragment != null && mapFragment.isVisible()) {
|
}
|
||||||
|
if (mapFragment != null && mapFragment.isVisible()) {
|
||||||
if (mapFragment.backButtonClicked()) {
|
if (mapFragment.backButtonClicked()) {
|
||||||
// Explore map fragment handled the event no further action required.
|
// Explore map fragment handled the event no further action required.
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -213,6 +226,10 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void loadNearbyMapFromExplore() {
|
||||||
|
mapFragment.loadNearbyMapFromExplore();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
||||||
|
import fr.free.nrw.commons.contributions.MainActivity;
|
||||||
import fr.free.nrw.commons.databinding.FragmentExploreMapBinding;
|
import fr.free.nrw.commons.databinding.FragmentExploreMapBinding;
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.explore.ExploreMapRootFragment;
|
import fr.free.nrw.commons.explore.ExploreMapRootFragment;
|
||||||
|
|
@ -115,6 +116,11 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
SystemThemeUtils systemThemeUtils;
|
SystemThemeUtils systemThemeUtils;
|
||||||
LocationPermissionsHelper locationPermissionsHelper;
|
LocationPermissionsHelper locationPermissionsHelper;
|
||||||
|
|
||||||
|
// Nearby map state (if we came from Nearby)
|
||||||
|
private double prevZoom;
|
||||||
|
private double prevLatitude;
|
||||||
|
private double prevLongitude;
|
||||||
|
|
||||||
private ExploreMapPresenter presenter;
|
private ExploreMapPresenter presenter;
|
||||||
|
|
||||||
public FragmentExploreMapBinding binding;
|
public FragmentExploreMapBinding binding;
|
||||||
|
|
@ -160,6 +166,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
ViewGroup container,
|
ViewGroup container,
|
||||||
Bundle savedInstanceState
|
Bundle savedInstanceState
|
||||||
) {
|
) {
|
||||||
|
loadNearbyMapData();
|
||||||
binding = FragmentExploreMapBinding.inflate(getLayoutInflater());
|
binding = FragmentExploreMapBinding.inflate(getLayoutInflater());
|
||||||
return binding.getRoot();
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
@ -169,12 +176,14 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
setSearchThisAreaButtonVisibility(false);
|
setSearchThisAreaButtonVisibility(false);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
binding.tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution), Html.FROM_HTML_MODE_LEGACY));
|
binding.tvAttribution.setText(
|
||||||
|
Html.fromHtml(getString(R.string.map_attribution), Html.FROM_HTML_MODE_LEGACY));
|
||||||
} else {
|
} else {
|
||||||
binding.tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
|
binding.tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
|
||||||
}
|
}
|
||||||
initNetworkBroadCastReceiver();
|
initNetworkBroadCastReceiver();
|
||||||
locationPermissionsHelper = new LocationPermissionsHelper(getActivity(),locationManager,this);
|
locationPermissionsHelper = new LocationPermissionsHelper(getActivity(), locationManager,
|
||||||
|
this);
|
||||||
if (presenter == null) {
|
if (presenter == null) {
|
||||||
presenter = new ExploreMapPresenter(bookmarkLocationDao);
|
presenter = new ExploreMapPresenter(bookmarkLocationDao);
|
||||||
}
|
}
|
||||||
|
|
@ -204,9 +213,14 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
scaleBarOverlay.setBackgroundPaint(barPaint);
|
scaleBarOverlay.setBackgroundPaint(barPaint);
|
||||||
scaleBarOverlay.enableScaleBar();
|
scaleBarOverlay.enableScaleBar();
|
||||||
binding.mapView.getOverlays().add(scaleBarOverlay);
|
binding.mapView.getOverlays().add(scaleBarOverlay);
|
||||||
binding.mapView.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);
|
binding.mapView.getZoomController()
|
||||||
|
.setVisibility(CustomZoomButtonsController.Visibility.NEVER);
|
||||||
binding.mapView.setMultiTouchControls(true);
|
binding.mapView.setMultiTouchControls(true);
|
||||||
binding.mapView.getController().setZoom(ZOOM_LEVEL);
|
|
||||||
|
if (!isCameFromNearbyMap()) {
|
||||||
|
binding.mapView.getController().setZoom(ZOOM_LEVEL);
|
||||||
|
}
|
||||||
|
|
||||||
performMapReadyActions();
|
performMapReadyActions();
|
||||||
|
|
||||||
binding.mapView.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() {
|
binding.mapView.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() {
|
||||||
|
|
@ -295,7 +309,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
unregisterNetworkReceiver();
|
unregisterNetworkReceiver();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregisters the networkReceiver
|
* Unregisters the networkReceiver
|
||||||
*/
|
*/
|
||||||
|
|
@ -328,11 +342,51 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
isPermissionDenied = true;
|
isPermissionDenied = true;
|
||||||
}
|
}
|
||||||
lastKnownLocation = MapUtils.getDefaultLatLng();
|
lastKnownLocation = MapUtils.getDefaultLatLng();
|
||||||
moveCameraToPosition(
|
|
||||||
new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude()));
|
// if we came from 'Show in Explore' in Nearby, load Nearby map center and zoom
|
||||||
|
if (isCameFromNearbyMap()) {
|
||||||
|
moveCameraToPosition(
|
||||||
|
new GeoPoint(prevLatitude, prevLongitude),
|
||||||
|
prevZoom,
|
||||||
|
1L
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
moveCameraToPosition(
|
||||||
|
new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude()));
|
||||||
|
}
|
||||||
presenter.onMapReady(exploreMapController);
|
presenter.onMapReady(exploreMapController);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Nearby map camera data from fragment arguments if any.
|
||||||
|
*/
|
||||||
|
public void loadNearbyMapData() {
|
||||||
|
// get fragment arguments
|
||||||
|
if (getArguments() != null) {
|
||||||
|
prevZoom = getArguments().getDouble("prev_zoom");
|
||||||
|
prevLatitude = getArguments().getDouble("prev_latitude");
|
||||||
|
prevLongitude = getArguments().getDouble("prev_longitude");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if fragment arguments contain data from Nearby map, indicating that the user navigated
|
||||||
|
* from Nearby using 'Show in Explore'.
|
||||||
|
*
|
||||||
|
* @return true if user navigated from Nearby map
|
||||||
|
**/
|
||||||
|
public boolean isCameFromNearbyMap() {
|
||||||
|
return prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadNearbyMapFromExplore() {
|
||||||
|
((MainActivity) getContext()).loadNearbyMapFromExplore(
|
||||||
|
binding.mapView.getZoomLevelDouble(),
|
||||||
|
binding.mapView.getMapCenter().getLatitude(),
|
||||||
|
binding.mapView.getMapCenter().getLongitude()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private void initViews() {
|
private void initViews() {
|
||||||
Timber.d("init views called");
|
Timber.d("init views called");
|
||||||
initBottomSheets();
|
initBottomSheets();
|
||||||
|
|
@ -346,7 +400,8 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
*/
|
*/
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private void initBottomSheets() {
|
private void initBottomSheets() {
|
||||||
bottomSheetDetailsBehavior = BottomSheetBehavior.from(binding.bottomSheetDetailsBinding.getRoot());
|
bottomSheetDetailsBehavior = BottomSheetBehavior.from(
|
||||||
|
binding.bottomSheetDetailsBinding.getRoot());
|
||||||
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
binding.bottomSheetDetailsBinding.getRoot().setVisibility(View.VISIBLE);
|
binding.bottomSheetDetailsBinding.getRoot().setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
@ -404,23 +459,25 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
if (currentLatLng == null) {
|
if (currentLatLng == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentLatLng.equals(getLastMapFocus())) { // Means we are checking around current location
|
if (currentLatLng.equals(
|
||||||
|
getLastMapFocus())) { // Means we are checking around current location
|
||||||
nearbyPlacesInfoObservable = presenter.loadAttractionsFromLocation(currentLatLng,
|
nearbyPlacesInfoObservable = presenter.loadAttractionsFromLocation(currentLatLng,
|
||||||
getLastMapFocus(), true);
|
getLastMapFocus(), true);
|
||||||
} else {
|
} else {
|
||||||
nearbyPlacesInfoObservable = presenter.loadAttractionsFromLocation(getLastMapFocus(),
|
nearbyPlacesInfoObservable = presenter.loadAttractionsFromLocation(getLastMapFocus(),
|
||||||
currentLatLng, false);
|
currentLatLng, false);
|
||||||
}
|
}
|
||||||
compositeDisposable.add(nearbyPlacesInfoObservable
|
getCompositeDisposable().add(nearbyPlacesInfoObservable
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(explorePlacesInfo -> {
|
.subscribe(explorePlacesInfo -> {
|
||||||
mediaList = explorePlacesInfo.mediaList;
|
mediaList = explorePlacesInfo.mediaList;
|
||||||
if(mediaList == null) {
|
if (mediaList == null) {
|
||||||
showResponseMessage(getString(R.string.no_pictures_in_this_area));
|
showResponseMessage(getString(R.string.no_pictures_in_this_area));
|
||||||
}
|
}
|
||||||
updateMapMarkers(explorePlacesInfo);
|
updateMapMarkers(explorePlacesInfo);
|
||||||
lastMapFocus = new GeoPoint(currentLatLng.getLatitude(), currentLatLng.getLongitude());
|
lastMapFocus = new GeoPoint(currentLatLng.getLatitude(),
|
||||||
|
currentLatLng.getLongitude());
|
||||||
},
|
},
|
||||||
throwable -> {
|
throwable -> {
|
||||||
Timber.d(throwable);
|
Timber.d(throwable);
|
||||||
|
|
@ -474,9 +531,9 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER);
|
locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER);
|
||||||
locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
|
locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
|
||||||
setProgressBarVisibility(true);
|
setProgressBarVisibility(true);
|
||||||
}
|
} else {
|
||||||
else {
|
locationPermissionsHelper.showLocationOffDialog(getActivity(),
|
||||||
locationPermissionsHelper.showLocationOffDialog(getActivity(), R.string.ask_to_turn_location_on_text);
|
R.string.ask_to_turn_location_on_text);
|
||||||
}
|
}
|
||||||
presenter.onMapReady(exploreMapController);
|
presenter.onMapReady(exploreMapController);
|
||||||
registerUnregisterLocationListener(false);
|
registerUnregisterLocationListener(false);
|
||||||
|
|
@ -508,7 +565,8 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
recenterToUserLocation = true;
|
recenterToUserLocation = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
recenterMarkerToPosition(new GeoPoint(currentLatLng.getLatitude(), currentLatLng.getLongitude()));
|
recenterMarkerToPosition(
|
||||||
|
new GeoPoint(currentLatLng.getLatitude(), currentLatLng.getLongitude()));
|
||||||
binding.mapView.getController()
|
binding.mapView.getController()
|
||||||
.animateTo(new GeoPoint(currentLatLng.getLatitude(), currentLatLng.getLongitude()));
|
.animateTo(new GeoPoint(currentLatLng.getLatitude(), currentLatLng.getLongitude()));
|
||||||
if (lastMapFocus != null) {
|
if (lastMapFocus != null) {
|
||||||
|
|
@ -545,10 +603,12 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
* @param place Place of clicked nearby marker
|
* @param place Place of clicked nearby marker
|
||||||
*/
|
*/
|
||||||
private void passInfoToSheet(final Place place) {
|
private void passInfoToSheet(final Place place) {
|
||||||
binding.bottomSheetDetailsBinding.directionsButton.setOnClickListener(view -> Utils.handleGeoCoordinates(getActivity(),
|
binding.bottomSheetDetailsBinding.directionsButton.setOnClickListener(
|
||||||
place.getLocation()));
|
view -> Utils.handleGeoCoordinates(getActivity(),
|
||||||
|
place.getLocation(), binding.mapView.getZoomLevelDouble()));
|
||||||
|
|
||||||
binding.bottomSheetDetailsBinding.commonsButton.setVisibility(place.hasCommonsLink() ? View.VISIBLE : View.GONE);
|
binding.bottomSheetDetailsBinding.commonsButton.setVisibility(
|
||||||
|
place.hasCommonsLink() ? View.VISIBLE : View.GONE);
|
||||||
binding.bottomSheetDetailsBinding.commonsButton.setOnClickListener(
|
binding.bottomSheetDetailsBinding.commonsButton.setOnClickListener(
|
||||||
view -> Utils.handleWebUrl(getContext(), place.siteLinks.getCommonsLink()));
|
view -> Utils.handleWebUrl(getContext(), place.siteLinks.getCommonsLink()));
|
||||||
|
|
||||||
|
|
@ -562,7 +622,8 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
binding.bottomSheetDetailsBinding.title.setText(place.name.substring(5, place.name.lastIndexOf(".")));
|
binding.bottomSheetDetailsBinding.title.setText(
|
||||||
|
place.name.substring(5, place.name.lastIndexOf(".")));
|
||||||
binding.bottomSheetDetailsBinding.category.setText(place.distance);
|
binding.bottomSheetDetailsBinding.category.setText(place.distance);
|
||||||
// Remove label since it is double information
|
// Remove label since it is double information
|
||||||
String descriptionText = place.getLongDescription()
|
String descriptionText = place.getLongDescription()
|
||||||
|
|
@ -640,40 +701,43 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
* @param nearbyBaseMarker The NearbyBaseMarker object representing the marker to be added.
|
* @param nearbyBaseMarker The NearbyBaseMarker object representing the marker to be added.
|
||||||
*/
|
*/
|
||||||
private void addMarkerToMap(BaseMarker nearbyBaseMarker) {
|
private void addMarkerToMap(BaseMarker nearbyBaseMarker) {
|
||||||
ArrayList<OverlayItem> items = new ArrayList<>();
|
if (isAttachedToActivity()) {
|
||||||
Bitmap icon = nearbyBaseMarker.getIcon();
|
ArrayList<OverlayItem> items = new ArrayList<>();
|
||||||
Drawable d = new BitmapDrawable(getResources(), icon);
|
Bitmap icon = nearbyBaseMarker.getIcon();
|
||||||
GeoPoint point = new GeoPoint(
|
Drawable d = new BitmapDrawable(getResources(), icon);
|
||||||
nearbyBaseMarker.getPlace().location.getLatitude(),
|
GeoPoint point = new GeoPoint(
|
||||||
nearbyBaseMarker.getPlace().location.getLongitude());
|
nearbyBaseMarker.getPlace().location.getLatitude(),
|
||||||
OverlayItem item = new OverlayItem(nearbyBaseMarker.getPlace().name, null,
|
nearbyBaseMarker.getPlace().location.getLongitude());
|
||||||
point);
|
OverlayItem item = new OverlayItem(nearbyBaseMarker.getPlace().name, null,
|
||||||
item.setMarker(d);
|
point);
|
||||||
items.add(item);
|
item.setMarker(d);
|
||||||
ItemizedOverlayWithFocus overlay = new ItemizedOverlayWithFocus(items,
|
items.add(item);
|
||||||
new OnItemGestureListener<OverlayItem>() {
|
ItemizedOverlayWithFocus overlay = new ItemizedOverlayWithFocus(items,
|
||||||
@Override
|
new OnItemGestureListener<OverlayItem>() {
|
||||||
public boolean onItemSingleTapUp(int index, OverlayItem item) {
|
@Override
|
||||||
final Place place = nearbyBaseMarker.getPlace();
|
public boolean onItemSingleTapUp(int index, OverlayItem item) {
|
||||||
if (clickedMarker != null) {
|
final Place place = nearbyBaseMarker.getPlace();
|
||||||
removeMarker(clickedMarker);
|
if (clickedMarker != null) {
|
||||||
addMarkerToMap(clickedMarker);
|
removeMarker(clickedMarker);
|
||||||
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
addMarkerToMap(clickedMarker);
|
||||||
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
bottomSheetDetailsBehavior.setState(
|
||||||
|
BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
}
|
||||||
|
clickedMarker = nearbyBaseMarker;
|
||||||
|
passInfoToSheet(place);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
clickedMarker = nearbyBaseMarker;
|
|
||||||
passInfoToSheet(place);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onItemLongPress(int index, OverlayItem item) {
|
public boolean onItemLongPress(int index, OverlayItem item) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, getContext());
|
}, getContext());
|
||||||
|
|
||||||
overlay.setFocusItemsOnTap(true);
|
overlay.setFocusItemsOnTap(true);
|
||||||
binding.mapView.getOverlays().add(overlay); // Add the overlay to the map
|
binding.mapView.getOverlays().add(overlay); // Add the overlay to the map
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -707,68 +771,72 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void clearAllMarkers() {
|
public void clearAllMarkers() {
|
||||||
binding.mapView.getOverlayManager().clear();
|
if (isAttachedToActivity()) {
|
||||||
GeoPoint geoPoint = mapCenter;
|
binding.mapView.getOverlayManager().clear();
|
||||||
if (geoPoint != null) {
|
GeoPoint geoPoint = mapCenter;
|
||||||
List<Overlay> overlays = binding.mapView.getOverlays();
|
if (geoPoint != null) {
|
||||||
ScaleDiskOverlay diskOverlay =
|
List<Overlay> overlays = binding.mapView.getOverlays();
|
||||||
new ScaleDiskOverlay(this.getContext(),
|
ScaleDiskOverlay diskOverlay =
|
||||||
geoPoint, 2000, GeoConstants.UnitOfMeasure.foot);
|
new ScaleDiskOverlay(this.getContext(),
|
||||||
Paint circlePaint = new Paint();
|
geoPoint, 2000, GeoConstants.UnitOfMeasure.foot);
|
||||||
circlePaint.setColor(Color.rgb(128, 128, 128));
|
Paint circlePaint = new Paint();
|
||||||
circlePaint.setStyle(Paint.Style.STROKE);
|
circlePaint.setColor(Color.rgb(128, 128, 128));
|
||||||
circlePaint.setStrokeWidth(2f);
|
circlePaint.setStyle(Paint.Style.STROKE);
|
||||||
diskOverlay.setCirclePaint2(circlePaint);
|
circlePaint.setStrokeWidth(2f);
|
||||||
Paint diskPaint = new Paint();
|
diskOverlay.setCirclePaint2(circlePaint);
|
||||||
diskPaint.setColor(Color.argb(40, 128, 128, 128));
|
Paint diskPaint = new Paint();
|
||||||
diskPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
diskPaint.setColor(Color.argb(40, 128, 128, 128));
|
||||||
diskOverlay.setCirclePaint1(diskPaint);
|
diskPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||||
diskOverlay.setDisplaySizeMin(900);
|
diskOverlay.setCirclePaint1(diskPaint);
|
||||||
diskOverlay.setDisplaySizeMax(1700);
|
diskOverlay.setDisplaySizeMin(900);
|
||||||
binding.mapView.getOverlays().add(diskOverlay);
|
diskOverlay.setDisplaySizeMax(1700);
|
||||||
org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker(
|
binding.mapView.getOverlays().add(diskOverlay);
|
||||||
binding.mapView);
|
org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker(
|
||||||
startMarker.setPosition(geoPoint);
|
binding.mapView);
|
||||||
startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER,
|
startMarker.setPosition(geoPoint);
|
||||||
org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM);
|
startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER,
|
||||||
startMarker.setIcon(
|
org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM);
|
||||||
ContextCompat.getDrawable(this.getContext(), R.drawable.current_location_marker));
|
startMarker.setIcon(
|
||||||
startMarker.setTitle("Your Location");
|
ContextCompat.getDrawable(this.getContext(),
|
||||||
startMarker.setTextLabelFontSize(24);
|
R.drawable.current_location_marker));
|
||||||
binding.mapView.getOverlays().add(startMarker);
|
startMarker.setTitle("Your Location");
|
||||||
}
|
startMarker.setTextLabelFontSize(24);
|
||||||
ScaleBarOverlay scaleBarOverlay = new ScaleBarOverlay(binding.mapView);
|
binding.mapView.getOverlays().add(startMarker);
|
||||||
scaleBarOverlay.setScaleBarOffset(15, 25);
|
|
||||||
Paint barPaint = new Paint();
|
|
||||||
barPaint.setARGB(200, 255, 250, 250);
|
|
||||||
scaleBarOverlay.setBackgroundPaint(barPaint);
|
|
||||||
scaleBarOverlay.enableScaleBar();
|
|
||||||
binding.mapView.getOverlays().add(scaleBarOverlay);
|
|
||||||
binding.mapView.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() {
|
|
||||||
@Override
|
|
||||||
public boolean singleTapConfirmedHelper(GeoPoint p) {
|
|
||||||
if (clickedMarker != null) {
|
|
||||||
removeMarker(clickedMarker);
|
|
||||||
addMarkerToMap(clickedMarker);
|
|
||||||
binding.mapView.invalidate();
|
|
||||||
} else {
|
|
||||||
Timber.e("CLICKED MARKER IS NULL");
|
|
||||||
}
|
|
||||||
if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
|
|
||||||
// Back should first hide the bottom sheet if it is expanded
|
|
||||||
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
|
||||||
} else if (isDetailsBottomSheetVisible()) {
|
|
||||||
hideBottomDetailsSheet();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
ScaleBarOverlay scaleBarOverlay = new ScaleBarOverlay(binding.mapView);
|
||||||
|
scaleBarOverlay.setScaleBarOffset(15, 25);
|
||||||
|
Paint barPaint = new Paint();
|
||||||
|
barPaint.setARGB(200, 255, 250, 250);
|
||||||
|
scaleBarOverlay.setBackgroundPaint(barPaint);
|
||||||
|
scaleBarOverlay.enableScaleBar();
|
||||||
|
binding.mapView.getOverlays().add(scaleBarOverlay);
|
||||||
|
binding.mapView.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() {
|
||||||
|
@Override
|
||||||
|
public boolean singleTapConfirmedHelper(GeoPoint p) {
|
||||||
|
if (clickedMarker != null) {
|
||||||
|
removeMarker(clickedMarker);
|
||||||
|
addMarkerToMap(clickedMarker);
|
||||||
|
binding.mapView.invalidate();
|
||||||
|
} else {
|
||||||
|
Timber.e("CLICKED MARKER IS NULL");
|
||||||
|
}
|
||||||
|
if (bottomSheetDetailsBehavior.getState()
|
||||||
|
== BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
|
// Back should first hide the bottom sheet if it is expanded
|
||||||
|
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
} else if (isDetailsBottomSheetVisible()) {
|
||||||
|
hideBottomDetailsSheet();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean longPressHelper(GeoPoint p) {
|
public boolean longPressHelper(GeoPoint p) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
binding.mapView.setMultiTouchControls(true);
|
binding.mapView.setMultiTouchControls(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -825,6 +893,18 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
binding.mapView.getController().animateTo(geoPoint);
|
binding.mapView.getController().animateTo(geoPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the camera of the map view to the specified GeoPoint at specified zoom level and speed
|
||||||
|
* using an animation.
|
||||||
|
*
|
||||||
|
* @param geoPoint The GeoPoint representing the new camera position for the map.
|
||||||
|
* @param zoom Zoom level of the map camera
|
||||||
|
* @param speed Speed of animation
|
||||||
|
*/
|
||||||
|
private void moveCameraToPosition(GeoPoint geoPoint, double zoom, long speed) {
|
||||||
|
binding.mapView.getController().animateTo(geoPoint, zoom, speed);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public fr.free.nrw.commons.location.LatLng getLastMapFocus() {
|
public fr.free.nrw.commons.location.LatLng getLastMapFocus() {
|
||||||
return lastMapFocus == null ? getMapCenter() : new fr.free.nrw.commons.location.LatLng(
|
return lastMapFocus == null ? getMapCenter() : new fr.free.nrw.commons.location.LatLng(
|
||||||
|
|
@ -850,14 +930,17 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
-0.07483536015053005, 1f);
|
-0.07483536015053005, 1f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
moveCameraToPosition(new GeoPoint(latLnge.getLatitude(),latLnge.getLongitude()));
|
if (!isCameFromNearbyMap()) {
|
||||||
|
moveCameraToPosition(new GeoPoint(latLnge.getLatitude(), latLnge.getLongitude()));
|
||||||
|
}
|
||||||
return latLnge;
|
return latLnge;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public fr.free.nrw.commons.location.LatLng getMapFocus() {
|
public fr.free.nrw.commons.location.LatLng getMapFocus() {
|
||||||
fr.free.nrw.commons.location.LatLng mapFocusedLatLng = new fr.free.nrw.commons.location.LatLng(
|
fr.free.nrw.commons.location.LatLng mapFocusedLatLng = new fr.free.nrw.commons.location.LatLng(
|
||||||
binding.mapView.getMapCenter().getLatitude(), binding.mapView.getMapCenter().getLongitude(), 100);
|
binding.mapView.getMapCenter().getLatitude(),
|
||||||
|
binding.mapView.getMapCenter().getLongitude(), 100);
|
||||||
return mapFocusedLatLng;
|
return mapFocusedLatLng;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -910,9 +993,19 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public void onLocationPermissionDenied(String toastMessage) {}
|
* helper function to confirm that this fragment has been attached.
|
||||||
|
**/
|
||||||
|
public boolean isAttachedToActivity() {
|
||||||
|
boolean attached = isVisible() && getActivity() != null;
|
||||||
|
return attached;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLocationPermissionGranted() {}
|
public void onLocationPermissionDenied(String toastMessage) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLocationPermissionGranted() {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -426,7 +426,7 @@ object FilePicker : Constants {
|
||||||
fun onCanceled(source: ImageSource, type: Int)
|
fun onCanceled(source: ImageSource, type: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HandleActivityResult {
|
fun interface HandleActivityResult {
|
||||||
fun onHandleActivityResult(callbacks: Callbacks)
|
fun onHandleActivityResult(callbacks: Callbacks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -123,10 +123,13 @@ data class LatLng(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a URI for a Google Maps intent at the location.
|
* Gets a URI for a Google Maps intent at the location.
|
||||||
|
*
|
||||||
|
* @paraam zoom The zoom level
|
||||||
|
* @return URI for the intent
|
||||||
*/
|
*/
|
||||||
fun getGmmIntentUri(): Uri {
|
fun getGmmIntentUri(zoom: Double): Uri = Uri.parse(
|
||||||
return Uri.parse("geo:$latitude,$longitude?z=16")
|
"geo:$latitude,$longitude?q=$latitude,$longitude&z=${zoom}"
|
||||||
}
|
)
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeDouble(latitude)
|
parcel.writeDouble(latitude)
|
||||||
|
|
|
||||||
|
|
@ -430,7 +430,11 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback {
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
position?.let { Utils.handleGeoCoordinates(this, it) }
|
position?.let {
|
||||||
|
mapView?.zoomLevelDouble?.let { zoomLevel ->
|
||||||
|
Utils.handleGeoCoordinates(this, it, zoomLevel)
|
||||||
|
} ?: Utils.handleGeoCoordinates(this, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewTreeObserver
|
||||||
import android.view.ViewTreeObserver.OnGlobalLayoutListener
|
import android.view.ViewTreeObserver.OnGlobalLayoutListener
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
|
|
@ -405,9 +406,14 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
|
||||||
* Gets the height of the frame layout as soon as the view is ready and updates aspect ratio
|
* Gets the height of the frame layout as soon as the view is ready and updates aspect ratio
|
||||||
* of the picture.
|
* of the picture.
|
||||||
*/
|
*/
|
||||||
view.post {
|
view.post{
|
||||||
frameLayoutHeight = binding.mediaDetailFrameLayout.measuredHeight
|
val width = binding.mediaDetailScrollView.width
|
||||||
updateAspectRatio(binding.mediaDetailScrollView.width)
|
if (width > 0) {
|
||||||
|
frameLayoutHeight = binding.mediaDetailFrameLayout.measuredHeight
|
||||||
|
updateAspectRatio(width)
|
||||||
|
} else {
|
||||||
|
view.postDelayed({ updateAspectRatio(binding.root.width) }, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return view
|
return view
|
||||||
|
|
@ -493,7 +499,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
|
||||||
|
|
||||||
val contributionsFragment: ContributionsFragment? = this.getContributionsFragmentParent()
|
val contributionsFragment: ContributionsFragment? = this.getContributionsFragmentParent()
|
||||||
if (contributionsFragment?.binding != null) {
|
if (contributionsFragment?.binding != null) {
|
||||||
contributionsFragment.binding.cardViewNearby.visibility = View.GONE
|
contributionsFragment.binding!!.cardViewNearby.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
// detail provider is null when fragment is shown in review activity
|
// detail provider is null when fragment is shown in review activity
|
||||||
|
|
@ -650,10 +656,8 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onDepictionsLoaded(idAndCaptions: List<IdAndCaptions>) {
|
private fun onDepictionsLoaded(idAndCaptions: List<IdAndCaptions>) {
|
||||||
binding.depictsLayout.visibility =
|
binding.depictsLayout.visibility = View.VISIBLE
|
||||||
if (idAndCaptions.isEmpty()) View.GONE else View.VISIBLE
|
binding.depictionsEditButton.visibility = View.VISIBLE
|
||||||
binding.depictionsEditButton.visibility =
|
|
||||||
if (idAndCaptions.isEmpty()) View.GONE else View.VISIBLE
|
|
||||||
buildDepictionList(idAndCaptions)
|
buildDepictionList(idAndCaptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -863,8 +867,22 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
|
||||||
*/
|
*/
|
||||||
private fun buildDepictionList(idAndCaptions: List<IdAndCaptions>) {
|
private fun buildDepictionList(idAndCaptions: List<IdAndCaptions>) {
|
||||||
binding.mediaDetailDepictionContainer.removeAllViews()
|
binding.mediaDetailDepictionContainer.removeAllViews()
|
||||||
|
|
||||||
|
// Create a mutable list from the original list
|
||||||
|
val mutableIdAndCaptions = idAndCaptions.toMutableList()
|
||||||
|
|
||||||
|
if (mutableIdAndCaptions.isEmpty()) {
|
||||||
|
// Create a placeholder IdAndCaptions object and add it to the list
|
||||||
|
mutableIdAndCaptions.add(
|
||||||
|
IdAndCaptions(
|
||||||
|
id = media?.pageId ?: "", // Use an empty string if media?.pageId is null
|
||||||
|
captions = mapOf(Locale.getDefault().language to getString(R.string.detail_panel_cats_none)) // Create a Map with the language as the key and the message as the value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val locale: String = Locale.getDefault().language
|
val locale: String = Locale.getDefault().language
|
||||||
for (idAndCaption: IdAndCaptions in idAndCaptions) {
|
for (idAndCaption: IdAndCaptions in mutableIdAndCaptions) {
|
||||||
binding.mediaDetailDepictionContainer.addView(
|
binding.mediaDetailDepictionContainer.addView(
|
||||||
buildDepictLabel(
|
buildDepictLabel(
|
||||||
getDepictionCaption(idAndCaption, locale),
|
getDepictionCaption(idAndCaption, locale),
|
||||||
|
|
@ -875,6 +893,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun getDepictionCaption(idAndCaption: IdAndCaptions, locale: String): String? {
|
private fun getDepictionCaption(idAndCaption: IdAndCaptions, locale: String): String? {
|
||||||
// Check if the Depiction Caption is available in user's locale
|
// Check if the Depiction Caption is available in user's locale
|
||||||
// if not then check for english, else show any available.
|
// if not then check for english, else show any available.
|
||||||
|
|
|
||||||
|
|
@ -185,10 +185,12 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
||||||
* or a fragment
|
* or a fragment
|
||||||
*/
|
*/
|
||||||
private void initProvider() {
|
private void initProvider() {
|
||||||
if (getParentFragment() != null) {
|
if (getParentFragment() instanceof MediaDetailProvider) {
|
||||||
provider = (MediaDetailProvider) getParentFragment();
|
provider = (MediaDetailProvider) getParentFragment();
|
||||||
} else {
|
} else if (getActivity() instanceof MediaDetailProvider) {
|
||||||
provider = (MediaDetailProvider) getActivity();
|
provider = (MediaDetailProvider) getActivity();
|
||||||
|
} else {
|
||||||
|
throw new ClassCastException("Parent must implement MediaDetailProvider");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ class NavTabLayout : BottomNavigationView {
|
||||||
|
|
||||||
private fun setTabViews() {
|
private fun setTabViews() {
|
||||||
val isLoginSkipped = (context as MainActivity)
|
val isLoginSkipped = (context as MainActivity)
|
||||||
.applicationKvStore.getBoolean("login_skipped")
|
.applicationKvStore?.getBoolean("login_skipped")
|
||||||
if (isLoginSkipped) {
|
if (isLoginSkipped == true) {
|
||||||
for (i in 0 until NavTabLoggedOut.size()) {
|
for (i in 0 until NavTabLoggedOut.size()) {
|
||||||
val navTab = NavTabLoggedOut.of(i)
|
val navTab = NavTabLoggedOut.of(i)
|
||||||
menu.add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon())
|
menu.add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon())
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,21 @@ class BottomSheetAdapter(
|
||||||
item.imageResourceId == R.drawable.ic_round_star_border_24px
|
item.imageResourceId == R.drawable.ic_round_star_border_24px
|
||||||
) {
|
) {
|
||||||
item.imageResourceId = icon
|
item.imageResourceId = icon
|
||||||
this.notifyItemChanged(index)
|
notifyItemChanged(index)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleBookmarkIcon() {
|
||||||
|
itemList.forEachIndexed { index, item ->
|
||||||
|
if(item.imageResourceId == R.drawable.ic_round_star_filled_24px) {
|
||||||
|
item.imageResourceId = R.drawable.ic_round_star_border_24px
|
||||||
|
notifyItemChanged(index)
|
||||||
|
return
|
||||||
|
} else if(item.imageResourceId == R.drawable.ic_round_star_border_24px){
|
||||||
|
item.imageResourceId = R.drawable.ic_round_star_filled_24px
|
||||||
|
notifyItemChanged(index)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
app/src/main/java/fr/free/nrw/commons/nearby/NearbyUtil.kt
Normal file
28
app/src/main/java/fr/free/nrw/commons/nearby/NearbyUtil.kt
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package fr.free.nrw.commons.nearby
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
object NearbyUtil {
|
||||||
|
|
||||||
|
fun getBookmarkLocationExists(
|
||||||
|
bookmarksLocationsDao: BookmarkLocationsDao,
|
||||||
|
name: String,
|
||||||
|
scope: LifecycleCoroutineScope?,
|
||||||
|
bottomSheetAdapter: BottomSheetAdapter,
|
||||||
|
) {
|
||||||
|
scope?.launch {
|
||||||
|
val isBookmarked = bookmarksLocationsDao.findBookmarkLocation(name)
|
||||||
|
Timber.i("isBookmarked: $isBookmarked")
|
||||||
|
if (isBookmarked) {
|
||||||
|
bottomSheetAdapter.updateBookmarkIcon(R.drawable.ic_round_star_filled_24px)
|
||||||
|
} else {
|
||||||
|
bottomSheetAdapter.updateBookmarkIcon(R.drawable.ic_round_star_border_24px)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -232,13 +232,23 @@ public class Place implements Parcelable {
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public String getWikiDataEntityId() {
|
public String getWikiDataEntityId() {
|
||||||
|
if (this.entityID != null && !this.entityID.equals("")) {
|
||||||
|
return this.entityID;
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasWikidataLink()) {
|
if (!hasWikidataLink()) {
|
||||||
Timber.d("Wikidata entity ID is null for place with sitelink %s", siteLinks.toString());
|
Timber.d("Wikidata entity ID is null for place with sitelink %s", siteLinks.toString());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Determine entityID from link
|
||||||
String wikiDataLink = siteLinks.getWikidataLink().toString();
|
String wikiDataLink = siteLinks.getWikidataLink().toString();
|
||||||
return wikiDataLink.replace("http://www.wikidata.org/entity/", "");
|
|
||||||
|
if (wikiDataLink.contains("http://www.wikidata.org/entity/")) {
|
||||||
|
this.entityID = wikiDataLink.substring("http://www.wikidata.org/entity/".length());
|
||||||
|
return this.entityID;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import android.view.View.INVISIBLE
|
||||||
import android.view.View.VISIBLE
|
import android.view.View.VISIBLE
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
|
|
@ -16,9 +17,11 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
|
||||||
import fr.free.nrw.commons.databinding.ItemPlaceBinding
|
import fr.free.nrw.commons.databinding.ItemPlaceBinding
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
fun placeAdapterDelegate(
|
fun placeAdapterDelegate(
|
||||||
bookmarkLocationDao: BookmarkLocationsDao,
|
bookmarkLocationDao: BookmarkLocationsDao,
|
||||||
|
scope: LifecycleCoroutineScope?,
|
||||||
onItemClick: ((Place) -> Unit)? = null,
|
onItemClick: ((Place) -> Unit)? = null,
|
||||||
onCameraClicked: (Place, ActivityResultLauncher<Array<String>>, ActivityResultLauncher<Intent>) -> Unit,
|
onCameraClicked: (Place, ActivityResultLauncher<Array<String>>, ActivityResultLauncher<Intent>) -> Unit,
|
||||||
onCameraLongPressed: () -> Boolean,
|
onCameraLongPressed: () -> Boolean,
|
||||||
|
|
@ -61,7 +64,10 @@ fun placeAdapterDelegate(
|
||||||
nearbyButtonLayout.galleryButton.setOnClickListener { onGalleryClicked(item, galleryPickLauncherForResult) }
|
nearbyButtonLayout.galleryButton.setOnClickListener { onGalleryClicked(item, galleryPickLauncherForResult) }
|
||||||
nearbyButtonLayout.galleryButton.setOnLongClickListener { onGalleryLongPressed() }
|
nearbyButtonLayout.galleryButton.setOnLongClickListener { onGalleryLongPressed() }
|
||||||
bookmarkButtonImage.setOnClickListener {
|
bookmarkButtonImage.setOnClickListener {
|
||||||
val isBookmarked = bookmarkLocationDao.updateBookmarkLocation(item)
|
var isBookmarked = false
|
||||||
|
scope?.launch {
|
||||||
|
isBookmarked = bookmarkLocationDao.updateBookmarkLocation(item)
|
||||||
|
}
|
||||||
bookmarkButtonImage.setImageResource(
|
bookmarkButtonImage.setImageResource(
|
||||||
if (isBookmarked) R.drawable.ic_round_star_filled_24px else R.drawable.ic_round_star_border_24px,
|
if (isBookmarked) R.drawable.ic_round_star_filled_24px else R.drawable.ic_round_star_border_24px,
|
||||||
)
|
)
|
||||||
|
|
@ -93,13 +99,15 @@ fun placeAdapterDelegate(
|
||||||
GONE
|
GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
bookmarkButtonImage.setImageResource(
|
scope?.launch {
|
||||||
if (bookmarkLocationDao.findBookmarkLocation(item)) {
|
bookmarkButtonImage.setImageResource(
|
||||||
R.drawable.ic_round_star_filled_24px
|
if (bookmarkLocationDao.findBookmarkLocation(item.name)) {
|
||||||
} else {
|
R.drawable.ic_round_star_filled_24px
|
||||||
R.drawable.ic_round_star_border_24px
|
} else {
|
||||||
},
|
R.drawable.ic_round_star_border_24px
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
nearbyButtonLayout.directionsButton.setOnLongClickListener { onDirectionsLongPressed() }
|
nearbyButtonLayout.directionsButton.setOnLongClickListener { onDirectionsLongPressed() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ public interface NearbyParentFragmentContract {
|
||||||
|
|
||||||
void setAdvancedQuery(String query);
|
void setAdvancedQuery(String query);
|
||||||
|
|
||||||
void toggleBookmarkedStatus(Place place);
|
void toggleBookmarkedStatus(Place place, LifecycleCoroutineScope scope);
|
||||||
|
|
||||||
void handleMapScrolled(LifecycleCoroutineScope scope, boolean isNetworkAvailable);
|
void handleMapScrolled(LifecycleCoroutineScope scope, boolean isNetworkAvailable);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -2,6 +2,7 @@ package fr.free.nrw.commons.nearby.fragments
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
|
||||||
import fr.free.nrw.commons.nearby.Place
|
import fr.free.nrw.commons.nearby.Place
|
||||||
import fr.free.nrw.commons.nearby.placeAdapterDelegate
|
import fr.free.nrw.commons.nearby.placeAdapterDelegate
|
||||||
|
|
@ -9,6 +10,7 @@ import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter
|
||||||
|
|
||||||
class PlaceAdapter(
|
class PlaceAdapter(
|
||||||
bookmarkLocationsDao: BookmarkLocationsDao,
|
bookmarkLocationsDao: BookmarkLocationsDao,
|
||||||
|
scope: LifecycleCoroutineScope? = null,
|
||||||
onPlaceClicked: ((Place) -> Unit)? = null,
|
onPlaceClicked: ((Place) -> Unit)? = null,
|
||||||
onBookmarkClicked: (Place, Boolean) -> Unit,
|
onBookmarkClicked: (Place, Boolean) -> Unit,
|
||||||
commonPlaceClickActions: CommonPlaceClickActions,
|
commonPlaceClickActions: CommonPlaceClickActions,
|
||||||
|
|
@ -18,6 +20,7 @@ class PlaceAdapter(
|
||||||
) : BaseDelegateAdapter<Place>(
|
) : BaseDelegateAdapter<Place>(
|
||||||
placeAdapterDelegate(
|
placeAdapterDelegate(
|
||||||
bookmarkLocationsDao,
|
bookmarkLocationsDao,
|
||||||
|
scope,
|
||||||
onPlaceClicked,
|
onPlaceClicked,
|
||||||
commonPlaceClickActions.onCameraClicked(),
|
commonPlaceClickActions.onCameraClicked(),
|
||||||
commonPlaceClickActions.onCameraLongPressed(),
|
commonPlaceClickActions.onCameraLongPressed(),
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class ResultTuple {
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
language = ""
|
language = "bug" // Basa Ugi language - TODO Respect the `Default description language` setting.
|
||||||
type = ""
|
type = ""
|
||||||
value = ""
|
value = ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,13 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.internal.wait
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
import java.lang.reflect.InvocationHandler
|
import java.lang.reflect.InvocationHandler
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.lang.reflect.Proxy
|
import java.lang.reflect.Proxy
|
||||||
|
|
@ -75,8 +79,8 @@ class NearbyParentFragmentPresenter
|
||||||
* - **connnectionCount**: number of parallel requests
|
* - **connnectionCount**: number of parallel requests
|
||||||
*/
|
*/
|
||||||
private object LoadPlacesAsyncOptions {
|
private object LoadPlacesAsyncOptions {
|
||||||
const val BATCH_SIZE = 3
|
const val BATCH_SIZE = 10
|
||||||
const val CONNECTION_COUNT = 3
|
const val CONNECTION_COUNT = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
private var schedulePlacesUpdateJob: Job? = null
|
private var schedulePlacesUpdateJob: Job? = null
|
||||||
|
|
@ -91,7 +95,7 @@ class NearbyParentFragmentPresenter
|
||||||
private object SchedulePlacesUpdateOptions {
|
private object SchedulePlacesUpdateOptions {
|
||||||
var skippedCount = 0
|
var skippedCount = 0
|
||||||
const val SKIP_LIMIT = 3
|
const val SKIP_LIMIT = 3
|
||||||
const val SKIP_DELAY_MS = 500L
|
const val SKIP_DELAY_MS = 100L
|
||||||
}
|
}
|
||||||
|
|
||||||
// used to tell the asynchronous place detail loading job that the places' bookmarked status
|
// used to tell the asynchronous place detail loading job that the places' bookmarked status
|
||||||
|
|
@ -133,25 +137,31 @@ class NearbyParentFragmentPresenter
|
||||||
* @param place The place whose bookmarked status is to be toggled. If the place is `null`,
|
* @param place The place whose bookmarked status is to be toggled. If the place is `null`,
|
||||||
* the operation is skipped.
|
* the operation is skipped.
|
||||||
*/
|
*/
|
||||||
override fun toggleBookmarkedStatus(place: Place?) {
|
override fun toggleBookmarkedStatus(
|
||||||
|
place: Place?,
|
||||||
|
scope: LifecycleCoroutineScope?
|
||||||
|
) {
|
||||||
if (place == null) return
|
if (place == null) return
|
||||||
val nowBookmarked = bookmarkLocationDao.updateBookmarkLocation(place)
|
var nowBookmarked: Boolean
|
||||||
bookmarkChangedPlaces.add(place)
|
scope?.launch {
|
||||||
val placeIndex =
|
nowBookmarked = bookmarkLocationDao.updateBookmarkLocation(place)
|
||||||
NearbyController.markerLabelList.indexOfFirst { it.place.location == place.location }
|
bookmarkChangedPlaces.add(place)
|
||||||
NearbyController.markerLabelList[placeIndex] = MarkerPlaceGroup(
|
val placeIndex =
|
||||||
nowBookmarked,
|
NearbyController.markerLabelList.indexOfFirst { it.place.location == place.location }
|
||||||
NearbyController.markerLabelList[placeIndex].place
|
NearbyController.markerLabelList[placeIndex] = MarkerPlaceGroup(
|
||||||
)
|
nowBookmarked,
|
||||||
nearbyParentFragmentView.setFilterState()
|
NearbyController.markerLabelList[placeIndex].place
|
||||||
|
)
|
||||||
|
nearbyParentFragmentView.setFilterState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachView(view: NearbyParentFragmentContract.View) {
|
override fun attachView(view: NearbyParentFragmentContract.View) {
|
||||||
this.nearbyParentFragmentView = view
|
nearbyParentFragmentView = view
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun detachView() {
|
override fun detachView() {
|
||||||
this.nearbyParentFragmentView = DUMMY
|
nearbyParentFragmentView = DUMMY
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeNearbyPreferences(applicationKvStore: JsonKvStore) {
|
override fun removeNearbyPreferences(applicationKvStore: JsonKvStore) {
|
||||||
|
|
@ -334,7 +344,7 @@ class NearbyParentFragmentPresenter
|
||||||
for (i in 0..updatedGroups.lastIndex) {
|
for (i in 0..updatedGroups.lastIndex) {
|
||||||
val repoPlace = placesRepository.fetchPlace(updatedGroups[i].place.entityID)
|
val repoPlace = placesRepository.fetchPlace(updatedGroups[i].place.entityID)
|
||||||
if (repoPlace != null && repoPlace.name != null && repoPlace.name != ""){
|
if (repoPlace != null && repoPlace.name != null && repoPlace.name != ""){
|
||||||
updatedGroups[i].isBookmarked = bookmarkLocationDao.findBookmarkLocation(repoPlace)
|
updatedGroups[i].isBookmarked = bookmarkLocationDao.findBookmarkLocation(repoPlace.name)
|
||||||
updatedGroups[i].place.apply {
|
updatedGroups[i].place.apply {
|
||||||
name = repoPlace.name
|
name = repoPlace.name
|
||||||
isMonument = repoPlace.isMonument
|
isMonument = repoPlace.isMonument
|
||||||
|
|
@ -372,20 +382,42 @@ class NearbyParentFragmentPresenter
|
||||||
collectResults.send(
|
collectResults.send(
|
||||||
fetchedPlaces.mapIndexed { index, place ->
|
fetchedPlaces.mapIndexed { index, place ->
|
||||||
Pair(indices[index], MarkerPlaceGroup(
|
Pair(indices[index], MarkerPlaceGroup(
|
||||||
bookmarkLocationDao.findBookmarkLocation(place),
|
bookmarkLocationDao.findBookmarkLocation(place.name),
|
||||||
place
|
place
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("NearbyPinDetails").e(e)
|
Timber.tag("NearbyPinDetails").e(e)
|
||||||
collectResults.send(indices.map { Pair(it, updatedGroups[it]) })
|
//HTTP request failed. Try individual places
|
||||||
|
for (i in indices) {
|
||||||
|
launch {
|
||||||
|
val onePlaceBatch = mutableListOf<Pair<Int, MarkerPlaceGroup>>()
|
||||||
|
try {
|
||||||
|
val fetchedPlace = nearbyController.getPlaces(
|
||||||
|
mutableListOf(updatedGroups[i].place)
|
||||||
|
)
|
||||||
|
|
||||||
|
onePlaceBatch.add(Pair(i, MarkerPlaceGroup(
|
||||||
|
bookmarkLocationDao.findBookmarkLocation(
|
||||||
|
fetchedPlace[0].name
|
||||||
|
),
|
||||||
|
fetchedPlace[0]
|
||||||
|
)))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("NearbyPinDetails").e(e)
|
||||||
|
onePlaceBatch.add(Pair(i, updatedGroups[i]))
|
||||||
|
}
|
||||||
|
collectResults.send(onePlaceBatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var collectCount = 0
|
var collectCount = 0
|
||||||
for (resultList in collectResults) {
|
while (collectCount < indicesToUpdate.size) {
|
||||||
|
val resultList = collectResults.receive()
|
||||||
for ((index, fetchedPlaceGroup) in resultList) {
|
for ((index, fetchedPlaceGroup) in resultList) {
|
||||||
val existingPlace = updatedGroups[index].place
|
val existingPlace = updatedGroups[index].place
|
||||||
val finalPlaceGroup = MarkerPlaceGroup(
|
val finalPlaceGroup = MarkerPlaceGroup(
|
||||||
|
|
@ -435,16 +467,14 @@ class NearbyParentFragmentPresenter
|
||||||
if (bookmarkChangedPlacesBacklog.containsKey(group.place.location)) {
|
if (bookmarkChangedPlacesBacklog.containsKey(group.place.location)) {
|
||||||
updatedGroups[index] = MarkerPlaceGroup(
|
updatedGroups[index] = MarkerPlaceGroup(
|
||||||
bookmarkLocationDao
|
bookmarkLocationDao
|
||||||
.findBookmarkLocation(updatedGroups[index].place),
|
.findBookmarkLocation(updatedGroups[index].place.name),
|
||||||
updatedGroups[index].place
|
updatedGroups[index].place
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
schedulePlacesUpdate(updatedGroups)
|
schedulePlacesUpdate(updatedGroups)
|
||||||
if (++collectCount == totalBatches) {
|
collectCount += resultList.size
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
collectResults.close()
|
collectResults.close()
|
||||||
}
|
}
|
||||||
|
|
@ -545,7 +575,7 @@ class NearbyParentFragmentPresenter
|
||||||
).sortedBy { it.getDistanceInDouble(mapFocus) }.take(NearbyController.MAX_RESULTS)
|
).sortedBy { it.getDistanceInDouble(mapFocus) }.take(NearbyController.MAX_RESULTS)
|
||||||
.map {
|
.map {
|
||||||
MarkerPlaceGroup(
|
MarkerPlaceGroup(
|
||||||
bookmarkLocationDao.findBookmarkLocation(it), it
|
bookmarkLocationDao.findBookmarkLocation(it.name), it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ensureActive()
|
ensureActive()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons.repository
|
package fr.free.nrw.commons.repository
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import fr.free.nrw.commons.Media
|
import fr.free.nrw.commons.Media
|
||||||
import fr.free.nrw.commons.category.CategoriesModel
|
import fr.free.nrw.commons.category.CategoriesModel
|
||||||
import fr.free.nrw.commons.category.CategoryItem
|
import fr.free.nrw.commons.category.CategoryItem
|
||||||
|
|
@ -203,8 +204,8 @@ class UploadRepository @Inject constructor(
|
||||||
* @param filePath file to be checked
|
* @param filePath file to be checked
|
||||||
* @return IMAGE_DUPLICATE or IMAGE_OK
|
* @return IMAGE_DUPLICATE or IMAGE_OK
|
||||||
*/
|
*/
|
||||||
fun checkDuplicateImage(filePath: String): Single<Int> {
|
fun checkDuplicateImage(originalFilePath: Uri?, modifiedFilePath: Uri?): Single<Int> {
|
||||||
return uploadModel.checkDuplicateImage(filePath)
|
return uploadModel.checkDuplicateImage(originalFilePath, modifiedFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ class ReviewActivity : BaseActivity() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.reviewImageView.setImageURI(media.imageUrl)
|
binding.reviewImageView.setImageURI(media.thumbUrl)
|
||||||
|
|
||||||
reviewController.onImageRefreshed(media) // filename is updated
|
reviewController.onImageRefreshed(media) // filename is updated
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package fr.free.nrw.commons.upload
|
package fr.free.nrw.commons.upload
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import fr.free.nrw.commons.location.LatLng
|
import fr.free.nrw.commons.location.LatLng
|
||||||
import fr.free.nrw.commons.media.MediaClient
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
import fr.free.nrw.commons.nearby.Place
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
|
@ -13,7 +15,7 @@ import io.reactivex.Single
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.FileInputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
@ -26,7 +28,8 @@ class ImageProcessingService @Inject constructor(
|
||||||
private val imageUtilsWrapper: ImageUtilsWrapper,
|
private val imageUtilsWrapper: ImageUtilsWrapper,
|
||||||
private val readFBMD: ReadFBMD,
|
private val readFBMD: ReadFBMD,
|
||||||
private val exifReader: EXIFReader,
|
private val exifReader: EXIFReader,
|
||||||
private val mediaClient: MediaClient
|
private val mediaClient: MediaClient,
|
||||||
|
private val appContext: Context
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Check image quality before upload - checks duplicate image - checks dark image - checks
|
* Check image quality before upload - checks duplicate image - checks dark image - checks
|
||||||
|
|
@ -47,7 +50,10 @@ class ImageProcessingService @Inject constructor(
|
||||||
val filePath = uploadItem.mediaUri?.path
|
val filePath = uploadItem.mediaUri?.path
|
||||||
|
|
||||||
return Single.zip(
|
return Single.zip(
|
||||||
checkDuplicateImage(filePath),
|
checkIfFileAlreadyExists(
|
||||||
|
originalFilePath = uploadItem.contentUri!!,
|
||||||
|
modifiedFilePath = uploadItem.mediaUri!!
|
||||||
|
),
|
||||||
checkImageGeoLocation(uploadItem.place, filePath, inAppPictureLocation),
|
checkImageGeoLocation(uploadItem.place, filePath, inAppPictureLocation),
|
||||||
checkDarkImage(filePath!!),
|
checkDarkImage(filePath!!),
|
||||||
checkFBMD(filePath),
|
checkFBMD(filePath),
|
||||||
|
|
@ -114,18 +120,31 @@ class ImageProcessingService @Inject constructor(
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if file already exists using original & modified file's SHA
|
||||||
|
*
|
||||||
|
* @param originalFilePath original file to be checked
|
||||||
|
* @param modifiedFilePath modified (after exif modifications) file to be checked
|
||||||
|
* @return IMAGE_DUPLICATE or IMAGE_OK
|
||||||
|
*/
|
||||||
|
fun checkIfFileAlreadyExists(originalFilePath: Uri, modifiedFilePath: Uri): Single<Int> {
|
||||||
|
return Single.zip(
|
||||||
|
checkDuplicateImage(inputStream = appContext.contentResolver.openInputStream(originalFilePath)!!),
|
||||||
|
checkDuplicateImage(inputStream = fileUtilsWrapper.getFileInputStream(modifiedFilePath.path))
|
||||||
|
) { resultForOriginal, resultForDuplicate ->
|
||||||
|
return@zip if (resultForOriginal == IMAGE_DUPLICATE || resultForDuplicate == IMAGE_DUPLICATE)
|
||||||
|
IMAGE_DUPLICATE else IMAGE_OK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for duplicate image
|
* Checks for duplicate image
|
||||||
*
|
*
|
||||||
* @param filePath file to be checked
|
* @param filePath file to be checked
|
||||||
* @return IMAGE_DUPLICATE or IMAGE_OK
|
* @return IMAGE_DUPLICATE or IMAGE_OK
|
||||||
*/
|
*/
|
||||||
fun checkDuplicateImage(filePath: String?): Single<Int> {
|
private fun checkDuplicateImage(inputStream: InputStream): Single<Int> {
|
||||||
Timber.d("Checking for duplicate image %s", filePath)
|
return Single.fromCallable { fileUtilsWrapper.getSHA1(inputStream) }
|
||||||
return Single.fromCallable { fileUtilsWrapper.getFileInputStream(filePath) }
|
|
||||||
.map { stream: FileInputStream? ->
|
|
||||||
fileUtilsWrapper.getSHA1(stream)
|
|
||||||
}
|
|
||||||
.flatMap { fileSha: String? ->
|
.flatMap { fileSha: String? ->
|
||||||
mediaClient.checkFileExistsUsingSha(fileSha)
|
mediaClient.checkFileExistsUsingSha(fileSha)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,7 @@ import javax.inject.Named
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The presenter class for PendingUploadsFragment and FailedUploadsFragment
|
* The presenter class for PendingUploadsFragment and FailedUploadsFragment
|
||||||
*/
|
*/ class PendingUploadsPresenter @Inject internal constructor(
|
||||||
class PendingUploadsPresenter @Inject internal constructor(
|
|
||||||
private val contributionBoundaryCallback: ContributionBoundaryCallback,
|
private val contributionBoundaryCallback: ContributionBoundaryCallback,
|
||||||
private val contributionsRemoteDataSource: ContributionsRemoteDataSource,
|
private val contributionsRemoteDataSource: ContributionsRemoteDataSource,
|
||||||
private val contributionsRepository: ContributionsRepository,
|
private val contributionsRepository: ContributionsRepository,
|
||||||
|
|
@ -89,12 +88,16 @@ class PendingUploadsPresenter @Inject internal constructor(
|
||||||
* @param context The context in which the operation is being performed.
|
* @param context The context in which the operation is being performed.
|
||||||
*/
|
*/
|
||||||
override fun deleteUpload(contribution: Contribution?, context: Context?) {
|
override fun deleteUpload(contribution: Contribution?, context: Context?) {
|
||||||
compositeDisposable.add(
|
contribution?.let {
|
||||||
contributionsRepository
|
contributionsRepository
|
||||||
.deleteContributionFromDB(contribution)
|
.deleteContributionFromDB(it)
|
||||||
.subscribeOn(ioThreadScheduler)
|
.subscribeOn(ioThreadScheduler)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
)
|
}?.let {
|
||||||
|
compositeDisposable.add(
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -149,7 +152,10 @@ class PendingUploadsPresenter @Inject internal constructor(
|
||||||
}
|
}
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
uploadRepository
|
uploadRepository
|
||||||
.checkDuplicateImage(contribution.localUriPath!!.path)
|
.checkDuplicateImage(
|
||||||
|
originalFilePath = contribution.contentUri!!,
|
||||||
|
modifiedFilePath = contribution.localUri!!
|
||||||
|
)
|
||||||
.subscribeOn(ioThreadScheduler)
|
.subscribeOn(ioThreadScheduler)
|
||||||
.subscribe({ imageCheckResult: Int ->
|
.subscribe({ imageCheckResult: Int ->
|
||||||
if (imageCheckResult == IMAGE_OK) {
|
if (imageCheckResult == IMAGE_OK) {
|
||||||
|
|
@ -218,7 +224,10 @@ class PendingUploadsPresenter @Inject internal constructor(
|
||||||
}
|
}
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
uploadRepository
|
uploadRepository
|
||||||
.checkDuplicateImage(contribution.localUriPath!!.path)
|
.checkDuplicateImage(
|
||||||
|
originalFilePath = contribution.contentUri!!,
|
||||||
|
modifiedFilePath = contribution.localUri!!
|
||||||
|
)
|
||||||
.subscribeOn(ioThreadScheduler)
|
.subscribeOn(ioThreadScheduler)
|
||||||
.subscribe { imageCheckResult: Int ->
|
.subscribe { imageCheckResult: Int ->
|
||||||
if (imageCheckResult == IMAGE_OK) {
|
if (imageCheckResult == IMAGE_OK) {
|
||||||
|
|
|
||||||
|
|
@ -701,7 +701,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun receiveExternalSharedItems() {
|
private fun receiveExternalSharedItems() {
|
||||||
uploadableFiles = contributionController!!.handleExternalImagesPicked(this, intent)
|
uploadableFiles = contributionController!!.handleExternalImagesPicked(this, intent).toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun receiveInternalSharedItems() {
|
private fun receiveInternalSharedItems() {
|
||||||
|
|
|
||||||
|
|
@ -103,8 +103,11 @@ class UploadModel @Inject internal constructor(
|
||||||
* @param filePath file to be checked
|
* @param filePath file to be checked
|
||||||
* @return IMAGE_DUPLICATE or IMAGE_OK
|
* @return IMAGE_DUPLICATE or IMAGE_OK
|
||||||
*/
|
*/
|
||||||
fun checkDuplicateImage(filePath: String?): Single<Int> =
|
fun checkDuplicateImage(originalFilePath: Uri?, modifiedFilePath: Uri?): Single<Int> =
|
||||||
imageProcessingService.checkDuplicateImage(filePath)
|
imageProcessingService.checkIfFileAlreadyExists(
|
||||||
|
originalFilePath = originalFilePath!!,
|
||||||
|
modifiedFilePath = modifiedFilePath!!
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls validateCaption() of ImageProcessingService to check caption of image
|
* Calls validateCaption() of ImageProcessingService to check caption of image
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons.upload.categories
|
package fr.free.nrw.commons.upload.categories
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -89,6 +90,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("StringFormatMatches")
|
||||||
private fun init() {
|
private fun init() {
|
||||||
if (binding == null) {
|
if (binding == null) {
|
||||||
return
|
return
|
||||||
|
|
@ -372,8 +374,9 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View {
|
||||||
|
|
||||||
(requireActivity() as AppCompatActivity).supportActionBar?.hide()
|
(requireActivity() as AppCompatActivity).supportActionBar?.hide()
|
||||||
|
|
||||||
|
|
||||||
if (parentFragment?.parentFragment?.parentFragment is ContributionsFragment) {
|
if (parentFragment?.parentFragment?.parentFragment is ContributionsFragment) {
|
||||||
((parentFragment?.parentFragment?.parentFragment) as ContributionsFragment).binding.cardViewNearby.visibility = View.GONE
|
((parentFragment?.parentFragment?.parentFragment) as ContributionsFragment).binding?.cardViewNearby?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import android.widget.CompoundButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
|
|
@ -60,8 +61,7 @@ import javax.inject.Named
|
||||||
class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContract.View,
|
class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContract.View,
|
||||||
UploadMediaDetailAdapter.EventListener {
|
UploadMediaDetailAdapter.EventListener {
|
||||||
|
|
||||||
private val startForResult = registerForActivityResult<Intent, ActivityResult>(
|
private lateinit var startForResult: ActivityResultLauncher<Intent>
|
||||||
ActivityResultContracts.StartActivityForResult(), ::onCameraPosition)
|
|
||||||
|
|
||||||
private val startForEditActivityResult = registerForActivityResult<Intent, ActivityResult>(
|
private val startForEditActivityResult = registerForActivityResult<Intent, ActivityResult>(
|
||||||
ActivityResultContracts.StartActivityForResult(), ::onEditActivityResult)
|
ActivityResultContracts.StartActivityForResult(), ::onEditActivityResult)
|
||||||
|
|
@ -135,6 +135,10 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra
|
||||||
if (savedInstanceState != null && uploadableFile == null) {
|
if (savedInstanceState != null && uploadableFile == null) {
|
||||||
uploadableFile = savedInstanceState.getParcelable(UPLOADABLE_FILE)
|
uploadableFile = savedInstanceState.getParcelable(UPLOADABLE_FILE)
|
||||||
}
|
}
|
||||||
|
// Register the ActivityResultLauncher for LocationPickerActivity
|
||||||
|
startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
onCameraPosition(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setImageToBeUploaded(
|
fun setImageToBeUploaded(
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ class UploadMediaPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lateinit var basicKvStoreFactory: (String) -> BasicKvStore
|
private var basicKvStoreFactory: ((String) -> BasicKvStore)? = null
|
||||||
|
|
||||||
override fun onAttachView(view: UploadMediaDetailsContract.View) {
|
override fun onAttachView(view: UploadMediaDetailsContract.View) {
|
||||||
this.view = view
|
this.view = view
|
||||||
|
|
@ -339,8 +339,10 @@ class UploadMediaPresenter @Inject constructor(
|
||||||
*/
|
*/
|
||||||
override fun checkImageQuality(uploadItem: UploadItem, index: Int) {
|
override fun checkImageQuality(uploadItem: UploadItem, index: Int) {
|
||||||
if ((uploadItem.imageQuality != IMAGE_OK) && (uploadItem.imageQuality != IMAGE_KEEP)) {
|
if ((uploadItem.imageQuality != IMAGE_OK) && (uploadItem.imageQuality != IMAGE_KEEP)) {
|
||||||
val value = basicKvStoreFactory(UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE)
|
|
||||||
.getString(UPLOAD_QUALITIES_KEY, null)
|
val value = basicKvStoreFactory?.let { it(UploadActivity.storeNameForCurrentUploadImagesSize) }
|
||||||
|
?.getString(UPLOAD_QUALITIES_KEY, null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val imageQuality = value.asJsonObject()["UploadItem$index"] as Int
|
val imageQuality = value.asJsonObject()["UploadItem$index"] as Int
|
||||||
view.showProgress(false)
|
view.showProgress(false)
|
||||||
|
|
@ -363,8 +365,9 @@ class UploadMediaPresenter @Inject constructor(
|
||||||
* @param index Index of the UploadItem which was deleted
|
* @param index Index of the UploadItem which was deleted
|
||||||
*/
|
*/
|
||||||
override fun updateImageQualitiesJSON(size: Int, index: Int) {
|
override fun updateImageQualitiesJSON(size: Int, index: Int) {
|
||||||
val value = basicKvStoreFactory(UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE)
|
val value = basicKvStoreFactory?.let { it(UploadActivity.storeNameForCurrentUploadImagesSize) }
|
||||||
.getString(UPLOAD_QUALITIES_KEY, null)
|
?.getString(UPLOAD_QUALITIES_KEY, null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val jsonObject = value.asJsonObject().apply {
|
val jsonObject = value.asJsonObject().apply {
|
||||||
for (i in index until (size - 1)) {
|
for (i in index until (size - 1)) {
|
||||||
|
|
@ -372,8 +375,9 @@ class UploadMediaPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
remove("UploadItem" + (size - 1))
|
remove("UploadItem" + (size - 1))
|
||||||
}
|
}
|
||||||
basicKvStoreFactory(UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE)
|
|
||||||
.putString(UPLOAD_QUALITIES_KEY, jsonObject.toString())
|
basicKvStoreFactory?.let { it(UploadActivity.storeNameForCurrentUploadImagesSize) }
|
||||||
|
?.putString(UPLOAD_QUALITIES_KEY, jsonObject.toString())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ class UploadWorker(
|
||||||
currentNotification.build(),
|
currentNotification.build(),
|
||||||
)
|
)
|
||||||
contribution!!.transferred = transferred
|
contribution!!.transferred = transferred
|
||||||
contributionDao.update(contribution).blockingAwait()
|
contributionDao.update(contribution!!).blockingAwait()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun onChunkUploaded(
|
open fun onChunkUploaded(
|
||||||
|
|
@ -469,7 +469,7 @@ class UploadWorker(
|
||||||
contribution: Contribution,
|
contribution: Contribution,
|
||||||
) {
|
) {
|
||||||
val wikiDataPlace = contribution.wikidataPlace
|
val wikiDataPlace = contribution.wikidataPlace
|
||||||
if (wikiDataPlace != null && wikiDataPlace.imageValue == null) {
|
if (wikiDataPlace != null) {
|
||||||
if (!contribution.hasInvalidLocation()) {
|
if (!contribution.hasInvalidLocation()) {
|
||||||
var revisionID: Long? = null
|
var revisionID: Long? = null
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
10
app/src/main/res/anim/rotate.xml
Normal file
10
app/src/main/res/anim/rotate.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<rotate
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:fromDegrees="0"
|
||||||
|
android:toDegrees="360"
|
||||||
|
android:pivotX="50%"
|
||||||
|
android:pivotY="50%"
|
||||||
|
android:duration="500"
|
||||||
|
android:repeatCount="infinite"
|
||||||
|
android:interpolator="@android:anim/linear_interpolator"/>
|
||||||
49
app/src/main/res/drawable/loading_icon.xml
Normal file
49
app/src/main/res/drawable/loading_icon.xml
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<vector
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M12,4 A8,8 0 1,1 4,12 A8,8 0 1,5 19.42 ,15"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:strokeColor="#2196F3"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:trimPathStart="0.2"
|
||||||
|
android:trimPathEnd="1.0"/>
|
||||||
|
</vector>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<vector
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M12,4 A8,8 0 1,1 4,12 A8,8 0 1,1 12,4"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:trimPathStart="0.0"
|
||||||
|
android:trimPathEnd="0.2"/>
|
||||||
|
</vector>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:right="12dp">
|
||||||
|
<rotate
|
||||||
|
android:fromDegrees="0"
|
||||||
|
android:toDegrees="360"
|
||||||
|
android:pivotX="50%"
|
||||||
|
android:pivotY="50%">
|
||||||
|
<shape android:shape="ring">
|
||||||
|
<size
|
||||||
|
android:width="12dp"
|
||||||
|
android:height="2dp"/>
|
||||||
|
<solid android:color="#2196F3"/>
|
||||||
|
</shape>
|
||||||
|
</rotate>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
|
|
@ -59,10 +59,12 @@
|
||||||
<fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText
|
<fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText
|
||||||
android:id="@+id/caption_item_edit_text"
|
android:id="@+id/caption_item_edit_text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
|
android:minLines="1"
|
||||||
|
android:maxLines="10"
|
||||||
android:hint="@string/share_caption_hint"
|
android:hint="@string/share_caption_hint"
|
||||||
android:imeOptions="actionNext|flagNoExtractUi"
|
android:imeOptions="actionNext|flagNoExtractUi"
|
||||||
android:inputType="text"
|
android:inputType="textMultiLine"
|
||||||
app:allowFormatting="false" />
|
app:allowFormatting="false" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
|
@ -103,7 +105,9 @@
|
||||||
<fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText
|
<fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText
|
||||||
android:id="@+id/description_item_edit_text"
|
android:id="@+id/description_item_edit_text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
|
android:minLines="1"
|
||||||
|
android:maxLines="10"
|
||||||
android:hint="@string/share_description_hint"
|
android:hint="@string/share_description_hint"
|
||||||
android:imeOptions="actionNext|flagNoExtractUi"
|
android:imeOptions="actionNext|flagNoExtractUi"
|
||||||
android:inputType="textMultiLine"
|
android:inputType="textMultiLine"
|
||||||
|
|
|
||||||
19
app/src/main/res/menu/explore_fragment_menu.xml
Normal file
19
app/src/main/res/menu/explore_fragment_menu.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
>
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_search"
|
||||||
|
android:title="@string/menu_search_button"
|
||||||
|
android:icon="?attr/search_icon"
|
||||||
|
android:orderInCategory="1"
|
||||||
|
app:showAsAction="ifRoom"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<item android:id="@+id/list_item_show_in_nearby"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:title="@string/show_in_nearby"
|
||||||
|
android:visible="false"
|
||||||
|
/>
|
||||||
|
</menu>
|
||||||
|
|
@ -12,6 +12,12 @@
|
||||||
android:icon="@drawable/ic_list_white_24dp"
|
android:icon="@drawable/ic_list_white_24dp"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<item android:id="@+id/list_item_show_in_explore"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:title="@string/show_in_explore"
|
||||||
|
/>
|
||||||
|
|
||||||
<item android:id="@+id/list_item_gpx"
|
<item android:id="@+id/list_item_gpx"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
* Asma
|
* Asma
|
||||||
* Azouz.anis
|
* Azouz.anis
|
||||||
* ButterflyOfFire
|
* ButterflyOfFire
|
||||||
|
* Cigaryno
|
||||||
* Claw eg
|
* Claw eg
|
||||||
* Dr-Taher
|
* Dr-Taher
|
||||||
* Dr. Mohammed
|
* Dr. Mohammed
|
||||||
|
|
@ -20,6 +21,7 @@
|
||||||
* NancyMilad
|
* NancyMilad
|
||||||
* OsamaK
|
* OsamaK
|
||||||
* Tala Ali
|
* Tala Ali
|
||||||
|
* XIDME
|
||||||
* أيوب
|
* أيوب
|
||||||
* أَحمد
|
* أَحمد
|
||||||
* ترجمان05
|
* ترجمان05
|
||||||
|
|
@ -408,7 +410,7 @@
|
||||||
<string name="error_fetching_nearby_monuments">خطأ في جلب المعالم القريبة.</string>
|
<string name="error_fetching_nearby_monuments">خطأ في جلب المعالم القريبة.</string>
|
||||||
<string name="no_recent_searches">لا توجد عمليات بحث حديثة</string>
|
<string name="no_recent_searches">لا توجد عمليات بحث حديثة</string>
|
||||||
<string name="delete_recent_searches_dialog">هل أنت متأكد من أنك تريد مسح سجل بحثك؟</string>
|
<string name="delete_recent_searches_dialog">هل أنت متأكد من أنك تريد مسح سجل بحثك؟</string>
|
||||||
<string name="cancel_upload_dialog">هل انت متأكد انك تريد الغاء هذا التحميل</string>
|
<string name="cancel_upload_dialog">هل أنت متأكد أنك تريد إلغاء هذا التحميل؟</string>
|
||||||
<string name="delete_search_dialog">هل تريد حذف هذا البحث؟</string>
|
<string name="delete_search_dialog">هل تريد حذف هذا البحث؟</string>
|
||||||
<string name="search_history_deleted">تم حذف سجل البحث</string>
|
<string name="search_history_deleted">تم حذف سجل البحث</string>
|
||||||
<string name="nominate_delete">ترشيح للحذف</string>
|
<string name="nominate_delete">ترشيح للحذف</string>
|
||||||
|
|
@ -882,4 +884,7 @@
|
||||||
<string name="account_vanish_request_confirm">الاختفاء هو <b>الملاذ الأخير</b> ويجب <b>استخدامه فقط عندما ترغب في التوقف عن التحرير إلى الأبد</b> وأيضًا لإخفاء أكبر عدد ممكن من ارتباطاتك السابقة.<br/><br/> يتم حذف الحساب على ويكيميديا كومنز عن طريق تغيير اسم حسابك بحيث لا يتمكن الآخرون من التعرف على مساهماتك في عملية تسمى اختفاء الحساب. <b>لا يضمن الاختفاء عدم الكشف عن الهوية تمامًا أو إزالة المساهمات في المشاريع</b> .</string>
|
<string name="account_vanish_request_confirm">الاختفاء هو <b>الملاذ الأخير</b> ويجب <b>استخدامه فقط عندما ترغب في التوقف عن التحرير إلى الأبد</b> وأيضًا لإخفاء أكبر عدد ممكن من ارتباطاتك السابقة.<br/><br/> يتم حذف الحساب على ويكيميديا كومنز عن طريق تغيير اسم حسابك بحيث لا يتمكن الآخرون من التعرف على مساهماتك في عملية تسمى اختفاء الحساب. <b>لا يضمن الاختفاء عدم الكشف عن الهوية تمامًا أو إزالة المساهمات في المشاريع</b> .</string>
|
||||||
<string name="caption">الشرح</string>
|
<string name="caption">الشرح</string>
|
||||||
<string name="caption_copied_to_clipboard">تم نسخ التسمية التوضيحية إلى الحافظة</string>
|
<string name="caption_copied_to_clipboard">تم نسخ التسمية التوضيحية إلى الحافظة</string>
|
||||||
|
<string name="congratulations_all_pictures_in_this_album_have_been_either_uploaded_or_marked_as_not_for_upload">مبروك، جميع الصور الموجودة في هذا الألبوم تم تحميلها أو تم وضع علامة عليها بأنها غير قابلة للتحميل.</string>
|
||||||
|
<string name="show_in_explore">عرض في استكشاف</string>
|
||||||
|
<string name="show_in_nearby">عرض في المناطق القريبة</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@
|
||||||
<string name="no_uploads_yet">Inda nun xubió denguna foto.</string>
|
<string name="no_uploads_yet">Inda nun xubió denguna foto.</string>
|
||||||
<string name="menu_retry_upload">Reintentar</string>
|
<string name="menu_retry_upload">Reintentar</string>
|
||||||
<string name="menu_cancel_upload">Zarrar</string>
|
<string name="menu_cancel_upload">Zarrar</string>
|
||||||
<string name="media_upload_policy">El presentar esta imaxe, declaro que ye una obra propia, que nun contien material con derechu d\'autor o «selfies», y que s\'atien a les <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">polítiques de Wikimedia Commons</a>.</string>
|
<string name="media_upload_policy">El presentar esta imaxe, declaro que ye una obra propia, que nun contién material con derechu d\'autor o «selfies», y que s\'atien a les <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">polítiques de Wikimedia Commons</a>.</string>
|
||||||
<string name="menu_download">Descargar</string>
|
<string name="menu_download">Descargar</string>
|
||||||
<string name="preference_license">Llicencia predeterminada</string>
|
<string name="preference_license">Llicencia predeterminada</string>
|
||||||
<string name="use_previous">Usar un títulu y descripción anterior</string>
|
<string name="use_previous">Usar un títulu y descripción anterior</string>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
* Toghrul Rahimli
|
* Toghrul Rahimli
|
||||||
* Wertuose
|
* Wertuose
|
||||||
* Şeyx Şamil
|
* Şeyx Şamil
|
||||||
|
* Əkrəm
|
||||||
* Əkrəm Cəfər
|
* Əkrəm Cəfər
|
||||||
-->
|
-->
|
||||||
<resources>
|
<resources>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue