mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-28 21:33:53 +01:00
Fix conflicts
This commit is contained in:
commit
5b88111289
324 changed files with 9238 additions and 2883 deletions
|
|
@ -1,10 +1,16 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.customtabs.CustomTabsIntent;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||
import fr.free.nrw.commons.ui.widget.HtmlTextView;
|
||||
|
||||
|
|
@ -27,10 +33,45 @@ public class AboutActivity extends NavigationBaseActivity {
|
|||
|
||||
ButterKnife.bind(this);
|
||||
|
||||
String aboutText = getString(R.string.about_license, getString(R.string.trademarked_name));
|
||||
String aboutText = getString(R.string.about_license);
|
||||
aboutLicenseText.setHtmlText(aboutText);
|
||||
|
||||
versionText.setText(BuildConfig.VERSION_NAME);
|
||||
initDrawer();
|
||||
}
|
||||
|
||||
@OnClick(R.id.facebook_launch_icon)
|
||||
public void launchFacebook(View view) {
|
||||
|
||||
Intent intent;
|
||||
try {
|
||||
intent = new Intent(Intent.ACTION_VIEW, Uri.parse("fb://page/" + "1921335171459985"));
|
||||
intent.setPackage("com.facebook.katana");
|
||||
startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
Utils.handleWebUrl(this,Uri.parse("https://www.facebook.com/" + "1921335171459985"));
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.github_launch_icon)
|
||||
public void launchGithub(View view) {
|
||||
Utils.handleWebUrl(this,Uri.parse("https://commons-app.github.io/\\"));
|
||||
}
|
||||
|
||||
@OnClick(R.id.website_launch_icon)
|
||||
public void launchWebsite(View view) {
|
||||
Utils.handleWebUrl(this,Uri.parse("https://commons-app.github.io/\\"));
|
||||
}
|
||||
|
||||
@OnClick(R.id.about_credits)
|
||||
public void launchCredits(View view) {
|
||||
Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/blob/master/CREDITS/\\"));
|
||||
}
|
||||
|
||||
@OnClick(R.id.about_privacy_policy)
|
||||
public void launchPrivacyPolicy(View view) {
|
||||
Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\\"));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
|
@ -18,16 +19,13 @@ import java.io.File;
|
|||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.android.AndroidInjector;
|
||||
import dagger.android.DaggerApplication;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.data.Category;
|
||||
import fr.free.nrw.commons.category.CategoryDao;
|
||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.di.CommonsApplicationComponent;
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
||||
import fr.free.nrw.commons.di.DaggerCommonsApplicationComponent;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||
import fr.free.nrw.commons.utils.FileUtils;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
|
@ -42,25 +40,26 @@ import timber.log.Timber;
|
|||
resDialogCommentPrompt = R.string.crash_dialog_comment_prompt,
|
||||
resDialogOkToast = R.string.crash_dialog_ok_toast
|
||||
)
|
||||
public class CommonsApplication extends DaggerApplication {
|
||||
public class CommonsApplication extends Application {
|
||||
|
||||
@Inject SessionManager sessionManager;
|
||||
@Inject DBOpenHelper dbOpenHelper;
|
||||
|
||||
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
|
||||
@Inject @Named("application_preferences") SharedPreferences applicationPrefs;
|
||||
@Inject @Named("prefs") SharedPreferences otherPrefs;
|
||||
|
||||
|
||||
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using Android Commons app";
|
||||
|
||||
|
||||
public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
|
||||
|
||||
|
||||
public static final String LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com";
|
||||
|
||||
|
||||
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
|
||||
|
||||
private CommonsApplicationComponent component;
|
||||
|
||||
private RefWatcher refWatcher;
|
||||
|
||||
|
||||
/**
|
||||
* Used to declare and initialize various components and dependencies
|
||||
*/
|
||||
|
|
@ -68,6 +67,11 @@ public class CommonsApplication extends DaggerApplication {
|
|||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
ApplicationlessInjection
|
||||
.getInstance(this)
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
|
||||
Fresco.initialize(this);
|
||||
if (setupLeakCanary() == RefWatcher.DISABLED) {
|
||||
return;
|
||||
|
|
@ -85,6 +89,7 @@ public class CommonsApplication extends DaggerApplication {
|
|||
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helps in setting up LeakCanary library
|
||||
* @return instance of LeakCanary
|
||||
|
|
@ -95,7 +100,7 @@ public class CommonsApplication extends DaggerApplication {
|
|||
}
|
||||
return LeakCanary.install(this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Provides a way to get member refWatcher
|
||||
*
|
||||
|
|
@ -106,28 +111,6 @@ public class CommonsApplication extends DaggerApplication {
|
|||
CommonsApplication application = (CommonsApplication) context.getApplicationContext();
|
||||
return application.refWatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helps in injecting dependency library Dagger
|
||||
* @return Dagger injector
|
||||
*/
|
||||
@Override
|
||||
protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
|
||||
return injector();
|
||||
}
|
||||
|
||||
/**
|
||||
* used to create injector of application component
|
||||
* @return Application component of Dagger
|
||||
*/
|
||||
public CommonsApplicationComponent injector() {
|
||||
if (component == null) {
|
||||
component = DaggerCommonsApplicationComponent.builder()
|
||||
.appModule(new CommonsApplicationModule(this))
|
||||
.build();
|
||||
}
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* clears data of current application
|
||||
|
|
@ -152,9 +135,10 @@ public class CommonsApplication extends DaggerApplication {
|
|||
.subscribe(() -> {
|
||||
Timber.d("All accounts have been removed");
|
||||
//TODO: fix preference manager
|
||||
defaultPrefs.edit().clear().commit();
|
||||
applicationPrefs.edit().clear().commit();
|
||||
applicationPrefs.edit().putBoolean("firstrun", false).apply();otherPrefs.edit().clear().commit();
|
||||
defaultPrefs.edit().clear().apply();
|
||||
applicationPrefs.edit().clear().apply();
|
||||
applicationPrefs.edit().putBoolean("firstrun", false).apply();
|
||||
otherPrefs.edit().clear().apply();
|
||||
updateAllDatabases();
|
||||
|
||||
logoutListener.onLogoutComplete();
|
||||
|
|
@ -168,9 +152,9 @@ public class CommonsApplication extends DaggerApplication {
|
|||
dbOpenHelper.getReadableDatabase().close();
|
||||
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
||||
|
||||
ModifierSequence.Table.onDelete(db);
|
||||
Category.Table.onDelete(db);
|
||||
Contribution.Table.onDelete(db);
|
||||
ModifierSequenceDao.Table.onDelete(db);
|
||||
CategoryDao.Table.onDelete(db);
|
||||
ContributionDao.Table.onDelete(db);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import android.os.IBinder;
|
|||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import dagger.android.DaggerService;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerService;
|
||||
|
||||
public abstract class HandlerService<T> extends DaggerService {
|
||||
public abstract class HandlerService<T> extends CommonsDaggerService {
|
||||
private volatile Looper threadLooper;
|
||||
private volatile ServiceHandler threadHandler;
|
||||
private String serviceName;
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ public class MediaDataExtractor {
|
|||
/**
|
||||
* Take our metadata and inject it into a live Media object.
|
||||
* Media object might contain stale or cached data, or emptiness.
|
||||
* @param media
|
||||
* @param media Media object to inject into
|
||||
*/
|
||||
public void fill(Media media) {
|
||||
if (!fetched) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.facebook.drawee.view.SimpleDraweeView;
|
|||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
@ -38,6 +39,10 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
|||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the media. Fetches its thumbnail if necessary.
|
||||
* @param media the new media
|
||||
*/
|
||||
public void setMedia(Media media) {
|
||||
if (currentThumbnailTask != null) {
|
||||
currentThumbnailTask.cancel(true);
|
||||
|
|
@ -63,8 +68,15 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
|||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes MediaWikiImageView.
|
||||
*/
|
||||
private void init() {
|
||||
((CommonsApplication) getContext().getApplicationContext()).injector().inject(this);
|
||||
ApplicationlessInjection
|
||||
.getInstance(getContext()
|
||||
.getApplicationContext())
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
setHierarchy(GenericDraweeHierarchyBuilder
|
||||
.newInstance(getResources())
|
||||
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
||||
|
|
@ -74,6 +86,10 @@ public class MediaWikiImageView extends SimpleDraweeView {
|
|||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the image from the URL.
|
||||
* @param url the URL of the image
|
||||
*/
|
||||
private void setImageUrl(@Nullable String url) {
|
||||
setImageURI(url);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.customtabs.CustomTabsIntent;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
|
|
@ -11,6 +15,7 @@ import java.io.BufferedReader;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
|
|
@ -65,7 +70,7 @@ public class Utils {
|
|||
/**
|
||||
* Capitalizes the first character of a string.
|
||||
*
|
||||
* @param string
|
||||
* @param string String to alter
|
||||
* @return string with capitalized first character
|
||||
*/
|
||||
public static String capitalize(String string) {
|
||||
|
|
@ -159,4 +164,15 @@ public class Utils {
|
|||
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
public static void handleWebUrl(Context context,Uri url){
|
||||
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
|
||||
builder.setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor));
|
||||
builder.setSecondaryToolbarColor(ContextCompat.getColor(context, R.color.primaryDarkColor));
|
||||
builder.setExitAnimations(context, android.R.anim.slide_in_left, android.R.anim.slide_out_right);
|
||||
CustomTabsIntent customTabsIntent = builder.build();
|
||||
customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
customTabsIntent.launchUrl(context, url);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,19 @@ import android.content.Context;
|
|||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.accounts.AccountManager.ERROR_CODE_REMOTE_EXCEPTION;
|
||||
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
|
||||
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY;
|
||||
import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MODIFICATIONS_AUTHORITY;
|
||||
|
||||
public class AccountUtil {
|
||||
|
||||
public static final String ACCOUNT_TYPE = "fr.free.nrw.commons";
|
||||
public static final String AUTH_COOKIE = "authCookie";
|
||||
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
|
||||
private final Context context;
|
||||
|
||||
public AccountUtil(Context context) {
|
||||
|
|
@ -51,8 +53,8 @@ public class AccountUtil {
|
|||
}
|
||||
|
||||
// FIXME: If the user turns it off, it shouldn't be auto turned back on
|
||||
ContentResolver.setSyncAutomatically(account, ContributionsContentProvider.AUTHORITY, true); // Enable sync by default!
|
||||
ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
|
||||
ContentResolver.setSyncAutomatically(account, CONTRIBUTION_AUTHORITY, true); // Enable sync by default!
|
||||
ContentResolver.setSyncAutomatically(account, MODIFICATIONS_AUTHORITY, true); // Enable sync by default!
|
||||
}
|
||||
|
||||
private AccountManager accountManager() {
|
||||
|
|
|
|||
|
|
@ -1,71 +1,29 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.AccountManagerFuture;
|
||||
import android.os.Bundle;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
|
||||
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
|
||||
|
||||
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
|
||||
|
||||
@Inject SessionManager sessionManager;
|
||||
|
||||
@Inject
|
||||
MediaWikiApi mediaWikiApi;
|
||||
private String authCookie;
|
||||
|
||||
|
||||
private void getAuthCookie(Account account, AccountManager accountManager) {
|
||||
Single.fromCallable(() -> accountManager.blockingGetAuthToken(account, "", false))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.doOnError(Timber::e)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
this:: onAuthCookieAcquired,
|
||||
throwable -> onAuthFailure());
|
||||
}
|
||||
|
||||
private void addAccount(AccountManager accountManager) {
|
||||
Single.just(accountManager.addAccount(ACCOUNT_TYPE, null, null,
|
||||
null, AuthenticatedActivity.this, null, null))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map(AccountManagerFuture::getResult)
|
||||
.doOnEvent((bundle, throwable) -> {
|
||||
if (!bundle.containsKey(KEY_ACCOUNT_NAME)) {
|
||||
throw new RuntimeException("Bundle doesn't contain account-name key: "
|
||||
+ KEY_ACCOUNT_NAME);
|
||||
}
|
||||
})
|
||||
.map(bundle -> bundle.getString(KEY_ACCOUNT_NAME))
|
||||
.doOnError(Timber::e)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(s -> {
|
||||
Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
|
||||
Account curAccount = allAccounts[0];
|
||||
getAuthCookie(curAccount, accountManager);
|
||||
},
|
||||
throwable -> onAuthFailure());
|
||||
}
|
||||
|
||||
protected void requestAuthToken() {
|
||||
if (authCookie != null) {
|
||||
onAuthCookieAcquired(authCookie);
|
||||
return;
|
||||
}
|
||||
AccountManager accountManager = AccountManager.get(this);
|
||||
Account curAccount = sessionManager.getCurrentAccount();
|
||||
if (curAccount == null) {
|
||||
addAccount(accountManager);
|
||||
} else {
|
||||
getAuthCookie(curAccount, accountManager);
|
||||
authCookie = sessionManager.getAuthCookie();
|
||||
if (authCookie != null) {
|
||||
onAuthCookieAcquired(authCookie);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,14 +32,14 @@ public abstract class AuthenticatedActivity extends NavigationBaseActivity {
|
|||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
authCookie = savedInstanceState.getString("authCookie");
|
||||
authCookie = savedInstanceState.getString(AUTH_COOKIE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString("authCookie", authCookie);
|
||||
outState.putString(AUTH_COOKIE, authCookie);
|
||||
}
|
||||
|
||||
protected abstract void onAuthCookieAcquired(String authCookie);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountAuthenticatorActivity;
|
||||
import android.accounts.AccountAuthenticatorResponse;
|
||||
import android.accounts.AccountManager;
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
|
@ -8,37 +12,48 @@ import android.os.Bundle;
|
|||
import android.support.annotation.ColorRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.design.widget.TextInputLayout;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import dagger.android.AndroidInjection;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.PageTitle;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.WelcomeActivity;
|
||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.view.KeyEvent.KEYCODE_ENTER;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
|
||||
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
|
||||
|
||||
public class LoginActivity extends AccountAuthenticatorActivity {
|
||||
|
||||
|
|
@ -57,6 +72,8 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
@BindView(R.id.loginTwoFactor) EditText twoFactorEdit;
|
||||
@BindView(R.id.error_message_container) ViewGroup errorMessageContainer;
|
||||
@BindView(R.id.error_message) TextView errorMessage;
|
||||
@BindView(R.id.login_credentials) TextView loginCredentials;
|
||||
@BindView(R.id.two_factor_container)TextInputLayout twoFactorContainer;
|
||||
ProgressDialog progressDialog;
|
||||
private AppCompatDelegate delegate;
|
||||
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
||||
|
|
@ -66,22 +83,51 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
setTheme(Utils.isDarkTheme(this) ? R.style.DarkAppTheme : R.style.LightAppTheme);
|
||||
getDelegate().installViewFactory();
|
||||
getDelegate().onCreate(savedInstanceState);
|
||||
AndroidInjection.inject(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
ApplicationlessInjection
|
||||
.getInstance(this.getApplicationContext())
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
|
||||
setContentView(R.layout.activity_login);
|
||||
|
||||
ButterKnife.bind(this);
|
||||
|
||||
usernameEdit.addTextChangedListener(textWatcher);
|
||||
usernameEdit.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
hideKeyboard(v);
|
||||
}
|
||||
});
|
||||
|
||||
passwordEdit.addTextChangedListener(textWatcher);
|
||||
passwordEdit.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
hideKeyboard(v);
|
||||
}
|
||||
});
|
||||
|
||||
twoFactorEdit.addTextChangedListener(textWatcher);
|
||||
passwordEdit.setOnEditorActionListener(newLoginInputActionListener());
|
||||
|
||||
loginButton.setOnClickListener(view -> performLogin());
|
||||
signupButton.setOnClickListener(view -> signUp());
|
||||
|
||||
if(BuildConfig.FLAVOR == "beta"){
|
||||
loginCredentials.setText(getString(R.string.login_credential));
|
||||
} else {
|
||||
loginCredentials.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void hideKeyboard(View view) {
|
||||
InputMethodManager inputMethodManager =(InputMethodManager)this.getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
|
@ -117,14 +163,109 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
super.onDestroy();
|
||||
}
|
||||
|
||||
private LoginTask getLoginTask() {
|
||||
return new LoginTask(
|
||||
this,
|
||||
canonicializeUsername(usernameEdit.getText().toString()),
|
||||
passwordEdit.getText().toString(),
|
||||
twoFactorEdit.getText().toString(),
|
||||
accountUtil, mwApi, defaultPrefs
|
||||
);
|
||||
private void performLogin() {
|
||||
Timber.d("Login to start!");
|
||||
final String username = canonicializeUsername(usernameEdit.getText().toString());
|
||||
final String password = passwordEdit.getText().toString();
|
||||
String twoFactorCode = twoFactorEdit.getText().toString();
|
||||
|
||||
showLoggingProgressBar();
|
||||
Observable.fromCallable(() -> login(username, password, twoFactorCode))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> handleLogin(username, password, result));
|
||||
}
|
||||
|
||||
private String login(String username, String password, String twoFactorCode) {
|
||||
try {
|
||||
if (twoFactorCode.isEmpty()) {
|
||||
return mwApi.login(username, password);
|
||||
} else {
|
||||
return mwApi.login(username, password, twoFactorCode);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Do something better!
|
||||
return "NetworkFailure";
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLogin(String username, String password, String result) {
|
||||
Timber.d("Login done!");
|
||||
if (result.equals("PASS")) {
|
||||
handlePassResult(username, password);
|
||||
} else {
|
||||
handleOtherResults(result);
|
||||
}
|
||||
}
|
||||
|
||||
private void showLoggingProgressBar() {
|
||||
progressDialog = new ProgressDialog(this);
|
||||
progressDialog.setIndeterminate(true);
|
||||
progressDialog.setTitle(getString(R.string.logging_in_title));
|
||||
progressDialog.setMessage(getString(R.string.logging_in_message));
|
||||
progressDialog.setCanceledOnTouchOutside(false);
|
||||
progressDialog.show();
|
||||
}
|
||||
|
||||
private void handlePassResult(String username, String password) {
|
||||
showSuccessAndDismissDialog();
|
||||
requestAuthToken();
|
||||
AccountAuthenticatorResponse response = null;
|
||||
|
||||
Bundle extras = getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
Timber.d("Bundle of extras: %s", extras);
|
||||
response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
|
||||
if (response != null) {
|
||||
Bundle authResult = new Bundle();
|
||||
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username);
|
||||
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
|
||||
response.onResult(authResult);
|
||||
}
|
||||
}
|
||||
|
||||
accountUtil.createAccount(response, username, password);
|
||||
startMainActivity();
|
||||
}
|
||||
|
||||
protected void requestAuthToken() {
|
||||
AccountManager accountManager = AccountManager.get(this);
|
||||
Account curAccount = sessionManager.getCurrentAccount();
|
||||
if (curAccount != null) {
|
||||
accountManager.setAuthToken(curAccount, AUTH_TOKEN_TYPE, mwApi.getAuthCookie());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Match known failure message codes and provide messages.
|
||||
*
|
||||
* @param result String
|
||||
*/
|
||||
private void handleOtherResults(String result) {
|
||||
if (result.equals("NetworkFailure")) {
|
||||
// Matches NetworkFailure which is created by the doInBackground method
|
||||
showMessageAndCancelDialog(R.string.login_failed_network);
|
||||
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
|
||||
// Matches nosuchuser, nosuchusershort, noname
|
||||
showMessageAndCancelDialog(R.string.login_failed_username);
|
||||
emptySensitiveEditFields();
|
||||
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
|
||||
// Matches wrongpassword, wrongpasswordempty
|
||||
showMessageAndCancelDialog(R.string.login_failed_password);
|
||||
emptySensitiveEditFields();
|
||||
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
|
||||
// Matches unknown throttle error codes
|
||||
showMessageAndCancelDialog(R.string.login_failed_throttled);
|
||||
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
|
||||
// Matches login-userblocked
|
||||
showMessageAndCancelDialog(R.string.login_failed_blocked);
|
||||
} else if (result.equals("2FA")) {
|
||||
askUserForTwoFactorAuth();
|
||||
} else {
|
||||
// Occurs with unhandled login failure codes
|
||||
Timber.d("Login failed with reason: %s", result);
|
||||
showMessageAndCancelDialog(R.string.login_failed_generic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -176,12 +317,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
}
|
||||
|
||||
public void askUserForTwoFactorAuth() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
twoFactorEdit.setVisibility(View.VISIBLE);
|
||||
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
|
||||
} else {
|
||||
showMessageAndCancelDialog(R.string.login_failed_2fa_not_supported);
|
||||
}
|
||||
progressDialog.dismiss();
|
||||
twoFactorContainer.setVisibility(VISIBLE);
|
||||
twoFactorEdit.setVisibility(VISIBLE);
|
||||
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
|
||||
}
|
||||
|
||||
public void showMessageAndCancelDialog(@StringRes int resId) {
|
||||
|
|
@ -204,12 +343,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
finish();
|
||||
}
|
||||
|
||||
private void performLogin() {
|
||||
Timber.d("Login to start!");
|
||||
LoginTask task = getLoginTask();
|
||||
task.execute();
|
||||
}
|
||||
|
||||
private void signUp() {
|
||||
Intent intent = new Intent(this, SignupActivity.class);
|
||||
startActivity(intent);
|
||||
|
|
@ -233,7 +366,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
|
||||
errorMessage.setText(getString(resId));
|
||||
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
||||
errorMessageContainer.setVisibility(View.VISIBLE);
|
||||
errorMessageContainer.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
private AppCompatDelegate getDelegate() {
|
||||
|
|
@ -255,7 +388,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0
|
||||
&& (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE);
|
||||
&& (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != VISIBLE);
|
||||
loginButton.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,128 +0,0 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.accounts.AccountAuthenticatorResponse;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE;
|
||||
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
|
||||
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
|
||||
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||
|
||||
class LoginTask extends AsyncTask<String, String, String> {
|
||||
|
||||
private LoginActivity loginActivity;
|
||||
private String username;
|
||||
private String password;
|
||||
private String twoFactorCode = "";
|
||||
private AccountUtil accountUtil;
|
||||
private MediaWikiApi mwApi;
|
||||
|
||||
public LoginTask(LoginActivity loginActivity, String username, String password,
|
||||
String twoFactorCode, AccountUtil accountUtil,
|
||||
MediaWikiApi mwApi, SharedPreferences prefs) {
|
||||
this.loginActivity = loginActivity;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.twoFactorCode = twoFactorCode;
|
||||
this.accountUtil = accountUtil;
|
||||
this.mwApi = mwApi;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
loginActivity.progressDialog = new ProgressDialog(loginActivity);
|
||||
loginActivity.progressDialog.setIndeterminate(true);
|
||||
loginActivity.progressDialog.setTitle(loginActivity.getString(R.string.logging_in_title));
|
||||
loginActivity.progressDialog.setMessage(loginActivity.getString(R.string.logging_in_message));
|
||||
loginActivity.progressDialog.setCanceledOnTouchOutside(false);
|
||||
loginActivity.progressDialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
try {
|
||||
if (twoFactorCode.isEmpty()) {
|
||||
return mwApi.login(username, password);
|
||||
} else {
|
||||
return mwApi.login(username, password, twoFactorCode);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Do something better!
|
||||
return "NetworkFailure";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String result) {
|
||||
super.onPostExecute(result);
|
||||
Timber.d("Login done!");
|
||||
|
||||
if (result.equals("PASS")) {
|
||||
handlePassResult();
|
||||
} else {
|
||||
handleOtherResults(result);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePassResult() {
|
||||
loginActivity.showSuccessAndDismissDialog();
|
||||
|
||||
AccountAuthenticatorResponse response = null;
|
||||
|
||||
Bundle extras = loginActivity.getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
Timber.d("Bundle of extras: %s", extras);
|
||||
response = extras.getParcelable(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
|
||||
if (response != null) {
|
||||
Bundle authResult = new Bundle();
|
||||
authResult.putString(KEY_ACCOUNT_NAME, username);
|
||||
authResult.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
|
||||
response.onResult(authResult);
|
||||
}
|
||||
}
|
||||
|
||||
accountUtil.createAccount(response, username, password);
|
||||
loginActivity.startMainActivity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Match known failure message codes and provide messages.
|
||||
* @param result String
|
||||
*/
|
||||
private void handleOtherResults(String result) {
|
||||
if (result.equals("NetworkFailure")) {
|
||||
// Matches NetworkFailure which is created by the doInBackground method
|
||||
loginActivity.showMessageAndCancelDialog(R.string.login_failed_network);
|
||||
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
|
||||
// Matches nosuchuser, nosuchusershort, noname
|
||||
loginActivity.showMessageAndCancelDialog(R.string.login_failed_username);
|
||||
loginActivity.emptySensitiveEditFields();
|
||||
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
|
||||
// Matches wrongpassword, wrongpasswordempty
|
||||
loginActivity.showMessageAndCancelDialog(R.string.login_failed_password);
|
||||
loginActivity.emptySensitiveEditFields();
|
||||
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
|
||||
// Matches unknown throttle error codes
|
||||
loginActivity.showMessageAndCancelDialog(R.string.login_failed_throttled);
|
||||
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
|
||||
// Matches login-userblocked
|
||||
loginActivity.showMessageAndCancelDialog(R.string.login_failed_blocked);
|
||||
} else if (result.equals("2FA")) {
|
||||
loginActivity.askUserForTwoFactorAuth();
|
||||
} else {
|
||||
// Occurs with unhandled login failure codes
|
||||
Timber.d("Login failed with reason: %s", result);
|
||||
loginActivity.showMessageAndCancelDialog(R.string.login_failed_generic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,15 +2,13 @@ package fr.free.nrw.commons.auth;
|
|||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.AuthenticatorException;
|
||||
import android.accounts.OperationCanceledException;
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.IOException;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Observable;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||
|
||||
|
|
@ -21,11 +19,13 @@ public class SessionManager {
|
|||
private final Context context;
|
||||
private final MediaWikiApi mediaWikiApi;
|
||||
private Account currentAccount; // Unlike a savings account... ;-)
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
public SessionManager(Context context, MediaWikiApi mediaWikiApi) {
|
||||
public SessionManager(Context context, MediaWikiApi mediaWikiApi, SharedPreferences sharedPreferences) {
|
||||
this.context = context;
|
||||
this.mediaWikiApi = mediaWikiApi;
|
||||
this.currentAccount = null;
|
||||
this.sharedPreferences = sharedPreferences;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -51,14 +51,28 @@ public class SessionManager {
|
|||
}
|
||||
|
||||
accountManager.invalidateAuthToken(ACCOUNT_TYPE, mediaWikiApi.getAuthCookie());
|
||||
try {
|
||||
String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false);
|
||||
mediaWikiApi.setAuthCookie(authCookie);
|
||||
return true;
|
||||
} catch (OperationCanceledException | NullPointerException | IOException | AuthenticatorException e) {
|
||||
e.printStackTrace();
|
||||
String authCookie = getAuthCookie();
|
||||
|
||||
if (authCookie == null) {
|
||||
return false;
|
||||
}
|
||||
mediaWikiApi.setAuthCookie(authCookie);
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getAuthCookie() {
|
||||
boolean isLoggedIn = sharedPreferences.getBoolean("isUserLoggedIn", false);
|
||||
|
||||
if (!isLoggedIn) {
|
||||
Timber.e("User is not logged in");
|
||||
return null;
|
||||
} else {
|
||||
String authCookie = sharedPreferences.getString("getAuthCookie", null);
|
||||
if (authCookie == null) {
|
||||
Timber.e("Auth cookie is null even after login");
|
||||
}
|
||||
return authCookie;
|
||||
}
|
||||
}
|
||||
|
||||
public Completable clearAllAccounts() {
|
||||
|
|
|
|||
|
|
@ -5,53 +5,37 @@ import android.accounts.Account;
|
|||
import android.accounts.AccountAuthenticatorResponse;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.NetworkErrorException;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
|
||||
import static android.accounts.AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION;
|
||||
import static android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE;
|
||||
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
|
||||
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
|
||||
import static android.accounts.AccountManager.KEY_AUTHTOKEN;
|
||||
import static android.accounts.AccountManager.KEY_BOOLEAN_RESULT;
|
||||
import static android.accounts.AccountManager.KEY_ERROR_CODE;
|
||||
import static android.accounts.AccountManager.KEY_ERROR_MESSAGE;
|
||||
import static android.accounts.AccountManager.KEY_INTENT;
|
||||
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
|
||||
import static fr.free.nrw.commons.auth.LoginActivity.PARAM_USERNAME;
|
||||
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
|
||||
|
||||
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
||||
private static final String[] SYNC_AUTHORITIES = {ContributionsContentProvider.CONTRIBUTION_AUTHORITY, ModificationsContentProvider.MODIFICATIONS_AUTHORITY};
|
||||
|
||||
@NonNull
|
||||
private final Context context;
|
||||
private MediaWikiApi mediaWikiApi;
|
||||
|
||||
WikiAccountAuthenticator(Context context, MediaWikiApi mwApi) {
|
||||
public WikiAccountAuthenticator(@NonNull Context context) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
this.mediaWikiApi = mwApi;
|
||||
}
|
||||
|
||||
private Bundle unsupportedOperation() {
|
||||
@Override
|
||||
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(KEY_ERROR_CODE, ERROR_CODE_UNSUPPORTED_OPERATION);
|
||||
|
||||
// HACK: the docs indicate that this is a required key bit it's not displayed to the user.
|
||||
bundle.putString(KEY_ERROR_MESSAGE, "");
|
||||
|
||||
bundle.putString("test", "editProperties");
|
||||
return bundle;
|
||||
}
|
||||
|
||||
private boolean supportedAccountType(@Nullable String type) {
|
||||
return ACCOUNT_TYPE.equals(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle addAccount(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull String accountType, @Nullable String authTokenType,
|
||||
|
|
@ -59,86 +43,48 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
|||
throws NetworkErrorException {
|
||||
|
||||
if (!supportedAccountType(accountType)) {
|
||||
return unsupportedOperation();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("test", "addAccount");
|
||||
return bundle;
|
||||
}
|
||||
|
||||
return addAccount(response);
|
||||
}
|
||||
|
||||
private Bundle addAccount(AccountAuthenticatorResponse response) {
|
||||
Intent Intent = new Intent(context, LoginActivity.class);
|
||||
Intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(KEY_INTENT, Intent);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull Account account, @Nullable Bundle options)
|
||||
throws NetworkErrorException {
|
||||
return unsupportedOperation();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("test", "confirmCredentials");
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
||||
return unsupportedOperation();
|
||||
}
|
||||
|
||||
private String getAuthCookie(String username, String password) throws IOException {
|
||||
//TODO add 2fa support here
|
||||
String result = mediaWikiApi.login(username, password);
|
||||
if (result.equals("PASS")) {
|
||||
return mediaWikiApi.getAuthCookie();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
|
||||
String authTokenType, Bundle options) throws NetworkErrorException {
|
||||
// Extract the username and password from the Account Manager, and ask
|
||||
// the server for an appropriate AuthToken.
|
||||
final AccountManager am = AccountManager.get(context);
|
||||
final String password = am.getPassword(account);
|
||||
if (password != null) {
|
||||
String authCookie;
|
||||
try {
|
||||
authCookie = getAuthCookie(account.name, password);
|
||||
} catch (IOException e) {
|
||||
// Network error!
|
||||
e.printStackTrace();
|
||||
throw new NetworkErrorException(e);
|
||||
}
|
||||
if (authCookie != null) {
|
||||
final Bundle result = new Bundle();
|
||||
result.putString(KEY_ACCOUNT_NAME, account.name);
|
||||
result.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
|
||||
result.putString(KEY_AUTHTOKEN, authCookie);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, then we couldn't access the user's password - so we
|
||||
// need to re-prompt them for their credentials. We do that by creating
|
||||
// an intent to display our AuthenticatorActivity panel.
|
||||
final Intent intent = new Intent(context, LoginActivity.class);
|
||||
intent.putExtra(PARAM_USERNAME, account.name);
|
||||
intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
||||
final Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(KEY_INTENT, intent);
|
||||
public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull Account account, @NonNull String authTokenType,
|
||||
@Nullable Bundle options)
|
||||
throws NetworkErrorException {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("test", "getAuthToken");
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getAuthTokenLabel(@NonNull String authTokenType) {
|
||||
//Note: the wikipedia app actually returns a string here....
|
||||
//return supportedAccountType(authTokenType) ? context.getString(R.string.wikimedia) : null;
|
||||
return null;
|
||||
return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull Account account, @Nullable String authTokenType,
|
||||
@Nullable Bundle options)
|
||||
throws NetworkErrorException {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("test", "updateCredentials");
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
@ -147,16 +93,50 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
|||
@NonNull Account account, @NonNull String[] features)
|
||||
throws NetworkErrorException {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(KEY_BOOLEAN_RESULT, false);
|
||||
bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull Account account, @Nullable String authTokenType,
|
||||
@Nullable Bundle options) throws NetworkErrorException {
|
||||
return unsupportedOperation();
|
||||
private boolean supportedAccountType(@Nullable String type) {
|
||||
return ACCOUNT_TYPE.equals(type);
|
||||
}
|
||||
|
||||
private Bundle addAccount(AccountAuthenticatorResponse response) {
|
||||
Intent intent = new Intent(context, LoginActivity.class);
|
||||
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
private Bundle unsupportedOperation() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION);
|
||||
|
||||
// HACK: the docs indicate that this is a required key bit it's not displayed to the user.
|
||||
bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "");
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response,
|
||||
Account account) throws NetworkErrorException {
|
||||
Bundle result = super.getAccountRemovalAllowed(response, account);
|
||||
|
||||
if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
|
||||
&& !result.containsKey(AccountManager.KEY_INTENT)) {
|
||||
boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
|
||||
|
||||
if (allowed) {
|
||||
for (String auth : SYNC_AUTHORITIES) {
|
||||
ContentResolver.cancelSync(account, auth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,26 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerService;
|
||||
|
||||
import dagger.android.DaggerService;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
public class WikiAccountAuthenticatorService extends CommonsDaggerService {
|
||||
|
||||
import static android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT;
|
||||
|
||||
public class WikiAccountAuthenticatorService extends DaggerService {
|
||||
|
||||
@Inject MediaWikiApi mwApi;
|
||||
private WikiAccountAuthenticator wikiAccountAuthenticator = null;
|
||||
@Nullable
|
||||
private AbstractAccountAuthenticator authenticator;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
if (!intent.getAction().equals(ACTION_AUTHENTICATOR_INTENT)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (wikiAccountAuthenticator == null) {
|
||||
wikiAccountAuthenticator = new WikiAccountAuthenticator(this, mwApi);
|
||||
}
|
||||
return wikiAccountAuthenticator.getIBinder();
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
authenticator = new WikiAccountAuthenticator(this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return authenticator == null ? null : authenticator.getIBinder();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
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.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
|
@ -34,11 +38,11 @@ import javax.inject.Named;
|
|||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import dagger.android.support.DaggerFragment;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.data.Category;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.upload.MwVolleyApi;
|
||||
import fr.free.nrw.commons.upload.SingleUploadFragment;
|
||||
import fr.free.nrw.commons.utils.StringSortingUtils;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
|
|
@ -47,12 +51,11 @@ import timber.log.Timber;
|
|||
|
||||
import static android.view.KeyEvent.ACTION_UP;
|
||||
import static android.view.KeyEvent.KEYCODE_BACK;
|
||||
import static fr.free.nrw.commons.category.CategoryContentProvider.AUTHORITY;
|
||||
|
||||
/**
|
||||
* Displays the category suggestion and selection screen. Category search is initiated here.
|
||||
*/
|
||||
public class CategorizationFragment extends DaggerFragment {
|
||||
public class CategorizationFragment extends CommonsDaggerSupportFragment {
|
||||
|
||||
public static final int SEARCH_CATS_LIMIT = 25;
|
||||
|
||||
|
|
@ -69,17 +72,18 @@ public class CategorizationFragment extends DaggerFragment {
|
|||
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||
@Inject CategoryDao categoryDao;
|
||||
|
||||
private RVRendererAdapter<CategoryItem> categoriesAdapter;
|
||||
private OnCategoriesSaveHandler onCategoriesSaveHandler;
|
||||
private HashMap<String, ArrayList<String>> categoriesCache;
|
||||
private List<CategoryItem> selectedCategories = new ArrayList<>();
|
||||
private ContentProviderClient databaseClient;
|
||||
private TitleTextWatcher textWatcher = new TitleTextWatcher();
|
||||
|
||||
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
|
||||
if (item.isSelected()) {
|
||||
selectedCategories.add(item);
|
||||
updateCategoryCount(item, databaseClient);
|
||||
updateCategoryCount(item);
|
||||
} else {
|
||||
selectedCategories.remove(item);
|
||||
}
|
||||
|
|
@ -105,6 +109,15 @@ public class CategorizationFragment extends DaggerFragment {
|
|||
categoriesAdapter = adapterFactory.create(items);
|
||||
categoriesList.setAdapter(categoriesAdapter);
|
||||
|
||||
|
||||
categoriesFilter.addTextChangedListener(textWatcher);
|
||||
|
||||
categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
hideKeyboard(v);
|
||||
}
|
||||
});
|
||||
|
||||
RxTextView.textChanges(categoriesFilter)
|
||||
.takeUntil(RxView.detaches(categoriesFilter))
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
|
|
@ -113,6 +126,18 @@ public class CategorizationFragment extends DaggerFragment {
|
|||
return rootView;
|
||||
}
|
||||
|
||||
public void hideKeyboard(View view) {
|
||||
InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
categoriesFilter.removeTextChangedListener(textWatcher);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
menu.clear();
|
||||
|
|
@ -137,12 +162,6 @@ public class CategorizationFragment extends DaggerFragment {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
databaseClient.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
|
@ -178,7 +197,6 @@ public class CategorizationFragment extends DaggerFragment {
|
|||
setHasOptionsMenu(true);
|
||||
onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
|
||||
getActivity().setTitle(R.string.categories_activity_title);
|
||||
databaseClient = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY);
|
||||
}
|
||||
|
||||
private void updateCategoryList(String filter) {
|
||||
|
|
@ -261,7 +279,7 @@ public class CategorizationFragment extends DaggerFragment {
|
|||
}
|
||||
|
||||
private Observable<CategoryItem> recentCategories() {
|
||||
return Observable.fromIterable(Category.recentCategories(databaseClient, SEARCH_CATS_LIMIT))
|
||||
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
|
||||
.map(s -> new CategoryItem(s, false));
|
||||
}
|
||||
|
||||
|
|
@ -307,28 +325,22 @@ public class CategorizationFragment extends DaggerFragment {
|
|||
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
|
||||
//And that item does not equal the current year or previous year
|
||||
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
||||
//Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
|
||||
return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
|
||||
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)"));
|
||||
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")
|
||||
|| (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
|
||||
}
|
||||
|
||||
private void updateCategoryCount(CategoryItem item, ContentProviderClient client) {
|
||||
Category cat = lookupCategory(item.getName());
|
||||
cat.incTimesUsed();
|
||||
cat.save(client);
|
||||
}
|
||||
private void updateCategoryCount(CategoryItem item) {
|
||||
Category category = categoryDao.find(item.getName());
|
||||
|
||||
private Category lookupCategory(String name) {
|
||||
Category cat = Category.find(databaseClient, name);
|
||||
|
||||
if (cat == null) {
|
||||
// Newly used category...
|
||||
cat = new Category();
|
||||
cat.setName(name);
|
||||
cat.setLastUsed(new Date());
|
||||
cat.setTimesUsed(0);
|
||||
// Newly used category...
|
||||
if (category == null) {
|
||||
category = new Category(null, item.getName(), new Date(), 0);
|
||||
}
|
||||
|
||||
return cat;
|
||||
category.incTimesUsed();
|
||||
categoryDao.save(category);
|
||||
}
|
||||
|
||||
public int getCurrentSelectedCount() {
|
||||
|
|
@ -367,4 +379,21 @@ public class CategorizationFragment extends DaggerFragment {
|
|||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private class TitleTextWatcher implements TextWatcher {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
if (getActivity() != null) {
|
||||
getActivity().invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
96
app/src/main/java/fr/free/nrw/commons/category/Category.java
Normal file
96
app/src/main/java/fr/free/nrw/commons/category/Category.java
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Represents a category
|
||||
*/
|
||||
public class Category {
|
||||
private Uri contentUri;
|
||||
private String name;
|
||||
private Date lastUsed;
|
||||
private int timesUsed;
|
||||
|
||||
public Category() {
|
||||
}
|
||||
|
||||
public Category(Uri contentUri, String name, Date lastUsed, int timesUsed) {
|
||||
this.contentUri = contentUri;
|
||||
this.name = name;
|
||||
this.lastUsed = lastUsed;
|
||||
this.timesUsed = timesUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets name
|
||||
*
|
||||
* @return name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies name
|
||||
*
|
||||
* @param name Category name
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets last used date
|
||||
*
|
||||
* @return Last used date
|
||||
*/
|
||||
public Date getLastUsed() {
|
||||
// warning: Date objects are mutable.
|
||||
return (Date)lastUsed.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new last used date
|
||||
*/
|
||||
private void touch() {
|
||||
lastUsed = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets no. of times the category is used
|
||||
*
|
||||
* @return no. of times used
|
||||
*/
|
||||
public int getTimesUsed() {
|
||||
return timesUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments timesUsed by 1 and sets last used date as now.
|
||||
*/
|
||||
public void incTimesUsed() {
|
||||
timesUsed++;
|
||||
touch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content URI for this category
|
||||
*
|
||||
* @return content URI
|
||||
*/
|
||||
public Uri getContentUri() {
|
||||
return contentUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the content URI - marking this category as already saved in the database
|
||||
*
|
||||
* @param contentUri the content URI
|
||||
*/
|
||||
public void setContentUri(Uri contentUri) {
|
||||
this.contentUri = contentUri;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
|
|
@ -12,16 +11,16 @@ import android.text.TextUtils;
|
|||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.android.AndroidInjection;
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.content.UriMatcher.NO_MATCH;
|
||||
import static fr.free.nrw.commons.data.Category.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.data.Category.Table.COLUMN_ID;
|
||||
import static fr.free.nrw.commons.data.Category.Table.TABLE_NAME;
|
||||
import static fr.free.nrw.commons.category.CategoryDao.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.category.CategoryDao.Table.COLUMN_ID;
|
||||
import static fr.free.nrw.commons.category.CategoryDao.Table.TABLE_NAME;
|
||||
|
||||
public class CategoryContentProvider extends ContentProvider {
|
||||
public class CategoryContentProvider extends CommonsDaggerContentProvider {
|
||||
|
||||
public static final String AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
|
||||
// For URI matcher
|
||||
|
|
@ -44,12 +43,6 @@ public class CategoryContentProvider extends ContentProvider {
|
|||
|
||||
@Inject DBOpenHelper dbOpenHelper;
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
AndroidInjection.inject(this);
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
|
||||
|
|
|
|||
184
app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java
Normal file
184
app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.os.RemoteException;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
|
||||
public class CategoryDao {
|
||||
|
||||
private final Provider<ContentProviderClient> clientProvider;
|
||||
|
||||
@Inject
|
||||
public CategoryDao(@Named("category") Provider<ContentProviderClient> clientProvider) {
|
||||
this.clientProvider = clientProvider;
|
||||
}
|
||||
|
||||
public void save(Category category) {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
if (category.getContentUri() == null) {
|
||||
category.setContentUri(db.insert(CategoryContentProvider.BASE_URI, toContentValues(category)));
|
||||
} else {
|
||||
db.update(category.getContentUri(), toContentValues(category), null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find persisted category in database, based on its name.
|
||||
*
|
||||
* @param name Category's name
|
||||
* @return category from database, or null if not found
|
||||
*/
|
||||
@Nullable
|
||||
Category find(String name) {
|
||||
Cursor cursor = null;
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
cursor = db.query(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
Table.ALL_FIELDS,
|
||||
Table.COLUMN_NAME + "=?",
|
||||
new String[]{name},
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return fromCursor(cursor);
|
||||
}
|
||||
} 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 null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve recently-used categories, ordered by descending date.
|
||||
*
|
||||
* @return a list containing recent categories
|
||||
*/
|
||||
@NonNull
|
||||
List<String> recentCategories(int limit) {
|
||||
List<String> items = new ArrayList<>();
|
||||
Cursor cursor = null;
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
cursor = db.query(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
Table.ALL_FIELDS,
|
||||
null,
|
||||
new String[]{},
|
||||
Table.COLUMN_LAST_USED + " DESC");
|
||||
// fixme add a limit on the original query instead of falling out of the loop?
|
||||
while (cursor != null && cursor.moveToNext()
|
||||
&& cursor.getPosition() < limit) {
|
||||
items.add(fromCursor(cursor).getName());
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
db.release();
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
Category fromCursor(Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
return new Category(
|
||||
CategoryContentProvider.uriForId(cursor.getInt(0)),
|
||||
cursor.getString(1),
|
||||
new Date(cursor.getLong(2)),
|
||||
cursor.getInt(3)
|
||||
);
|
||||
}
|
||||
|
||||
private ContentValues toContentValues(Category category) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(CategoryDao.Table.COLUMN_NAME, category.getName());
|
||||
cv.put(CategoryDao.Table.COLUMN_LAST_USED, category.getLastUsed().getTime());
|
||||
cv.put(CategoryDao.Table.COLUMN_TIMES_USED, category.getTimesUsed());
|
||||
return cv;
|
||||
}
|
||||
|
||||
public static class Table {
|
||||
public static final String TABLE_NAME = "categories";
|
||||
|
||||
public static final String COLUMN_ID = "_id";
|
||||
static final String COLUMN_NAME = "name";
|
||||
static final String COLUMN_LAST_USED = "last_used";
|
||||
static final String COLUMN_TIMES_USED = "times_used";
|
||||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
public static final String[] ALL_FIELDS = {
|
||||
COLUMN_ID,
|
||||
COLUMN_NAME,
|
||||
COLUMN_LAST_USED,
|
||||
COLUMN_TIMES_USED
|
||||
};
|
||||
|
||||
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||
|
||||
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
|
||||
+ COLUMN_NAME + " STRING,"
|
||||
+ COLUMN_LAST_USED + " INTEGER,"
|
||||
+ COLUMN_TIMES_USED + " INTEGER"
|
||||
+ ");";
|
||||
|
||||
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(SQLiteDatabase db, int from, int to) {
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
if (from < 4) {
|
||||
// doesn't exist yet
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 4) {
|
||||
// table added in version 5
|
||||
onCreate(db);
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 5) {
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,8 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.RemoteException;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
|
@ -43,7 +37,6 @@ public class Contribution extends Media {
|
|||
public static final String SOURCE_GALLERY = "gallery";
|
||||
public static final String SOURCE_EXTERNAL = "external";
|
||||
|
||||
private ContentProviderClient client;
|
||||
private Uri contentUri;
|
||||
private String source;
|
||||
private String editSummary;
|
||||
|
|
@ -51,24 +44,42 @@ public class Contribution extends Media {
|
|||
private int state;
|
||||
private long transferred;
|
||||
private String decimalCoords;
|
||||
|
||||
private boolean isMultiple;
|
||||
|
||||
public boolean getMultiple() {
|
||||
return isMultiple;
|
||||
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp,
|
||||
int state, long dataLength, Date dateUploaded, long transferred,
|
||||
String source, String description, String creator, boolean isMultiple,
|
||||
int width, int height, String license) {
|
||||
super(localUri, imageUrl, filename, description, dataLength, timestamp, dateUploaded, creator);
|
||||
this.contentUri = contentUri;
|
||||
this.state = state;
|
||||
this.timestamp = timestamp;
|
||||
this.transferred = transferred;
|
||||
this.source = source;
|
||||
this.isMultiple = isMultiple;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.license = license;
|
||||
}
|
||||
|
||||
public void setMultiple(boolean multiple) {
|
||||
isMultiple = multiple;
|
||||
}
|
||||
|
||||
public Contribution(Uri localUri, String remoteUri, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) {
|
||||
super(localUri, remoteUri, filename, description, dataLength, dateCreated, dateUploaded, creator);
|
||||
public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength,
|
||||
Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) {
|
||||
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
|
||||
this.decimalCoords = decimalCoords;
|
||||
this.editSummary = editSummary;
|
||||
timestamp = new Date(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public Contribution(Parcel in) {
|
||||
super(in);
|
||||
contentUri = in.readParcelable(Uri.class.getClassLoader());
|
||||
source = in.readString();
|
||||
timestamp = (Date) in.readSerializable();
|
||||
state = in.readInt();
|
||||
transferred = in.readLong();
|
||||
isMultiple = in.readInt() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel parcel, int flags) {
|
||||
super.writeToParcel(parcel, flags);
|
||||
|
|
@ -80,14 +91,12 @@ public class Contribution extends Media {
|
|||
parcel.writeInt(isMultiple ? 1 : 0);
|
||||
}
|
||||
|
||||
public Contribution(Parcel in) {
|
||||
super(in);
|
||||
contentUri = in.readParcelable(Uri.class.getClassLoader());
|
||||
source = in.readString();
|
||||
timestamp = (Date) in.readSerializable();
|
||||
state = in.readInt();
|
||||
transferred = in.readLong();
|
||||
isMultiple = in.readInt() == 1;
|
||||
public boolean getMultiple() {
|
||||
return isMultiple;
|
||||
}
|
||||
|
||||
public void setMultiple(boolean multiple) {
|
||||
isMultiple = multiple;
|
||||
}
|
||||
|
||||
public long getTransferred() {
|
||||
|
|
@ -106,10 +115,18 @@ public class Contribution extends Media {
|
|||
return contentUri;
|
||||
}
|
||||
|
||||
public void setContentUri(Uri contentUri) {
|
||||
this.contentUri = contentUri;
|
||||
}
|
||||
|
||||
public Date getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(Date timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public int getState() {
|
||||
return state;
|
||||
}
|
||||
|
|
@ -155,62 +172,6 @@ public class Contribution extends Media {
|
|||
return buffer.toString();
|
||||
}
|
||||
|
||||
public void setContentProviderClient(ContentProviderClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public void save() {
|
||||
try {
|
||||
if (contentUri == null) {
|
||||
contentUri = client.insert(ContributionsContentProvider.BASE_URI, this.toContentValues());
|
||||
} else {
|
||||
client.update(contentUri, toContentValues(), null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
try {
|
||||
if (contentUri == null) {
|
||||
// noooo
|
||||
throw new RuntimeException("tried to delete item with no content URI");
|
||||
} else {
|
||||
client.delete(contentUri, null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ContentValues toContentValues() {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(Table.COLUMN_FILENAME, getFilename());
|
||||
if (getLocalUri() != null) {
|
||||
cv.put(Table.COLUMN_LOCAL_URI, getLocalUri().toString());
|
||||
}
|
||||
if (getImageUrl() != null) {
|
||||
cv.put(Table.COLUMN_IMAGE_URL, getImageUrl());
|
||||
}
|
||||
if (getDateUploaded() != null) {
|
||||
cv.put(Table.COLUMN_UPLOADED, getDateUploaded().getTime());
|
||||
}
|
||||
cv.put(Table.COLUMN_LENGTH, getDataLength());
|
||||
cv.put(Table.COLUMN_TIMESTAMP, getTimestamp().getTime());
|
||||
cv.put(Table.COLUMN_STATE, getState());
|
||||
cv.put(Table.COLUMN_TRANSFERRED, transferred);
|
||||
cv.put(Table.COLUMN_SOURCE, source);
|
||||
cv.put(Table.COLUMN_DESCRIPTION, description);
|
||||
cv.put(Table.COLUMN_CREATOR, creator);
|
||||
cv.put(Table.COLUMN_MULTIPLE, isMultiple ? 1 : 0);
|
||||
cv.put(Table.COLUMN_WIDTH, width);
|
||||
cv.put(Table.COLUMN_HEIGHT, height);
|
||||
cv.put(Table.COLUMN_LICENSE, license);
|
||||
return cv;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFilename(String filename) {
|
||||
this.filename = filename;
|
||||
|
|
@ -224,33 +185,6 @@ public class Contribution extends Media {
|
|||
timestamp = new Date(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public static Contribution fromCursor(Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
Contribution c = new Contribution();
|
||||
|
||||
//Check that cursor has a value to avoid CursorIndexOutOfBoundsException
|
||||
if (cursor.getCount() > 0) {
|
||||
c.contentUri = ContributionsContentProvider.uriForId(cursor.getInt(0));
|
||||
c.filename = cursor.getString(1);
|
||||
c.localUri = TextUtils.isEmpty(cursor.getString(2)) ? null : Uri.parse(cursor.getString(2));
|
||||
c.imageUrl = cursor.getString(3);
|
||||
c.timestamp = cursor.getLong(4) == 0 ? null : new Date(cursor.getLong(4));
|
||||
c.state = cursor.getInt(5);
|
||||
c.dataLength = cursor.getLong(6);
|
||||
c.dateUploaded = cursor.getLong(7) == 0 ? null : new Date(cursor.getLong(7));
|
||||
c.transferred = cursor.getLong(8);
|
||||
c.source = cursor.getString(9);
|
||||
c.description = cursor.getString(10);
|
||||
c.creator = cursor.getString(11);
|
||||
c.isMultiple = cursor.getInt(12) == 1;
|
||||
c.width = cursor.getInt(13);
|
||||
c.height = cursor.getInt(14);
|
||||
c.license = cursor.getString(15);
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return source;
|
||||
}
|
||||
|
|
@ -263,119 +197,8 @@ public class Contribution extends Media {
|
|||
this.localUri = localUri;
|
||||
}
|
||||
|
||||
public static class Table {
|
||||
public static final String TABLE_NAME = "contributions";
|
||||
|
||||
public static final String COLUMN_ID = "_id";
|
||||
public static final String COLUMN_FILENAME = "filename";
|
||||
public static final String COLUMN_LOCAL_URI = "local_uri";
|
||||
public static final String COLUMN_IMAGE_URL = "image_url";
|
||||
public static final String COLUMN_TIMESTAMP = "timestamp";
|
||||
public static final String COLUMN_STATE = "state";
|
||||
public static final String COLUMN_LENGTH = "length";
|
||||
public static final String COLUMN_UPLOADED = "uploaded";
|
||||
public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes
|
||||
public static final String COLUMN_SOURCE = "source";
|
||||
public static final String COLUMN_DESCRIPTION = "description";
|
||||
public static final String COLUMN_CREATOR = "creator"; // Initial uploader
|
||||
public static final String COLUMN_MULTIPLE = "multiple";
|
||||
public static final String COLUMN_WIDTH = "width";
|
||||
public static final String COLUMN_HEIGHT = "height";
|
||||
public static final String COLUMN_LICENSE = "license";
|
||||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
public static final String[] ALL_FIELDS = {
|
||||
COLUMN_ID,
|
||||
COLUMN_FILENAME,
|
||||
COLUMN_LOCAL_URI,
|
||||
COLUMN_IMAGE_URL,
|
||||
COLUMN_TIMESTAMP,
|
||||
COLUMN_STATE,
|
||||
COLUMN_LENGTH,
|
||||
COLUMN_UPLOADED,
|
||||
COLUMN_TRANSFERRED,
|
||||
COLUMN_SOURCE,
|
||||
COLUMN_DESCRIPTION,
|
||||
COLUMN_CREATOR,
|
||||
COLUMN_MULTIPLE,
|
||||
COLUMN_WIDTH,
|
||||
COLUMN_HEIGHT,
|
||||
COLUMN_LICENSE
|
||||
};
|
||||
|
||||
|
||||
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ "_id INTEGER PRIMARY KEY,"
|
||||
+ "filename STRING,"
|
||||
+ "local_uri STRING,"
|
||||
+ "image_url STRING,"
|
||||
+ "uploaded INTEGER,"
|
||||
+ "timestamp INTEGER,"
|
||||
+ "state INTEGER,"
|
||||
+ "length INTEGER,"
|
||||
+ "transferred INTEGER,"
|
||||
+ "source STRING,"
|
||||
+ "description STRING,"
|
||||
+ "creator STRING,"
|
||||
+ "multiple INTEGER,"
|
||||
+ "width INTEGER,"
|
||||
+ "height INTEGER,"
|
||||
+ "LICENSE STRING"
|
||||
+ ");";
|
||||
|
||||
|
||||
public static void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
||||
}
|
||||
|
||||
public static void onDelete(SQLiteDatabase db) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
if (from == 1) {
|
||||
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;");
|
||||
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;");
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 2) {
|
||||
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;");
|
||||
db.execSQL("UPDATE " + TABLE_NAME + " SET multiple = 0");
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 3) {
|
||||
// Do nothing
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 4) {
|
||||
// Do nothing -- added Category
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 5) {
|
||||
// Added width and height fields
|
||||
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;");
|
||||
db.execSQL("UPDATE " + TABLE_NAME + " SET width = 0");
|
||||
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;");
|
||||
db.execSQL("UPDATE " + TABLE_NAME + " SET height = 0");
|
||||
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;");
|
||||
db.execSQL("UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA_3 + "';");
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
}
|
||||
public void setDecimalCoords(String decimalCoords) {
|
||||
this.decimalCoords = decimalCoords;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
|
@ -396,6 +219,7 @@ public class Contribution extends Media {
|
|||
case Prefs.Licenses.CC_BY_SA:
|
||||
return "{{self|cc-by-sa-3.0}}";
|
||||
}
|
||||
|
||||
throw new RuntimeException("Unrecognized license value: " + license);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,11 +80,20 @@ public class ContributionController {
|
|||
//FIXME: Starts gallery (opens Google Photos)
|
||||
Intent pickImageIntent = new Intent(ACTION_GET_CONTENT);
|
||||
pickImageIntent.setType("image/*");
|
||||
<<<<<<< HEAD
|
||||
Timber.d("startGalleryPick() called with pickImageIntent");
|
||||
// See https://stackoverflow.com/questions/22366596/android-illegalstateexception-fragment-not-attached-to-activity-webview
|
||||
if (!fragment.isAdded()) {
|
||||
return;
|
||||
}
|
||||
=======
|
||||
// See https://stackoverflow.com/questions/22366596/android-illegalstateexception-fragment-not-attached-to-activity-webview
|
||||
if (!fragment.isAdded()) {
|
||||
Timber.d("Fragment is not added, startActivityForResult cannot be called");
|
||||
return;
|
||||
}
|
||||
Timber.d("startGalleryPick() called with pickImageIntent");
|
||||
>>>>>>> directNearbyUploadsNewLocal
|
||||
fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY);
|
||||
}
|
||||
|
||||
|
|
@ -105,12 +114,20 @@ public class ContributionController {
|
|||
}
|
||||
break;
|
||||
case SELECT_FROM_CAMERA:
|
||||
shareIntent.setType("image/jpeg"); //FIXME: Find out appropriate mime type
|
||||
//FIXME: Find out appropriate mime type
|
||||
// AFAIK this is the right type for a JPEG image
|
||||
// https://developer.android.com/training/sharing/send.html#send-binary-content
|
||||
shareIntent.setType("image/jpeg");
|
||||
shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri);
|
||||
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA);
|
||||
if (isDirectUpload) {
|
||||
shareIntent.putExtra("isDirectUpload", true);
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
break;
|
||||
default:
|
||||
>>>>>>> directNearbyUploadsNewLocal
|
||||
break;
|
||||
}
|
||||
Timber.i("Image selected");
|
||||
|
|
@ -132,5 +149,4 @@ public class ContributionController {
|
|||
lastGeneratedCaptureUri = savedInstanceState.getParcelable("lastGeneratedCaptureURI");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,281 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId;
|
||||
|
||||
public class ContributionDao {
|
||||
/*
|
||||
This sorts in the following order:
|
||||
Currently Uploading
|
||||
Failed (Sorted in ascending order of time added - FIFO)
|
||||
Queued to Upload (Sorted in ascending order of time added - FIFO)
|
||||
Completed (Sorted in descending order of time added)
|
||||
|
||||
This is why Contribution.STATE_COMPLETED is -1.
|
||||
*/
|
||||
static final String CONTRIBUTION_SORT = Table.COLUMN_STATE + " DESC, "
|
||||
+ Table.COLUMN_UPLOADED + " DESC , ("
|
||||
+ Table.COLUMN_TIMESTAMP + " * "
|
||||
+ Table.COLUMN_STATE + ")";
|
||||
|
||||
private final Provider<ContentProviderClient> clientProvider;
|
||||
|
||||
@Inject
|
||||
public ContributionDao(@Named("contribution") Provider<ContentProviderClient> clientProvider) {
|
||||
this.clientProvider = clientProvider;
|
||||
}
|
||||
|
||||
Cursor loadAllContributions() {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
return db.query(BASE_URI, ALL_FIELDS, "", null, CONTRIBUTION_SORT);
|
||||
} catch (RemoteException e) {
|
||||
return null;
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
|
||||
public void save(Contribution contribution) {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
if (contribution.getContentUri() == null) {
|
||||
contribution.setContentUri(db.insert(BASE_URI, toContentValues(contribution)));
|
||||
} else {
|
||||
db.update(contribution.getContentUri(), toContentValues(contribution), null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(Contribution contribution) {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
if (contribution.getContentUri() == null) {
|
||||
// noooo
|
||||
throw new RuntimeException("tried to delete item with no content URI");
|
||||
} else {
|
||||
db.delete(contribution.getContentUri(), null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
|
||||
ContentValues toContentValues(Contribution contribution) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(Table.COLUMN_FILENAME, contribution.getFilename());
|
||||
if (contribution.getLocalUri() != null) {
|
||||
cv.put(Table.COLUMN_LOCAL_URI, contribution.getLocalUri().toString());
|
||||
}
|
||||
if (contribution.getImageUrl() != null) {
|
||||
cv.put(Table.COLUMN_IMAGE_URL, contribution.getImageUrl());
|
||||
}
|
||||
if (contribution.getDateUploaded() != null) {
|
||||
cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime());
|
||||
}
|
||||
cv.put(Table.COLUMN_LENGTH, contribution.getDataLength());
|
||||
cv.put(Table.COLUMN_TIMESTAMP, contribution.getTimestamp().getTime());
|
||||
cv.put(Table.COLUMN_STATE, contribution.getState());
|
||||
cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred());
|
||||
cv.put(Table.COLUMN_SOURCE, contribution.getSource());
|
||||
cv.put(Table.COLUMN_DESCRIPTION, contribution.getDescription());
|
||||
cv.put(Table.COLUMN_CREATOR, contribution.getCreator());
|
||||
cv.put(Table.COLUMN_MULTIPLE, contribution.getMultiple() ? 1 : 0);
|
||||
cv.put(Table.COLUMN_WIDTH, contribution.getWidth());
|
||||
cv.put(Table.COLUMN_HEIGHT, contribution.getHeight());
|
||||
cv.put(Table.COLUMN_LICENSE, contribution.getLicense());
|
||||
return cv;
|
||||
}
|
||||
|
||||
public Contribution fromCursor(Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
//Check that cursor has a value to avoid CursorIndexOutOfBoundsException
|
||||
if (cursor.getCount() > 0) {
|
||||
return new Contribution(
|
||||
uriForId(cursor.getInt(0)),
|
||||
cursor.getString(1),
|
||||
parseUri(cursor.getString(2)),
|
||||
cursor.getString(3),
|
||||
parseTimestamp(cursor.getLong(4)),
|
||||
cursor.getInt(5),
|
||||
cursor.getLong(6),
|
||||
parseTimestamp(cursor.getLong(7)),
|
||||
cursor.getLong(8),
|
||||
cursor.getString(9),
|
||||
cursor.getString(10),
|
||||
cursor.getString(11),
|
||||
cursor.getInt(12) == 1,
|
||||
cursor.getInt(13),
|
||||
cursor.getInt(14),
|
||||
cursor.getString(15));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Date parseTimestamp(long timestamp) {
|
||||
return timestamp == 0 ? null : new Date(timestamp);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Uri parseUri(String uriString) {
|
||||
return TextUtils.isEmpty(uriString) ? null : Uri.parse(uriString);
|
||||
}
|
||||
|
||||
public static class Table {
|
||||
public static final String TABLE_NAME = "contributions";
|
||||
|
||||
public static final String COLUMN_ID = "_id";
|
||||
public static final String COLUMN_FILENAME = "filename";
|
||||
public static final String COLUMN_LOCAL_URI = "local_uri";
|
||||
public static final String COLUMN_IMAGE_URL = "image_url";
|
||||
public static final String COLUMN_TIMESTAMP = "timestamp";
|
||||
public static final String COLUMN_STATE = "state";
|
||||
public static final String COLUMN_LENGTH = "length";
|
||||
public static final String COLUMN_UPLOADED = "uploaded";
|
||||
public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes
|
||||
public static final String COLUMN_SOURCE = "source";
|
||||
public static final String COLUMN_DESCRIPTION = "description";
|
||||
public static final String COLUMN_CREATOR = "creator"; // Initial uploader
|
||||
public static final String COLUMN_MULTIPLE = "multiple";
|
||||
public static final String COLUMN_WIDTH = "width";
|
||||
public static final String COLUMN_HEIGHT = "height";
|
||||
public static final String COLUMN_LICENSE = "license";
|
||||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
public static final String[] ALL_FIELDS = {
|
||||
COLUMN_ID,
|
||||
COLUMN_FILENAME,
|
||||
COLUMN_LOCAL_URI,
|
||||
COLUMN_IMAGE_URL,
|
||||
COLUMN_TIMESTAMP,
|
||||
COLUMN_STATE,
|
||||
COLUMN_LENGTH,
|
||||
COLUMN_UPLOADED,
|
||||
COLUMN_TRANSFERRED,
|
||||
COLUMN_SOURCE,
|
||||
COLUMN_DESCRIPTION,
|
||||
COLUMN_CREATOR,
|
||||
COLUMN_MULTIPLE,
|
||||
COLUMN_WIDTH,
|
||||
COLUMN_HEIGHT,
|
||||
COLUMN_LICENSE
|
||||
};
|
||||
|
||||
public static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||
|
||||
public static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ "_id INTEGER PRIMARY KEY,"
|
||||
+ "filename STRING,"
|
||||
+ "local_uri STRING,"
|
||||
+ "image_url STRING,"
|
||||
+ "uploaded INTEGER,"
|
||||
+ "timestamp INTEGER,"
|
||||
+ "state INTEGER,"
|
||||
+ "length INTEGER,"
|
||||
+ "transferred INTEGER,"
|
||||
+ "source STRING,"
|
||||
+ "description STRING,"
|
||||
+ "creator STRING,"
|
||||
+ "multiple INTEGER,"
|
||||
+ "width INTEGER,"
|
||||
+ "height INTEGER,"
|
||||
+ "LICENSE STRING"
|
||||
+ ");";
|
||||
|
||||
// Upgrade from version 1 ->
|
||||
static final String ADD_CREATOR_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;";
|
||||
static final String ADD_DESCRIPTION_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;";
|
||||
|
||||
// Upgrade from version 2 ->
|
||||
static final String ADD_MULTIPLE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;";
|
||||
static final String SET_DEFAULT_MULTIPLE = "UPDATE " + TABLE_NAME + " SET multiple = 0";
|
||||
|
||||
// Upgrade from version 5 ->
|
||||
static final String ADD_WIDTH_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;";
|
||||
static final String SET_DEFAULT_WIDTH = "UPDATE " + TABLE_NAME + " SET width = 0";
|
||||
static final String ADD_HEIGHT_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;";
|
||||
static final String SET_DEFAULT_HEIGHT = "UPDATE " + TABLE_NAME + " SET height = 0";
|
||||
static final String ADD_LICENSE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;";
|
||||
static final String SET_DEFAULT_LICENSE = "UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA_3 + "';";
|
||||
|
||||
|
||||
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(SQLiteDatabase db, int from, int to) {
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
if (from == 1) {
|
||||
db.execSQL(ADD_DESCRIPTION_FIELD);
|
||||
db.execSQL(ADD_CREATOR_FIELD);
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 2) {
|
||||
db.execSQL(ADD_MULTIPLE_FIELD);
|
||||
db.execSQL(SET_DEFAULT_MULTIPLE);
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 3) {
|
||||
// Do nothing
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 4) {
|
||||
// Do nothing -- added Category
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 5) {
|
||||
// Added width and height fields
|
||||
db.execSQL(ADD_WIDTH_FIELD);
|
||||
db.execSQL(SET_DEFAULT_WIDTH);
|
||||
db.execSQL(ADD_HEIGHT_FIELD);
|
||||
db.execSQL(SET_DEFAULT_HEIGHT);
|
||||
db.execSQL(ADD_LICENSE_FIELD);
|
||||
db.execSQL(SET_DEFAULT_LICENSE);
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,8 +42,7 @@ import timber.log.Timber;
|
|||
|
||||
import static android.content.ContentResolver.requestSync;
|
||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
|
||||
import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.AUTHORITY;
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||
import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING;
|
||||
|
||||
|
|
@ -58,6 +57,7 @@ public class ContributionsActivity
|
|||
@Inject MediaWikiApi mediaWikiApi;
|
||||
@Inject SessionManager sessionManager;
|
||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||
@Inject ContributionDao contributionDao;
|
||||
|
||||
private Cursor allContributions;
|
||||
private ContributionsListFragment contributionsList;
|
||||
|
|
@ -65,21 +65,6 @@ public class ContributionsActivity
|
|||
private UploadService uploadService;
|
||||
private boolean isUploadServiceConnected;
|
||||
private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<>();
|
||||
private String CONTRIBUTION_SELECTION = "";
|
||||
|
||||
/*
|
||||
This sorts in the following order:
|
||||
Currently Uploading
|
||||
Failed (Sorted in ascending order of time added - FIFO)
|
||||
Queued to Upload (Sorted in ascending order of time added - FIFO)
|
||||
Completed (Sorted in descending order of time added)
|
||||
|
||||
This is why Contribution.STATE_COMPLETED is -1.
|
||||
*/
|
||||
private String CONTRIBUTION_SORT = Contribution.Table.COLUMN_STATE + " DESC, "
|
||||
+ Contribution.Table.COLUMN_UPLOADED + " DESC , ("
|
||||
+ Contribution.Table.COLUMN_TIMESTAMP + " * "
|
||||
+ Contribution.Table.COLUMN_STATE + ")";
|
||||
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
|
|
@ -94,7 +79,7 @@ public class ContributionsActivity
|
|||
@Override
|
||||
public void onServiceDisconnected(ComponentName componentName) {
|
||||
// this should never happen
|
||||
throw new RuntimeException("UploadService died but the rest of the process did not!");
|
||||
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -121,14 +106,13 @@ public class ContributionsActivity
|
|||
@Override
|
||||
protected void onAuthCookieAcquired(String authCookie) {
|
||||
// Do a sync everytime we get here!
|
||||
requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.AUTHORITY, new Bundle());
|
||||
requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.CONTRIBUTION_AUTHORITY, new Bundle());
|
||||
Intent uploadServiceIntent = new Intent(this, UploadService.class);
|
||||
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
||||
startService(uploadServiceIntent);
|
||||
bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
|
||||
|
||||
allContributions = getContentResolver().query(BASE_URI, ALL_FIELDS,
|
||||
CONTRIBUTION_SELECTION, null, CONTRIBUTION_SORT);
|
||||
allContributions = contributionDao.loadAllContributions();
|
||||
|
||||
getSupportLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
|
@ -186,24 +170,23 @@ public class ContributionsActivity
|
|||
|
||||
public void retryUpload(int i) {
|
||||
allContributions.moveToPosition(i);
|
||||
Contribution c = Contribution.fromCursor(allContributions);
|
||||
Contribution c = contributionDao.fromCursor(allContributions);
|
||||
if (c.getState() == STATE_FAILED) {
|
||||
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c);
|
||||
Timber.d("Restarting for %s", c.toContentValues());
|
||||
Timber.d("Restarting for %s", c.toString());
|
||||
} else {
|
||||
Timber.d("Skipping re-upload for non-failed %s", c.toContentValues());
|
||||
Timber.d("Skipping re-upload for non-failed %s", c.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteUpload(int i) {
|
||||
allContributions.moveToPosition(i);
|
||||
Contribution c = Contribution.fromCursor(allContributions);
|
||||
Contribution c = contributionDao.fromCursor(allContributions);
|
||||
if (c.getState() == STATE_FAILED) {
|
||||
Timber.d("Deleting failed contrib %s", c.toContentValues());
|
||||
c.setContentProviderClient(getContentResolver().acquireContentProviderClient(AUTHORITY));
|
||||
c.delete();
|
||||
Timber.d("Deleting failed contrib %s", c.toString());
|
||||
contributionDao.delete(c);
|
||||
} else {
|
||||
Timber.d("Skipping deletion for non-failed contrib %s", c.toContentValues());
|
||||
Timber.d("Skipping deletion for non-failed contrib %s", c.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -239,8 +222,8 @@ public class ContributionsActivity
|
|||
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
|
||||
int uploads = prefs.getInt(UPLOADS_SHOWING, 100);
|
||||
return new CursorLoader(this, BASE_URI,
|
||||
ALL_FIELDS, CONTRIBUTION_SELECTION, null,
|
||||
CONTRIBUTION_SORT + "LIMIT " + uploads);
|
||||
ALL_FIELDS, "", null,
|
||||
ContributionDao.CONTRIBUTION_SORT + "LIMIT " + uploads);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -249,7 +232,7 @@ public class ContributionsActivity
|
|||
|
||||
if (contributionsList.getAdapter() == null) {
|
||||
contributionsList.setAdapter(new ContributionsListAdapter(getApplicationContext(),
|
||||
cursor, 0));
|
||||
cursor, 0, contributionDao));
|
||||
} else {
|
||||
((CursorAdapter) contributionsList.getAdapter()).swapCursor(cursor);
|
||||
}
|
||||
|
|
@ -270,7 +253,7 @@ public class ContributionsActivity
|
|||
// not yet ready to return data
|
||||
return null;
|
||||
} else {
|
||||
return Contribution.fromCursor((Cursor) contributionsList.getAdapter().getItem(i));
|
||||
return contributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
|
|
@ -12,27 +11,27 @@ import android.text.TextUtils;
|
|||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.android.AndroidInjection;
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.content.UriMatcher.NO_MATCH;
|
||||
import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.contributions.Contribution.Table.TABLE_NAME;
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.TABLE_NAME;
|
||||
|
||||
public class ContributionsContentProvider extends ContentProvider {
|
||||
public class ContributionsContentProvider extends CommonsDaggerContentProvider {
|
||||
|
||||
private static final int CONTRIBUTIONS = 1;
|
||||
private static final int CONTRIBUTIONS_ID = 2;
|
||||
private static final String BASE_PATH = "contributions";
|
||||
private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH);
|
||||
public static final String AUTHORITY = "fr.free.nrw.commons.contributions.contentprovider";
|
||||
public static final String CONTRIBUTION_AUTHORITY = "fr.free.nrw.commons.contributions.contentprovider";
|
||||
|
||||
public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH);
|
||||
public static final Uri BASE_URI = Uri.parse("content://" + CONTRIBUTION_AUTHORITY + "/" + BASE_PATH);
|
||||
|
||||
static {
|
||||
uriMatcher.addURI(AUTHORITY, BASE_PATH, CONTRIBUTIONS);
|
||||
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID);
|
||||
uriMatcher.addURI(CONTRIBUTION_AUTHORITY, BASE_PATH, CONTRIBUTIONS);
|
||||
uriMatcher.addURI(CONTRIBUTION_AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID);
|
||||
}
|
||||
|
||||
public static Uri uriForId(int id) {
|
||||
|
|
@ -41,12 +40,6 @@ public class ContributionsContentProvider extends ContentProvider {
|
|||
|
||||
@Inject DBOpenHelper dbOpenHelper;
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
AndroidInjection.inject(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
|
||||
|
|
@ -93,7 +86,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
|||
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
long id = 0;
|
||||
long id;
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS:
|
||||
id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
||||
|
|
@ -165,7 +158,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
|||
*/
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
int rowsUpdated = 0;
|
||||
int rowsUpdated;
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS:
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, selection, selectionArgs);
|
||||
|
|
@ -176,7 +169,7 @@ public class ContributionsContentProvider extends ContentProvider {
|
|||
if (TextUtils.isEmpty(selection)) {
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||
contentValues,
|
||||
Contribution.Table.COLUMN_ID + " = ?",
|
||||
ContributionDao.Table.COLUMN_ID + " = ?",
|
||||
new String[]{String.valueOf(id)});
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ import fr.free.nrw.commons.R;
|
|||
|
||||
class ContributionsListAdapter extends CursorAdapter {
|
||||
|
||||
public ContributionsListAdapter(Context context, Cursor c, int flags) {
|
||||
private final ContributionDao contributionDao;
|
||||
|
||||
public ContributionsListAdapter(Context context, Cursor c, int flags, ContributionDao contributionDao) {
|
||||
super(context, c, flags);
|
||||
this.contributionDao = contributionDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -26,7 +29,7 @@ class ContributionsListAdapter extends CursorAdapter {
|
|||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
final ContributionViewHolder views = (ContributionViewHolder)view.getTag();
|
||||
final Contribution contribution = Contribution.fromCursor(cursor);
|
||||
final Contribution contribution = contributionDao.fromCursor(cursor);
|
||||
|
||||
views.imageView.setMedia(contribution);
|
||||
views.titleView.setText(contribution.getDisplayTitle());
|
||||
|
|
|
|||
|
|
@ -20,13 +20,15 @@ import android.widget.ListAdapter;
|
|||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import dagger.android.support.DaggerFragment;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.nearby.NearbyActivity;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
@ -36,7 +38,7 @@ import static android.app.Activity.RESULT_OK;
|
|||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
import static android.view.View.GONE;
|
||||
|
||||
public class ContributionsListFragment extends DaggerFragment {
|
||||
public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
||||
|
||||
@BindView(R.id.contributionsList)
|
||||
GridView contributionsList;
|
||||
|
|
@ -45,8 +47,12 @@ public class ContributionsListFragment extends DaggerFragment {
|
|||
@BindView(R.id.loadingContributionsProgressBar)
|
||||
ProgressBar progressBar;
|
||||
|
||||
@Inject @Named("prefs") SharedPreferences prefs;
|
||||
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
|
||||
@Inject
|
||||
@Named("prefs")
|
||||
SharedPreferences prefs;
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
SharedPreferences defaultPrefs;
|
||||
|
||||
private ContributionController controller;
|
||||
|
||||
|
|
@ -208,7 +214,7 @@ public class ContributionsListFragment extends DaggerFragment {
|
|||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
Timber.d("onRequestPermissionsResult: req code = " + " perm = "
|
||||
+ permissions + " grant =" + grantResults);
|
||||
+ Arrays.toString(permissions) + " grant =" + Arrays.toString(grantResults));
|
||||
|
||||
switch (requestCode) {
|
||||
// 1 = Storage allowed when gallery selected
|
||||
|
|
|
|||
|
|
@ -23,14 +23,14 @@ import java.util.TimeZone;
|
|||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.mwapi.LogEventResult;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
|
||||
import static fr.free.nrw.commons.contributions.Contribution.Table.COLUMN_FILENAME;
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.COLUMN_FILENAME;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
|
|
@ -81,7 +81,11 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
@Override
|
||||
public void onPerformSync(Account account, Bundle bundle, String authority,
|
||||
ContentProviderClient contentProviderClient, SyncResult syncResult) {
|
||||
((CommonsApplication) getContext().getApplicationContext()).injector().inject(this);
|
||||
ApplicationlessInjection
|
||||
.getInstance(getContext()
|
||||
.getApplicationContext())
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
|
||||
String user = account.name;
|
||||
String lastModified = prefs.getString("lastSyncTimestamp", "");
|
||||
|
|
@ -89,6 +93,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
LogEventResult result;
|
||||
Boolean done = false;
|
||||
String queryContinue = null;
|
||||
ContributionDao contributionDao = new ContributionDao(() -> contentProviderClient);
|
||||
while (!done) {
|
||||
|
||||
try {
|
||||
|
|
@ -121,7 +126,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
"", -1, dateUpdated, dateUpdated, user,
|
||||
"", "");
|
||||
contrib.setState(STATE_COMPLETED);
|
||||
imageValues.add(contrib.toContentValues());
|
||||
imageValues.add(contributionDao.toContentValues(contrib));
|
||||
|
||||
if (imageValues.size() % COMMIT_THRESHOLD == 0) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,276 +0,0 @@
|
|||
package fr.free.nrw.commons.data;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
import fr.free.nrw.commons.category.CategoryContentProvider;
|
||||
|
||||
/**
|
||||
* Represents a category
|
||||
*/
|
||||
public class Category {
|
||||
private Uri contentUri;
|
||||
|
||||
private String name;
|
||||
private Date lastUsed;
|
||||
private int timesUsed;
|
||||
|
||||
// Getters/setters
|
||||
/**
|
||||
* Gets name
|
||||
*
|
||||
* @return name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies name
|
||||
*
|
||||
* @param name Category name
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets last used date
|
||||
*
|
||||
* @return Last used date
|
||||
*/
|
||||
private Date getLastUsed() {
|
||||
// warning: Date objects are mutable.
|
||||
return (Date)lastUsed.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies last used date
|
||||
*
|
||||
* @param lastUsed Category date
|
||||
*/
|
||||
public void setLastUsed(Date lastUsed) {
|
||||
// warning: Date objects are mutable.
|
||||
this.lastUsed = (Date)lastUsed.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new last used date
|
||||
*/
|
||||
private void touch() {
|
||||
lastUsed = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets no. of times the category is used
|
||||
*
|
||||
* @return no. of times used
|
||||
*/
|
||||
private int getTimesUsed() {
|
||||
return timesUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies no. of times used
|
||||
*
|
||||
* @param timesUsed Category used times
|
||||
*/
|
||||
public void setTimesUsed(int timesUsed) {
|
||||
this.timesUsed = timesUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments timesUsed by 1 and sets last used date as now.
|
||||
*/
|
||||
public void incTimesUsed() {
|
||||
timesUsed++;
|
||||
touch();
|
||||
}
|
||||
|
||||
//region Database/content-provider stuff
|
||||
|
||||
/**
|
||||
* Persist category.
|
||||
* @param client ContentProviderClient to handle DB connection
|
||||
*/
|
||||
public void save(ContentProviderClient client) {
|
||||
try {
|
||||
if (contentUri == null) {
|
||||
contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues());
|
||||
} else {
|
||||
client.update(contentUri, toContentValues(), null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets content values
|
||||
*
|
||||
* @return Content values
|
||||
*/
|
||||
private ContentValues toContentValues() {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(Table.COLUMN_NAME, getName());
|
||||
cv.put(Table.COLUMN_LAST_USED, getLastUsed().getTime());
|
||||
cv.put(Table.COLUMN_TIMES_USED, getTimesUsed());
|
||||
return cv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets category from cursor
|
||||
* @param cursor Category cursor
|
||||
* @return Category from cursor
|
||||
*/
|
||||
private static Category fromCursor(Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
Category c = new Category();
|
||||
c.contentUri = CategoryContentProvider.uriForId(cursor.getInt(0));
|
||||
c.name = cursor.getString(1);
|
||||
c.lastUsed = new Date(cursor.getLong(2));
|
||||
c.timesUsed = cursor.getInt(3);
|
||||
return c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find persisted category in database, based on its name.
|
||||
* @param client ContentProviderClient to handle DB connection
|
||||
* @param name Category's name
|
||||
* @return category from database, or null if not found
|
||||
*/
|
||||
public static @Nullable Category find(ContentProviderClient client, String name) {
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = client.query(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
Category.Table.ALL_FIELDS,
|
||||
Category.Table.COLUMN_NAME + "=?",
|
||||
new String[]{name},
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return Category.fromCursor(cursor);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
// This feels lazy, but to hell with checked exceptions. :)
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve recently-used categories, ordered by descending date.
|
||||
* @return a list containing recent categories
|
||||
*/
|
||||
public static @NonNull ArrayList<String> recentCategories(ContentProviderClient client, int limit) {
|
||||
ArrayList<String> items = new ArrayList<>();
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = client.query(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
Category.Table.ALL_FIELDS,
|
||||
null,
|
||||
new String[]{},
|
||||
Category.Table.COLUMN_LAST_USED + " DESC");
|
||||
// fixme add a limit on the original query instead of falling out of the loop?
|
||||
while (cursor != null && cursor.moveToNext()
|
||||
&& cursor.getPosition() < limit) {
|
||||
Category cat = Category.fromCursor(cursor);
|
||||
items.add(cat.getName());
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public static class Table {
|
||||
public static final String TABLE_NAME = "categories";
|
||||
|
||||
public static final String COLUMN_ID = "_id";
|
||||
public static final String COLUMN_NAME = "name";
|
||||
public static final String COLUMN_LAST_USED = "last_used";
|
||||
public static final String COLUMN_TIMES_USED = "times_used";
|
||||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
public static final String[] ALL_FIELDS = {
|
||||
COLUMN_ID,
|
||||
COLUMN_NAME,
|
||||
COLUMN_LAST_USED,
|
||||
COLUMN_TIMES_USED
|
||||
};
|
||||
|
||||
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
|
||||
+ COLUMN_NAME + " STRING,"
|
||||
+ COLUMN_LAST_USED + " INTEGER,"
|
||||
+ COLUMN_TIMES_USED + " INTEGER"
|
||||
+ ");";
|
||||
|
||||
/**
|
||||
* Creates new table with provided SQLite database
|
||||
*
|
||||
* @param db Category database
|
||||
*/
|
||||
public static void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes existing table
|
||||
* @param db Category database
|
||||
*/
|
||||
public static void onDelete(SQLiteDatabase db) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates given database
|
||||
* @param db Category database
|
||||
* @param from Exiting category id
|
||||
* @param to New category id
|
||||
*/
|
||||
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
if (from < 4) {
|
||||
// doesn't exist yet
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 4) {
|
||||
// table added in version 5
|
||||
onCreate(db);
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 5) {
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
|
|
@ -4,8 +4,9 @@ import android.content.Context;
|
|||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
||||
import fr.free.nrw.commons.category.CategoryDao;
|
||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||
|
||||
public class DBOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
|
|
@ -13,7 +14,8 @@ public class DBOpenHelper extends SQLiteOpenHelper {
|
|||
private static final int DATABASE_VERSION = 6;
|
||||
|
||||
/**
|
||||
* Do not use, please call CommonsApplication.getDBOpenHelper()
|
||||
* Do not use directly - @Inject an instance where it's needed and let
|
||||
* dependency injection take care of managing this as a singleton.
|
||||
*/
|
||||
public DBOpenHelper(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
|
|
@ -21,15 +23,15 @@ public class DBOpenHelper extends SQLiteOpenHelper {
|
|||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase sqLiteDatabase) {
|
||||
Contribution.Table.onCreate(sqLiteDatabase);
|
||||
ModifierSequence.Table.onCreate(sqLiteDatabase);
|
||||
Category.Table.onCreate(sqLiteDatabase);
|
||||
ContributionDao.Table.onCreate(sqLiteDatabase);
|
||||
ModifierSequenceDao.Table.onCreate(sqLiteDatabase);
|
||||
CategoryDao.Table.onCreate(sqLiteDatabase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
|
||||
Contribution.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
ModifierSequence.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
Category.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
ContributionDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import fr.free.nrw.commons.auth.LoginActivity;
|
|||
import fr.free.nrw.commons.auth.SignupActivity;
|
||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||
import fr.free.nrw.commons.nearby.NearbyActivity;
|
||||
import fr.free.nrw.commons.notification.NotificationActivity;
|
||||
import fr.free.nrw.commons.settings.SettingsActivity;
|
||||
import fr.free.nrw.commons.upload.MultipleShareActivity;
|
||||
import fr.free.nrw.commons.upload.ShareActivity;
|
||||
|
|
@ -43,4 +44,6 @@ public abstract class ActivityBuilderModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract NearbyActivity bindNearbyActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract NotificationActivity bindNotificationActivity();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.Context;
|
||||
import android.support.v4.app.Fragment;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.android.AndroidInjector;
|
||||
import dagger.android.DispatchingAndroidInjector;
|
||||
import dagger.android.HasActivityInjector;
|
||||
import dagger.android.HasBroadcastReceiverInjector;
|
||||
import dagger.android.HasContentProviderInjector;
|
||||
import dagger.android.HasFragmentInjector;
|
||||
import dagger.android.HasServiceInjector;
|
||||
import dagger.android.support.HasSupportFragmentInjector;
|
||||
|
||||
public class ApplicationlessInjection
|
||||
implements
|
||||
HasActivityInjector,
|
||||
HasFragmentInjector,
|
||||
HasSupportFragmentInjector,
|
||||
HasServiceInjector,
|
||||
HasBroadcastReceiverInjector,
|
||||
HasContentProviderInjector {
|
||||
|
||||
private static ApplicationlessInjection instance = null;
|
||||
|
||||
@Inject DispatchingAndroidInjector<Activity> activityInjector;
|
||||
@Inject DispatchingAndroidInjector<BroadcastReceiver> broadcastReceiverInjector;
|
||||
@Inject DispatchingAndroidInjector<android.app.Fragment> fragmentInjector;
|
||||
@Inject DispatchingAndroidInjector<Fragment> supportFragmentInjector;
|
||||
@Inject DispatchingAndroidInjector<Service> serviceInjector;
|
||||
@Inject DispatchingAndroidInjector<ContentProvider> contentProviderInjector;
|
||||
|
||||
private CommonsApplicationComponent commonsApplicationComponent;
|
||||
|
||||
public ApplicationlessInjection(Context applicationContext) {
|
||||
commonsApplicationComponent = DaggerCommonsApplicationComponent.builder()
|
||||
.appModule(new CommonsApplicationModule(applicationContext)).build();
|
||||
commonsApplicationComponent.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DispatchingAndroidInjector<Activity> activityInjector() {
|
||||
return activityInjector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DispatchingAndroidInjector<android.app.Fragment> fragmentInjector() {
|
||||
return fragmentInjector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DispatchingAndroidInjector<Fragment> supportFragmentInjector() {
|
||||
return supportFragmentInjector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DispatchingAndroidInjector<BroadcastReceiver> broadcastReceiverInjector() {
|
||||
return broadcastReceiverInjector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DispatchingAndroidInjector<Service> serviceInjector() {
|
||||
return serviceInjector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidInjector<ContentProvider> contentProviderInjector() {
|
||||
return contentProviderInjector;
|
||||
}
|
||||
|
||||
public CommonsApplicationComponent getCommonsApplicationComponent() {
|
||||
return commonsApplicationComponent;
|
||||
}
|
||||
|
||||
public static ApplicationlessInjection getInstance(Context applicationContext) {
|
||||
if (instance == null) {
|
||||
synchronized (ApplicationlessInjection.class) {
|
||||
if (instance == null) {
|
||||
instance = new ApplicationlessInjection(applicationContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -8,8 +8,11 @@ import dagger.android.AndroidInjector;
|
|||
import dagger.android.support.AndroidSupportInjectionModule;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.MediaWikiImageView;
|
||||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
|
||||
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
|
||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||
import fr.free.nrw.commons.nearby.PlaceRenderer;
|
||||
|
||||
@Singleton
|
||||
@Component(modules = {
|
||||
|
|
@ -21,7 +24,7 @@ import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
|
|||
ServiceBuilderModule.class,
|
||||
ContentProviderBuilderModule.class
|
||||
})
|
||||
public interface CommonsApplicationComponent extends AndroidInjector<CommonsApplication> {
|
||||
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
|
||||
void inject(CommonsApplication application);
|
||||
|
||||
void inject(ContributionsSyncAdapter syncAdapter);
|
||||
|
|
@ -30,6 +33,14 @@ public interface CommonsApplicationComponent extends AndroidInjector<CommonsAppl
|
|||
|
||||
void inject(MediaWikiImageView mediaWikiImageView);
|
||||
|
||||
void inject(LoginActivity activity);
|
||||
|
||||
void inject(SettingsFragment fragment);
|
||||
|
||||
@Override
|
||||
void inject(ApplicationlessInjection instance);
|
||||
void inject(PlaceRenderer placeRenderer);
|
||||
|
||||
@Component.Builder
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
interface Builder {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.util.LruCache;
|
||||
|
|
@ -22,40 +24,75 @@ import fr.free.nrw.commons.nearby.NearbyPlaces;
|
|||
import fr.free.nrw.commons.upload.UploadController;
|
||||
|
||||
import static android.content.Context.MODE_PRIVATE;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY;
|
||||
import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MODIFICATIONS_AUTHORITY;
|
||||
|
||||
@Module
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public class CommonsApplicationModule {
|
||||
private CommonsApplication application;
|
||||
public static final String CATEGORY_AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
|
||||
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
public CommonsApplicationModule(CommonsApplication application) {
|
||||
this.application = application;
|
||||
private Context applicationContext;
|
||||
|
||||
public CommonsApplicationModule(Context applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Provides
|
||||
public AccountUtil providesAccountUtil() {
|
||||
return new AccountUtil(application);
|
||||
public Context providesApplicationContext() {
|
||||
return this.applicationContext;
|
||||
}
|
||||
|
||||
@Provides
|
||||
public AccountUtil providesAccountUtil(Context context) {
|
||||
return new AccountUtil(context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("category")
|
||||
public ContentProviderClient provideCategoryContentProviderClient(Context context) {
|
||||
return context.getContentResolver().acquireContentProviderClient(CATEGORY_AUTHORITY);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("contribution")
|
||||
public ContentProviderClient provideContributionContentProviderClient(Context context) {
|
||||
return context.getContentResolver().acquireContentProviderClient(CONTRIBUTION_AUTHORITY);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("modification")
|
||||
public ContentProviderClient provideModificationContentProviderClient(Context context) {
|
||||
return context.getContentResolver().acquireContentProviderClient(MODIFICATIONS_AUTHORITY);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("application_preferences")
|
||||
public SharedPreferences providesApplicationSharedPreferences() {
|
||||
return application.getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
|
||||
public SharedPreferences providesApplicationSharedPreferences(Context context) {
|
||||
return context.getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("default_preferences")
|
||||
public SharedPreferences providesDefaultSharedPreferences() {
|
||||
return PreferenceManager.getDefaultSharedPreferences(application);
|
||||
public SharedPreferences providesDefaultSharedPreferences(Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("prefs")
|
||||
public SharedPreferences providesOtherSharedPreferences() {
|
||||
return application.getSharedPreferences("prefs", MODE_PRIVATE);
|
||||
public SharedPreferences providesOtherSharedPreferences(Context context) {
|
||||
return context.getSharedPreferences("prefs", MODE_PRIVATE);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("direct_nearby_upload_prefs")
|
||||
public SharedPreferences providesDirectNearbyUploadPreferences(Context context) {
|
||||
return context.getSharedPreferences("direct_nearby_upload_prefs", MODE_PRIVATE);
|
||||
}
|
||||
|
||||
@Provides
|
||||
<<<<<<< HEAD
|
||||
@Named("direct_nearby_upload_prefs")
|
||||
public SharedPreferences providesDirectNearbyUploadPreferences() {
|
||||
return application.getSharedPreferences("direct_nearby_upload_prefs", MODE_PRIVATE);
|
||||
|
|
@ -64,24 +101,30 @@ public class CommonsApplicationModule {
|
|||
@Provides
|
||||
public UploadController providesUploadController(SessionManager sessionManager, @Named("default_preferences") SharedPreferences sharedPreferences) {
|
||||
return new UploadController(sessionManager, application, sharedPreferences);
|
||||
=======
|
||||
public UploadController providesUploadController(SessionManager sessionManager, @Named("default_preferences") SharedPreferences sharedPreferences, Context context) {
|
||||
return new UploadController(sessionManager, context, sharedPreferences);
|
||||
>>>>>>> directNearbyUploadsNewLocal
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public SessionManager providesSessionManager(MediaWikiApi mediaWikiApi) {
|
||||
return new SessionManager(application, mediaWikiApi);
|
||||
public SessionManager providesSessionManager(Context context,
|
||||
MediaWikiApi mediaWikiApi,
|
||||
@Named("default_preferences") SharedPreferences sharedPreferences) {
|
||||
return new SessionManager(context, mediaWikiApi, sharedPreferences);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public MediaWikiApi provideMediaWikiApi() {
|
||||
return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
|
||||
public MediaWikiApi provideMediaWikiApi(Context context, @Named("default_preferences") SharedPreferences sharedPreferences) {
|
||||
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, sharedPreferences);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public LocationServiceManager provideLocationServiceManager() {
|
||||
return new LocationServiceManager(application);
|
||||
public LocationServiceManager provideLocationServiceManager(Context context) {
|
||||
return new LocationServiceManager(context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -92,8 +135,8 @@ public class CommonsApplicationModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
public DBOpenHelper provideDBOpenHelper() {
|
||||
return new DBOpenHelper(application);
|
||||
public DBOpenHelper provideDBOpenHelper(Context context) {
|
||||
return new DBOpenHelper(context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -107,4 +150,4 @@ public class CommonsApplicationModule {
|
|||
public LruCache<String, String> provideLruCache() {
|
||||
return new LruCache<>(1024);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.android.AndroidInjector;
|
||||
import dagger.android.DispatchingAndroidInjector;
|
||||
import dagger.android.support.HasSupportFragmentInjector;
|
||||
|
||||
public abstract class CommonsDaggerAppCompatActivity extends AppCompatActivity implements HasSupportFragmentInjector {
|
||||
|
||||
@Inject
|
||||
DispatchingAndroidInjector<Fragment> supportFragmentInjector;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
inject();
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidInjector<Fragment> supportFragmentInjector() {
|
||||
return supportFragmentInjector;
|
||||
}
|
||||
|
||||
private void inject() {
|
||||
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext());
|
||||
|
||||
AndroidInjector<Activity> activityInjector = injection.activityInjector();
|
||||
|
||||
if (activityInjector == null) {
|
||||
throw new NullPointerException("ApplicationlessInjection.activityInjector() returned null");
|
||||
}
|
||||
|
||||
activityInjector.inject(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import dagger.android.AndroidInjector;
|
||||
|
||||
public abstract class CommonsDaggerBroadcastReceiver extends BroadcastReceiver {
|
||||
|
||||
public CommonsDaggerBroadcastReceiver() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
inject(context);
|
||||
}
|
||||
|
||||
private void inject(Context context) {
|
||||
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(context.getApplicationContext());
|
||||
|
||||
AndroidInjector<BroadcastReceiver> serviceInjector = injection.broadcastReceiverInjector();
|
||||
|
||||
if (serviceInjector == null) {
|
||||
throw new NullPointerException("ApplicationlessInjection.broadcastReceiverInjector() returned null");
|
||||
}
|
||||
serviceInjector.inject(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
|
||||
import dagger.android.AndroidInjector;
|
||||
|
||||
|
||||
public abstract class CommonsDaggerContentProvider extends ContentProvider {
|
||||
|
||||
public CommonsDaggerContentProvider() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
inject();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void inject() {
|
||||
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getContext());
|
||||
|
||||
AndroidInjector<ContentProvider> serviceInjector = injection.contentProviderInjector();
|
||||
|
||||
if (serviceInjector == null) {
|
||||
throw new NullPointerException("ApplicationlessInjection.contentProviderInjector() returned null");
|
||||
}
|
||||
|
||||
serviceInjector.inject(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.app.Service;
|
||||
|
||||
import dagger.android.AndroidInjector;
|
||||
|
||||
public abstract class CommonsDaggerIntentService extends IntentService {
|
||||
|
||||
public CommonsDaggerIntentService(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
inject();
|
||||
super.onCreate();
|
||||
}
|
||||
|
||||
private void inject() {
|
||||
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext());
|
||||
|
||||
AndroidInjector<Service> serviceInjector = injection.serviceInjector();
|
||||
|
||||
if (serviceInjector == null) {
|
||||
throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null");
|
||||
}
|
||||
|
||||
serviceInjector.inject(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import android.app.Service;
|
||||
|
||||
import dagger.android.AndroidInjector;
|
||||
|
||||
public abstract class CommonsDaggerService extends Service {
|
||||
|
||||
public CommonsDaggerService() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
inject();
|
||||
super.onCreate();
|
||||
}
|
||||
|
||||
private void inject() {
|
||||
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext());
|
||||
|
||||
AndroidInjector<Service> serviceInjector = injection.serviceInjector();
|
||||
|
||||
if (serviceInjector == null) {
|
||||
throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null");
|
||||
}
|
||||
|
||||
serviceInjector.inject(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.support.v4.app.Fragment;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.android.AndroidInjector;
|
||||
import dagger.android.DispatchingAndroidInjector;
|
||||
import dagger.android.support.HasSupportFragmentInjector;
|
||||
|
||||
public abstract class CommonsDaggerSupportFragment extends Fragment implements HasSupportFragmentInjector {
|
||||
|
||||
@Inject
|
||||
DispatchingAndroidInjector<Fragment> childFragmentInjector;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
inject();
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidInjector<Fragment> supportFragmentInjector() {
|
||||
return childFragmentInjector;
|
||||
}
|
||||
|
||||
|
||||
public void inject() {
|
||||
HasSupportFragmentInjector hasSupportFragmentInjector = findHasFragmentInjector();
|
||||
|
||||
AndroidInjector<Fragment> fragmentInjector = hasSupportFragmentInjector.supportFragmentInjector();
|
||||
|
||||
if (fragmentInjector == null) {
|
||||
throw new NullPointerException(String.format("%s.supportFragmentInjector() returned null", hasSupportFragmentInjector.getClass().getCanonicalName()));
|
||||
}
|
||||
|
||||
fragmentInjector.inject(this);
|
||||
}
|
||||
|
||||
private HasSupportFragmentInjector findHasFragmentInjector() {
|
||||
Fragment parentFragment = this;
|
||||
|
||||
while ((parentFragment = parentFragment.getParentFragment()) != null) {
|
||||
if (parentFragment instanceof HasSupportFragmentInjector) {
|
||||
return (HasSupportFragmentInjector) parentFragment;
|
||||
}
|
||||
}
|
||||
|
||||
Activity activity = getActivity();
|
||||
|
||||
if (activity instanceof HasSupportFragmentInjector) {
|
||||
return (HasSupportFragmentInjector) activity;
|
||||
}
|
||||
|
||||
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(activity.getApplicationContext());
|
||||
if (injection != null) {
|
||||
return injection;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(String.format("No injector was found for %s", getClass().getCanonicalName()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,137 +1,162 @@
|
|||
package fr.free.nrw.commons.location;
|
||||
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class LatLng {
|
||||
|
||||
private final double latitude;
|
||||
private final double longitude;
|
||||
private final float accuracy;
|
||||
|
||||
/** Accepts latitude and longitude.
|
||||
* North and South values are cut off at 90°
|
||||
*
|
||||
* @param latitude double value
|
||||
* @param longitude double value
|
||||
*/
|
||||
public LatLng(double latitude, double longitude, float accuracy) {
|
||||
if (-180.0D <= longitude && longitude < 180.0D) {
|
||||
this.longitude = longitude;
|
||||
} else {
|
||||
this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D;
|
||||
}
|
||||
this.latitude = Math.max(-90.0D, Math.min(90.0D, latitude));
|
||||
this.accuracy = accuracy;
|
||||
}
|
||||
|
||||
public static LatLng from(@NonNull Location location) {
|
||||
return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
boolean var1 = true;
|
||||
byte var2 = 1;
|
||||
long var3 = Double.doubleToLongBits(this.latitude);
|
||||
int var5 = 31 * var2 + (int)(var3 ^ var3 >>> 32);
|
||||
var3 = Double.doubleToLongBits(this.longitude);
|
||||
var5 = 31 * var5 + (int)(var3 ^ var3 >>> 32);
|
||||
return var5;
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
} else if (!(o instanceof LatLng)) {
|
||||
return false;
|
||||
} else {
|
||||
LatLng var2 = (LatLng)o;
|
||||
return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(var2.latitude) && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(var2.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "lat/lng: (" + this.latitude + "," + this.longitude + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds the float to 4 digits and returns absolute value.
|
||||
*
|
||||
* @param coordinate A coordinate value as string.
|
||||
* @return String of the rounded number.
|
||||
*/
|
||||
private String formatCoordinate(double coordinate) {
|
||||
double roundedNumber = Math.round(coordinate * 10000d) / 10000d;
|
||||
double absoluteNumber = Math.abs(roundedNumber);
|
||||
return String.valueOf(absoluteNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns "N" or "S" depending on the latitude.
|
||||
*
|
||||
* @return "N" or "S".
|
||||
*/
|
||||
private String getNorthSouth() {
|
||||
if (this.latitude < 0) {
|
||||
return "S";
|
||||
}
|
||||
|
||||
return "N";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns "E" or "W" depending on the longitude.
|
||||
*
|
||||
* @return "E" or "W".
|
||||
*/
|
||||
private String getEastWest() {
|
||||
if (this.longitude >= 0 && this.longitude < 180) {
|
||||
return "E";
|
||||
}
|
||||
|
||||
return "W";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a nicely formatted coordinate string. Used e.g. in
|
||||
* the detail view.
|
||||
*
|
||||
* @return The formatted string.
|
||||
*/
|
||||
public String getPrettyCoordinateString() {
|
||||
return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", "
|
||||
+ formatCoordinate(this.longitude) + " " + this.getEastWest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the location accuracy in meter.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public float getAccuracy() {
|
||||
return accuracy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the longitude in degrees.
|
||||
*
|
||||
* @return double
|
||||
*/
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the latitude in degrees.
|
||||
*
|
||||
* @return double
|
||||
*/
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
public Uri getGmmIntentUri() {
|
||||
return Uri.parse("geo:0,0?q=" + latitude + "," + longitude);
|
||||
}
|
||||
}
|
||||
package fr.free.nrw.commons.location;
|
||||
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* a latitude and longitude point with accuracy information, often of a picture
|
||||
*/
|
||||
public class LatLng {
|
||||
|
||||
private final double latitude;
|
||||
private final double longitude;
|
||||
private final float accuracy;
|
||||
|
||||
/**
|
||||
* Accepts latitude and longitude.
|
||||
* North and South values are cut off at 90°
|
||||
*
|
||||
* @param latitude the latitude
|
||||
* @param longitude the longitude
|
||||
* @param accuracy the accuracy
|
||||
*
|
||||
* Examples:
|
||||
* the Statue of Liberty is located at 40.69° N, 74.04° W
|
||||
* The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0)
|
||||
* where positive signifies north, east and negative signifies south, west.
|
||||
*/
|
||||
public LatLng(double latitude, double longitude, float accuracy) {
|
||||
if (-180.0D <= longitude && longitude < 180.0D) {
|
||||
this.longitude = longitude;
|
||||
} else {
|
||||
this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D;
|
||||
}
|
||||
this.latitude = Math.max(-90.0D, Math.min(90.0D, latitude));
|
||||
this.accuracy = accuracy;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the latitude and longitude of a given non-null location
|
||||
* @param location the non-null location of the user
|
||||
* @return LatLng the Latitude and Longitude of a given location
|
||||
*/
|
||||
public static LatLng from(@NonNull Location location) {
|
||||
return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a hash code for the longitude and longitude
|
||||
*/
|
||||
public int hashCode() {
|
||||
byte var1 = 1;
|
||||
long var2 = Double.doubleToLongBits(this.latitude);
|
||||
int var3 = 31 * var1 + (int)(var2 ^ var2 >>> 32);
|
||||
var2 = Double.doubleToLongBits(this.longitude);
|
||||
var3 = 31 * var3 + (int)(var2 ^ var2 >>> 32);
|
||||
return var3;
|
||||
}
|
||||
|
||||
/**
|
||||
* checks for equality of two LatLng objects
|
||||
* @param o the second LatLng object
|
||||
*/
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
} else if (!(o instanceof LatLng)) {
|
||||
return false;
|
||||
} else {
|
||||
LatLng var2 = (LatLng)o;
|
||||
return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(var2.latitude) && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(var2.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a string representation of the latitude and longitude
|
||||
*/
|
||||
public String toString() {
|
||||
return "lat/lng: (" + this.latitude + "," + this.longitude + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds the float to 4 digits and returns absolute value.
|
||||
*
|
||||
* @param coordinate A coordinate value as string.
|
||||
* @return String of the rounded number.
|
||||
*/
|
||||
private String formatCoordinate(double coordinate) {
|
||||
double roundedNumber = Math.round(coordinate * 10000d) / 10000d;
|
||||
double absoluteNumber = Math.abs(roundedNumber);
|
||||
return String.valueOf(absoluteNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns "N" or "S" depending on the latitude.
|
||||
*
|
||||
* @return "N" or "S".
|
||||
*/
|
||||
private String getNorthSouth() {
|
||||
if (this.latitude < 0) {
|
||||
return "S";
|
||||
}
|
||||
|
||||
return "N";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns "E" or "W" depending on the longitude.
|
||||
*
|
||||
* @return "E" or "W".
|
||||
*/
|
||||
private String getEastWest() {
|
||||
if (this.longitude >= 0 && this.longitude < 180) {
|
||||
return "E";
|
||||
}
|
||||
|
||||
return "W";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a nicely formatted coordinate string. Used e.g. in
|
||||
* the detail view.
|
||||
*
|
||||
* @return The formatted string.
|
||||
*/
|
||||
public String getPrettyCoordinateString() {
|
||||
return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", "
|
||||
+ formatCoordinate(this.longitude) + " " + this.getEastWest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the location accuracy in meter.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public float getAccuracy() {
|
||||
return accuracy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the longitude in degrees.
|
||||
*
|
||||
* @return double
|
||||
*/
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the latitude in degrees.
|
||||
*
|
||||
* @return double
|
||||
*/
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
public Uri getGmmIntentUri() {
|
||||
return Uri.parse("geo:0,0?q=" + latitude + "," + longitude);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,20 +30,40 @@ public class LocationServiceManager implements LocationListener {
|
|||
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
|
||||
private boolean isLocationManagerRegistered = false;
|
||||
|
||||
/**
|
||||
* Constructs a new instance of LocationServiceManager.
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
public LocationServiceManager(Context context) {
|
||||
this.context = context;
|
||||
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current status of the GPS provider.
|
||||
*
|
||||
* @return true if the GPS provider is enabled
|
||||
*/
|
||||
public boolean isProviderEnabled() {
|
||||
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the location permission is granted.
|
||||
*
|
||||
* @return true if the location permission is granted
|
||||
*/
|
||||
public boolean isLocationPermissionGranted() {
|
||||
return ContextCompat.checkSelfPermission(context,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the location permission to be granted.
|
||||
*
|
||||
* @param activity the activity
|
||||
*/
|
||||
public void requestPermissions(Activity activity) {
|
||||
if (activity.isFinishing()) {
|
||||
return;
|
||||
|
|
@ -54,11 +74,9 @@ public class LocationServiceManager implements LocationListener {
|
|||
}
|
||||
|
||||
public boolean isPermissionExplanationRequired(Activity activity) {
|
||||
if (activity.isFinishing()) {
|
||||
return false;
|
||||
}
|
||||
return ActivityCompat.shouldShowRequestPermissionRationale(activity,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
return !activity.isFinishing() &&
|
||||
ActivityCompat.shouldShowRequestPermissionRationale(activity,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
}
|
||||
|
||||
public LatLng getLastLocation() {
|
||||
|
|
@ -68,7 +86,8 @@ public class LocationServiceManager implements LocationListener {
|
|||
return LatLng.from(lastLocation);
|
||||
}
|
||||
|
||||
/** Registers a LocationManager to listen for current location.
|
||||
/**
|
||||
* Registers a LocationManager to listen for current location.
|
||||
*/
|
||||
public void registerLocationManager() {
|
||||
if (!isLocationManagerRegistered)
|
||||
|
|
@ -76,6 +95,12 @@ public class LocationServiceManager implements LocationListener {
|
|||
&& requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests location updates from the specified provider.
|
||||
*
|
||||
* @param locationProvider the location provider
|
||||
* @return true if successful
|
||||
*/
|
||||
private boolean requestLocationUpdatesFromProvider(String locationProvider) {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(locationProvider,
|
||||
|
|
@ -92,7 +117,16 @@ public class LocationServiceManager implements LocationListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a given location is better than the current best location.
|
||||
*
|
||||
* @param location the location to be tested
|
||||
* @param currentBestLocation the current best location
|
||||
* @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly
|
||||
* LOCATION_SLIGHTLY_CHANGED if location changed slightly
|
||||
*/
|
||||
protected LocationChangeType isBetterLocation(Location location, Location currentBestLocation) {
|
||||
|
||||
if (currentBestLocation == null) {
|
||||
// A new location is always better than no location
|
||||
return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
|
||||
|
|
@ -148,7 +182,8 @@ public class LocationServiceManager implements LocationListener {
|
|||
return provider1.equals(provider2);
|
||||
}
|
||||
|
||||
/** Unregisters location manager.
|
||||
/**
|
||||
* Unregisters location manager.
|
||||
*/
|
||||
public void unregisterLocationManager() {
|
||||
isLocationManagerRegistered = false;
|
||||
|
|
@ -159,12 +194,22 @@ public class LocationServiceManager implements LocationListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new listener to the list of location listeners.
|
||||
*
|
||||
* @param listener the new listener
|
||||
*/
|
||||
public void addLocationListener(LocationUpdateListener listener) {
|
||||
if (!locationListeners.contains(listener)) {
|
||||
locationListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener from the list of location listeners.
|
||||
*
|
||||
* @param listener the listener to be removed
|
||||
*/
|
||||
public void removeLocationListener(LocationUpdateListener listener) {
|
||||
locationListeners.remove(listener);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import java.util.Locale;
|
|||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import dagger.android.support.DaggerFragment;
|
||||
import fr.free.nrw.commons.License;
|
||||
import fr.free.nrw.commons.LicenseList;
|
||||
import fr.free.nrw.commons.Media;
|
||||
|
|
@ -32,12 +31,12 @@ import fr.free.nrw.commons.MediaDataExtractor;
|
|||
import fr.free.nrw.commons.MediaWikiImageView;
|
||||
import fr.free.nrw.commons.PageTitle;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.ui.widget.CompatTextView;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class MediaDetailFragment extends DaggerFragment {
|
||||
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
||||
|
||||
private boolean editable;
|
||||
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
|
||||
|
|
@ -77,7 +76,7 @@ public class MediaDetailFragment extends DaggerFragment {
|
|||
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
|
||||
private ViewTreeObserver.OnScrollChangedListener scrollListener;
|
||||
private DataSetObserver dataObserver;
|
||||
private AsyncTask<Void,Void,Boolean> detailFetchTask;
|
||||
private AsyncTask<Void, Void, Boolean> detailFetchTask;
|
||||
private LicenseList licenseList;
|
||||
|
||||
@Override
|
||||
|
|
@ -96,7 +95,7 @@ public class MediaDetailFragment extends DaggerFragment {
|
|||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider)getActivity();
|
||||
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity();
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
editable = savedInstanceState.getBoolean("editable");
|
||||
|
|
@ -157,7 +156,8 @@ public class MediaDetailFragment extends DaggerFragment {
|
|||
return view;
|
||||
}
|
||||
|
||||
@Override public void onResume() {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Media media = detailProvider.getMediaAtPosition(index);
|
||||
if (media == null) {
|
||||
|
|
@ -239,13 +239,13 @@ public class MediaDetailFragment extends DaggerFragment {
|
|||
detailFetchTask.cancel(true);
|
||||
detailFetchTask = null;
|
||||
}
|
||||
if (layoutListener != null) {
|
||||
if (layoutListener != null && getView() != null) {
|
||||
getView().getViewTreeObserver().removeGlobalOnLayoutListener(layoutListener); // old Android was on crack. CRACK IS WHACK
|
||||
layoutListener = null;
|
||||
}
|
||||
if (scrollListener != null) {
|
||||
if (scrollListener != null && getView() != null) {
|
||||
getView().getViewTreeObserver().removeOnScrollChangedListener(scrollListener);
|
||||
scrollListener = null;
|
||||
scrollListener = null;
|
||||
}
|
||||
if (dataObserver != null) {
|
||||
detailProvider.unregisterDataSetObserver(dataObserver);
|
||||
|
|
@ -290,7 +290,7 @@ public class MediaDetailFragment extends DaggerFragment {
|
|||
|
||||
private View buildCatLabel(final String catName, ViewGroup categoryContainer) {
|
||||
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false);
|
||||
final CompatTextView textView = (CompatTextView)item.findViewById(R.id.mediaDetailCategoryItemText);
|
||||
final CompatTextView textView = (CompatTextView) item.findViewById(R.id.mediaDetailCategoryItemText);
|
||||
|
||||
textView.setText(catName);
|
||||
if (categoriesLoaded && categoriesPresent) {
|
||||
|
|
@ -309,7 +309,7 @@ public class MediaDetailFragment extends DaggerFragment {
|
|||
// You must face the darkness alone
|
||||
int scrollY = scrollView.getScrollY();
|
||||
int scrollMax = getView().getHeight();
|
||||
float scrollPercentage = (float)scrollY / (float)scrollMax;
|
||||
float scrollPercentage = (float) scrollY / (float) scrollMax;
|
||||
final float transparencyMax = 0.75f;
|
||||
if (scrollPercentage > transparencyMax) {
|
||||
scrollPercentage = transparencyMax;
|
||||
|
|
@ -363,7 +363,8 @@ public class MediaDetailFragment extends DaggerFragment {
|
|||
}
|
||||
|
||||
|
||||
private @Nullable String licenseLink(Media media) {
|
||||
private @Nullable
|
||||
String licenseLink(Media media) {
|
||||
String licenseKey = media.getLicense();
|
||||
if (licenseKey == null || licenseKey.equals("")) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@ import android.view.ViewGroup;
|
|||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.android.support.DaggerFragment;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
|
||||
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||
|
|
@ -41,11 +41,15 @@ import static android.content.Context.DOWNLOAD_SERVICE;
|
|||
import static android.content.Intent.ACTION_VIEW;
|
||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
public class MediaDetailPagerFragment extends DaggerFragment implements ViewPager.OnPageChangeListener {
|
||||
public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener {
|
||||
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject SessionManager sessionManager;
|
||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||
@Inject
|
||||
MediaWikiApi mwApi;
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
SharedPreferences prefs;
|
||||
|
||||
private ViewPager pager;
|
||||
private Boolean editable;
|
||||
|
|
@ -164,13 +168,19 @@ public class MediaDetailPagerFragment extends DaggerFragment implements ViewPage
|
|||
req.allowScanningByMediaScanner();
|
||||
req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !(ContextCompat.checkSelfPermission(getContext(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
ContextCompat.checkSelfPermission(getContext(), READ_EXTERNAL_STORAGE)
|
||||
!= PERMISSION_GRANTED
|
||||
&& getView() != null) {
|
||||
Snackbar.make(getView(), R.string.read_storage_permission_rationale,
|
||||
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
|
||||
view -> ActivityCompat.requestPermissions(getActivity(),
|
||||
new String[]{READ_EXTERNAL_STORAGE}, 1)).show();
|
||||
} else {
|
||||
((DownloadManager) getActivity().getSystemService(DOWNLOAD_SERVICE)).enqueue(req);
|
||||
DownloadManager systemService = (DownloadManager) getActivity().getSystemService(DOWNLOAD_SERVICE);
|
||||
if (systemService != null) {
|
||||
systemService.enqueue(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package fr.free.nrw.commons.modifications;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
|
|
@ -12,24 +11,26 @@ import android.text.TextUtils;
|
|||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.android.AndroidInjection;
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ModificationsContentProvider extends ContentProvider {
|
||||
import static fr.free.nrw.commons.modifications.ModifierSequenceDao.Table.TABLE_NAME;
|
||||
|
||||
public class ModificationsContentProvider extends CommonsDaggerContentProvider {
|
||||
|
||||
private static final int MODIFICATIONS = 1;
|
||||
private static final int MODIFICATIONS_ID = 2;
|
||||
|
||||
public static final String AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider";
|
||||
private static final String BASE_PATH = "modifications";
|
||||
public static final String MODIFICATIONS_AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider";
|
||||
public static final String BASE_PATH = "modifications";
|
||||
|
||||
public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH);
|
||||
public static final Uri BASE_URI = Uri.parse("content://" + MODIFICATIONS_AUTHORITY + "/" + BASE_PATH);
|
||||
|
||||
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
static {
|
||||
uriMatcher.addURI(AUTHORITY, BASE_PATH, MODIFICATIONS);
|
||||
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID);
|
||||
uriMatcher.addURI(MODIFICATIONS_AUTHORITY, BASE_PATH, MODIFICATIONS);
|
||||
uriMatcher.addURI(MODIFICATIONS_AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID);
|
||||
}
|
||||
|
||||
public static Uri uriForId(int id) {
|
||||
|
|
@ -38,16 +39,10 @@ public class ModificationsContentProvider extends ContentProvider {
|
|||
|
||||
@Inject DBOpenHelper dbOpenHelper;
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
AndroidInjection.inject(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
||||
queryBuilder.setTables(ModifierSequence.Table.TABLE_NAME);
|
||||
queryBuilder.setTables(TABLE_NAME);
|
||||
|
||||
int uriType = uriMatcher.match(uri);
|
||||
|
||||
|
|
@ -75,10 +70,10 @@ public class ModificationsContentProvider extends ContentProvider {
|
|||
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
long id = 0;
|
||||
long id;
|
||||
switch (uriType) {
|
||||
case MODIFICATIONS:
|
||||
id = sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, contentValues);
|
||||
id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||
|
|
@ -94,7 +89,7 @@ public class ModificationsContentProvider extends ContentProvider {
|
|||
switch (uriType) {
|
||||
case MODIFICATIONS_ID:
|
||||
String id = uri.getLastPathSegment();
|
||||
sqlDB.delete(ModifierSequence.Table.TABLE_NAME,
|
||||
sqlDB.delete(TABLE_NAME,
|
||||
"_id = ?",
|
||||
new String[] { id }
|
||||
);
|
||||
|
|
@ -114,7 +109,7 @@ public class ModificationsContentProvider extends ContentProvider {
|
|||
case MODIFICATIONS:
|
||||
for (ContentValues value: values) {
|
||||
Timber.d("Inserting! %s", value);
|
||||
sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, value);
|
||||
sqlDB.insert(TABLE_NAME, null, value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
|
@ -137,10 +132,10 @@ public class ModificationsContentProvider extends ContentProvider {
|
|||
*/
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
int rowsUpdated = 0;
|
||||
int rowsUpdated;
|
||||
switch (uriType) {
|
||||
case MODIFICATIONS:
|
||||
rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME,
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||
contentValues,
|
||||
selection,
|
||||
selectionArgs);
|
||||
|
|
@ -149,9 +144,9 @@ public class ModificationsContentProvider extends ContentProvider {
|
|||
int id = Integer.valueOf(uri.getLastPathSegment());
|
||||
|
||||
if (TextUtils.isEmpty(selection)) {
|
||||
rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME,
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||
contentValues,
|
||||
ModifierSequence.Table.COLUMN_ID + " = ?",
|
||||
ModifierSequenceDao.Table.COLUMN_ID + " = ?",
|
||||
new String[] { String.valueOf(id) } );
|
||||
} else {
|
||||
throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package fr.free.nrw.commons.modifications;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.AuthenticatorException;
|
||||
import android.accounts.OperationCanceledException;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.Context;
|
||||
|
|
@ -16,15 +13,21 @@ import java.io.IOException;
|
|||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject ContributionDao contributionDao;
|
||||
@Inject ModifierSequenceDao modifierSequenceDao;
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
|
||||
super(context, autoInitialize);
|
||||
|
|
@ -33,7 +36,11 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
@Override
|
||||
public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) {
|
||||
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
|
||||
((CommonsApplication)getContext().getApplicationContext()).injector().inject(this);
|
||||
ApplicationlessInjection
|
||||
.getInstance(getContext()
|
||||
.getApplicationContext())
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
|
||||
Cursor allModifications;
|
||||
try {
|
||||
|
|
@ -48,16 +55,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
return;
|
||||
}
|
||||
|
||||
String authCookie;
|
||||
try {
|
||||
authCookie = AccountManager.get(getContext()).blockingGetAuthToken(account, "", false);
|
||||
} catch (OperationCanceledException | AuthenticatorException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
Timber.d("Could not authenticate :(");
|
||||
return;
|
||||
}
|
||||
|
||||
String authCookie = sessionManager.getAuthCookie();
|
||||
if (isNullOrWhiteSpace(authCookie)) {
|
||||
Timber.d("Could not authenticate :(");
|
||||
return;
|
||||
|
|
@ -79,28 +77,36 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
|
||||
ContentProviderClient contributionsClient = null;
|
||||
try {
|
||||
contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY);
|
||||
contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.CONTRIBUTION_AUTHORITY);
|
||||
|
||||
while (!allModifications.isAfterLast()) {
|
||||
ModifierSequence sequence = ModifierSequence.fromCursor(allModifications);
|
||||
sequence.setContentProviderClient(contentProviderClient);
|
||||
ModifierSequence sequence = modifierSequenceDao.fromCursor(allModifications);
|
||||
Contribution contrib;
|
||||
|
||||
Cursor contributionCursor;
|
||||
|
||||
if (contributionsClient == null) {
|
||||
Timber.e("ContributionsClient is null. This should not happen!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
contributionCursor = contributionsClient.query(sequence.getMediaUri(), null, null, null, null);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
contributionCursor.moveToFirst();
|
||||
contrib = Contribution.fromCursor(contributionCursor);
|
||||
|
||||
if (contrib.getState() == Contribution.STATE_COMPLETED) {
|
||||
if (contributionCursor != null) {
|
||||
contributionCursor.moveToFirst();
|
||||
}
|
||||
|
||||
contrib = contributionDao.fromCursor(contributionCursor);
|
||||
|
||||
if (contrib != null && contrib.getState() == Contribution.STATE_COMPLETED) {
|
||||
String pageContent;
|
||||
try {
|
||||
pageContent = mwApi.revisionsByFilename(contrib.getFilename());
|
||||
} catch (IOException e) {
|
||||
Timber.d("Network fuckup on modifications sync!");
|
||||
Timber.d("Network messed up on modifications sync!");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -111,17 +117,17 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
try {
|
||||
editResult = mwApi.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
|
||||
} catch (IOException e) {
|
||||
Timber.d("Network fuckup on modifications sync!");
|
||||
Timber.d("Network messed up on modifications sync!");
|
||||
continue;
|
||||
}
|
||||
|
||||
Timber.d("Response is %s", editResult);
|
||||
|
||||
if (!editResult.equals("Success")) {
|
||||
if (!"Success".equals(editResult)) {
|
||||
// FIXME: Log this somewhere else
|
||||
Timber.d("Non success result! %s", editResult);
|
||||
} else {
|
||||
sequence.delete();
|
||||
modifierSequenceDao.delete(sequence);
|
||||
}
|
||||
}
|
||||
allModifications.moveToNext();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,8 @@
|
|||
package fr.free.nrw.commons.modifications;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -17,14 +11,13 @@ public class ModifierSequence {
|
|||
private Uri mediaUri;
|
||||
private ArrayList<PageModifier> modifiers;
|
||||
private Uri contentUri;
|
||||
private ContentProviderClient client;
|
||||
|
||||
public ModifierSequence(Uri mediaUri) {
|
||||
this.mediaUri = mediaUri;
|
||||
modifiers = new ArrayList<>();
|
||||
}
|
||||
|
||||
public ModifierSequence(Uri mediaUri, JSONObject data) {
|
||||
ModifierSequence(Uri mediaUri, JSONObject data) {
|
||||
this(mediaUri);
|
||||
JSONArray modifiersJSON = data.optJSONArray("modifiers");
|
||||
for (int i = 0; i < modifiersJSON.length(); i++) {
|
||||
|
|
@ -32,7 +25,7 @@ public class ModifierSequence {
|
|||
}
|
||||
}
|
||||
|
||||
public Uri getMediaUri() {
|
||||
Uri getMediaUri() {
|
||||
return mediaUri;
|
||||
}
|
||||
|
||||
|
|
@ -40,14 +33,14 @@ public class ModifierSequence {
|
|||
modifiers.add(modifier);
|
||||
}
|
||||
|
||||
public String executeModifications(String pageName, String pageContents) {
|
||||
String executeModifications(String pageName, String pageContents) {
|
||||
for (PageModifier modifier: modifiers) {
|
||||
pageContents = modifier.doModification(pageName, pageContents);
|
||||
}
|
||||
return pageContents;
|
||||
}
|
||||
|
||||
public String getEditSummary() {
|
||||
String getEditSummary() {
|
||||
StringBuilder editSummary = new StringBuilder();
|
||||
for (PageModifier modifier: modifiers) {
|
||||
editSummary.append(modifier.getEditSumary()).append(" ");
|
||||
|
|
@ -56,97 +49,16 @@ public class ModifierSequence {
|
|||
return editSummary.toString();
|
||||
}
|
||||
|
||||
public JSONObject toJSON() {
|
||||
JSONObject data = new JSONObject();
|
||||
try {
|
||||
JSONArray modifiersJSON = new JSONArray();
|
||||
for (PageModifier modifier: modifiers) {
|
||||
modifiersJSON.put(modifier.toJSON());
|
||||
}
|
||||
data.put("modifiers", modifiersJSON);
|
||||
return data;
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
ArrayList<PageModifier> getModifiers() {
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
public ContentValues toContentValues() {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(Table.COLUMN_MEDIA_URI, mediaUri.toString());
|
||||
cv.put(Table.COLUMN_DATA, toJSON().toString());
|
||||
return cv;
|
||||
Uri getContentUri() {
|
||||
return contentUri;
|
||||
}
|
||||
|
||||
public static ModifierSequence fromCursor(Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
ModifierSequence ms = null;
|
||||
try {
|
||||
ms = new ModifierSequence(Uri.parse(cursor.getString(1)),
|
||||
new JSONObject(cursor.getString(2)));
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
ms.contentUri = ModificationsContentProvider.uriForId(cursor.getInt(0));
|
||||
|
||||
return ms;
|
||||
void setContentUri(Uri contentUri) {
|
||||
this.contentUri = contentUri;
|
||||
}
|
||||
|
||||
public void save() {
|
||||
try {
|
||||
if (contentUri == null) {
|
||||
contentUri = client.insert(ModificationsContentProvider.BASE_URI, this.toContentValues());
|
||||
} else {
|
||||
client.update(contentUri, toContentValues(), null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
try {
|
||||
client.delete(contentUri, null, null);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setContentProviderClient(ContentProviderClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public static class Table {
|
||||
public static final String TABLE_NAME = "modifications";
|
||||
|
||||
public static final String COLUMN_ID = "_id";
|
||||
public static final String COLUMN_MEDIA_URI = "mediauri";
|
||||
public static final String COLUMN_DATA = "data";
|
||||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
public static final String[] ALL_FIELDS = {
|
||||
COLUMN_ID,
|
||||
COLUMN_MEDIA_URI,
|
||||
COLUMN_DATA
|
||||
};
|
||||
|
||||
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ "_id INTEGER PRIMARY KEY,"
|
||||
+ "mediauri STRING,"
|
||||
+ "data STRING"
|
||||
+ ");";
|
||||
|
||||
public static void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
||||
}
|
||||
|
||||
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
public static void onDelete(SQLiteDatabase db) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
onCreate(db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
package fr.free.nrw.commons.modifications;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
|
||||
public class ModifierSequenceDao {
|
||||
|
||||
private final Provider<ContentProviderClient> clientProvider;
|
||||
|
||||
@Inject
|
||||
public ModifierSequenceDao(@Named("modification") Provider<ContentProviderClient> clientProvider) {
|
||||
this.clientProvider = clientProvider;
|
||||
}
|
||||
|
||||
public void save(ModifierSequence sequence) {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
if (sequence.getContentUri() == null) {
|
||||
sequence.setContentUri(db.insert(ModificationsContentProvider.BASE_URI, toContentValues(sequence)));
|
||||
} else {
|
||||
db.update(sequence.getContentUri(), toContentValues(sequence), null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(ModifierSequence sequence) {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
db.delete(sequence.getContentUri(), null, null);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
|
||||
ModifierSequence fromCursor(Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
ModifierSequence ms;
|
||||
try {
|
||||
ms = new ModifierSequence(Uri.parse(cursor.getString(1)),
|
||||
new JSONObject(cursor.getString(2)));
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
ms.setContentUri( ModificationsContentProvider.uriForId(cursor.getInt(0)));
|
||||
|
||||
return ms;
|
||||
}
|
||||
|
||||
private JSONObject toJSON(ModifierSequence sequence) {
|
||||
JSONObject data = new JSONObject();
|
||||
try {
|
||||
JSONArray modifiersJSON = new JSONArray();
|
||||
for (PageModifier modifier: sequence.getModifiers()) {
|
||||
modifiersJSON.put(modifier.toJSON());
|
||||
}
|
||||
data.put("modifiers", modifiersJSON);
|
||||
return data;
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private ContentValues toContentValues(ModifierSequence sequence) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(Table.COLUMN_MEDIA_URI, sequence.getMediaUri().toString());
|
||||
cv.put(Table.COLUMN_DATA, toJSON(sequence).toString());
|
||||
return cv;
|
||||
}
|
||||
|
||||
public static class Table {
|
||||
static final String TABLE_NAME = "modifications";
|
||||
|
||||
static final String COLUMN_ID = "_id";
|
||||
static final String COLUMN_MEDIA_URI = "mediauri";
|
||||
static final String COLUMN_DATA = "data";
|
||||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
public static final String[] ALL_FIELDS = {
|
||||
COLUMN_ID,
|
||||
COLUMN_MEDIA_URI,
|
||||
COLUMN_DATA
|
||||
};
|
||||
|
||||
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||
|
||||
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ "_id INTEGER PRIMARY KEY,"
|
||||
+ "mediauri STRING,"
|
||||
+ "data STRING"
|
||||
+ ");";
|
||||
|
||||
public static void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
||||
}
|
||||
|
||||
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
||||
db.execSQL(DROP_TABLE_STATEMENT);
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
public static void onDelete(SQLiteDatabase db) {
|
||||
db.execSQL(DROP_TABLE_STATEMENT);
|
||||
onCreate(db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package fr.free.nrw.commons.mwapi;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
|
@ -21,6 +23,8 @@ import org.apache.http.params.CoreProtocolPNames;
|
|||
import org.apache.http.util.EntityUtils;
|
||||
import org.mediawiki.api.ApiResult;
|
||||
import org.mediawiki.api.MWApi;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
|
@ -36,11 +40,17 @@ import java.util.concurrent.Callable;
|
|||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.PageTitle;
|
||||
import fr.free.nrw.commons.notification.Notification;
|
||||
import in.yuvi.http.fluent.Http;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN;
|
||||
import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationFromApiResult;
|
||||
import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationType;
|
||||
import static fr.free.nrw.commons.notification.NotificationUtils.isCommonsNotification;
|
||||
|
||||
/**
|
||||
* @author Addshore
|
||||
*/
|
||||
|
|
@ -50,17 +60,27 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
private static final String THUMB_SIZE = "640";
|
||||
private AbstractHttpClient httpClient;
|
||||
private MWApi api;
|
||||
private Context context;
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
public ApacheHttpClientMediaWikiApi(String apiURL) {
|
||||
public ApacheHttpClientMediaWikiApi(Context context, String apiURL, SharedPreferences sharedPreferences) {
|
||||
this.context = context;
|
||||
BasicHttpParams params = new BasicHttpParams();
|
||||
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
||||
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
|
||||
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
|
||||
schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
|
||||
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
|
||||
params.setParameter(CoreProtocolPNames.USER_AGENT, "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE);
|
||||
params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent());
|
||||
httpClient = new DefaultHttpClient(cm, params);
|
||||
api = new MWApi(apiURL, httpClient);
|
||||
this.sharedPreferences = sharedPreferences;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String getUserAgent() {
|
||||
return "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
|
@ -75,11 +95,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
* @throws IOException On api request IO issue
|
||||
*/
|
||||
public String login(String username, String password) throws IOException {
|
||||
String loginToken = getLoginToken();
|
||||
Timber.d("Login token is %s", loginToken);
|
||||
return getErrorCodeToReturn(api.action("clientlogin")
|
||||
.param("rememberMe", "1")
|
||||
.param("username", username)
|
||||
.param("password", password)
|
||||
.param("logintoken", getLoginToken())
|
||||
.param("logintoken", loginToken)
|
||||
.param("loginreturnurl", "https://commons.wikimedia.org")
|
||||
.post());
|
||||
}
|
||||
|
|
@ -92,12 +114,14 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
* @throws IOException On api request IO issue
|
||||
*/
|
||||
public String login(String username, String password, String twoFactorCode) throws IOException {
|
||||
String loginToken = getLoginToken();
|
||||
Timber.d("Login token is %s", loginToken);
|
||||
return getErrorCodeToReturn(api.action("clientlogin")
|
||||
.param("rememberMe", "1")
|
||||
.param("rememberMe", "true")
|
||||
.param("username", username)
|
||||
.param("password", password)
|
||||
.param("logintoken", getLoginToken())
|
||||
.param("logincontinue", "1")
|
||||
.param("logintoken", loginToken)
|
||||
.param("logincontinue", "true")
|
||||
.param("OATHToken", twoFactorCode)
|
||||
.post());
|
||||
}
|
||||
|
|
@ -122,14 +146,17 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
String status = loginApiResult.getString("/api/clientlogin/@status");
|
||||
if (status.equals("PASS")) {
|
||||
api.isLoggedIn = true;
|
||||
setAuthCookieOnLogin(true);
|
||||
return status;
|
||||
} else if (status.equals("FAIL")) {
|
||||
setAuthCookieOnLogin(false);
|
||||
return loginApiResult.getString("/api/clientlogin/@messagecode");
|
||||
} else if (
|
||||
status.equals("UI")
|
||||
&& loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
|
||||
&& loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
|
||||
) {
|
||||
setAuthCookieOnLogin(false);
|
||||
return "2FA";
|
||||
}
|
||||
|
||||
|
|
@ -137,6 +164,18 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
return "genericerror-" + status;
|
||||
}
|
||||
|
||||
private void setAuthCookieOnLogin(boolean isLoggedIn) {
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
if (isLoggedIn) {
|
||||
editor.putBoolean("isUserLoggedIn", true);
|
||||
editor.putString("getAuthCookie", api.getAuthCookie());
|
||||
} else {
|
||||
editor.putBoolean("isUserLoggedIn", false);
|
||||
editor.remove("getAuthCookie");
|
||||
}
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthCookie() {
|
||||
return api.getAuthCookie();
|
||||
|
|
@ -353,6 +392,42 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
.getString("/api/query/pages/page/revisions/rev");
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public List<Notification> getNotifications() {
|
||||
ApiResult notificationNode = null;
|
||||
try {
|
||||
notificationNode = api.action("query")
|
||||
.param("notprop", "list")
|
||||
.param("format", "xml")
|
||||
.param("meta", "notifications")
|
||||
.param("notfilter", "!read")
|
||||
.get()
|
||||
.getNode("/api/query/notifications/list");
|
||||
} catch (IOException e) {
|
||||
Timber.e("Failed to obtain searchCategories", e);
|
||||
}
|
||||
|
||||
if (notificationNode == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
List<Notification> notifications = new ArrayList<>();
|
||||
|
||||
NodeList childNodes = notificationNode.getDocument().getChildNodes();
|
||||
|
||||
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||
Node node = childNodes.item(i);
|
||||
if (isCommonsNotification(node)
|
||||
&& !getNotificationType(node).equals(UNKNOWN)) {
|
||||
notifications.add(getNotificationFromApiResult(context, node));
|
||||
}
|
||||
}
|
||||
|
||||
return notifications;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean existingFile(String fileSha1) throws IOException {
|
||||
return api.action("query")
|
||||
|
|
|
|||
|
|
@ -5,11 +5,15 @@ import android.support.annotation.Nullable;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.notification.Notification;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
public interface MediaWikiApi {
|
||||
String getUserAgent();
|
||||
|
||||
String getAuthCookie();
|
||||
|
||||
void setAuthCookie(String authCookie);
|
||||
|
|
@ -43,6 +47,9 @@ public interface MediaWikiApi {
|
|||
@NonNull
|
||||
Observable<String> allCategories(String filter, int searchCatsLimit);
|
||||
|
||||
@NonNull
|
||||
List<Notification> getNotifications() throws IOException;
|
||||
|
||||
@NonNull
|
||||
Observable<String> searchTitles(String title, int searchCatsLimit);
|
||||
|
||||
|
|
@ -51,6 +58,8 @@ public interface MediaWikiApi {
|
|||
|
||||
boolean existingFile(String fileSha1) throws IOException;
|
||||
|
||||
|
||||
|
||||
@NonNull
|
||||
LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,16 +13,14 @@ import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
|||
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
public class DirectUpload {
|
||||
class DirectUpload {
|
||||
|
||||
private ContributionController controller;
|
||||
private Fragment fragment;
|
||||
private SharedPreferences prefs;
|
||||
|
||||
DirectUpload(Fragment fragment, ContributionController controller, SharedPreferences prefs) {
|
||||
DirectUpload(Fragment fragment, ContributionController controller) {
|
||||
this.fragment = fragment;
|
||||
this.controller = controller;
|
||||
this.prefs = prefs;
|
||||
}
|
||||
|
||||
void initiateCameraUpload() {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import android.os.Build;
|
|||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.BottomSheetBehavior;
|
||||
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
|
|
@ -71,6 +73,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName();
|
||||
private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName();
|
||||
|
||||
@BindView(R.id.swipe_container) SwipeRefreshLayout swipeLayout;
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
|
@ -78,6 +81,8 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
ButterKnife.bind(this);
|
||||
resumeFragment();
|
||||
bundle = new Bundle();
|
||||
|
||||
initBottomSheetBehaviour();
|
||||
initDrawer();
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +95,14 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|
|||
private void initBottomSheetBehaviour() {
|
||||
|
||||
transparentView.setAlpha(0);
|
||||
swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
lockNearbyView(false);
|
||||
refreshView(true);
|
||||
}
|
||||
});
|
||||
|
||||
bottomSheet.getLayoutParams().height = getWindowManager()
|
||||
.getDefaultDisplay().getHeight() / 16 * 9;
|
||||
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package fr.free.nrw.commons.nearby;
|
||||
|
||||
|
||||
import android.support.v4.app.Fragment;
|
||||
|
||||
import com.pedrogomez.renderers.ListAdapteeCollection;
|
||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||
import com.pedrogomez.renderers.RendererBuilder;
|
||||
|
|
@ -7,11 +10,25 @@ import com.pedrogomez.renderers.RendererBuilder;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.contributions.ContributionController;
|
||||
|
||||
class NearbyAdapterFactory {
|
||||
|
||||
private Fragment fragment;
|
||||
private ContributionController controller;
|
||||
|
||||
NearbyAdapterFactory(){
|
||||
|
||||
}
|
||||
|
||||
NearbyAdapterFactory(Fragment fragment, ContributionController controller) {
|
||||
this.fragment = fragment;
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
public RVRendererAdapter<Place> create(List<Place> placeList) {
|
||||
RendererBuilder<Place> builder = new RendererBuilder<Place>()
|
||||
.bind(Place.class, new PlaceRenderer());
|
||||
.bind(Place.class, new PlaceRenderer(fragment, controller));
|
||||
ListAdapteeCollection<Place> collection = new ListAdapteeCollection<>(
|
||||
placeList != null ? placeList : Collections.emptyList());
|
||||
return new RVRendererAdapter<>(builder, collection);
|
||||
|
|
|
|||
|
|
@ -39,11 +39,13 @@ public class NearbyController {
|
|||
|
||||
/**
|
||||
* Prepares Place list to make their distance information update later.
|
||||
*
|
||||
* @param curLatLng current location for user
|
||||
* @return Place list without distance information
|
||||
* @return NearbyPlacesInfo a variable holds Place list without distance information
|
||||
* and boundary coordinates of current Place List
|
||||
*/
|
||||
//public List<Place> loadAttractionsFromLocation(LatLng curLatLng) {
|
||||
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) {
|
||||
|
||||
Timber.d("Loading attractions near %s", curLatLng);
|
||||
NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo();
|
||||
|
||||
|
|
@ -51,10 +53,12 @@ public class NearbyController {
|
|||
return null;
|
||||
}
|
||||
List<Place> places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage());
|
||||
|
||||
LatLng[] boundaryCoordinates = {places.get(0).location, // south
|
||||
places.get(0).location, // north
|
||||
places.get(0).location, // west
|
||||
places.get(0).location};// east, init with a random location
|
||||
|
||||
if (curLatLng != null) {
|
||||
Timber.d("Sorting places by distance...");
|
||||
final Map<Place, Double> distances = new HashMap<>();
|
||||
|
|
@ -89,6 +93,7 @@ public class NearbyController {
|
|||
|
||||
/**
|
||||
* Loads attractions from location for list view, we need to return Place data type.
|
||||
*
|
||||
* @param curLatLng users current location
|
||||
* @param placeList list of nearby places in Place data type
|
||||
* @return Place list that holds nearby places
|
||||
|
|
@ -97,7 +102,7 @@ public class NearbyController {
|
|||
LatLng curLatLng,
|
||||
List<Place> placeList) {
|
||||
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
|
||||
for (Place place: placeList) {
|
||||
for (Place place : placeList) {
|
||||
String distance = formatDistanceBetween(curLatLng, place.location);
|
||||
place.setDistance(distance);
|
||||
}
|
||||
|
|
@ -105,7 +110,8 @@ public class NearbyController {
|
|||
}
|
||||
|
||||
/**
|
||||
*Loads attractions from location for map view, we need to return BaseMarkerOption data type.
|
||||
* Loads attractions from location for map view, we need to return BaseMarkerOption data type.
|
||||
*
|
||||
* @param curLatLng users current location
|
||||
* @param placeList list of nearby places in Place data type
|
||||
* @return BaseMarkerOptions list that holds nearby places
|
||||
|
|
@ -122,26 +128,28 @@ public class NearbyController {
|
|||
|
||||
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
|
||||
|
||||
Bitmap icon = UiUtils.getBitmap(
|
||||
VectorDrawableCompat.create(
|
||||
context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme()
|
||||
));
|
||||
VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(
|
||||
context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme()
|
||||
);
|
||||
if (vectorDrawable != null) {
|
||||
Bitmap icon = UiUtils.getBitmap(vectorDrawable);
|
||||
|
||||
for (Place place: placeList) {
|
||||
String distance = formatDistanceBetween(curLatLng, place.location);
|
||||
place.setDistance(distance);
|
||||
for (Place place : placeList) {
|
||||
String distance = formatDistanceBetween(curLatLng, place.location);
|
||||
place.setDistance(distance);
|
||||
|
||||
NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
|
||||
nearbyBaseMarker.title(place.name);
|
||||
nearbyBaseMarker.position(
|
||||
new com.mapbox.mapboxsdk.geometry.LatLng(
|
||||
place.location.getLatitude(),
|
||||
place.location.getLongitude()));
|
||||
nearbyBaseMarker.place(place);
|
||||
nearbyBaseMarker.icon(IconFactory.getInstance(context)
|
||||
.fromBitmap(icon));
|
||||
NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
|
||||
nearbyBaseMarker.title(place.name);
|
||||
nearbyBaseMarker.position(
|
||||
new com.mapbox.mapboxsdk.geometry.LatLng(
|
||||
place.location.getLatitude(),
|
||||
place.location.getLongitude()));
|
||||
nearbyBaseMarker.place(place);
|
||||
nearbyBaseMarker.icon(IconFactory.getInstance(context)
|
||||
.fromBitmap(icon));
|
||||
|
||||
baseMarkerOptions.add(nearbyBaseMarker);
|
||||
baseMarkerOptions.add(nearbyBaseMarker);
|
||||
}
|
||||
}
|
||||
return baseMarkerOptions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
package fr.free.nrw.commons.nearby;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
|
|
@ -17,12 +21,17 @@ import java.lang.reflect.Type;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import dagger.android.support.AndroidSupportInjection;
|
||||
import dagger.android.support.DaggerFragment;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.contributions.ContributionController;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.utils.UriDeserializer;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
public class NearbyListFragment extends DaggerFragment {
|
||||
private static final Type LIST_TYPE = new TypeToken<List<Place>>() {
|
||||
}.getType();
|
||||
|
|
@ -34,6 +43,7 @@ public class NearbyListFragment extends DaggerFragment {
|
|||
|
||||
private NearbyAdapterFactory adapterFactory;
|
||||
private RecyclerView recyclerView;
|
||||
private ContributionController controller;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
|
|
@ -41,6 +51,12 @@ public class NearbyListFragment extends DaggerFragment {
|
|||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
AndroidSupportInjection.inject(this);
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
ViewGroup container,
|
||||
|
|
@ -49,7 +65,9 @@ public class NearbyListFragment extends DaggerFragment {
|
|||
View view = inflater.inflate(R.layout.fragment_nearby, container, false);
|
||||
recyclerView = view.findViewById(R.id.listView);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
adapterFactory = new NearbyAdapterFactory();
|
||||
|
||||
controller = new ContributionController(this);
|
||||
adapterFactory = new NearbyAdapterFactory(this, controller);
|
||||
return view;
|
||||
}
|
||||
|
||||
|
|
@ -79,6 +97,47 @@ public class NearbyListFragment extends DaggerFragment {
|
|||
|
||||
placeList = NearbyController.loadAttractionsFromLocationToPlaces(curLatLng, placeList);
|
||||
}
|
||||
|
||||
return placeList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Timber.d("onRequestPermissionsResult: req code = " + " perm = " + permissions + " grant =" + grantResults);
|
||||
|
||||
switch (requestCode) {
|
||||
// 1 = "Read external storage" allowed when gallery selected
|
||||
case 1: {
|
||||
if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) {
|
||||
Timber.d("Call controller.startGalleryPick()");
|
||||
controller.startGalleryPick();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// 3 = "Write external storage" allowed when camera selected
|
||||
case 3: {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Timber.d("Call controller.startCameraCapture()");
|
||||
controller.startCameraCapture();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (resultCode == RESULT_OK) {
|
||||
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
requestCode, resultCode, data);
|
||||
controller.handleImagePicked(requestCode, data, true);
|
||||
} else {
|
||||
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
|
||||
requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package fr.free.nrw.commons.nearby;
|
|||
import android.animation.ObjectAnimator;
|
||||
import android.animation.TypeEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
|
|
@ -15,7 +16,6 @@ import android.support.design.widget.BottomSheetBehavior;
|
|||
import android.support.design.widget.CoordinatorLayout;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
|
@ -115,7 +115,6 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Bundle bundle = this.getArguments();
|
||||
|
||||
Gson gson = new GsonBuilder()
|
||||
.registerTypeAdapter(Uri.class, new UriDeserializer())
|
||||
.create();
|
||||
|
|
@ -388,6 +387,8 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
private void setupMapView(Bundle savedInstanceState) {
|
||||
MapboxMapOptions options = new MapboxMapOptions()
|
||||
.styleUrl(Style.OUTDOORS)
|
||||
.logoEnabled(false)
|
||||
.attributionEnabled(false)
|
||||
.camera(new CameraPosition.Builder()
|
||||
.target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude()))
|
||||
.zoom(11)
|
||||
|
|
@ -546,6 +547,8 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
|
||||
icon.setImageResource(place.getLabel().getIcon());
|
||||
|
||||
title.setText(place.name);
|
||||
distance.setText(place.distance);
|
||||
description.setText(place.getLongDescription());
|
||||
title.setText(place.name.toString());
|
||||
distance.setText(place.distance.toString());
|
||||
|
|
@ -553,7 +556,8 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
fabCamera.setOnClickListener(view -> {
|
||||
Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
|
||||
controller = new ContributionController(this);
|
||||
DirectUpload directUpload = new DirectUpload(this, controller, prefs);
|
||||
|
||||
DirectUpload directUpload = new DirectUpload(this, controller);
|
||||
storeSharedPrefs();
|
||||
directUpload.initiateCameraUpload();
|
||||
});
|
||||
|
|
@ -561,9 +565,14 @@ public class NearbyMapFragment extends DaggerFragment {
|
|||
fabGallery.setOnClickListener(view -> {
|
||||
Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
|
||||
controller = new ContributionController(this);
|
||||
DirectUpload directUpload = new DirectUpload(this, controller, prefs);
|
||||
|
||||
DirectUpload directUpload = new DirectUpload(this, controller);
|
||||
storeSharedPrefs();
|
||||
directUpload.initiateGalleryUpload();
|
||||
|
||||
//TODO: App crashes after image upload completes
|
||||
//TODO: Handle onRequestPermissionsResult
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public class NearbyPlaces {
|
|||
|
||||
public NearbyPlaces() {
|
||||
try {
|
||||
wikidataQuery = FileUtils.readFromResource("/assets/queries/nearby_query.rq");
|
||||
wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq");
|
||||
Timber.v(wikidataQuery);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
|
|
|||
|
|
@ -1,23 +1,31 @@
|
|||
package fr.free.nrw.commons.nearby;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import butterknife.ButterKnife;
|
||||
import dagger.android.support.DaggerFragment;
|
||||
import dagger.android.support.AndroidSupportInjection;
|
||||
import fr.free.nrw.commons.R;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Tells user that Nearby Places cannot be displayed if location permissions are denied
|
||||
*/
|
||||
public class NoPermissionsFragment extends DaggerFragment {
|
||||
public class NoPermissionsFragment extends Fragment {
|
||||
|
||||
public NoPermissionsFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
AndroidSupportInjection.inject(this);
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package fr.free.nrw.commons.nearby;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.transition.TransitionManager;
|
||||
import android.support.v7.widget.PopupMenu;
|
||||
import android.util.Log;
|
||||
|
|
@ -17,11 +20,17 @@ import com.pedrogomez.renderers.Renderer;
|
|||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.contributions.ContributionController;
|
||||
import timber.log.Timber;
|
||||
|
||||
class PlaceRenderer extends Renderer<Place> {
|
||||
public class PlaceRenderer extends Renderer<Place> {
|
||||
|
||||
@BindView(R.id.tvName) TextView tvName;
|
||||
@BindView(R.id.tvDesc) TextView tvDesc;
|
||||
|
|
@ -29,11 +38,13 @@ class PlaceRenderer extends Renderer<Place> {
|
|||
@BindView(R.id.icon) ImageView icon;
|
||||
@BindView(R.id.buttonLayout) LinearLayout buttonLayout;
|
||||
@BindView(R.id.cameraButton) LinearLayout cameraButton;
|
||||
@BindView(R.id.galeryButton) LinearLayout galeryButton;
|
||||
|
||||
@BindView(R.id.galleryButton) LinearLayout galleryButton;
|
||||
@BindView(R.id.directionsButton) LinearLayout directionsButton;
|
||||
@BindView(R.id.iconOverflow) LinearLayout iconOverflow;
|
||||
@BindView(R.id.cameraButtonText) TextView cameraButtonText;
|
||||
@BindView(R.id.galeryButtonText) TextView galeryButtonText;
|
||||
@BindView(R.id.galleryButtonText) TextView galleryButtonText;
|
||||
|
||||
@BindView(R.id.directionsButtonText) TextView directionsButtonText;
|
||||
@BindView(R.id.iconOverflowText) TextView iconOverflowText;
|
||||
|
||||
|
|
@ -41,8 +52,20 @@ class PlaceRenderer extends Renderer<Place> {
|
|||
private static ArrayList<LinearLayout> openedItems;
|
||||
private Place place;
|
||||
|
||||
private Fragment fragment;
|
||||
private ContributionController controller;
|
||||
|
||||
PlaceRenderer(){
|
||||
|
||||
@Inject @Named("prefs") SharedPreferences prefs;
|
||||
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
|
||||
|
||||
public PlaceRenderer(){
|
||||
openedItems = new ArrayList<>();
|
||||
}
|
||||
|
||||
public PlaceRenderer(Fragment fragment, ContributionController controller) {
|
||||
this.fragment = fragment;
|
||||
this.controller = controller;
|
||||
openedItems = new ArrayList<>();
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +104,28 @@ class PlaceRenderer extends Renderer<Place> {
|
|||
}
|
||||
});
|
||||
|
||||
//TODO: Set onClickListeners for camera and gallery in list here
|
||||
cameraButton.setOnClickListener(view2 -> {
|
||||
Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
|
||||
DirectUpload directUpload = new DirectUpload(fragment, controller);
|
||||
storeSharedPrefs();
|
||||
directUpload.initiateCameraUpload();
|
||||
});
|
||||
|
||||
galleryButton.setOnClickListener(view3 -> {
|
||||
Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
|
||||
DirectUpload directUpload = new DirectUpload(fragment, controller);
|
||||
storeSharedPrefs();
|
||||
directUpload.initiateGalleryUpload();
|
||||
});
|
||||
}
|
||||
|
||||
private void storeSharedPrefs() {
|
||||
SharedPreferences.Editor editor = directPrefs.edit();
|
||||
Timber.d("directPrefs stored");
|
||||
editor.putString("Title", place.getName());
|
||||
editor.putString("Desc", place.getLongDescription());
|
||||
editor.apply();
|
||||
|
||||
}
|
||||
|
||||
private void closeLayout(LinearLayout buttonLayout){
|
||||
|
|
@ -94,9 +138,11 @@ class PlaceRenderer extends Renderer<Place> {
|
|||
|
||||
@Override
|
||||
public void render() {
|
||||
|
||||
place = getContent();
|
||||
tvName.setText(place.name);
|
||||
String descriptionText = place.getLabel().getText();
|
||||
String descriptionText = place.getLongDescription();
|
||||
|
||||
if (descriptionText.equals("?")) {
|
||||
descriptionText = getContext().getString(R.string.no_description_found);
|
||||
}
|
||||
|
|
@ -114,6 +160,7 @@ class PlaceRenderer extends Renderer<Place> {
|
|||
|
||||
iconOverflow.setVisibility(showMenu() ? View.VISIBLE : View.GONE);
|
||||
iconOverflow.setOnClickListener(v -> popupMenuListener());
|
||||
|
||||
}
|
||||
|
||||
private void popupMenuListener() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
public class MarkReadResponse {
|
||||
@SuppressWarnings("unused") @Nullable
|
||||
private String result;
|
||||
|
||||
public String result() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static class QueryMarkReadResponse {
|
||||
@SuppressWarnings("unused") @Nullable private MarkReadResponse echomarkread;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
/**
|
||||
* Created by root on 18.12.2017.
|
||||
*/
|
||||
|
||||
public class Notification {
|
||||
public NotificationType notificationType;
|
||||
public String notificationText;
|
||||
public String date;
|
||||
public String description;
|
||||
public String link;
|
||||
|
||||
public Notification(NotificationType notificationType, String notificationText, String date, String description, String link) {
|
||||
this.notificationType = notificationType;
|
||||
this.notificationText = notificationText;
|
||||
this.date = date;
|
||||
this.description = description;
|
||||
this.link = link;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.widget.DividerItemDecoration;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Created by root on 18.12.2017.
|
||||
*/
|
||||
|
||||
public class NotificationActivity extends NavigationBaseActivity {
|
||||
NotificationAdapterFactory notificationAdapterFactory;
|
||||
|
||||
@BindView(R.id.listView) RecyclerView recyclerView;
|
||||
|
||||
@Inject NotificationController controller;
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_notification);
|
||||
ButterKnife.bind(this);
|
||||
initListView();
|
||||
initDrawer();
|
||||
}
|
||||
|
||||
private void initListView() {
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
DividerItemDecoration itemDecor = new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL);
|
||||
recyclerView.addItemDecoration(itemDecor);
|
||||
addNotifications();
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void addNotifications() {
|
||||
Timber.d("Add notifications");
|
||||
|
||||
Observable.fromCallable(() -> controller.getNotifications())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(notificationList -> {
|
||||
Collections.reverse(notificationList);
|
||||
Timber.d("Number of notifications is %d", notificationList.size());
|
||||
setAdapter(notificationList);
|
||||
}, throwable -> Timber.e(throwable, "Error occurred while loading notifications"));
|
||||
}
|
||||
|
||||
private void handleUrl(String url) {
|
||||
if (url == null || url.equals("")) {
|
||||
return;
|
||||
}
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
|
||||
}
|
||||
|
||||
private void setAdapter(List<Notification> notificationList) {
|
||||
notificationAdapterFactory = new NotificationAdapterFactory(notification -> {
|
||||
Timber.d("Notification clicked %s", notification.link);
|
||||
handleUrl(notification.link);
|
||||
});
|
||||
RVRendererAdapter<Notification> adapter = notificationAdapterFactory.create(notificationList);
|
||||
recyclerView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
public static void startYourself(Context context) {
|
||||
Intent intent = new Intent(context, NotificationActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.pedrogomez.renderers.ListAdapteeCollection;
|
||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||
import com.pedrogomez.renderers.RendererBuilder;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by root on 19.12.2017.
|
||||
*/
|
||||
|
||||
class NotificationAdapterFactory {
|
||||
private NotificationRenderer.NotificationClicked listener;
|
||||
|
||||
NotificationAdapterFactory(@NonNull NotificationRenderer.NotificationClicked listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public RVRendererAdapter<Notification> create(List<Notification> notifications) {
|
||||
RendererBuilder<Notification> builder = new RendererBuilder<Notification>()
|
||||
.bind(Notification.class, new NotificationRenderer(listener));
|
||||
ListAdapteeCollection<Notification> collection = new ListAdapteeCollection<>(
|
||||
notifications != null ? notifications : Collections.<Notification>emptyList());
|
||||
return new RVRendererAdapter<>(builder, collection);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
|
||||
/**
|
||||
* Created by root on 19.12.2017.
|
||||
*/
|
||||
@Singleton
|
||||
public class NotificationController {
|
||||
|
||||
private MediaWikiApi mediaWikiApi;
|
||||
private SessionManager sessionManager;
|
||||
|
||||
@Inject
|
||||
public NotificationController(MediaWikiApi mediaWikiApi, SessionManager sessionManager) {
|
||||
this.mediaWikiApi = mediaWikiApi;
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
public List<Notification> getNotifications() throws IOException {
|
||||
if (mediaWikiApi.validateLogin()) {
|
||||
return mediaWikiApi.getNotifications();
|
||||
} else {
|
||||
Boolean authTokenValidated = sessionManager.revalidateAuthToken();
|
||||
if (authTokenValidated != null && authTokenValidated) {
|
||||
return mediaWikiApi.getNotifications();
|
||||
}
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.pedrogomez.renderers.Renderer;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.R;
|
||||
|
||||
/**
|
||||
* Created by root on 19.12.2017.
|
||||
*/
|
||||
|
||||
public class NotificationRenderer extends Renderer<Notification> {
|
||||
@BindView(R.id.title) TextView title;
|
||||
@BindView(R.id.description) TextView description;
|
||||
@BindView(R.id.time) TextView time;
|
||||
@BindView(R.id.icon) ImageView icon;
|
||||
private NotificationClicked listener;
|
||||
|
||||
|
||||
NotificationRenderer(NotificationClicked listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpView(View view) { }
|
||||
|
||||
@Override
|
||||
protected void hookListeners(View rootView) {
|
||||
rootView.setOnClickListener(v -> listener.notificationClicked(getContent()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
|
||||
View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false);
|
||||
ButterKnife.bind(this, inflatedView);
|
||||
return inflatedView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render() {
|
||||
Notification notification = getContent();
|
||||
title.setText(notification.notificationText);
|
||||
time.setText(notification.date);
|
||||
description.setText(notification.description);
|
||||
switch (notification.notificationType) {
|
||||
case THANK_YOU_EDIT:
|
||||
icon.setImageResource(R.drawable.ic_edit_black_24dp);
|
||||
break;
|
||||
default:
|
||||
icon.setImageResource(R.drawable.round_icon_unknown);
|
||||
}
|
||||
}
|
||||
|
||||
public interface NotificationClicked{
|
||||
void notificationClicked(Notification notification);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
public enum NotificationType {
|
||||
THANK_YOU_EDIT("thank-you-edit"),
|
||||
EDIT_USER_TALK("edit-user-talk"),
|
||||
MENTION("mention"),
|
||||
WELCOME("welcome"),
|
||||
UNKNOWN("unknown");
|
||||
private String type;
|
||||
|
||||
NotificationType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public static NotificationType handledValueOf(String name) {
|
||||
for (NotificationType e : values()) {
|
||||
if (e.getType().equals(name)) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.R;
|
||||
|
||||
public class NotificationUtils {
|
||||
|
||||
private static final String COMMONS_WIKI = "commonswiki";
|
||||
|
||||
public static boolean isCommonsNotification(Node document) {
|
||||
if (document == null || !document.hasAttributes()) {
|
||||
return false;
|
||||
}
|
||||
Element element = (Element) document;
|
||||
return COMMONS_WIKI.equals(element.getAttribute("wiki"));
|
||||
}
|
||||
|
||||
public static NotificationType getNotificationType(Node document) {
|
||||
Element element = (Element) document;
|
||||
String type = element.getAttribute("type");
|
||||
return NotificationType.handledValueOf(type);
|
||||
}
|
||||
|
||||
public static Notification getNotificationFromApiResult(Context context, Node document) {
|
||||
NotificationType type = getNotificationType(document);
|
||||
|
||||
String notificationText = "";
|
||||
String link = getNotificationLink(document);
|
||||
String description = getNotificationDescription(document);
|
||||
switch (type) {
|
||||
case THANK_YOU_EDIT:
|
||||
notificationText = context.getString(R.string.notifications_thank_you_edit);
|
||||
break;
|
||||
case EDIT_USER_TALK:
|
||||
notificationText = getUserTalkMessage(context, document);
|
||||
break;
|
||||
case MENTION:
|
||||
notificationText = getMentionMessage(context, document);
|
||||
break;
|
||||
case WELCOME:
|
||||
notificationText = getWelcomeMessage(context, document);
|
||||
break;
|
||||
}
|
||||
return new Notification(type, notificationText, getTimestamp(document), description, link);
|
||||
}
|
||||
|
||||
public static String getMentionMessage(Context context, Node document) {
|
||||
String format = context.getString(R.string.notifications_mention);
|
||||
return String.format(format, getAgent(document), getNotificationDescription(document));
|
||||
}
|
||||
|
||||
public static String getUserTalkMessage(Context context, Node document) {
|
||||
String format = context.getString(R.string.notifications_talk_page_message);
|
||||
return String.format(format, getAgent(document));
|
||||
}
|
||||
|
||||
public static String getWelcomeMessage(Context context, Node document) {
|
||||
String welcomeMessageFormat = context.getString(R.string.notifications_welcome);
|
||||
return String.format(welcomeMessageFormat, getAgent(document));
|
||||
}
|
||||
|
||||
private static String getAgent(Node document) {
|
||||
Element agentElement = (Element) getNode(document, "agent");
|
||||
if (agentElement != null) {
|
||||
return agentElement.getAttribute("name");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String getTimestamp(Node document) {
|
||||
Element timestampElement = (Element) getNode(document, "timestamp");
|
||||
if (timestampElement != null) {
|
||||
return timestampElement.getAttribute("date");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String getNotificationLink(Node document) {
|
||||
String format = "%s%s";
|
||||
Element titleElement = (Element) getNode(document, "title");
|
||||
if (titleElement != null) {
|
||||
String fullName = titleElement.getAttribute("full");
|
||||
return String.format(format, BuildConfig.HOME_URL, fullName);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String getNotificationDescription(Node document) {
|
||||
Element titleElement = (Element) getNode(document, "title");
|
||||
if (titleElement != null) {
|
||||
return titleElement.getAttribute("text");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Node getNode(Node node, String nodeName) {
|
||||
NodeList childNodes = node.getChildNodes();
|
||||
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||
Node nodeItem = childNodes.item(i);
|
||||
Element item = (Element) nodeItem;
|
||||
if (item.getTagName().equals(nodeName)) {
|
||||
return nodeItem;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -25,11 +25,11 @@ import java.io.File;
|
|||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.android.AndroidInjection;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.utils.FileUtils;
|
||||
|
||||
public class SettingsFragment extends PreferenceFragment {
|
||||
|
|
@ -40,8 +40,11 @@ public class SettingsFragment extends PreferenceFragment {
|
|||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
AndroidInjection.inject(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
ApplicationlessInjection
|
||||
.getInstance(getActivity().getApplicationContext())
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
|
||||
// Load the preferences from an XML resource
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
|
@ -67,7 +70,12 @@ public class SettingsFragment extends PreferenceFragment {
|
|||
uploadLimit.setText(uploads + "");
|
||||
uploadLimit.setSummary(uploads + "");
|
||||
uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
int value = Integer.parseInt(newValue.toString());
|
||||
int value;
|
||||
try {
|
||||
value = Integer.parseInt(newValue.toString());
|
||||
} catch(Exception e) {
|
||||
value = 100; //Default number
|
||||
}
|
||||
final SharedPreferences.Editor editor = prefs.edit();
|
||||
if (value > 500) {
|
||||
new AlertDialog.Builder(getActivity())
|
||||
|
|
@ -81,9 +89,9 @@ public class SettingsFragment extends PreferenceFragment {
|
|||
uploadLimit.setSummary(500 + "");
|
||||
uploadLimit.setText(500 + "");
|
||||
} else {
|
||||
editor.putInt(Prefs.UPLOADS_SHOWING, Integer.parseInt(newValue.toString()));
|
||||
editor.putInt(Prefs.UPLOADS_SHOWING, value);
|
||||
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED,true);
|
||||
uploadLimit.setSummary(newValue.toString());
|
||||
uploadLimit.setSummary(String.valueOf(value));
|
||||
}
|
||||
editor.apply();
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import android.content.Intent;
|
|||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import dagger.android.support.DaggerAppCompatActivity;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity;
|
||||
|
||||
public abstract class BaseActivity extends DaggerAppCompatActivity {
|
||||
public abstract class BaseActivity extends CommonsDaggerAppCompatActivity {
|
||||
boolean currentTheme;
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import android.accounts.AccountManager;
|
|||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.NavigationView;
|
||||
import android.support.v4.widget.DrawerLayout;
|
||||
|
|
@ -27,6 +28,7 @@ import fr.free.nrw.commons.auth.AccountUtil;
|
|||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||
import fr.free.nrw.commons.nearby.NearbyActivity;
|
||||
import fr.free.nrw.commons.notification.NotificationActivity;
|
||||
import fr.free.nrw.commons.settings.SettingsActivity;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
@ -118,8 +120,9 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
|||
return true;
|
||||
case R.id.action_feedback:
|
||||
drawerLayout.closeDrawer(navigationView);
|
||||
Intent feedbackIntent = new Intent(Intent.ACTION_SEND);
|
||||
Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO);
|
||||
feedbackIntent.setType("message/rfc822");
|
||||
feedbackIntent.setData(Uri.parse("mailto:"));
|
||||
feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
|
||||
new String[]{CommonsApplication.FEEDBACK_EMAIL});
|
||||
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
|
||||
|
|
@ -143,6 +146,10 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
|||
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
|
||||
.show();
|
||||
return true;
|
||||
case R.id.action_notifications:
|
||||
drawerLayout.closeDrawer(navigationView);
|
||||
NotificationActivity.startYourself(this);
|
||||
return true;
|
||||
default:
|
||||
Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package fr.free.nrw.commons.ui.widget;
|
||||
|
||||
/**
|
||||
* Created by mikel on 07/08/2017.
|
||||
/*
|
||||
*Created by mikel on 07/08/2017.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
|
|
@ -20,20 +20,22 @@ import fr.free.nrw.commons.utils.UiUtils;
|
|||
* a text view compatible with older versions of the platform
|
||||
*/
|
||||
public class CompatTextView extends AppCompatTextView {
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new instance of CompatTextView
|
||||
*
|
||||
* @param context the view context
|
||||
*/
|
||||
public CompatTextView(Context context) {
|
||||
super(context);
|
||||
init(null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new instance of CompatTextView
|
||||
*
|
||||
* @param context the view context
|
||||
* @param attrs the set of attributes for the view
|
||||
* @param attrs the set of attributes for the view
|
||||
*/
|
||||
public CompatTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
|
@ -42,6 +44,7 @@ public class CompatTextView extends AppCompatTextView {
|
|||
|
||||
/**
|
||||
* Constructs a new instance of CompatTextView
|
||||
*
|
||||
* @param context
|
||||
* @param attrs
|
||||
* @param defStyleAttr
|
||||
|
|
@ -53,6 +56,7 @@ public class CompatTextView extends AppCompatTextView {
|
|||
|
||||
/**
|
||||
* initializes the view
|
||||
*
|
||||
* @param attrs the attribute set of the view, which can be null
|
||||
*/
|
||||
private void init(@Nullable AttributeSet attrs) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public abstract class OverlayDialog extends DialogFragment {
|
|||
|
||||
/**
|
||||
* creates a DialogFragment with the correct style and theme
|
||||
* @param savedInstanceState
|
||||
* @param savedInstanceState bundle re-constructed from a previous saved state
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.BitmapRegionDecoder;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import fr.free.nrw.commons.utils.ImageUtils;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Created by bluesir9 on 16/9/17.
|
||||
*
|
||||
* <p>Responsible for checking if the picture that the user is trying to upload is useful or not. Will attempt to filter
|
||||
* away completely black,fuzzy/blurry pictures(for now).
|
||||
*
|
||||
* <p>todo: Detect selfies?
|
||||
*/
|
||||
|
||||
public class DetectUnwantedPicturesAsync extends AsyncTask<Void, Void, ImageUtils.Result> {
|
||||
|
||||
interface Callback {
|
||||
void onResult(ImageUtils.Result result);
|
||||
}
|
||||
|
||||
private final Callback callback;
|
||||
private final String imageMediaFilePath;
|
||||
|
||||
DetectUnwantedPicturesAsync(String imageMediaFilePath, Callback callback) {
|
||||
this.callback = callback;
|
||||
this.imageMediaFilePath = imageMediaFilePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ImageUtils.Result doInBackground(Void... voids) {
|
||||
try {
|
||||
Timber.d("FilePath: " + imageMediaFilePath);
|
||||
if (imageMediaFilePath == null) {
|
||||
return ImageUtils.Result.IMAGE_OK;
|
||||
}
|
||||
|
||||
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(imageMediaFilePath,false);
|
||||
|
||||
return ImageUtils.checkIfImageIsTooDark(decoder);
|
||||
} catch (IOException ioe) {
|
||||
Timber.e(ioe, "IO Exception");
|
||||
return ImageUtils.Result.IMAGE_OK;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(ImageUtils.Result result) {
|
||||
super.onPostExecute(result);
|
||||
//callback to UI so that it can take necessary decision based on the result obtained
|
||||
callback.onResult(result);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,20 +3,25 @@ package fr.free.nrw.commons.upload;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.Date;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
@ -28,7 +33,7 @@ public class FileUtils {
|
|||
* other file-based ContentProviders.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param uri The Uri to query.
|
||||
* @param uri The Uri to query.
|
||||
* @author paulburke
|
||||
*/
|
||||
// Can be safely suppressed, checks for isKitKat before running isDocumentUri
|
||||
|
|
@ -36,6 +41,7 @@ public class FileUtils {
|
|||
@Nullable
|
||||
public static String getPath(Context context, Uri uri) {
|
||||
|
||||
String returnPath = null;
|
||||
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
// DocumentProvider
|
||||
|
|
@ -47,31 +53,34 @@ public class FileUtils {
|
|||
final String type = split[0];
|
||||
|
||||
if ("primary".equalsIgnoreCase(type)) {
|
||||
return Environment.getExternalStorageDirectory() + "/" + split[1];
|
||||
returnPath = Environment.getExternalStorageDirectory() + "/" + split[1];
|
||||
}
|
||||
}
|
||||
// DownloadsProvider
|
||||
else if (isDownloadsDocument(uri)) {
|
||||
} else if (isDownloadsDocument(uri)) { // DownloadsProvider
|
||||
|
||||
final String id = DocumentsContract.getDocumentId(uri);
|
||||
final Uri contentUri = ContentUris.withAppendedId(
|
||||
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
|
||||
|
||||
return getDataColumn(context, contentUri, null, null);
|
||||
}
|
||||
// MediaProvider
|
||||
else if (isMediaDocument(uri)) {
|
||||
returnPath = getDataColumn(context, contentUri, null, null);
|
||||
} else if (isMediaDocument(uri)) { // MediaProvider
|
||||
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
Uri contentUri = null;
|
||||
if ("image".equals(type)) {
|
||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("video".equals(type)) {
|
||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("audio".equals(type)) {
|
||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
switch (type) {
|
||||
case "image":
|
||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
break;
|
||||
case "video":
|
||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||
break;
|
||||
case "audio":
|
||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
final String selection = "_id=?";
|
||||
|
|
@ -79,16 +88,55 @@ public class FileUtils {
|
|||
split[1]
|
||||
};
|
||||
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
returnPath = getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
}
|
||||
}
|
||||
// MediaStore (and general)
|
||||
else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||
return getDataColumn(context, uri, null, null);
|
||||
returnPath = getDataColumn(context, uri, null, null);
|
||||
}
|
||||
// File
|
||||
else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
return uri.getPath();
|
||||
returnPath = uri.getPath();
|
||||
}
|
||||
|
||||
if(returnPath == null) {
|
||||
//fetching path may fail depending on the source URI and all hope is lost
|
||||
//so we will create and use a copy of the file, which seems to work
|
||||
String copyPath = null;
|
||||
try {
|
||||
ParcelFileDescriptor descriptor
|
||||
= context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
if (descriptor != null) {
|
||||
|
||||
SharedPreferences sharedPref = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true);
|
||||
if (useExtStorage) {
|
||||
copyPath = Environment.getExternalStorageDirectory().toString()
|
||||
+ "/CommonsApp/" + new Date().getTime() + ".jpg";
|
||||
File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
|
||||
newFile.mkdir();
|
||||
FileUtils.copy(
|
||||
descriptor.getFileDescriptor(),
|
||||
copyPath);
|
||||
Timber.d("Filepath (copied): %s", copyPath);
|
||||
return copyPath;
|
||||
}
|
||||
copyPath = context.getCacheDir().getAbsolutePath()
|
||||
+ "/" + new Date().getTime() + ".jpg";
|
||||
FileUtils.copy(
|
||||
descriptor.getFileDescriptor(),
|
||||
copyPath);
|
||||
Timber.d("Filepath (copied): %s", copyPath);
|
||||
return copyPath;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Timber.w(e, "Error in file " + copyPath);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return returnPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -109,7 +157,7 @@ public class FileUtils {
|
|||
String[] selectionArgs) {
|
||||
|
||||
Cursor cursor = null;
|
||||
final String column = "_data";
|
||||
final String column = MediaStore.Images.ImageColumns.DATA;
|
||||
final String[] projection = {
|
||||
column
|
||||
};
|
||||
|
|
@ -163,7 +211,8 @@ public class FileUtils {
|
|||
|
||||
/**
|
||||
* Copy content from source file to destination file.
|
||||
* @param source stream copied from
|
||||
*
|
||||
* @param source stream copied from
|
||||
* @param destination stream copied to
|
||||
* @throws IOException thrown when failing to read source or opening destination file
|
||||
*/
|
||||
|
|
@ -176,7 +225,8 @@ public class FileUtils {
|
|||
|
||||
/**
|
||||
* Copy content from source file to destination file.
|
||||
* @param source file descriptor copied from
|
||||
*
|
||||
* @param source file descriptor copied from
|
||||
* @param destination file path copied to
|
||||
* @throws IOException thrown when failing to read source or opening destination file
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -113,11 +113,11 @@ public class GPSExtractor {
|
|||
*/
|
||||
@Nullable
|
||||
public String getCoords(boolean useGPS) {
|
||||
String latitude = "";
|
||||
String longitude = "";
|
||||
String latitude_ref = "";
|
||||
String longitude_ref = "";
|
||||
String decimalCoords = "";
|
||||
String latitude;
|
||||
String longitude;
|
||||
String latitudeRef;
|
||||
String longitudeRef;
|
||||
String decimalCoords;
|
||||
|
||||
//If image has no EXIF data and user has enabled GPS setting, get user's location
|
||||
if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) {
|
||||
|
|
@ -150,15 +150,15 @@ public class GPSExtractor {
|
|||
Timber.d("EXIF data has location info");
|
||||
|
||||
latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
|
||||
latitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
|
||||
latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
|
||||
longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
|
||||
longitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
|
||||
longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
|
||||
|
||||
if (latitude!=null && latitude_ref!=null && longitude!=null && longitude_ref!=null) {
|
||||
Timber.d("Latitude: %s %s", latitude, latitude_ref);
|
||||
Timber.d("Longitude: %s %s", longitude, longitude_ref);
|
||||
if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) {
|
||||
Timber.d("Latitude: %s %s", latitude, latitudeRef);
|
||||
Timber.d("Longitude: %s %s", longitude, longitudeRef);
|
||||
|
||||
decimalCoords = getDecimalCoords(latitude, latitude_ref, longitude, longitude_ref);
|
||||
decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef);
|
||||
return decimalCoords;
|
||||
} else {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package fr.free.nrw.commons.upload;
|
|||
|
||||
import android.Manifest;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
|
@ -12,7 +11,9 @@ import android.database.DataSetObserver;
|
|||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
|
|
@ -22,6 +23,7 @@ import android.view.inputmethod.InputMethodManager;
|
|||
import android.widget.AdapterView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -40,6 +42,7 @@ import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
|||
import fr.free.nrw.commons.modifications.CategoryModifier;
|
||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import timber.log.Timber;
|
||||
|
|
@ -51,10 +54,17 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
|||
MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
|
||||
OnCategoriesSaveHandler {
|
||||
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject SessionManager sessionManager;
|
||||
@Inject UploadController uploadController;
|
||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||
@Inject
|
||||
MediaWikiApi mwApi;
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
@Inject
|
||||
UploadController uploadController;
|
||||
@Inject
|
||||
ModifierSequenceDao modifierSequenceDao;
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
SharedPreferences prefs;
|
||||
|
||||
private ArrayList<Contribution> photosList = null;
|
||||
|
||||
|
|
@ -62,6 +72,8 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
|||
private MediaDetailPagerFragment mediaDetails;
|
||||
private CategorizationFragment categorizationFragment;
|
||||
|
||||
private boolean locationPermitted = false;
|
||||
|
||||
@Override
|
||||
public Media getMediaAtPosition(int i) {
|
||||
return photosList.get(i);
|
||||
|
|
@ -165,20 +177,18 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
|||
@Override
|
||||
public void onCategoriesSave(List<String> categories) {
|
||||
if (categories.size() > 0) {
|
||||
ContentProviderClient client = getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY);
|
||||
for (Contribution contribution : photosList) {
|
||||
ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
|
||||
|
||||
categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
|
||||
categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
|
||||
|
||||
categoriesSequence.setContentProviderClient(client);
|
||||
categoriesSequence.save();
|
||||
modifierSequenceDao.save(categoriesSequence);
|
||||
}
|
||||
}
|
||||
// FIXME: Make sure that the content provider is up
|
||||
// This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
|
||||
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
|
||||
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, true); // Enable sync by default!
|
||||
finish();
|
||||
}
|
||||
|
||||
|
|
@ -208,6 +218,14 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
|||
|
||||
getSupportFragmentManager().addOnBackStackChangedListener(this);
|
||||
requestAuthToken();
|
||||
|
||||
//TODO: 15/10/17 should location permission be explicitly requested if not provided?
|
||||
//check if location permission is enabled
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||
locationPermitted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -241,7 +259,7 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
|||
mwApi.setAuthCookie(authCookie);
|
||||
Intent intent = getIntent();
|
||||
|
||||
if (intent.getAction().equals(Intent.ACTION_SEND_MULTIPLE)) {
|
||||
if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
|
||||
if (photosList == null) {
|
||||
photosList = new ArrayList<>();
|
||||
ArrayList<Uri> urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||
|
|
@ -253,6 +271,11 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
|||
up.setTag("sequence", i);
|
||||
up.setSource(Contribution.SOURCE_EXTERNAL);
|
||||
up.setMultiple(true);
|
||||
String imageGpsCoordinates = extractImageGpsData(uri);
|
||||
if (imageGpsCoordinates != null) {
|
||||
Timber.d("GPS data for image found!");
|
||||
up.setDecimalCoords(imageGpsCoordinates);
|
||||
}
|
||||
photosList.add(up);
|
||||
}
|
||||
}
|
||||
|
|
@ -279,7 +302,49 @@ public class MultipleShareActivity extends AuthenticatedActivity
|
|||
|
||||
@Override
|
||||
public void onBackStackChanged() {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible()) ;
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible());
|
||||
}
|
||||
|
||||
/**
|
||||
* Will attempt to extract the gps coordinates using exif data or by using the current
|
||||
* location if available for the image who's imageUri has been provided.
|
||||
* @param imageUri The uri of the image who's GPS coordinates data we wish to extract
|
||||
* @return GPS coordinates as a String as is returned by {@link GPSExtractor}
|
||||
*/
|
||||
@Nullable
|
||||
private String extractImageGpsData(Uri imageUri) {
|
||||
Timber.d("Entering extractImagesGpsData");
|
||||
|
||||
if (imageUri == null) {
|
||||
//now why would you do that???
|
||||
return null;
|
||||
}
|
||||
|
||||
GPSExtractor gpsExtractor = null;
|
||||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(imageUri,"r");
|
||||
if (fd != null) {
|
||||
gpsExtractor = new GPSExtractor(fd.getFileDescriptor(),this,prefs);
|
||||
}
|
||||
} else {
|
||||
String filePath = FileUtils.getPath(this,imageUri);
|
||||
if (filePath != null) {
|
||||
gpsExtractor = new GPSExtractor(filePath,this,prefs);
|
||||
}
|
||||
}
|
||||
|
||||
if (gpsExtractor != null) {
|
||||
//get image coordinates from exif data or user location
|
||||
return gpsExtractor.getCoords(locationPermitted);
|
||||
}
|
||||
|
||||
} catch (FileNotFoundException fnfe) {
|
||||
Timber.w(fnfe);
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,17 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
|
|
@ -27,12 +30,12 @@ import android.widget.TextView;
|
|||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||
import com.facebook.drawee.view.SimpleDraweeView;
|
||||
|
||||
import dagger.android.support.DaggerFragment;
|
||||
import dagger.android.support.AndroidSupportInjection;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
|
||||
public class MultipleUploadListFragment extends DaggerFragment {
|
||||
public class MultipleUploadListFragment extends Fragment {
|
||||
|
||||
public interface OnMultipleUploadInitiatedHandler {
|
||||
void OnMultipleUploadInitiated();
|
||||
|
|
@ -56,6 +59,12 @@ public class MultipleUploadListFragment extends DaggerFragment {
|
|||
private RelativeLayout overlay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
AndroidSupportInjection.inject(this);
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
private class PhotoDisplayAdapter extends BaseAdapter {
|
||||
|
||||
@Override
|
||||
|
|
@ -170,9 +179,21 @@ public class MultipleUploadListFragment extends DaggerFragment {
|
|||
photosGrid.setColumnWidth(photoSize.x);
|
||||
|
||||
baseTitle.addTextChangedListener(textWatcher);
|
||||
|
||||
baseTitle.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
hideKeyboard(v);
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public void hideKeyboard(View view) {
|
||||
InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
baseTitle.removeTextChangedListener(textWatcher);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ import android.support.annotation.RequiresApi;
|
|||
import android.support.design.widget.Snackbar;
|
||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
|
@ -46,10 +48,16 @@ import fr.free.nrw.commons.caching.CacheController;
|
|||
import fr.free.nrw.commons.category.CategorizationFragment;
|
||||
import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||
import fr.free.nrw.commons.modifications.CategoryModifier;
|
||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
import fr.free.nrw.commons.utils.ImageUtils;
|
||||
>>>>>>> directNearbyUploadsNewLocal
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
@ -60,10 +68,10 @@ import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE;
|
|||
* Activity for the title/desc screen after image is selected. Also starts processing image
|
||||
* GPS coordinates or user location (if enabled in Settings) for category suggestions.
|
||||
*/
|
||||
public class ShareActivity
|
||||
extends AuthenticatedActivity
|
||||
public class ShareActivity
|
||||
extends AuthenticatedActivity
|
||||
implements SingleUploadFragment.OnUploadActionInitiated,
|
||||
OnCategoriesSaveHandler {
|
||||
OnCategoriesSaveHandler,SimilarImageDialogFragment.onResponse {
|
||||
|
||||
private static final int REQUEST_PERM_ON_CREATE_STORAGE = 1;
|
||||
private static final int REQUEST_PERM_ON_CREATE_LOCATION = 2;
|
||||
|
|
@ -71,11 +79,19 @@ public class ShareActivity
|
|||
private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4;
|
||||
private CategorizationFragment categorizationFragment;
|
||||
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject CacheController cacheController;
|
||||
@Inject SessionManager sessionManager;
|
||||
@Inject UploadController uploadController;
|
||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||
@Inject
|
||||
MediaWikiApi mwApi;
|
||||
@Inject
|
||||
CacheController cacheController;
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
@Inject
|
||||
UploadController uploadController;
|
||||
@Inject
|
||||
ModifierSequenceDao modifierSequenceDao;
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
SharedPreferences prefs;
|
||||
|
||||
private String source;
|
||||
private String mimeType;
|
||||
|
|
@ -87,6 +103,7 @@ public class ShareActivity
|
|||
private boolean cacheFound;
|
||||
|
||||
private GPSExtractor imageObj;
|
||||
private GPSExtractor tempImageObj;
|
||||
private String decimalCoords;
|
||||
|
||||
private boolean useNewPermissions = false;
|
||||
|
|
@ -97,9 +114,14 @@ public class ShareActivity
|
|||
private String description;
|
||||
private Snackbar snackbar;
|
||||
private boolean duplicateCheckPassed = false;
|
||||
<<<<<<< HEAD
|
||||
|
||||
private boolean isNearbyUpload = false;
|
||||
|
||||
=======
|
||||
private boolean haveCheckedForOtherImages = false;
|
||||
private boolean isNearbyUpload = false;
|
||||
>>>>>>> directNearbyUploadsNewLocal
|
||||
/**
|
||||
* Called when user taps the submit button.
|
||||
*/
|
||||
|
|
@ -167,13 +189,12 @@ public class ShareActivity
|
|||
|
||||
categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
|
||||
categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
|
||||
categoriesSequence.setContentProviderClient(getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY));
|
||||
categoriesSequence.save();
|
||||
modifierSequenceDao.save(categoriesSequence);
|
||||
}
|
||||
|
||||
// FIXME: Make sure that the content provider is up
|
||||
// This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
|
||||
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
|
||||
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, true); // Enable sync by default!
|
||||
|
||||
finish();
|
||||
}
|
||||
|
|
@ -221,7 +242,7 @@ public class ShareActivity
|
|||
//Receive intent from ContributionController.java when user selects picture to upload
|
||||
Intent intent = getIntent();
|
||||
|
||||
if (intent.getAction().equals(Intent.ACTION_SEND)) {
|
||||
if (Intent.ACTION_SEND.equals(intent.getAction())) {
|
||||
mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
|
||||
source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
|
||||
|
|
@ -287,7 +308,7 @@ public class ShareActivity
|
|||
REQUEST_PERM_ON_CREATE_LOCATION);
|
||||
}
|
||||
}
|
||||
performPreuploadProcessingOfFile();
|
||||
performPreUploadProcessingOfFile();
|
||||
|
||||
|
||||
SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView");
|
||||
|
|
@ -311,7 +332,7 @@ public class ShareActivity
|
|||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
backgroundImageView.setImageURI(mediaUri);
|
||||
storagePermitted = true;
|
||||
performPreuploadProcessingOfFile();
|
||||
performPreUploadProcessingOfFile();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -319,7 +340,7 @@ public class ShareActivity
|
|||
if (grantResults.length >= 1
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
locationPermitted = true;
|
||||
performPreuploadProcessingOfFile();
|
||||
performPreUploadProcessingOfFile();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -328,12 +349,12 @@ public class ShareActivity
|
|||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
backgroundImageView.setImageURI(mediaUri);
|
||||
storagePermitted = true;
|
||||
performPreuploadProcessingOfFile();
|
||||
performPreUploadProcessingOfFile();
|
||||
}
|
||||
if (grantResults.length >= 2
|
||||
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
|
||||
locationPermitted = true;
|
||||
performPreuploadProcessingOfFile();
|
||||
performPreUploadProcessingOfFile();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -344,7 +365,7 @@ public class ShareActivity
|
|||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
//It is OK to call this at both (1) and (4) because if perm had been granted at
|
||||
//snackbar, user should not be prompted at submit button
|
||||
performPreuploadProcessingOfFile();
|
||||
performPreUploadProcessingOfFile();
|
||||
|
||||
//Uploading only begins if storage permission granted from arrow icon
|
||||
uploadBegins();
|
||||
|
|
@ -355,7 +376,7 @@ public class ShareActivity
|
|||
}
|
||||
}
|
||||
|
||||
private void performPreuploadProcessingOfFile() {
|
||||
private void performPreUploadProcessingOfFile() {
|
||||
if (!useNewPermissions || storagePermitted) {
|
||||
if (!duplicateCheckPassed) {
|
||||
//Test SHA1 of image to see if it matches SHA1 of a file on Commons
|
||||
|
|
@ -370,7 +391,17 @@ public class ShareActivity
|
|||
Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
|
||||
duplicateCheckPassed = (result == DUPLICATE_PROCEED
|
||||
|| result == NO_DUPLICATE);
|
||||
}, mwApi);
|
||||
/*
|
||||
TODO: 16/9/17 should we run DetectUnwantedPicturesAsync if DUPLICATE_PROCEED is returned? Since that means
|
||||
we are processing images that are already on server???...
|
||||
*/
|
||||
|
||||
if (duplicateCheckPassed) {
|
||||
//image can be uploaded, so now check if its a useless picture or not
|
||||
performUnwantedPictureDetectionProcess();
|
||||
}
|
||||
|
||||
},mwApi);
|
||||
fileAsyncTask.execute();
|
||||
} catch (IOException e) {
|
||||
Timber.d(e, "IO Exception: ");
|
||||
|
|
@ -384,6 +415,37 @@ public class ShareActivity
|
|||
}
|
||||
}
|
||||
|
||||
private void performUnwantedPictureDetectionProcess() {
|
||||
String imageMediaFilePath = FileUtils.getPath(this,mediaUri);
|
||||
DetectUnwantedPicturesAsync detectUnwantedPicturesAsync = new DetectUnwantedPicturesAsync(imageMediaFilePath, result -> {
|
||||
|
||||
if (result != ImageUtils.Result.IMAGE_OK) {
|
||||
//show appropriate error message
|
||||
String errorMessage = result == ImageUtils.Result.IMAGE_DARK ? getString(R.string.upload_image_too_dark) : getString(R.string.upload_image_blurry);
|
||||
AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(this);
|
||||
errorDialogBuilder.setMessage(errorMessage);
|
||||
errorDialogBuilder.setTitle(getString(R.string.warning));
|
||||
errorDialogBuilder.setPositiveButton(getString(R.string.no), (dialogInterface, i) -> {
|
||||
//user does not wish to upload the picture, take them back to ContributionsActivity
|
||||
Intent intent = new Intent(ShareActivity.this, ContributionsActivity.class);
|
||||
dialogInterface.dismiss();
|
||||
startActivity(intent);
|
||||
});
|
||||
errorDialogBuilder.setNegativeButton(getString(R.string.yes), (dialogInterface, i) -> {
|
||||
//user wishes to go ahead with the upload of this picture, just dismiss this dialog
|
||||
dialogInterface.dismiss();
|
||||
});
|
||||
|
||||
AlertDialog errorDialog = errorDialogBuilder.create();
|
||||
if (!isFinishing()) {
|
||||
errorDialog.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
detectUnwantedPicturesAsync.execute();
|
||||
}
|
||||
|
||||
private Snackbar requestPermissionUsingSnackBar(String rationale,
|
||||
final String[] perms,
|
||||
final int code) {
|
||||
|
|
@ -461,13 +523,93 @@ public class ShareActivity
|
|||
if (imageObj != null) {
|
||||
// Gets image coords from exif data or user location
|
||||
decimalCoords = imageObj.getCoords(gpsEnabled);
|
||||
useImageCoords();
|
||||
if(decimalCoords==null || !imageObj.imageCoordsExists){
|
||||
// Check if the location is from GPS or EXIF
|
||||
// Find other photos taken around the same time which has gps coordinates
|
||||
Timber.d("EXIF:false");
|
||||
Timber.d("EXIF call"+(imageObj==tempImageObj));
|
||||
if(!haveCheckedForOtherImages)
|
||||
findOtherImages(gpsEnabled);// Do not do repeat the process
|
||||
}
|
||||
else {
|
||||
// As the selected image has GPS data in EXIF go ahead with the same.
|
||||
useImageCoords();
|
||||
}
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
Timber.w("File not found: " + mediaUri, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void findOtherImages(boolean gpsEnabled) {
|
||||
Timber.d("filePath"+getPathOfMediaOrCopy());
|
||||
String filePath = getPathOfMediaOrCopy();
|
||||
long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created
|
||||
File folder = new File(filePath.substring(0,filePath.lastIndexOf('/')));
|
||||
File[] files = folder.listFiles();
|
||||
Timber.d("folderTime Number:"+files.length);
|
||||
|
||||
for(File file : files){
|
||||
if(file.lastModified()-timeOfCreation<=(120*1000) && file.lastModified()-timeOfCreation>=-(120*1000)){
|
||||
//Make sure the photos were taken within 20seconds
|
||||
Timber.d("fild date:"+file.lastModified()+ " time of creation"+timeOfCreation);
|
||||
tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos
|
||||
ParcelFileDescriptor descriptor
|
||||
= null;
|
||||
try {
|
||||
descriptor = getContentResolver().openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r");
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
if (descriptor != null) {
|
||||
tempImageObj = new GPSExtractor(descriptor.getFileDescriptor(),this, prefs);
|
||||
}
|
||||
} else {
|
||||
if (filePath != null) {
|
||||
tempImageObj = new GPSExtractor(file.getAbsolutePath(), this, prefs);
|
||||
}
|
||||
}
|
||||
|
||||
if(tempImageObj!=null){
|
||||
Timber.d("not null fild EXIF"+tempImageObj.imageCoordsExists +" coords"+tempImageObj.getCoords(gpsEnabled));
|
||||
if(tempImageObj.getCoords(gpsEnabled)!=null && tempImageObj.imageCoordsExists){
|
||||
// Current image has gps coordinates and it's not current gps locaiton
|
||||
Timber.d("This fild has image coords:"+ file.getAbsolutePath());
|
||||
// Create a dialog fragment for the suggestion
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString("originalImagePath",filePath);
|
||||
args.putString("possibleImagePath",file.getAbsolutePath());
|
||||
newFragment.setArguments(args);
|
||||
newFragment.show(fragmentManager, "dialog");
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
haveCheckedForOtherImages = true; //Finished checking for other images
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostiveResponse() {
|
||||
imageObj = tempImageObj;
|
||||
decimalCoords = imageObj.getCoords(false);// Not necessary to use gps as image already ha EXIF data
|
||||
Timber.d("EXIF from tempImageObj");
|
||||
useImageCoords();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNegativeResponse() {
|
||||
Timber.d("EXIF from imageObj");
|
||||
useImageCoords();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates retrieval of image coordinates or user coordinates, and caching of coordinates.
|
||||
* Then initiates the calls to MediaWiki API through an instance of MwVolleyApi.
|
||||
|
|
@ -475,6 +617,7 @@ public class ShareActivity
|
|||
public void useImageCoords() {
|
||||
if (decimalCoords != null) {
|
||||
Timber.d("Decimal coords of image: %s", decimalCoords);
|
||||
Timber.d("is EXIF data present:"+imageObj.imageCoordsExists+" from findOther image:"+(imageObj==tempImageObj));
|
||||
|
||||
// Only set cache for this point if image has coords
|
||||
if (imageObj.imageCoordsExists) {
|
||||
|
|
@ -498,7 +641,10 @@ public class ShareActivity
|
|||
Timber.d("Cache found, setting categoryList in MwVolleyApi to %s", displayCatList);
|
||||
MwVolleyApi.setGpsCat(displayCatList);
|
||||
}
|
||||
}else{
|
||||
Timber.d("EXIF: no coords");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.widget.Button;
|
||||
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||
import com.facebook.drawee.view.SimpleDraweeView;
|
||||
import com.facebook.imagepipeline.listener.RequestListener;
|
||||
import com.facebook.imagepipeline.listener.RequestLoggingListener;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
|
||||
/**
|
||||
* Created by harisanker on 14/2/18.
|
||||
*/
|
||||
|
||||
public class SimilarImageDialogFragment extends DialogFragment {
|
||||
SimpleDraweeView originalImage;
|
||||
SimpleDraweeView possibleImage;
|
||||
Button positiveButton;
|
||||
Button negativeButton;
|
||||
onResponse mOnResponse;//Implemented interface from shareActivity
|
||||
Boolean gotResponse = false;
|
||||
public SimilarImageDialogFragment() {
|
||||
}
|
||||
public interface onResponse{
|
||||
public void onPostiveResponse();
|
||||
public void onNegativeResponse();
|
||||
}
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_similar_image_dialog, container, false);
|
||||
Set<RequestListener> requestListeners = new HashSet<>();
|
||||
requestListeners.add(new RequestLoggingListener());
|
||||
|
||||
originalImage =(SimpleDraweeView) view.findViewById(R.id.orginalImage);
|
||||
possibleImage =(SimpleDraweeView) view.findViewById(R.id.possibleImage);
|
||||
positiveButton = (Button) view.findViewById(R.id.postive_button);
|
||||
negativeButton = (Button) view.findViewById(R.id.negative_button);
|
||||
|
||||
originalImage.setHierarchy(GenericDraweeHierarchyBuilder
|
||||
.newInstance(getResources())
|
||||
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
||||
R.drawable.ic_image_black_24dp,getContext().getTheme()))
|
||||
.setFailureImage(VectorDrawableCompat.create(getResources(),
|
||||
R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
|
||||
.build());
|
||||
possibleImage.setHierarchy(GenericDraweeHierarchyBuilder
|
||||
.newInstance(getResources())
|
||||
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
||||
R.drawable.ic_image_black_24dp,getContext().getTheme()))
|
||||
.setFailureImage(VectorDrawableCompat.create(getResources(),
|
||||
R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
|
||||
.build());
|
||||
|
||||
originalImage.setImageURI(Uri.fromFile(new File(getArguments().getString("originalImagePath"))));
|
||||
possibleImage.setImageURI(Uri.fromFile(new File(getArguments().getString("possibleImagePath"))));
|
||||
|
||||
negativeButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
mOnResponse.onNegativeResponse();
|
||||
gotResponse = true;
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
positiveButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
mOnResponse.onPostiveResponse();
|
||||
gotResponse = true;
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mOnResponse = (onResponse) getActivity();//Interface Implementation
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
Dialog dialog = super.onCreateDialog(savedInstanceState);
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
// I user dismisses dialog by pressing outside the dialog.
|
||||
if(!gotResponse)
|
||||
mOnResponse.onNegativeResponse();
|
||||
super.onDismiss(dialog);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
import android.app.Activity;
|
||||
>>>>>>> directNearbyUploadsNewLocal
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
|
@ -12,6 +16,7 @@ import android.support.annotation.NonNull;
|
|||
import android.support.v7.app.AlertDialog;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
|
|
@ -37,16 +42,16 @@ import butterknife.ButterKnife;
|
|||
import butterknife.OnClick;
|
||||
import butterknife.OnItemSelected;
|
||||
import butterknife.OnTouch;
|
||||
import dagger.android.support.DaggerFragment;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.view.MotionEvent.ACTION_DOWN;
|
||||
import static android.view.MotionEvent.ACTION_UP;
|
||||
|
||||
public class SingleUploadFragment extends DaggerFragment {
|
||||
public class SingleUploadFragment extends CommonsDaggerSupportFragment {
|
||||
|
||||
@BindView(R.id.titleEdit) EditText titleEdit;
|
||||
@BindView(R.id.descEdit) EditText descEdit;
|
||||
|
|
@ -147,11 +152,29 @@ public class SingleUploadFragment extends DaggerFragment {
|
|||
|
||||
titleEdit.addTextChangedListener(textWatcher);
|
||||
|
||||
titleEdit.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
hideKeyboard(v);
|
||||
}
|
||||
});
|
||||
|
||||
descEdit.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if(!hasFocus){
|
||||
hideKeyboard(v);
|
||||
}
|
||||
});
|
||||
|
||||
setLicenseSummary(license);
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
public void hideKeyboard(View view) {
|
||||
Log.i("hide", "hideKeyboard: ");
|
||||
InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
titleEdit.removeTextChangedListener(textWatcher);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
|
|
@ -36,6 +37,9 @@ public class UploadController {
|
|||
void onUploadStarted(Contribution contribution);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new UploadController.
|
||||
*/
|
||||
public UploadController(SessionManager sessionManager, Context context, SharedPreferences sharedPreferences) {
|
||||
this.sessionManager = sessionManager;
|
||||
this.context = context;
|
||||
|
|
@ -46,17 +50,20 @@ public class UploadController {
|
|||
private ServiceConnection uploadServiceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName componentName, IBinder binder) {
|
||||
uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder)binder).getService();
|
||||
uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder).getService();
|
||||
isUploadServiceConnected = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName componentName) {
|
||||
// this should never happen
|
||||
throw new RuntimeException("UploadService died but the rest of the process did not!");
|
||||
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepares the upload service.
|
||||
*/
|
||||
public void prepareService() {
|
||||
Intent uploadServiceIntent = new Intent(context, UploadService.class);
|
||||
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
||||
|
|
@ -64,12 +71,26 @@ public class UploadController {
|
|||
context.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the upload service.
|
||||
*/
|
||||
public void cleanup() {
|
||||
if (isUploadServiceConnected) {
|
||||
context.unbindService(uploadServiceConnection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new upload task.
|
||||
*
|
||||
* @param title the title of the contribution
|
||||
* @param mediaUri the media URI of the contribution
|
||||
* @param description the description of the contribution
|
||||
* @param mimeType the MIME type of the contribution
|
||||
* @param source the source of the contribution
|
||||
* @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615")
|
||||
* @param onComplete the progress tracker
|
||||
*/
|
||||
public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, ContributionUploadProgress onComplete) {
|
||||
Contribution contribution;
|
||||
|
||||
|
|
@ -85,6 +106,12 @@ public class UploadController {
|
|||
startUpload(contribution, onComplete);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new upload task.
|
||||
*
|
||||
* @param contribution the contribution object
|
||||
* @param onComplete the progress tracker
|
||||
*/
|
||||
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
|
||||
//Set creator, desc, and license
|
||||
if (TextUtils.isEmpty(contribution.getCreator())) {
|
||||
|
|
@ -110,15 +137,17 @@ public class UploadController {
|
|||
ContentResolver contentResolver = context.getContentResolver();
|
||||
try {
|
||||
if (contribution.getDataLength() <= 0) {
|
||||
length = contentResolver
|
||||
.openAssetFileDescriptor(contribution.getLocalUri(), "r")
|
||||
.getLength();
|
||||
if (length == -1) {
|
||||
// Let us find out the long way!
|
||||
length = countBytes(contentResolver
|
||||
.openInputStream(contribution.getLocalUri()));
|
||||
AssetFileDescriptor assetFileDescriptor = contentResolver
|
||||
.openAssetFileDescriptor(contribution.getLocalUri(), "r");
|
||||
if (assetFileDescriptor != null) {
|
||||
length = assetFileDescriptor.getLength();
|
||||
if (length == -1) {
|
||||
// Let us find out the long way!
|
||||
length = countBytes(contentResolver
|
||||
.openInputStream(contribution.getLocalUri()));
|
||||
}
|
||||
contribution.setDataLength(length);
|
||||
}
|
||||
contribution.setDataLength(length);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "IO Exception: ");
|
||||
|
|
@ -128,7 +157,7 @@ public class UploadController {
|
|||
Timber.e(e, "Security Exception: ");
|
||||
}
|
||||
|
||||
String mimeType = (String)contribution.getTag("mimeType");
|
||||
String mimeType = (String) contribution.getTag("mimeType");
|
||||
Boolean imagePrefix = false;
|
||||
|
||||
if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
|
||||
|
|
@ -173,6 +202,13 @@ public class UploadController {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Counts the number of bytes in {@code stream}.
|
||||
*
|
||||
* @param stream the stream
|
||||
* @return the number of bytes in {@code stream}
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
private long countBytes(InputStream stream) throws IOException {
|
||||
long count = 0;
|
||||
BufferedInputStream bis = new BufferedInputStream(stream);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import android.annotation.SuppressLint;
|
|||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Intent;
|
||||
|
|
@ -31,6 +30,7 @@ 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.Contribution;
|
||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||
|
|
@ -51,9 +51,9 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
@Inject MediaWikiApi mwApi;
|
||||
@Inject SessionManager sessionManager;
|
||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
||||
@Inject ContributionDao contributionDao;
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
private ContentProviderClient contributionsProviderClient;
|
||||
private NotificationCompat.Builder curProgressNotification;
|
||||
private int toUpload;
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build());
|
||||
|
||||
contribution.setTransferred(transferred);
|
||||
contribution.save();
|
||||
contributionDao.save(contribution);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -113,7 +113,6 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
contributionsProviderClient.release();
|
||||
Timber.d("UploadService.onDestroy; %s are yet to be uploaded", unfinishedUploads);
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +121,6 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
super.onCreate();
|
||||
|
||||
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
contributionsProviderClient = this.getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -144,9 +142,7 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
|
||||
contribution.setState(Contribution.STATE_QUEUED);
|
||||
contribution.setTransferred(0);
|
||||
contribution.setContentProviderClient(contributionsProviderClient);
|
||||
|
||||
contribution.save();
|
||||
contributionDao.save(contribution);
|
||||
toUpload++;
|
||||
if (curProgressNotification != null && toUpload != 1) {
|
||||
curProgressNotification.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload));
|
||||
|
|
@ -165,13 +161,13 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent.getAction().equals(ACTION_START_SERVICE) && freshStart) {
|
||||
if (ACTION_START_SERVICE.equals(intent.getAction()) && freshStart) {
|
||||
ContentValues failedValues = new ContentValues();
|
||||
failedValues.put(Contribution.Table.COLUMN_STATE, Contribution.STATE_FAILED);
|
||||
failedValues.put(ContributionDao.Table.COLUMN_STATE, Contribution.STATE_FAILED);
|
||||
|
||||
int updated = getContentResolver().update(ContributionsContentProvider.BASE_URI,
|
||||
failedValues,
|
||||
Contribution.Table.COLUMN_STATE + " = ? OR " + Contribution.Table.COLUMN_STATE + " = ?",
|
||||
ContributionDao.Table.COLUMN_STATE + " = ? OR " + ContributionDao.Table.COLUMN_STATE + " = ?",
|
||||
new String[]{ String.valueOf(Contribution.STATE_QUEUED), String.valueOf(Contribution.STATE_IN_PROGRESS) }
|
||||
);
|
||||
Timber.d("Set %d uploads to failed", updated);
|
||||
|
|
@ -261,7 +257,7 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
contribution.setImageUrl(uploadResult.getImageUrl());
|
||||
contribution.setState(Contribution.STATE_COMPLETED);
|
||||
contribution.setDateUploaded(uploadResult.getDateUploaded());
|
||||
contribution.save();
|
||||
contributionDao.save(contribution);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Timber.d("I have a network fuckup");
|
||||
|
|
@ -273,7 +269,7 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
toUpload--;
|
||||
if (toUpload == 0) {
|
||||
// Sync modifications right after all uplaods are processed
|
||||
ContentResolver.requestSync(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, new Bundle());
|
||||
ContentResolver.requestSync(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, new Bundle());
|
||||
stopForeground(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -292,7 +288,7 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
notificationManager.notify(NOTIFICATION_UPLOAD_FAILED, failureNotification);
|
||||
|
||||
contribution.setState(Contribution.STATE_FAILED);
|
||||
contribution.save();
|
||||
contributionDao.save(contribution);
|
||||
}
|
||||
|
||||
private String findUniqueFilename(String fileName) throws IOException {
|
||||
|
|
|
|||
36
app/src/main/java/fr/free/nrw/commons/utils/DateUtils.java
Normal file
36
app/src/main/java/fr/free/nrw/commons/utils/DateUtils.java
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package fr.free.nrw.commons.utils;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
public class DateUtils {
|
||||
public static String getTimeAgo(Date currDate, Date itemDate) {
|
||||
Calendar c = Calendar.getInstance();
|
||||
c.setTime(currDate);
|
||||
int yearNow = c.get(Calendar.YEAR);
|
||||
int monthNow = c.get(Calendar.MONTH);
|
||||
int dayNow = c.get(Calendar.DAY_OF_MONTH);
|
||||
int hourNow = c.get(Calendar.HOUR_OF_DAY);
|
||||
int minuteNow = c.get(Calendar.MINUTE);
|
||||
c.setTime(itemDate);
|
||||
int videoYear = c.get(Calendar.YEAR);
|
||||
int videoMonth = c.get(Calendar.MONTH);
|
||||
int videoDays = c.get(Calendar.DAY_OF_MONTH);
|
||||
int videoHour = c.get(Calendar.HOUR_OF_DAY);
|
||||
int videoMinute = c.get(Calendar.MINUTE);
|
||||
|
||||
if (yearNow != videoYear) {
|
||||
return (String.valueOf(yearNow - videoYear) + "-" + "years");
|
||||
} else if (monthNow != videoMonth) {
|
||||
return (String.valueOf(monthNow - videoMonth) + "-" + "months");
|
||||
} else if (dayNow != videoDays) {
|
||||
return (String.valueOf(dayNow - videoDays) + "-" + "days");
|
||||
} else if (hourNow != videoHour) {
|
||||
return (String.valueOf(hourNow - videoHour) + "-" + "hours");
|
||||
} else if (minuteNow != videoMinute) {
|
||||
return (String.valueOf(minuteNow - videoMinute) + "-" + "minutes");
|
||||
} else {
|
||||
return "0-seconds";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,11 @@ import timber.log.Timber;
|
|||
|
||||
public class DialogUtil {
|
||||
|
||||
/**
|
||||
* Dismisses a dialog safely.
|
||||
* @param activity the activity
|
||||
* @param dialog the dialog to be dismissed
|
||||
*/
|
||||
public static void dismissSafely(@Nullable Activity activity, @Nullable DialogFragment dialog) {
|
||||
boolean isActivityDestroyed = false;
|
||||
|
||||
|
|
@ -33,6 +38,11 @@ public class DialogUtil {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog safely.
|
||||
* @param activity the activity
|
||||
* @param dialog the dialog to be shown
|
||||
*/
|
||||
public static void showSafely(Activity activity, Dialog dialog) {
|
||||
if (activity == null || dialog == null) {
|
||||
Timber.d("Show called with null activity / dialog. Ignoring.");
|
||||
|
|
@ -54,6 +64,11 @@ public class DialogUtil {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog safely.
|
||||
* @param activity the activity
|
||||
* @param dialog the dialog to be shown
|
||||
*/
|
||||
public static void showSafely(FragmentActivity activity, DialogFragment dialog) {
|
||||
boolean isActivityDestroyed = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ public class ExecutorUtils {
|
|||
}
|
||||
};
|
||||
|
||||
public static Executor uiExecutor() { return uiExecutor; }
|
||||
public static Executor uiExecutor() {
|
||||
return uiExecutor;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,31 +4,34 @@ import android.os.Environment;
|
|||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class FileUtils {
|
||||
/**
|
||||
* Read and return the content of a resource file as string.
|
||||
*
|
||||
* @param fileName asset file's path (e.g. "/assets/queries/nearby_query.rq")
|
||||
* @param fileName asset file's path (e.g. "/queries/nearby_query.rq")
|
||||
* @return the content of the file
|
||||
*/
|
||||
public static String readFromResource(String fileName) throws IOException {
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
reader = new BufferedReader(
|
||||
new InputStreamReader(
|
||||
CommonsApplication.class.getResourceAsStream(fileName), "UTF-8"));
|
||||
InputStream inputStream = FileUtils.class.getResourceAsStream(fileName);
|
||||
if (inputStream == null) {
|
||||
throw new FileNotFoundException(fileName);
|
||||
}
|
||||
reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
buffer.append(line + "\n");
|
||||
buffer.append(line).append("\n");
|
||||
}
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
|
|
|
|||
135
app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java
Normal file
135
app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
package fr.free.nrw.commons.utils;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapRegionDecoder;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Created by bluesir9 on 3/10/17.
|
||||
*/
|
||||
|
||||
public class ImageUtils {
|
||||
//atleast 50% of the image in question should be considered dark for the entire image to be dark
|
||||
private static final double MINIMUM_DARKNESS_FACTOR = 0.50;
|
||||
//atleast 50% of the image in question should be considered blurry for the entire image to be blurry
|
||||
private static final double MINIMUM_BLURRYNESS_FACTOR = 0.50;
|
||||
private static final int LAPLACIAN_VARIANCE_THRESHOLD = 70;
|
||||
|
||||
public enum Result {
|
||||
IMAGE_DARK,
|
||||
IMAGE_OK
|
||||
}
|
||||
|
||||
/**
|
||||
* BitmapRegionDecoder allows us to process a large bitmap by breaking it down into smaller rectangles. The rectangles
|
||||
* are obtained by setting an initial width, height and start position of the rectangle as a factor of the width and
|
||||
* height of the original bitmap and then manipulating the width, height and position to loop over the entire original
|
||||
* bitmap. Each individual rectangle is independently processed to check if its too dark. Based on
|
||||
* the factor of "bright enough" individual rectangles amongst the total rectangles into which the image
|
||||
* was divided, we will declare the image as wanted/unwanted
|
||||
*
|
||||
* @param bitmapRegionDecoder BitmapRegionDecoder for the image we wish to process
|
||||
* @return Result.IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
|
||||
* Result.IMAGE_DARK if image is too dark
|
||||
*/
|
||||
public static Result checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
|
||||
if (bitmapRegionDecoder == null) {
|
||||
Timber.e("Expected bitmapRegionDecoder was null");
|
||||
return Result.IMAGE_OK;
|
||||
}
|
||||
|
||||
int loadImageHeight = bitmapRegionDecoder.getHeight();
|
||||
int loadImageWidth = bitmapRegionDecoder.getWidth();
|
||||
|
||||
int checkImageTopPosition = 0;
|
||||
int checkImageBottomPosition = loadImageHeight / 10;
|
||||
int checkImageLeftPosition = 0;
|
||||
int checkImageRightPosition = loadImageWidth / 10;
|
||||
|
||||
int totalDividedRectangles = 0;
|
||||
int numberOfDarkRectangles = 0;
|
||||
|
||||
while ((checkImageRightPosition <= loadImageWidth) && (checkImageLeftPosition < checkImageRightPosition)) {
|
||||
while ((checkImageBottomPosition <= loadImageHeight) && (checkImageTopPosition < checkImageBottomPosition)) {
|
||||
Timber.d("left: " + checkImageLeftPosition + " right: " + checkImageRightPosition + " top: " + checkImageTopPosition + " bottom: " + checkImageBottomPosition);
|
||||
|
||||
Rect rect = new Rect(checkImageLeftPosition,checkImageTopPosition,checkImageRightPosition,checkImageBottomPosition);
|
||||
totalDividedRectangles++;
|
||||
|
||||
Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect,null);
|
||||
|
||||
if (checkIfImageIsDark(processBitmap)) {
|
||||
numberOfDarkRectangles++;
|
||||
}
|
||||
|
||||
checkImageTopPosition = checkImageBottomPosition;
|
||||
checkImageBottomPosition += (checkImageBottomPosition < (loadImageHeight - checkImageBottomPosition)) ? checkImageBottomPosition : (loadImageHeight - checkImageBottomPosition);
|
||||
}
|
||||
|
||||
checkImageTopPosition = 0; //reset to start
|
||||
checkImageBottomPosition = loadImageHeight / 10; //reset to start
|
||||
checkImageLeftPosition = checkImageRightPosition;
|
||||
checkImageRightPosition += (checkImageRightPosition < (loadImageWidth - checkImageRightPosition)) ? checkImageRightPosition : (loadImageWidth - checkImageRightPosition);
|
||||
}
|
||||
|
||||
Timber.d("dark rectangles count = " + numberOfDarkRectangles + ", total rectangles count = " + totalDividedRectangles);
|
||||
|
||||
if (numberOfDarkRectangles > totalDividedRectangles * MINIMUM_DARKNESS_FACTOR) {
|
||||
return Result.IMAGE_DARK;
|
||||
}
|
||||
|
||||
return Result.IMAGE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls the pixels into an array and then runs through it while checking the brightness of each pixel.
|
||||
* The calculation of brightness of each pixel is done by extracting the RGB constituents of the pixel
|
||||
* and then applying the formula to calculate its "Luminance". If this brightness value is less than
|
||||
* 50 then the pixel is considered to be dark. Based on the MINIMUM_DARKNESS_FACTOR if enough pixels
|
||||
* are dark then the entire bitmap is considered to be dark.
|
||||
*
|
||||
* <p>For more information on this brightness/darkness calculation technique refer the accepted answer
|
||||
* on this -> https://stackoverflow.com/questions/35914461/how-to-detect-dark-photos-in-android/35914745
|
||||
* SO question and follow the trail.
|
||||
*
|
||||
* @param bitmap The bitmap that needs to be checked.
|
||||
* @return true if bitmap is dark or null, false if bitmap is bright
|
||||
*/
|
||||
private static boolean checkIfImageIsDark(Bitmap bitmap) {
|
||||
if (bitmap == null) {
|
||||
Timber.e("Expected bitmap was null");
|
||||
return true;
|
||||
}
|
||||
|
||||
int bitmapWidth = bitmap.getWidth();
|
||||
int bitmapHeight = bitmap.getHeight();
|
||||
|
||||
int allPixelsCount = bitmapWidth * bitmapHeight;
|
||||
int[] bitmapPixels = new int[allPixelsCount];
|
||||
|
||||
bitmap.getPixels(bitmapPixels,0,bitmapWidth,0,0,bitmapWidth,bitmapHeight);
|
||||
boolean isImageDark = false;
|
||||
int darkPixelsCount = 0;
|
||||
|
||||
for (int pixel : bitmapPixels) {
|
||||
int r = Color.red(pixel);
|
||||
int g = Color.green(pixel);
|
||||
int b = Color.blue(pixel);
|
||||
|
||||
int brightness = (int) (0.2126 * r + 0.7152 * g + 0.0722 * b);
|
||||
if (brightness < 50) {
|
||||
//pixel is dark
|
||||
darkPixelsCount++;
|
||||
if (darkPixelsCount > allPixelsCount * MINIMUM_DARKNESS_FACTOR) {
|
||||
isImageDark = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isImageDark;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,11 +7,6 @@ import android.widget.Toast;
|
|||
public class ViewUtil {
|
||||
|
||||
public static void showLongToast(final Context context, @StringRes final int stringResId) {
|
||||
ExecutorUtils.uiExecutor().execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(context, context.getString(stringResId), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResId), Toast.LENGTH_LONG).show());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue