Merge branch 'master' into media_detail_enhance

This commit is contained in:
Mikel 2017-08-02 21:24:47 +01:00
commit a025cc97d5
213 changed files with 1422 additions and 1101 deletions

View file

@ -33,8 +33,6 @@ after_success:
after_failure:
- echo '*** Connected Test Rsults ***'
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/androidTests/connected/*Test.html
- echo '*** Lint Results ***'
- cat ${TRAVIS_BUILD_DIR}/app/build/reports/lint-results.xml
jdk:
# - openjdk8 # not yet available
- oraclejdk8

View file

@ -1,5 +1,19 @@
# Wikimedia Commons for Android
## v2.5.0 beta
- Added one-time popup for beta users to provide feedback on IEG renewal proposal
- Added link to Commons policies in ShareActivity
- Various string fixes
- Switched to using vector icons for map markers
- Added filter for irrelevant categories
- Fixed various crashes
- Incremented target SDK to 25
- Improved appearance of navigation drawer
- Replaced proprietary app image in tutorial with one that isn't Telegram
- Fixed camera issue with FileProvider
- Added RxJava library, migrated to Java 8
- Various code and continuous integration optimizations
## v2.4.2 beta
- Added option to launch tutorial again from nav drawer
- Added marker for current location in Nearby map

View file

@ -24,6 +24,8 @@ their contribution to the product.
* Dmitry Brant
* Adam Shorland
* John Lubbock
* Mikel Pascual
* Jan Piotrowski
3rd party open source libraries used:
* Butterknife

View file

@ -4,11 +4,14 @@ The Wikimedia Commons Android app allows users to upload pictures from their And
Initially started by the Wikimedia Foundation, this app is now maintained by volunteers. Anyone is welcome to improve it, just choose among the [open issues](https://github.com/commons-app/apps-android-commons/issues) and send us a pull request :-)
We are currently applying for an [IEG renewal][15] to work on the app for the next 6 months. Feedback is very much welcomed.
<a href="https://f-droid.org/repository/browse/?fdid=fr.free.nrw.commons" target="_blank">
<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="90"/></a>
<a href="https://play.google.com/store/apps/details?id=fr.free.nrw.commons" target="_blank">
<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="90"/></a>
## Develop with Android Studio or IntelliJ ##
[Download Android Studio][1] (recommended) or [IntelliJ][2].
@ -80,6 +83,32 @@ Captured files are not currently stored within the app, but are passed by conten
Thumbnail images are not currently cached.
## Volunteers welcome! ##
We are always looking for volunteers, feel free to step in! It is very easy:
1. Fork the repository and clone it to your computer, then follow the build instructions above.
2. Choose an [unassigned issue](https://github.com/commons-app/apps-android-commons/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee) that sounds interesting to you.
3. Read the issue's comments to make sure you understand what is the bug, or what feature is being proposed.
4. Write a "I start working on this" comment on the issue
5. Write the code :-)
6. Commit and push
7. Go to your fork's Github webpage, select the "Pull Requests" tab and click "create a pull request", as a comment, write something like "Fix for issue #12345 crash when rotating screen", then submit the pull request.
8. Within a few hours or days, a core developer will review your patch, and either merge it or suggest a few corrections.
9. If you change your mind, or if it is too difficult, no problem, just write "Sorry I don't work on this anymore" on the issue, if possible including feedback (for instance what approaches failed) and ideas.
Thanks a lot!
## Translating the app ##
Thanks to the translation work of many volunteers this app is available in a multitude of languages.
Translation of the text content of the Wikimedia Commons Android app happens on the [Commons Android App project][10] on [translatewiki.net][11]. If you want to help translate the app please create an account there (to get "translate rights" edit 20 [random keys][13] or ask in their [chat][14]).
The translations from the translatewiki project are [periodically committed directly to this project][12] by the translatewiki team and later released with the normal updates to the Play Store.
[1]: https://developer.android.com/studio/index.html
[2]: http://www.jetbrains.com/idea/download/index.html
@ -90,3 +119,9 @@ Thumbnail images are not currently cached.
[7]: https://github.com/commons-app/apps-android-commons/issues
[8]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
[9]: https://commons-app.github.io/
[10]: https://translatewiki.net/w/i.php?title=Special:Translate&group=commons-android
[11]: https://translatewiki.net
[12]: https://github.com/commons-app/apps-android-commons/commits/master?author=translatewiki
[13]: https://translatewiki.net/wiki/Special:TranslationStash?
[14]: https://translatewiki.net/wiki/Special:WebChat
[15]: https://meta.wikimedia.org/wiki/Grants:Project/Improve_%27Upload_to_Commons%27_Android_App/Renewal

View file

@ -1,4 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'me.tatarka.retrolambda'
apply plugin: 'jacoco-android'
apply from: 'quality.gradle'
apply plugin: 'com.getkeepsafe.dexcount'
@ -51,8 +52,8 @@ android {
defaultConfig {
applicationId 'fr.free.nrw.commons'
versionCode 73
versionName '2.4.2'
versionCode 74
versionName '2.5.0'
minSdkVersion project.minSdkVersion
targetSdkVersion project.targetSdkVersion
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@ -75,6 +76,11 @@ android {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
//FIXME: Temporary fix for https://github.com/commons-app/apps-android-commons/issues/709
configurations.all {
resolutionStrategy.force 'com.android.support:support-annotations:25.2.0'

View file

@ -25,7 +25,7 @@ public class NearbyControllerTest {
@Before
public void setup() {
instrumentationContext = InstrumentationRegistry.getContext();
instrumentationContext = InstrumentationRegistry.getTargetContext();
}
@Test public void testNullAttractions() {

View file

@ -9,9 +9,8 @@ import android.support.test.espresso.matcher.ViewMatchers;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.view.View;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -20,9 +19,6 @@ import java.util.Map;
import fr.free.nrw.commons.settings.SettingsActivity;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class SettingsActivityTest {
@ -65,8 +61,8 @@ public class SettingsActivityTest {
@Test
public void oneLicenseIsChecked() {
// click "License" (the first item)
Espresso.onData(anything())
.inAdapterView(findPreferenceList())
Espresso.onData(Matchers.anything())
.inAdapterView(ViewMatchers.withId(android.R.id.list))
.atPosition(0)
.perform(ViewActions.click());
@ -78,8 +74,8 @@ public class SettingsActivityTest {
@Test
public void afterClickingCcby4ItWillStay() {
// click "License" (the first item)
Espresso.onData(anything())
.inAdapterView(findPreferenceList())
Espresso.onData(Matchers.anything())
.inAdapterView(ViewMatchers.withId(android.R.id.list))
.atPosition(0)
.perform(ViewActions.click());
@ -89,8 +85,8 @@ public class SettingsActivityTest {
).perform(ViewActions.click());
// click "License" (the first item)
Espresso.onData(anything())
.inAdapterView(findPreferenceList())
Espresso.onData(Matchers.anything())
.inAdapterView(ViewMatchers.withId(android.R.id.list))
.atPosition(0)
.perform(ViewActions.click());
@ -100,12 +96,4 @@ public class SettingsActivityTest {
ViewMatchers.withText(R.string.license_name_cc_by_four)
));
}
private static Matcher<View> findPreferenceList() {
return allOf(
ViewMatchers.isDescendantOfA(ViewMatchers.withId(R.id.settingsFragment)),
ViewMatchers.withResourceName("list"),
ViewMatchers.hasFocus()
);
}
}

View file

@ -8,9 +8,11 @@ import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.ui.widget.HtmlTextView;
public class AboutActivity extends NavigationBaseActivity {
@BindView(R.id.about_version) TextView versionText;
@BindView(R.id.about_license) HtmlTextView aboutLicenseText;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -19,6 +21,9 @@ public class AboutActivity extends NavigationBaseActivity {
ButterKnife.bind(this);
String aboutText = getString(R.string.about_license, getString(R.string.trademarked_name));
aboutLicenseText.setHtmlText(aboutText);
versionText.setText(BuildConfig.VERSION_NAME);
initDrawer();
}

View file

@ -11,6 +11,7 @@ import android.content.pm.PackageManager;
import android.database.sqlite.SQLiteDatabase;
import android.preference.PreferenceManager;
import android.support.v4.util.LruCache;
import android.util.Log;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.stetho.Stetho;
@ -139,6 +140,10 @@ public class CommonsApplication extends Application {
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
Fresco.initialize(this);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
CommonsApplication.getInstance());
// Increase counter by one, starts from 1
prefs.edit().putInt("app_start_counter", prefs.getInt("app_start_counter" ,0) + 1).commit();
//For caching area -> categories
cacheData = new CacheController();
@ -197,8 +202,8 @@ public class CommonsApplication extends Application {
AccountManager accountManager = AccountManager.get(this);
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType());
for (int index = 0; index < allAccounts.length; index++) {
accountManager.removeAccount(allAccounts[index], null, null);
for (Account allAccount : allAccounts) {
accountManager.removeAccount(allAccount, null, null);
}
//TODO: fix preference manager

View file

@ -142,7 +142,7 @@ public class Media implements Parcelable {
return coordinates;
}
public void setCoordinates(LatLng coordinates) {
public void setCoordinates(@Nullable LatLng coordinates) {
this.coordinates = coordinates;
}
@ -201,7 +201,7 @@ public class Media implements Parcelable {
this.filename = filename;
}
public Media(Uri localUri, String imageUrl, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator) {
public Media(Uri localUri, String imageUrl, String filename, String description, long dataLength, Date dateCreated, @Nullable Date dateUploaded, String creator) {
this();
this.localUri = localUri;
this.imageUrl = imageUrl;

View file

@ -27,12 +27,7 @@ public class WelcomeActivity extends BaseActivity {
pager.setAdapter(adapter);
indicator.setViewPager(pager);
adapter.setCallback(new WelcomePagerAdapter.Callback() {
@Override
public void onYesClicked() {
finish();
}
});
adapter.setCallback(this::finish);
}
@Override

View file

@ -26,6 +26,9 @@ import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import timber.log.Timber;
import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
public class LoginActivity extends AccountAuthenticatorActivity {
@ -61,20 +64,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
usernameEdit.addTextChangedListener(textWatcher);
passwordEdit.addTextChangedListener(textWatcher);
twoFactorEdit.addTextChangedListener(textWatcher);
passwordEdit.setOnEditorActionListener( newLoginInputActionListener() );
passwordEdit.setOnEditorActionListener(newLoginInputActionListener());
loginButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
performLogin();
}
});
signupButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
signUp(v);
}
});
loginButton.setOnClickListener(this::performLogin);
signupButton.setOnClickListener(this::signUp);
}
private class LoginTextWatcher implements TextWatcher {
@ -98,20 +91,17 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
private TextView.OnEditorActionListener newLoginInputActionListener() {
return new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
if (loginButton.isEnabled()) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
performLogin();
return true;
} else if ((keyEvent != null) && keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
performLogin();
return true;
}
return (textView, actionId, keyEvent) -> {
if (loginButton.isEnabled()) {
if (actionId == IME_ACTION_DONE) {
performLogin(textView);
return true;
} else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) {
performLogin(textView);
return true;
}
return false;
}
return false;
};
}
@ -142,7 +132,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
super.onDestroy();
}
private void performLogin() {
private void performLogin(View view) {
Timber.d("Login to start!");
LoginTask task = getLoginTask();
task.execute();
@ -151,7 +141,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
private LoginTask getLoginTask() {
return new LoginTask(
this,
canonicializeUsername( usernameEdit.getText().toString() ),
canonicializeUsername(usernameEdit.getText().toString()),
passwordEdit.getText().toString(),
twoFactorEdit.getText().toString()
);
@ -162,16 +152,16 @@ public class LoginActivity extends AccountAuthenticatorActivity {
* @param username String
* @return String canonicial username
*/
private String canonicializeUsername( String username ) {
private String canonicializeUsername(String username) {
return new PageTitle(username).getText();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
@ -186,20 +176,20 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
public void askUserForTwoFactorAuth() {
if(BuildConfig.DEBUG) {
if (BuildConfig.DEBUG) {
twoFactorEdit.setVisibility(View.VISIBLE);
showUserToastAndCancelDialog( R.string.login_failed_2fa_needed );
}else{
showUserToastAndCancelDialog( R.string.login_failed_2fa_not_supported );
showUserToastAndCancelDialog(R.string.login_failed_2fa_needed);
} else {
showUserToastAndCancelDialog(R.string.login_failed_2fa_not_supported);
}
}
public void showUserToastAndCancelDialog( int resId ) {
showUserToast( resId );
public void showUserToastAndCancelDialog(int resId) {
showUserToast(resId);
progressDialog.cancel();
}
private void showUserToast( int resId ) {
private void showUserToast(int resId) {
Toast.makeText(this, resId, Toast.LENGTH_LONG).show();
}

View file

@ -67,7 +67,7 @@ class LoginTask extends AsyncTask<String, String, String> {
if (result.equals("PASS")) {
handlePassResult();
} else {
handleOtherResults( result );
handleOtherResults(result);
}
}
@ -88,38 +88,38 @@ class LoginTask extends AsyncTask<String, String, String> {
}
}
AccountUtil.createAccount( response, username, password );
AccountUtil.createAccount(response, username, password);
loginActivity.startMainActivity();
}
/**
* Match known failure message codes and provide messages
* Match known failure message codes and provide messages.
* @param result String
*/
private void handleOtherResults( String result ) {
private void handleOtherResults(String result) {
if (result.equals("NetworkFailure")) {
// Matches NetworkFailure which is created by the doInBackground method
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_network );
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_network);
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
// Matches nosuchuser, nosuchusershort, noname
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_username );
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_username);
loginActivity.emptySensitiveEditFields();
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
// Matches wrongpassword, wrongpasswordempty
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_password );
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_password);
loginActivity.emptySensitiveEditFields();
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
// Matches unknown throttle error codes
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_throttled );
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_throttled);
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
// Matches login-userblocked
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_blocked );
loginActivity.showUserToastAndCancelDialog(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.showUserToastAndCancelDialog( R.string.login_failed_generic );
loginActivity.showUserToastAndCancelDialog(R.string.login_failed_generic);
}
}
}

View file

@ -5,17 +5,17 @@ import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class WikiAccountAuthenticatorService extends Service{
public class WikiAccountAuthenticatorService extends Service {
private static WikiAccountAuthenticator wikiAccountAuthenticator = null;
@Override
public IBinder onBind(Intent intent) {
if (!intent.getAction().equals(AccountManager.ACTION_AUTHENTICATOR_INTENT)) {
return null;
return null;
}
if(wikiAccountAuthenticator == null) {
if (wikiAccountAuthenticator == null) {
wikiAccountAuthenticator = new WikiAccountAuthenticator(this);
}
return wikiAccountAuthenticator.getIBinder();

View file

@ -74,14 +74,14 @@ public class CacheController {
double offset = 100;
//Coordinate offsets in radians
double dLat = offset/EARTH_RADIUS;
double dLon = offset/(EARTH_RADIUS*Math.cos(Math.PI*lat/180));
double dLat = offset / EARTH_RADIUS;
double dLon = offset / (EARTH_RADIUS * Math.cos(Math.PI * lat / 180));
//OffsetPosition, decimal degrees
yPlus = lat + dLat * 180/Math.PI;
yMinus = lat - dLat * 180/Math.PI;
xPlus = lon + dLon * 180/Math.PI;
xMinus = lon - dLon * 180/Math.PI;
yPlus = lat + dLat * 180 / Math.PI;
yMinus = lat - dLat * 180 / Math.PI;
xPlus = lon + dLon * 180 / Math.PI;
xMinus = lon - dLon * 180 / Math.PI;
Timber.d("Search within: xMinus=%s, yMinus=%s, xPlus=%s, yPlus=%s",
xMinus, yMinus, xPlus, yPlus);
}

View file

@ -1,67 +0,0 @@
package fr.free.nrw.commons.category;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckedTextView;
import java.util.ArrayList;
import fr.free.nrw.commons.R;
public class CategoriesAdapter extends BaseAdapter {
private LayoutInflater mInflater;
private ArrayList<CategorizationFragment.CategoryItem> items;
public CategoriesAdapter(Context context, ArrayList<CategorizationFragment.CategoryItem> items) {
this.items = items;
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@Override
public int getCount() {
return items.size();
}
@Override
public Object getItem(int i) {
return items.get(i);
}
public ArrayList<CategorizationFragment.CategoryItem> getItems() {
return items;
}
public void setItems(ArrayList<CategorizationFragment.CategoryItem> items) {
this.items = items;
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
CheckedTextView checkedView;
if(view == null) {
checkedView = (CheckedTextView) mInflater.inflate(R.layout.layout_categories_item, null);
} else {
checkedView = (CheckedTextView) view;
}
CategorizationFragment.CategoryItem item = (CategorizationFragment.CategoryItem) this.getItem(i);
checkedView.setChecked(item.selected);
checkedView.setText(item.name);
checkedView.setTag(i);
return checkedView;
}
}

View file

@ -0,0 +1,24 @@
package fr.free.nrw.commons.category;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder;
import java.util.Collections;
import java.util.List;
class CategoriesAdapterFactory {
private final CategoriesRenderer.CategoryClickedListener listener;
CategoriesAdapterFactory(CategoriesRenderer.CategoryClickedListener listener) {
this.listener = listener;
}
public RVRendererAdapter<CategoryItem> create(List<CategoryItem> placeList) {
RendererBuilder<CategoryItem> builder = new RendererBuilder<CategoryItem>()
.bind(CategoryItem.class, new CategoriesRenderer(listener));
ListAdapteeCollection<CategoryItem> collection = new ListAdapteeCollection<>(
placeList != null ? placeList : Collections.<CategoryItem>emptyList());
return new RVRendererAdapter<>(builder, collection);
}
}

View file

@ -0,0 +1,57 @@
package fr.free.nrw.commons.category;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckedTextView;
import com.pedrogomez.renderers.Renderer;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
class CategoriesRenderer extends Renderer<CategoryItem> {
@BindView(R.id.tvName) CheckedTextView checkedView;
private final CategoryClickedListener listener;
CategoriesRenderer(CategoryClickedListener listener) {
this.listener = listener;
}
@Override
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
return layoutInflater.inflate(R.layout.layout_categories_item, viewGroup, false);
}
@Override
protected void setUpView(View view) {
ButterKnife.bind(this, view);
}
@Override
protected void hookListeners(View view) {
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CategoryItem item = getContent();
item.setSelected(!item.isSelected());
checkedView.setChecked(item.isSelected());
if (listener != null) {
listener.categoryClicked(item);
}
}
});
}
@Override
public void render() {
CategoryItem item = getContent();
checkedView.setChecked(item.isSelected());
checkedView.setText(item.getName());
}
interface CategoryClickedListener {
void categoryClicked(CategoryItem item);
}
}

View file

@ -1,36 +1,33 @@
package fr.free.nrw.commons.category;
import android.content.ContentProviderClient;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
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.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.CheckedTextView;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
@ -40,90 +37,191 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoriesRenderer.CategoryClickedListener;
import fr.free.nrw.commons.upload.MwVolleyApi;
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 Fragment {
public interface OnCategoriesSaveHandler {
void onCategoriesSave(ArrayList<String> categories);
}
public class CategorizationFragment extends Fragment implements CategoryClickedListener {
public static final int SEARCH_CATS_LIMIT = 25;
ListView categoriesList;
protected EditText categoriesFilter;
@BindView(R.id.categoriesListBox)
RecyclerView categoriesList;
@BindView(R.id.categoriesSearchBox)
EditText categoriesFilter;
@BindView(R.id.categoriesSearchInProgress)
ProgressBar categoriesSearchInProgress;
@BindView(R.id.categoriesNotFound)
TextView categoriesNotFoundView;
@BindView(R.id.categoriesExplanation)
TextView categoriesSkip;
private CategoryTextWatcher textWatcher = new CategoryTextWatcher();
CategoriesAdapter categoriesAdapter;
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
private RVRendererAdapter<CategoryItem> categoriesAdapter;
private OnCategoriesSaveHandler onCategoriesSaveHandler;
protected HashMap<String, ArrayList<String>> categoriesCache;
private HashMap<String, ArrayList<String>> categoriesCache;
private ArrayList<String> selectedCategories = new ArrayList<>();
private ContentProviderClient client;
private PrefixUpdater prefixUpdaterSub;
private MethodAUpdater methodAUpdaterSub;
private final CategoryTextWatcher textWatcher = new CategoryTextWatcher();
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(this);
private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
private final ArrayList<String> titleCatItems = new ArrayList<>();
private final CountDownLatch mergeLatch = new CountDownLatch(1);
// LHS guarantees ordered insertions, allowing for prioritized method A results
private final Set<String> results = new LinkedHashSet<>();
PrefixUpdater prefixUpdaterSub;
MethodAUpdater methodAUpdaterSub;
private final ArrayList<String> titleCatItems = new ArrayList<>();
final CountDownLatch mergeLatch = new CountDownLatch(1);
@SuppressWarnings("unchecked")
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_categorization, container, false);
ButterKnife.bind(this, rootView);
private ContentProviderClient client;
categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
protected final static int SEARCH_CATS_LIMIT = 25;
categoriesSkip.setOnClickListener(view -> {
getActivity().onBackPressed();
getActivity().finish();
});
public static class CategoryItem implements Parcelable {
public String name;
public boolean selected;
public static Creator<CategoryItem> CREATOR = new Creator<CategoryItem>() {
@Override
public CategoryItem createFromParcel(Parcel parcel) {
return new CategoryItem(parcel);
}
@Override
public CategoryItem[] newArray(int i) {
return new CategoryItem[0];
}
};
public CategoryItem(String name, boolean selected) {
this.name = name;
this.selected = selected;
ArrayList<CategoryItem> items;
if (savedInstanceState == null) {
items = new ArrayList<>();
categoriesCache = new HashMap<>();
} else {
items = savedInstanceState.getParcelableArrayList("currentCategories");
categoriesCache = (HashMap<String, ArrayList<String>>) savedInstanceState
.getSerializable("categoriesCache");
}
public CategoryItem(Parcel in) {
name = in.readString();
selected = in.readInt() == 1;
}
categoriesAdapter = adapterFactory.create(items);
categoriesList.setAdapter(categoriesAdapter);
categoriesFilter.addTextChangedListener(textWatcher);
@Override
public int describeContents() {
return 0;
}
startUpdatingCategoryList();
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(name);
parcel.writeInt(selected ? 1 : 0);
return rootView;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
menu.clear();
inflater.inflate(R.menu.fragment_categorization, menu);
}
@Override
public void onResume() {
super.onResume();
View rootView = getView();
if (rootView != null) {
rootView.setFocusableInTouchMode(true);
rootView.requestFocus();
rootView.setOnKeyListener((v, keyCode, event) -> {
if (event.getAction() == ACTION_UP && keyCode == KEYCODE_BACK) {
backButtonDialog();
return true;
}
return false;
});
}
}
@Override
public void onDestroyView() {
categoriesFilter.removeTextChangedListener(textWatcher);
super.onDestroyView();
}
@Override
public void onDestroy() {
super.onDestroy();
client.release();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
int itemCount = categoriesAdapter.getItemCount();
ArrayList<CategoryItem> items = new ArrayList<>(itemCount);
for (int i = 0; i < itemCount; i++) {
items.add(categoriesAdapter.getItem(i));
}
outState.putParcelableArrayList("currentCategories", items);
outState.putSerializable("categoriesCache", categoriesCache);
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.menu_save_categories:
int numberSelected = 0;
selectedCategories = new ArrayList<>();
int count = categoriesAdapter.getItemCount();
for (int i = 0; i < count; i++) {
CategoryItem item = categoriesAdapter.getItem(i);
if (item.isSelected()) {
selectedCategories.add(item.getName());
numberSelected++;
}
}
//If no categories selected, display warning to user
if (numberSelected == 0) {
new AlertDialog.Builder(getActivity())
.setMessage("Images without categories are rarely usable. "
+ "Are you sure you want to submit without selecting "
+ "categories?")
.setTitle("No Categories Selected")
.setPositiveButton("No, go back", (dialog, id) -> {
//Exit menuItem so user can select their categories
})
.setNegativeButton("Yes, submit", (dialog, id) -> {
//Proceed to submission
onCategoriesSaveHandler.onCategoriesSave(selectedCategories);
})
.create()
.show();
} else {
//Proceed to submission
onCategoriesSaveHandler.onCategoriesSave(selectedCategories);
return true;
}
}
return super.onOptionsItemSelected(menuItem);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
getActivity().setTitle(R.string.categories_activity_title);
client = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY);
}
public HashMap<String, ArrayList<String>> getCategoriesCache() {
return categoriesCache;
}
/**
* Retrieves category suggestions from title input
*
* @return a list containing title-related categories
*/
protected ArrayList<String> titleCatQuery() {
private ArrayList<String> titleCatQuery() {
TitleCategories titleCategoriesSub;
//Retrieve the title that was saved when user tapped submit icon
@ -157,37 +255,42 @@ public class CategorizationFragment extends Fragment {
/**
* Retrieves recently-used categories
*
* @return a list containing recent categories
*/
protected ArrayList<String> recentCatQuery() {
private ArrayList<String> recentCatQuery() {
ArrayList<String> items = new ArrayList<>();
Cursor cursor = null;
try {
Cursor cursor = client.query(
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.moveToNext() && cursor.getPosition() < SEARCH_CATS_LIMIT) {
while (cursor != null && cursor.moveToNext()
&& cursor.getPosition() < SEARCH_CATS_LIMIT) {
Category cat = Category.fromCursor(cursor);
items.add(cat.getName());
}
cursor.close();
}
catch (RemoteException e) {
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return items;
}
/**
* Merges nearby categories, categories suggested based on title, and recent categories... without duplicates.
* Merges nearby categories, categories suggested based on title, and recent categories...
* without duplicates.
*
* @return a list containing merged categories
*/
protected ArrayList<String> mergeItems() {
ArrayList<String> mergeItems() {
Set<String> mergedItems = new LinkedHashSet<>();
Timber.d("Calling APIs for GPS cats, title cats and recent cats...");
@ -214,7 +317,8 @@ public class CategorizationFragment extends Fragment {
mergedItems.addAll(recentItems);
Timber.d("Adding recent items: %s", recentItems);
//Needs to be an ArrayList and not a List unless we want to modify a big portion of preexisting code
// Needs to be an ArrayList and not a List unless we want to modify a big portion
// of preexisting code
ArrayList<String> mergedItemsList = new ArrayList<>(mergedItems);
Timber.d("Merged item list: %s", mergedItemsList);
@ -223,18 +327,20 @@ public class CategorizationFragment extends Fragment {
/**
* Displays categories found to the user as they type in the search box
*
* @param categories a list of all categories found for the search string
* @param filter the search string
* @param filter the search string
*/
protected void setCatsAfterAsync(ArrayList<String> categories, String filter) {
private void setCatsAfterAsync(ArrayList<String> categories, String filter) {
if (getActivity() != null) {
ArrayList<CategoryItem> items = new ArrayList<>();
HashSet<String> existingKeys = new HashSet<>();
for (CategoryItem item : categoriesAdapter.getItems()) {
if (item.selected) {
int count = categoriesAdapter.getItemCount();
for (int i = 0; i < count; i++) {
CategoryItem item = categoriesAdapter.getItem(i);
if (item.isSelected()) {
items.add(item);
existingKeys.add(item.name);
existingKeys.add(item.getName());
}
}
for (String category : categories) {
@ -243,8 +349,8 @@ public class CategorizationFragment extends Fragment {
}
}
categoriesAdapter.setItems(items);
categoriesAdapter.notifyDataSetInvalidated();
categoriesAdapter.setCollection(new ListAdapteeCollection<>(items));
categoriesAdapter.notifyDataSetChanged();
categoriesSearchInProgress.setVisibility(View.GONE);
if (categories.isEmpty()) {
@ -258,8 +364,7 @@ public class CategorizationFragment extends Fragment {
} else {
categoriesList.smoothScrollToPosition(existingKeys.size());
}
}
else {
} else {
Timber.e("Error: Fragment is null");
}
}
@ -272,7 +377,6 @@ public class CategorizationFragment extends Fragment {
* above Prefix results.
*/
private void requestSearchResults() {
final CountDownLatch latch = new CountDownLatch(1);
prefixUpdaterSub = new PrefixUpdater(this) {
@ -282,8 +386,7 @@ public class CategorizationFragment extends Fragment {
try {
result = super.doInBackground();
latch.await();
}
catch (InterruptedException e) {
} catch (InterruptedException e) {
Timber.w(e);
//Thread.currentThread().interrupt();
}
@ -325,7 +428,6 @@ public class CategorizationFragment extends Fragment {
}
private void startUpdatingCategoryList() {
if (prefixUpdaterSub != null) {
prefixUpdaterSub.cancel(true);
}
@ -339,238 +441,34 @@ public class CategorizationFragment extends Fragment {
public int getCurrentSelectedCount() {
int count = 0;
for(CategoryItem item: categoriesAdapter.getItems()) {
if(item.selected) {
int numberOfItems = categoriesAdapter.getItemCount();
for (int i = 0; i < numberOfItems; i++) {
CategoryItem item = categoriesAdapter.getItem(i);
if (item.isSelected()) {
count++;
}
}
return count;
}
private Category lookupCategory(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.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();
}
}
// Newly used category...
Category cat = new Category();
cat.setName(name);
cat.setLastUsed(new Date());
cat.setTimesUsed(0);
return cat;
}
private class CategoryCountUpdater extends AsyncTask<Void, Void, Void> {
private String name;
public CategoryCountUpdater(String name) {
this.name = name;
}
@Override
protected Void doInBackground(Void... voids) {
Category cat = lookupCategory(name);
cat.incTimesUsed();
cat.setContentProviderClient(client);
cat.save();
return null; // Make the compiler happy.
}
}
private void updateCategoryCount(String name) {
new CategoryCountUpdater(name).executeOnExecutor(executor);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_categorization, null);
categoriesList = (ListView) rootView.findViewById(R.id.categoriesListBox);
categoriesFilter = (EditText) rootView.findViewById(R.id.categoriesSearchBox);
categoriesSearchInProgress = (ProgressBar) rootView.findViewById(R.id.categoriesSearchInProgress);
categoriesNotFoundView = (TextView) rootView.findViewById(R.id.categoriesNotFound);
categoriesSkip = (TextView) rootView.findViewById(R.id.categoriesExplanation);
categoriesSkip.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
getActivity().onBackPressed();
getActivity().finish();
}
});
ArrayList<CategoryItem> items;
if(savedInstanceState == null) {
items = new ArrayList<>();
categoriesCache = new HashMap<>();
} else {
items = savedInstanceState.getParcelableArrayList("currentCategories");
categoriesCache = (HashMap<String, ArrayList<String>>) savedInstanceState.getSerializable("categoriesCache");
}
categoriesAdapter = new CategoriesAdapter(getActivity(), items);
categoriesList.setAdapter(categoriesAdapter);
categoriesList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int index, long id) {
CheckedTextView checkedView = (CheckedTextView) view;
CategoryItem item = (CategoryItem) adapterView.getAdapter().getItem(index);
item.selected = !item.selected;
checkedView.setChecked(item.selected);
if (item.selected) {
updateCategoryCount(item.name);
}
}
});
categoriesFilter.addTextChangedListener(textWatcher);
startUpdatingCategoryList();
return rootView;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
menu.clear();
inflater.inflate(R.menu.fragment_categorization, menu);
}
@Override
public void onResume() {
super.onResume();
View rootView = getView();
if (rootView != null) {
rootView.setFocusableInTouchMode(true);
rootView.requestFocus();
rootView.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
backButtonDialog();
return true;
}
return false;
}
});
}
}
@Override
public void onDestroyView() {
categoriesFilter.removeTextChangedListener(textWatcher);
super.onDestroyView();
}
public void backButtonDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage("Are you sure you want to go back? The image will not have any categories saved.")
.setTitle("Warning");
builder.setPositiveButton("No", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
//No need to do anything, user remains on categorization screen
}
});
builder.setNegativeButton("Yes", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
getActivity().finish();
}
});
AlertDialog dialog = builder.create();
dialog.show();
new AlertDialog.Builder(getActivity())
.setMessage("Are you sure you want to go back? The image will not "
+ "have any categories saved.")
.setTitle("Warning")
.setPositiveButton("No", (dialog, id) -> {
//No need to do anything, user remains on categorization screen
})
.setNegativeButton("Yes", (dialog, id) -> getActivity().finish())
.create()
.show();
}
@Override
public void onDestroy() {
super.onDestroy();
client.release();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList("currentCategories", categoriesAdapter.getItems());
outState.putSerializable("categoriesCache", categoriesCache);
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
switch(menuItem.getItemId()) {
case R.id.menu_save_categories:
int numberSelected = 0;
for(CategoryItem item: categoriesAdapter.getItems()) {
if(item.selected) {
selectedCategories.add(item.name);
numberSelected++;
}
}
//If no categories selected, display warning to user
if (numberSelected == 0) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage("Images without categories are rarely usable. Are you sure you want to submit without selecting categories?")
.setTitle("No Categories Selected");
builder.setPositiveButton("No, go back", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
//Exit menuItem so user can select their categories
return;
}
});
builder.setNegativeButton("Yes, submit", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
//Proceed to submission
onCategoriesSaveHandler.onCategoriesSave(selectedCategories);
return;
}
});
AlertDialog dialog = builder.create();
dialog.show();
} else {
//Proceed to submission
onCategoriesSaveHandler.onCategoriesSave(selectedCategories);
return true;
}
public void categoryClicked(CategoryItem item) {
if (item.isSelected()) {
new CategoryCountUpdater(item.getName(), client).executeOnExecutor(executor);
}
return super.onOptionsItemSelected(menuItem);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
getActivity().setTitle(R.string.categories_activity_title);
client = getActivity().getContentResolver().acquireContentProviderClient(CategoryContentProvider.AUTHORITY);
}
private class CategoryTextWatcher implements TextWatcher {

View file

@ -26,7 +26,7 @@ public class Category {
this.name = name;
}
public Date getLastUsed() {
private Date getLastUsed() {
// warning: Date objects are mutable.
return (Date)lastUsed.clone();
}
@ -36,11 +36,11 @@ public class Category {
this.lastUsed = (Date)lastUsed.clone();
}
public void touch() {
private void touch() {
lastUsed = new Date();
}
public int getTimesUsed() {
private int getTimesUsed() {
return timesUsed;
}
@ -60,17 +60,17 @@ public class Category {
public void save() {
try {
if(contentUri == null) {
if (contentUri == null) {
contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues());
} else {
client.update(contentUri, toContentValues(), null, null);
}
} catch(RemoteException e) {
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
public ContentValues toContentValues() {
private ContentValues toContentValues() {
ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_NAME, getName());
cv.put(Table.COLUMN_LAST_USED, getLastUsed().getTime());
@ -121,23 +121,23 @@ public class Category {
}
public static void onUpdate(SQLiteDatabase db, int from, int to) {
if(from == to) {
if (from == to) {
return;
}
if(from < 4) {
if (from < 4) {
// doesn't exist yet
from++;
onUpdate(db, from, to);
return;
}
if(from == 4) {
if (from == 4) {
// table added in version 5
onCreate(db);
from++;
onUpdate(db, from, to);
return;
}
if(from == 5) {
if (from == 5) {
from++;
onUpdate(db, from, to);
return;

View file

@ -7,6 +7,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import fr.free.nrw.commons.CommonsApplication;
@ -41,8 +42,10 @@ public class CategoryContentProvider extends ContentProvider {
return false;
}
@SuppressWarnings("ConstantConditions")
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(Category.Table.TABLE_NAME);
@ -53,7 +56,8 @@ public class CategoryContentProvider extends ContentProvider {
switch(uriType) {
case CATEGORIES:
cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
cursor = queryBuilder.query(db, projection, selection, selectionArgs,
null, null, sortOrder);
break;
case CATEGORIES_ID:
cursor = queryBuilder.query(db,
@ -75,15 +79,16 @@ public class CategoryContentProvider extends ContentProvider {
}
@Override
public String getType(Uri uri) {
public String getType(@NonNull Uri uri) {
return null;
}
@SuppressWarnings("ConstantConditions")
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
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 CATEGORIES:
id = sqlDB.insert(Category.Table.TABLE_NAME, null, contentValues);
@ -96,12 +101,13 @@ public class CategoryContentProvider extends ContentProvider {
}
@Override
public int delete(Uri uri, String s, String[] strings) {
public int delete(@NonNull Uri uri, String s, String[] strings) {
return 0;
}
@SuppressWarnings("ConstantConditions")
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (CategoryContentProvider)");
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
@ -122,8 +128,10 @@ public class CategoryContentProvider extends ContentProvider {
return values.length;
}
@SuppressWarnings("ConstantConditions")
@Override
public int update(Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
public int update(@NonNull Uri uri, ContentValues contentValues, String selection,
String[] selectionArgs) {
/*
SQL Injection warnings: First, note that we're not exposing this to the outside world (exported="false")
Even then, we should make sure to sanitize all user input appropriately. Input that passes through ContentValues
@ -133,7 +141,7 @@ public class CategoryContentProvider extends ContentProvider {
*/
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated = 0;
int rowsUpdated;
switch (uriType) {
case CATEGORIES_ID:
int id = Integer.valueOf(uri.getLastPathSegment());
@ -144,7 +152,8 @@ public class CategoryContentProvider extends ContentProvider {
Category.Table.COLUMN_ID + " = ?",
new String[] { String.valueOf(id) } );
} else {
throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID");
throw new IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID");
}
break;
default:

View file

@ -0,0 +1,59 @@
package fr.free.nrw.commons.category;
import android.content.ContentProviderClient;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.RemoteException;
import java.util.Date;
class CategoryCountUpdater extends AsyncTask<Void, Void, Void> {
private final String name;
private final ContentProviderClient client;
CategoryCountUpdater(String name, ContentProviderClient client) {
this.name = name;
this.client = client;
}
@Override
protected Void doInBackground(Void... voids) {
Category cat = lookupCategory(name);
cat.incTimesUsed();
cat.setContentProviderClient(client);
cat.save();
return null; // Make the compiler happy.
}
private Category lookupCategory(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();
}
}
// Newly used category...
Category cat = new Category();
cat.setName(name);
cat.setLastUsed(new Date());
cat.setTimesUsed(0);
return cat;
}
}

View file

@ -0,0 +1,54 @@
package fr.free.nrw.commons.category;
import android.os.Parcel;
import android.os.Parcelable;
class CategoryItem implements Parcelable {
private final String name;
private boolean selected;
public static Creator<CategoryItem> CREATOR = new Creator<CategoryItem>() {
@Override
public CategoryItem createFromParcel(Parcel parcel) {
return new CategoryItem(parcel);
}
@Override
public CategoryItem[] newArray(int i) {
return new CategoryItem[0];
}
};
CategoryItem(String name, boolean selected) {
this.name = name;
this.selected = selected;
}
private CategoryItem(Parcel in) {
name = in.readString();
selected = in.readInt() == 1;
}
public String getName() {
return name;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(name);
parcel.writeInt(selected ? 1 : 0);
}
}

View file

@ -13,6 +13,8 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber;
import static fr.free.nrw.commons.category.CategorizationFragment.SEARCH_CATS_LIMIT;
/**
* Sends asynchronous queries to the Commons MediaWiki API to retrieve categories that are close to
* the keyword typed in by the user. The 'srsearch' action-specific parameter is used for this
@ -20,8 +22,8 @@ import timber.log.Timber;
*/
class MethodAUpdater extends AsyncTask<Void, Void, List<String>> {
private final CategorizationFragment catFragment;
private String filter;
private CategorizationFragment catFragment;
MethodAUpdater(CategorizationFragment catFragment) {
this.catFragment = catFragment;
@ -84,7 +86,7 @@ class MethodAUpdater extends AsyncTask<Void, Void, List<String>> {
//URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch=
try {
categories = api.searchCategories(CategorizationFragment.SEARCH_CATS_LIMIT, filter);
categories = api.searchCategories(SEARCH_CATS_LIMIT, filter);
Timber.d("Method A URL filter %s", categories);
} catch (IOException e) {
Timber.e(e, "IO Exception: ");

View file

@ -0,0 +1,7 @@
package fr.free.nrw.commons.category;
import java.util.ArrayList;
public interface OnCategoriesSaveHandler {
void onCategoriesSave(ArrayList<String> categories);
}

View file

@ -7,6 +7,7 @@ import android.view.View;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@ -14,18 +15,20 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber;
import static fr.free.nrw.commons.category.CategorizationFragment.SEARCH_CATS_LIMIT;
/**
* Sends asynchronous queries to the Commons MediaWiki API to retrieve categories that share the
* same prefix as the keyword typed in by the user. The 'acprefix' action-specific parameter is used
* for this purpose. This class should be subclassed in CategorizationFragment.java to aggregate
* the results.
*/
public class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
private final CategorizationFragment catFragment;
private String filter;
private CategorizationFragment catFragment;
public PrefixUpdater(CategorizationFragment catFragment) {
PrefixUpdater(CategorizationFragment catFragment) {
this.catFragment = catFragment;
}
@ -47,7 +50,7 @@ public class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
* @param items Unfiltered list of categories
* @return Filtered category list
*/
private List<String> filterYears(List<String> items) {
private List<String> filterIrrelevantResults(List<String> items) {
Iterator<String> iterator;
@ -62,15 +65,18 @@ public class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
Timber.d("Previous year: %s", prevYearInString);
//Copy to Iterator to prevent ConcurrentModificationException when removing item
for (iterator = items.iterator(); iterator.hasNext(); ) {
for (iterator = items.iterator(); iterator.hasNext();) {
String s = iterator.next();
//Check if s contains a 4-digit word anywhere within the string (.* is wildcard)
//And that s does not equal the current year or previous year
if (s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString)) {
Timber.d("Filtering out year %s", s);
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
if ((s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString))
|| s.matches("(.*)needing(.*)")||s.matches("(.*)taken on(.*)")) {
Timber.d("Filtering out irrelevant result: %s", s);
iterator.remove();
}
}
Timber.d("Items: %s", items);
@ -83,14 +89,15 @@ public class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
if (TextUtils.isEmpty(filter)) {
ArrayList<String> mergedItems = new ArrayList<>(catFragment.mergeItems());
Timber.d("Merged items, waiting for filter");
return new ArrayList<>(filterYears(mergedItems));
return new ArrayList<>(filterIrrelevantResults(mergedItems));
}
//if user types in something that is in cache, return cached category
if (catFragment.categoriesCache.containsKey(filter)) {
ArrayList<String> cachedItems = new ArrayList<>(catFragment.categoriesCache.get(filter));
HashMap<String, ArrayList<String>> categoriesCache = catFragment.getCategoriesCache();
if (categoriesCache.containsKey(filter)) {
ArrayList<String> cachedItems = new ArrayList<>(categoriesCache.get(filter));
Timber.d("Found cache items, waiting for filter");
return new ArrayList<>(filterYears(cachedItems));
return new ArrayList<>(filterIrrelevantResults(cachedItems));
}
//otherwise if user has typed something in that isn't in cache, search API for matching categories
@ -98,7 +105,7 @@ public class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
List<String> categories = new ArrayList<>();
try {
categories = api.allCategories(CategorizationFragment.SEARCH_CATS_LIMIT, this.filter);
categories = api.allCategories(SEARCH_CATS_LIMIT, this.filter);
Timber.d("Prefix URL filter %s", categories);
} catch (IOException e) {
Timber.e(e, "IO Exception: ");
@ -107,6 +114,6 @@ public class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
}
Timber.d("Found categories from Prefix search, waiting for filter");
return new ArrayList<>(filterYears(categories));
return new ArrayList<>(filterIrrelevantResults(categories));
}
}

View file

@ -19,17 +19,12 @@ class TitleCategories extends AsyncTask<Void, Void, List<String>> {
private final static int SEARCH_CATS_LIMIT = 25;
private String title;
private final String title;
TitleCategories(String title) {
this.title = title;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected List<String> doInBackground(Void... voids) {

View file

@ -9,11 +9,8 @@ public class BackgroundPoolExceptionHandler implements ExceptionHandler {
public void onException(@NonNull final Throwable t) {
//Crash for debug build
if (BuildConfig.DEBUG) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
throw new RuntimeException(t);
}
Thread thread = new Thread(() -> {
throw new RuntimeException(t);
});
thread.start();
}

View file

@ -11,17 +11,13 @@ class ThreadFactoryMaker {
private int count = 0;
@Override
public Thread newThread(final Runnable runnable) {
public Thread newThread(@NonNull final Runnable runnable) {
count++;
Runnable wrapperRunnable = new Runnable() {
@Override
public void run() {
Process.setThreadPriority(priority);
runnable.run();
}
Runnable wrapperRunnable = () -> {
Process.setThreadPriority(priority);
runnable.run();
};
Thread t = new Thread(wrapperRunnable, String.format("%s-%s", name, count));
return t;
return new Thread(wrapperRunnable, String.format("%s-%s", name, count));
}
};
}

View file

@ -39,7 +39,7 @@ public class ThreadPoolExecutorService implements Executor {
}
@Override
public void execute(Runnable command) {
public void execute(@NonNull Runnable command) {
backgroundPool.execute(command);
}

View file

@ -3,11 +3,13 @@ package fr.free.nrw.commons.contributions;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
@ -17,6 +19,7 @@ import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.support.v7.app.AlertDialog;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -27,7 +30,10 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
@ -110,6 +116,12 @@ public class ContributionsActivity
super.onPause();
}
@Override
protected void onStart() {
super.onStart();
displayFeedbackPopup();
}
@Override
protected void onAuthCookieAcquired(String authCookie) {
// Do a sync everytime we get here!
@ -342,4 +354,56 @@ public class ContributionsActivity
Intent contributionsIntent = new Intent(context, ContributionsActivity.class);
context.startActivity(contributionsIntent);
}
private void displayFeedbackPopup() {
Date popupMessageEndDate = null;
try {
String validUntil = "23/08/2017";
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
popupMessageEndDate = sdf.parse(validUntil);
} catch (ParseException e) {
e.printStackTrace();
}
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
CommonsApplication.getInstance());
// boolean to save users request about displaying popup
boolean displayFeedbackPopup = prefs.getBoolean("display_feedbak_popup", true);
// boolean to recognize is application re-started. Will be used for "remind me later" option
int appStartCounter = prefs.getInt("app_start_counter" ,0);
// if time is valid and shared pref says display
if (new Date().before(popupMessageEndDate) && displayFeedbackPopup && (appStartCounter == 4)) {
new AlertDialog.Builder(this)
.setTitle(getResources().getString(R.string.feedback_popup_title))
.setMessage(getResources().getString(R.string.feedback_popup_description))
.setPositiveButton(getResources().getString(R.string.feedback_popup_accept),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// Go to the page
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri
.parse(getResources()
.getString(R.string.feedback_page_url)));
startActivity(browserIntent);
// No need to dislay this window to the user again.
prefs.edit().putBoolean("display_feedbak_popup" , false).commit();
dialog.dismiss();
}
})
.setNegativeButton(getResources().getString(R.string.feedback_popup_decline),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Dismiss the dialog and not to show it later
prefs.edit().putBoolean("display_feedbak_popup", false).commit();
dialog.dismiss();
}
})
.create().show();
}
}
}

View file

@ -7,6 +7,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import fr.free.nrw.commons.CommonsApplication;
@ -38,7 +39,7 @@ public class ContributionsContentProvider extends ContentProvider{
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(Contribution.Table.TABLE_NAME);
@ -71,12 +72,12 @@ public class ContributionsContentProvider extends ContentProvider{
}
@Override
public String getType(Uri uri) {
public String getType(@NonNull Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
long id = 0;
@ -92,7 +93,7 @@ public class ContributionsContentProvider extends ContentProvider{
}
@Override
public int delete(Uri uri, String s, String[] strings) {
public int delete(@NonNull Uri uri, String s, String[] strings) {
int rows = 0;
int uriType = uriMatcher.match(uri);
@ -114,7 +115,7 @@ public class ContributionsContentProvider extends ContentProvider{
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (ContributionsContentProvider)");
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
@ -136,7 +137,7 @@ public class ContributionsContentProvider extends ContentProvider{
}
@Override
public int update(Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
public int update(@NonNull Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
/*
SQL Injection warnings: First, note that we're not exposing this to the outside world (exported="false")
Even then, we should make sure to sanitize all user input appropriately. Input that passes through ContentValues

View file

@ -8,6 +8,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
@ -29,6 +30,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.nearby.NearbyActivity;
import timber.log.Timber;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.app.Activity.RESULT_OK;
public class ContributionsListFragment extends Fragment {
@ -110,11 +112,11 @@ public class ContributionsListFragment extends Fragment {
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(getActivity(),
Manifest.permission.READ_EXTERNAL_STORAGE)
READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// Should we show an explanation?
if (shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE)) {
if (shouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
@ -122,15 +124,9 @@ public class ContributionsListFragment extends Fragment {
new AlertDialog.Builder(getActivity())
.setMessage(getString(R.string.storage_permission_rationale))
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
requestPermissions(
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
1);
dialog.dismiss();
}
.setPositiveButton("OK", (dialog, which) -> {
requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, 1);
dialog.dismiss();
})
.setNegativeButton("Cancel", null)
.create()
@ -140,7 +136,7 @@ public class ContributionsListFragment extends Fragment {
// No explanation needed, we can request the permission.
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
requestPermissions(new String[]{READ_EXTERNAL_STORAGE},
1);
// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
@ -167,7 +163,7 @@ public class ContributionsListFragment extends Fragment {
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Timber.d("onRequestPermissionsResult: req code = " + " perm = " + permissions + " grant =" + grantResults);

View file

@ -14,10 +14,10 @@ import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler;
import fr.free.nrw.commons.concurrency.ThreadPoolExecutorService;
import timber.log.Timber;
public class UploadCountClient {
class UploadCountClient {
private ThreadPoolExecutorService threadPoolExecutor;
public UploadCountClient() {
UploadCountClient() {
threadPoolExecutor = new ThreadPoolExecutorService.Builder("bg-pool")
.setPoolSize(Runtime.getRuntime().availableProcessors())
.setExceptionHandler(new BackgroundPoolExceptionHandler())
@ -27,29 +27,26 @@ public class UploadCountClient {
private static final String UPLOAD_COUNT_URL_TEMPLATE =
"https://tools.wmflabs.org/urbanecmbot/uploadsbyuser/uploadsbyuser.py?user=%s";
public ListenableFuture<Integer> getUploadCount(final String userName) {
ListenableFuture<Integer> getUploadCount(final String userName) {
final SettableFuture<Integer> future = SettableFuture.create();
threadPoolExecutor.schedule(new Runnable() {
@Override
public void run() {
URL url;
threadPoolExecutor.schedule(() -> {
URL url;
try {
url = new URL(String.format(Locale.ENGLISH, UPLOAD_COUNT_URL_TEMPLATE,
new PageTitle(userName).getText()));
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
try {
url = new URL(String.format(Locale.ENGLISH, UPLOAD_COUNT_URL_TEMPLATE,
new PageTitle(userName).getText()));
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
try {
BufferedReader bufferedReader = new BufferedReader(new
InputStreamReader(urlConnection.getInputStream()));
String uploadCount = bufferedReader.readLine();
bufferedReader.close();
future.set(Integer.parseInt(uploadCount));
} finally {
urlConnection.disconnect();
}
} catch (Exception e) {
Timber.e("Error getting upload count Error", e);
future.setException(e);
BufferedReader bufferedReader = new BufferedReader(new
InputStreamReader(urlConnection.getInputStream()));
String uploadCount = bufferedReader.readLine();
bufferedReader.close();
future.set(Integer.parseInt(uploadCount));
} finally {
urlConnection.disconnect();
}
} catch (Exception e) {
Timber.e("Error getting upload count Error", e);
future.setException(e);
}
});
return future;

View file

@ -119,12 +119,7 @@ public class MediaDetailFragment extends Fragment {
licenseList = new LicenseList(getActivity());
// Progressively darken the image in the background when we scroll detail pane up
scrollListener = new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
updateTheDarkness();
}
};
scrollListener = this::updateTheDarkness;
view.getViewTreeObserver().addOnScrollChangedListener(scrollListener);
// Layout layoutListener to size the spacer item relative to the available space.
@ -216,9 +211,23 @@ public class MediaDetailFragment extends Fragment {
if (success) {
extractor.fill(media);
setTextFields(media);
setOnClickListeners(media);
} else {
// Set text of desc, license, and categories
desc.setText(prettyDescription(media));
license.setText(prettyLicense(media));
coordinates.setText(prettyCoordinates(media));
uploadedDate.setText(prettyUploadedDate(media));
categoryNames.clear();
categoryNames.addAll(media.getCategories());
categoriesLoaded = true;
categoriesPresent = (categoryNames.size() > 0);
if (!categoriesPresent) {
// Stick in a filler element.
categoryNames.add(getString(R.string.detail_panel_cats_none));
}
rebuildCatList();
} else {
Timber.d("Failed to load photo details.");
}
}
@ -251,41 +260,6 @@ public class MediaDetailFragment extends Fragment {
super.onDestroyView();
}
private void setTextFields(Media media) {
desc.setText(prettyDescription(media));
license.setText(prettyLicense(media));
coordinates.setText(prettyCoordinates(media));
uploadedDate.setText(prettyUploadedDate(media));
categoryNames.clear();
categoryNames.addAll(media.getCategories());
categoriesLoaded = true;
categoriesPresent = (categoryNames.size() > 0);
if (!categoriesPresent) {
// Stick in a filler element.
categoryNames.add(getString(R.string.detail_panel_cats_none));
}
rebuildCatList();
}
private void setOnClickListeners(final Media media) {
license.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
openWebBrowser(licenseLink(media));
}
});
if (media.getCoordinates() != null) {
coordinates.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
openMap(media.getCoordinates());
}
});
}
}
private void rebuildCatList() {
categoryContainer.removeAllViews();
// @fixme add the category items
@ -301,15 +275,12 @@ public class MediaDetailFragment extends Fragment {
textView.setText(catName);
if (categoriesLoaded && categoriesPresent) {
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String selectedCategoryTitle = "Category:" + catName;
Intent viewIntent = new Intent();
viewIntent.setAction(Intent.ACTION_VIEW);
viewIntent.setData(new PageTitle(selectedCategoryTitle).getCanonicalUri());
startActivity(viewIntent);
}
textView.setOnClickListener(view -> {
String selectedCategoryTitle = "Category:" + catName;
Intent viewIntent = new Intent();
viewIntent.setAction(Intent.ACTION_VIEW);
viewIntent.setData(new PageTitle(selectedCategoryTitle).getCanonicalUri());
startActivity(viewIntent);
});
}
return item;
@ -371,33 +342,4 @@ public class MediaDetailFragment extends Fragment {
}
return media.getCoordinates().getPrettyCoordinateString();
}
private @Nullable String licenseLink(Media media) {
String licenseKey = media.getLicense();
if (licenseKey == null || licenseKey.equals("")) {
return null;
}
License licenseObj = licenseList.get(licenseKey);
if (licenseObj == null) {
return null;
} else {
return licenseObj.getUrl(Locale.getDefault().getLanguage());
}
}
private void openWebBrowser(String url) {
Intent browser = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(browser);
}
private void openMap(LatLng coordinates) {
//Open map app at given position
Uri gmmIntentUri = Uri.parse(
"geo:0,0?q=" + coordinates.getLatitude() + "," + coordinates.getLatitude());
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) {
startActivity(mapIntent);
}
}
}

View file

@ -35,6 +35,19 @@ import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.mwapi.EventLog;
public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPageChangeListener {
public interface MediaDetailProvider {
Media getMediaAtPosition(int i);
int getTotalMediaCount();
void notifyDatasetChanged();
void registerDataSetObserver(DataSetObserver observer);
void unregisterDataSetObserver(DataSetObserver observer);
}
private ViewPager pager;
private Boolean editable;
private CommonsApplication app;
@ -48,14 +61,6 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
this.editable = editable;
}
public interface MediaDetailProvider {
Media getMediaAtPosition(int i);
int getTotalMediaCount();
void notifyDatasetChanged();
void registerDataSetObserver(DataSetObserver observer);
void unregisterDataSetObserver(DataSetObserver observer);
}
//FragmentStatePagerAdapter allows user to swipe across collection of images (no. of images undetermined)
private class MediaDetailAdapter extends FragmentStatePagerAdapter {
@ -65,14 +70,9 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
@Override
public Fragment getItem(int i) {
if(i == 0) {
if (i == 0) {
// See bug https://code.google.com/p/android/issues/detail?id=27526
pager.postDelayed(new Runnable() {
@Override
public void run() {
getActivity().supportInvalidateOptionsMenu();
}
}, 5);
pager.postDelayed(() -> getActivity().supportInvalidateOptionsMenu(), 5);
}
return MediaDetailFragment.forMedia(i, editable);
}
@ -95,14 +95,11 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
final int pageNumber = savedInstanceState.getInt("current-page");
// Adapter doesn't seem to be loading immediately.
// Dear God, please forgive us for our sins
view.postDelayed(new Runnable() {
@Override
public void run() {
pager.setAdapter(adapter);
pager.setCurrentItem(pageNumber, false);
getActivity().supportInvalidateOptionsMenu();
adapter.notifyDataSetChanged();
}
view.postDelayed(() -> {
pager.setAdapter(adapter);
pager.setCurrentItem(pageNumber, false);
getActivity().supportInvalidateOptionsMenu();
adapter.notifyDataSetChanged();
}, 100);
} else {
pager.setAdapter(adapter);
@ -120,7 +117,7 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState != null) {
if (savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable");
}
app = CommonsApplication.getInstance();
@ -191,13 +188,8 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !(ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)) {
Snackbar.make(getView(), R.string.storage_permission_rationale,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
ActivityCompat.requestPermissions(getActivity(),
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
}
}).show();
.setAction(R.string.ok, view -> ActivityCompat.requestPermissions(getActivity(),
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1)).show();
} else {
final DownloadManager manager = (DownloadManager)getActivity().getSystemService(Context.DOWNLOAD_SERVICE);
manager.enqueue(req);
@ -206,13 +198,13 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if(!editable) { // Disable menu options for editable views
if (!editable) { // Disable menu options for editable views
menu.clear(); // see http://stackoverflow.com/a/8495697/17865
inflater.inflate(R.menu.fragment_image_detail, menu);
if(pager != null) {
if (pager != null) {
MediaDetailProvider provider = (MediaDetailProvider)getActivity();
Media m = provider.getMediaAtPosition(pager.getCurrentItem());
if(m != null) {
if (m != null) {
// Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
menu.findItem(R.id.menu_retry_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_cancel_current_image).setEnabled(false).setVisible(false);

View file

@ -7,6 +7,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import fr.free.nrw.commons.CommonsApplication;
@ -39,7 +40,7 @@ public class ModificationsContentProvider extends ContentProvider{
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(ModifierSequence.Table.TABLE_NAME);
@ -61,12 +62,12 @@ public class ModificationsContentProvider extends ContentProvider{
}
@Override
public String getType(Uri uri) {
public String getType(@NonNull Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
long id = 0;
@ -82,7 +83,7 @@ public class ModificationsContentProvider extends ContentProvider{
}
@Override
public int delete(Uri uri, String s, String[] strings) {
public int delete(@NonNull Uri uri, String s, String[] strings) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
switch (uriType) {
@ -99,7 +100,7 @@ public class ModificationsContentProvider extends ContentProvider{
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (ModificationsContentProvider)");
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
@ -121,7 +122,7 @@ public class ModificationsContentProvider extends ContentProvider{
}
@Override
public int update(Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
public int update(@NonNull Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
/*
SQL Injection warnings: First, note that we're not exposing this to the outside world (exported="false")
Even then, we should make sure to sanitize all user input appropriately. Input that passes through ContentValues

View file

@ -351,12 +351,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
@Override
@NonNull
public UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, final ProgressListener progressListener) throws IOException {
ApiResult result = api.upload(filename, file, dataLength, pageContents, editSummary, new in.yuvi.http.fluent.ProgressListener() {
@Override
public void onProgress(long transferred, long total) {
progressListener.onProgress(transferred, total);
}
});
ApiResult result = api.upload(filename, file, dataLength, pageContents, editSummary, progressListener::onProgress);
Log.e("WTF", "Result: "+result.toString());

View file

@ -2,7 +2,6 @@ package fr.free.nrw.commons.nearby;
import android.Manifest;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.LocationManager;
@ -41,7 +40,8 @@ import timber.log.Timber;
public class NearbyActivity extends NavigationBaseActivity {
@BindView(R.id.progressBar) ProgressBar progressBar;
@BindView(R.id.progressBar)
ProgressBar progressBar;
private boolean isMapViewActive = false;
private static final int LOCATION_REQUEST = 1;
@ -115,15 +115,11 @@ public class NearbyActivity extends NavigationBaseActivity {
new AlertDialog.Builder(this)
.setMessage(getString(R.string.location_permission_rationale))
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCompat.requestPermissions(NearbyActivity.this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
LOCATION_REQUEST);
dialog.dismiss();
}
.setPositiveButton("OK", (dialog, which) -> {
ActivityCompat.requestPermissions(NearbyActivity.this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
LOCATION_REQUEST);
dialog.dismiss();
})
.setNegativeButton("Cancel", null)
.create()
@ -175,26 +171,19 @@ public class NearbyActivity extends NavigationBaseActivity {
LocationManager manager = (LocationManager) getSystemService(LOCATION_SERVICE);
if (!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
Timber.d("GPS is not enabled");
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
alertDialogBuilder.setMessage(R.string.gps_disabled)
new AlertDialog.Builder(this)
.setMessage(R.string.gps_disabled)
.setCancelable(false)
.setPositiveButton(R.string.enable_gps,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
Intent callGPSSettingIntent = new Intent(
android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
Timber.d("Loaded settings page");
startActivityForResult(callGPSSettingIntent, 1);
}
});
alertDialogBuilder.setNegativeButton(R.string.menu_cancel_upload,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
AlertDialog alert = alertDialogBuilder.create();
alert.show();
(dialog, id) -> {
Intent callGPSSettingIntent = new Intent(
android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
Timber.d("Loaded settings page");
startActivityForResult(callGPSSettingIntent, 1);
})
.setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> dialog.cancel())
.create()
.show();
} else {
Timber.d("GPS is enabled");
}
@ -257,7 +246,7 @@ public class NearbyActivity extends NavigationBaseActivity {
private final Context mContext;
private NearbyAsyncTask (Context context) {
private NearbyAsyncTask(Context context) {
mContext = context;
}

View file

@ -1,5 +1,7 @@
package fr.free.nrw.commons.nearby;
import android.support.annotation.NonNull;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder;
@ -10,7 +12,7 @@ import java.util.List;
class NearbyAdapterFactory {
private PlaceRenderer.PlaceClickedListener listener;
NearbyAdapterFactory(PlaceRenderer.PlaceClickedListener listener) {
NearbyAdapterFactory(@NonNull PlaceRenderer.PlaceClickedListener listener) {
this.listener = listener;
}

View file

@ -16,6 +16,17 @@ import fr.free.nrw.commons.utils.UriDeserializer;
import fr.free.nrw.commons.utils.UriSerializer;
public class NearbyBaseMarker extends BaseMarkerOptions<NearbyMarker, NearbyBaseMarker> {
public static final Parcelable.Creator<NearbyBaseMarker> CREATOR = new Parcelable.Creator<NearbyBaseMarker>() {
public NearbyBaseMarker createFromParcel(Parcel in) {
return new NearbyBaseMarker(in);
}
public NearbyBaseMarker[] newArray(int size) {
return new NearbyBaseMarker[size];
}
};
private Place place;
NearbyBaseMarker() {
@ -74,15 +85,4 @@ public class NearbyBaseMarker extends BaseMarkerOptions<NearbyMarker, NearbyBase
dest.writeString(title);
dest.writeString(gson.toJson(place));
}
public static final Parcelable.Creator<NearbyBaseMarker> CREATOR
= new Parcelable.Creator<NearbyBaseMarker>() {
public NearbyBaseMarker createFromParcel(Parcel in) {
return new NearbyBaseMarker(in);
}
public NearbyBaseMarker[] newArray(int size) {
return new NearbyBaseMarker[size];
}
};
}

View file

@ -2,9 +2,10 @@ package fr.free.nrw.commons.nearby;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.preference.PreferenceManager;
import android.support.graphics.drawable.VectorDrawableCompat;
import com.mapbox.mapboxsdk.annotations.Icon;
import com.mapbox.mapboxsdk.annotations.IconFactory;
import java.util.ArrayList;
@ -18,6 +19,7 @@ import java.util.Map;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.UiUtils;
import timber.log.Timber;
import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween;
@ -50,13 +52,10 @@ public class NearbyController {
distances.put(place, computeDistanceBetween(place.location, curLatLng));
}
Collections.sort(places,
new Comparator<Place>() {
@Override
public int compare(Place lhs, Place rhs) {
double lhsDistance = distances.get(lhs);
double rhsDistance = distances.get(rhs);
return (int) (lhsDistance - rhsDistance);
}
(lhs, rhs) -> {
double lhsDistance = distances.get(lhs);
double rhsDistance = distances.get(rhs);
return (int) (lhsDistance - rhsDistance);
}
);
}
@ -98,13 +97,15 @@ 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()
));
for (Place place: placeList) {
String distance = formatDistanceBetween(curLatLng, place.location);
place.setDistance(distance);
Icon icon = IconFactory.getInstance(context)
.fromResource(R.drawable.custom_map_marker);
NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
nearbyBaseMarker.title(place.name);
nearbyBaseMarker.position(
@ -112,7 +113,8 @@ public class NearbyController {
place.location.getLatitude(),
place.location.getLongitude()));
nearbyBaseMarker.place(place);
nearbyBaseMarker.icon(icon);
nearbyBaseMarker.icon(IconFactory.getInstance(context)
.fromBitmap(icon));
baseMarkerOptions.add(nearbyBaseMarker);
}

View file

@ -63,15 +63,10 @@ public class NearbyInfoDialog extends OverlayDialog {
overflowButton.setVisibility(showMenu() ? View.VISIBLE : View.GONE);
overflowButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
popupMenuListener();
}
});
overflowButton.setOnClickListener(this::popupMenuListener);
}
private void popupMenuListener() {
private void popupMenuListener(View v) {
PopupMenu popupMenu = new PopupMenu(getActivity(), overflowButton);
popupMenu.inflate(R.menu.nearby_info_dialog_options);

View file

@ -48,12 +48,7 @@ public class NearbyListFragment extends Fragment {
View view = inflater.inflate(R.layout.fragment_nearby, container, false);
recyclerView = (RecyclerView) view.findViewById(R.id.listView);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
adapterFactory = new NearbyAdapterFactory(new PlaceRenderer.PlaceClickedListener() {
@Override
public void placeClicked(Place place) {
NearbyInfoDialog.showYourself(getActivity(), place);
}
});
adapterFactory = new NearbyAdapterFactory(place -> NearbyInfoDialog.showYourself(getActivity(), place));
return view;
}

View file

@ -88,25 +88,19 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
// create map
mapView = new MapView(getActivity(), options);
mapView.onCreate(savedInstanceState);
mapView.getMapAsync(new OnMapReadyCallback() {
@Override
public void onMapReady(MapboxMap mapboxMap) {
mapboxMap.addMarkers(baseMarkerOptions);
mapView.getMapAsync(mapboxMap -> {
mapboxMap.addMarkers(baseMarkerOptions);
mapboxMap.setOnMarkerClickListener(new MapboxMap.OnMarkerClickListener() {
@Override
public boolean onMarkerClick(@NonNull Marker marker) {
if (marker instanceof NearbyMarker) {
NearbyMarker nearbyMarker = (NearbyMarker) marker;
Place place = nearbyMarker.getNearbyBaseMarker().getPlace();
NearbyInfoDialog.showYourself(getActivity(), place);
}
return false;
}
});
mapboxMap.setOnMarkerClickListener(marker -> {
if (marker instanceof NearbyMarker) {
NearbyMarker nearbyMarker = (NearbyMarker) marker;
Place place = nearbyMarker.getNearbyBaseMarker().getPlace();
NearbyInfoDialog.showYourself(getActivity(), place);
}
return false;
});
addCurrentLocationMarker(mapboxMap);
}
addCurrentLocationMarker(mapboxMap);
});
if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme",true)) {
mapView.setStyleUrl(getResources().getString(R.string.map_theme_dark));

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.nearby;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -19,7 +20,7 @@ class PlaceRenderer extends Renderer<Place> {
@BindView(R.id.icon) ImageView icon;
private final PlaceClickedListener listener;
PlaceRenderer(PlaceClickedListener listener) {
PlaceRenderer(@NonNull PlaceClickedListener listener) {
this.listener = listener;
}
@ -35,14 +36,7 @@ class PlaceRenderer extends Renderer<Place> {
@Override
protected void hookListeners(View view) {
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.placeClicked(getContent());
}
}
});
view.setOnClickListener(v -> listener.placeClicked(getContent()));
}
@Override

View file

@ -28,21 +28,15 @@ public class SettingsFragment extends PreferenceFragment {
licensePreference.setSummary(getString(Utils.licenseNameFor(licensePreference.getValue())));
// Keep summary updated when changing value
licensePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
preference.setSummary(getString(Utils.licenseNameFor((String) newValue)));
return true;
}
licensePreference.setOnPreferenceChangeListener((preference, newValue) -> {
preference.setSummary(getString(Utils.licenseNameFor((String) newValue)));
return true;
});
CheckBoxPreference themePreference = (CheckBoxPreference) findPreference("theme");
themePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
getActivity().recreate();
return true;
}
themePreference.setOnPreferenceChangeListener((preference, newValue) -> {
getActivity().recreate();
return true;
});
final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads");
@ -51,36 +45,27 @@ public class SettingsFragment extends PreferenceFragment {
int uploads = sharedPref.getInt(Prefs.UPLOADS_SHOWING, 100);
uploadLimit.setText(uploads + "");
uploadLimit.setSummary(uploads + "");
uploadLimit.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
int value = Integer.parseInt(newValue.toString());
final SharedPreferences.Editor editor = sharedPref.edit();
if (value > 500) {
new AlertDialog.Builder(getActivity())
.setTitle(R.string.maximum_limit)
.setMessage(R.string.maximum_limit_alert)
.setPositiveButton(android.R.string.yes,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
}
})
.setIcon(android.R.drawable.ic_dialog_alert)
.show();
editor.putInt(Prefs.UPLOADS_SHOWING, 500);
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED,true);
uploadLimit.setSummary(500 + "");
uploadLimit.setText(500 + "");
} else {
editor.putInt(Prefs.UPLOADS_SHOWING, Integer.parseInt(newValue.toString()));
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED,true);
uploadLimit.setSummary(newValue.toString());
}
editor.apply();
return true;
uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> {
int value = Integer.parseInt(newValue.toString());
final SharedPreferences.Editor editor = sharedPref.edit();
if (value > 500) {
new AlertDialog.Builder(getActivity())
.setTitle(R.string.maximum_limit)
.setMessage(R.string.maximum_limit_alert)
.setPositiveButton(android.R.string.yes, (dialog, which) -> {})
.setIcon(android.R.drawable.ic_dialog_alert)
.show();
editor.putInt(Prefs.UPLOADS_SHOWING, 500);
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED,true);
uploadLimit.setSummary(500 + "");
uploadLimit.setText(500 + "");
} else {
editor.putInt(Prefs.UPLOADS_SHOWING, Integer.parseInt(newValue.toString()));
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED,true);
uploadLimit.setSummary(newValue.toString());
}
editor.apply();
return true;
});

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.theme;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
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;
@ -55,12 +56,7 @@ public class NavigationBaseActivity extends BaseActivity
public void initBackButton() {
int backStackEntryCount = getSupportFragmentManager().getBackStackEntryCount();
toggle.setDrawerIndicatorEnabled(backStackEntryCount == 0);
toggle.setToolbarNavigationClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onBackPressed();
}
});
toggle.setToolbarNavigationClickListener(v -> onBackPressed());
}
public void initBack() {
@ -114,29 +110,29 @@ public class NavigationBaseActivity extends BaseActivity
Toast.makeText(this, R.string.no_email_client, Toast.LENGTH_SHORT).show();
}
return true;
case R.id.action_developer_plans:
drawerLayout.closeDrawer(navigationView);
// Go to the page
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri
.parse(getResources()
.getString(R.string.feedback_page_url)));
startActivity(browserIntent);
return true;
case R.id.action_logout:
new AlertDialog.Builder(this)
.setMessage(R.string.logout_verification)
.setCancelable(false)
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
((CommonsApplication) getApplicationContext())
.clearApplicationData(NavigationBaseActivity.this);
Intent nearbyIntent = new Intent(
NavigationBaseActivity.this, LoginActivity.class);
nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(nearbyIntent);
finish();
}
})
.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
.setPositiveButton(R.string.yes, (dialog, which) -> {
((CommonsApplication) getApplicationContext())
.clearApplicationData(NavigationBaseActivity.this);
Intent nearbyIntent = new Intent(
NavigationBaseActivity.this, LoginActivity.class);
nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(nearbyIntent);
finish();
})
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
.show();
return true;
default:

View file

@ -19,4 +19,8 @@ public class HtmlTextView extends AppCompatTextView {
setMovementMethod(LinkMovementMethod.getInstance());
setText(Utils.fromHtml(getText().toString()));
}
public void setHtmlText(String newText) {
setText(Utils.fromHtml(newText));
}
}

View file

@ -72,21 +72,13 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(R.string.file_exists)
.setTitle(R.string.warning);
builder.setPositiveButton(R.string.no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
//Go back to ContributionsActivity
Intent intent = new Intent(context, ContributionsActivity.class);
context.startActivity(intent);
callback.onResult(Result.DUPLICATE_CANCELLED);
}
});
builder.setNegativeButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
callback.onResult(Result.DUPLICATE_PROCEED);
}
builder.setPositiveButton(R.string.no, (dialog, id) -> {
//Go back to ContributionsActivity
Intent intent = new Intent(context, ContributionsActivity.class);
context.startActivity(intent);
callback.onResult(Result.DUPLICATE_CANCELLED);
});
builder.setNegativeButton(R.string.yes, (dialog, id) -> callback.onResult(Result.DUPLICATE_PROCEED));
AlertDialog dialog = builder.create();
dialog.show();

View file

@ -11,6 +11,7 @@ import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
@ -28,6 +29,7 @@ import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
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.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.modifications.CategoryModifier;
@ -43,7 +45,7 @@ public class MultipleShareActivity
AdapterView.OnItemClickListener,
FragmentManager.OnBackStackChangedListener,
MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
CategorizationFragment.OnCategoriesSaveHandler {
OnCategoriesSaveHandler {
private CommonsApplication app;
private ArrayList<Contribution> photosList = null;
@ -104,7 +106,7 @@ public class MultipleShareActivity
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
multipleUploadBegins();
}
@ -125,19 +127,16 @@ public class MultipleShareActivity
Contribution up = photosList.get(i);
final int uploadCount = i + 1; // Goddamn Java
uploadController.startUpload(up, new UploadController.ContributionUploadProgress() {
@Override
public void onUploadStarted(Contribution contribution) {
dialog.setProgress(uploadCount);
if(uploadCount == photosList.size()) {
dialog.dismiss();
Toast startingToast = Toast.makeText(
CommonsApplication.getInstance(),
R.string.uploading_started,
Toast.LENGTH_LONG
);
startingToast.show();
}
uploadController.startUpload(up, contribution -> {
dialog.setProgress(uploadCount);
if (uploadCount == photosList.size()) {
dialog.dismiss();
Toast startingToast = Toast.makeText(
CommonsApplication.getInstance(),
R.string.uploading_started,
Toast.LENGTH_LONG
);
startingToast.show();
}
});
}

View file

@ -57,7 +57,7 @@ public class MwVolleyApi {
Timber.d("URL: %s", apiUrl);
JsonRequest<QueryResponse> request = new QueryRequest(apiUrl,
new LogResponseListener<QueryResponse>(), new LogResponseErrorListener());
new LogResponseListener<>(), new LogResponseErrorListener());
getQueue().add(request);
}

View file

@ -9,6 +9,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.design.widget.Snackbar;
@ -36,6 +37,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
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.modifications.CategoryModifier;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
@ -44,6 +46,9 @@ import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.EventLog;
import timber.log.Timber;
import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.DUPLICATE_PROCEED;
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.
@ -51,7 +56,7 @@ import timber.log.Timber;
public class ShareActivity
extends AuthenticatedActivity
implements SingleUploadFragment.OnUploadActionInitiated,
CategorizationFragment.OnCategoriesSaveHandler {
OnCategoriesSaveHandler {
private static final int REQUEST_PERM_ON_CREATE_STORAGE = 1;
private static final int REQUEST_PERM_ON_CREATE_LOCATION = 2;
@ -133,12 +138,9 @@ public class ShareActivity
Timber.d("Cache the categories found");
}
uploadController.startUpload(title, mediaUri, description, mimeType, source, decimalCoords, new UploadController.ContributionUploadProgress() {
@Override
public void onUploadStarted(Contribution contribution) {
ShareActivity.this.contribution = contribution;
showPostUpload();
}
uploadController.startUpload(title, mediaUri, description, mimeType, source, decimalCoords, c -> {
ShareActivity.this.contribution = c;
showPostUpload();
});
}
@ -317,7 +319,7 @@ public class ShareActivity
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) {
@NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_PERM_ON_CREATE_STORAGE: {
if (grantResults.length >= 1
@ -379,14 +381,10 @@ public class ShareActivity
Timber.d("File SHA1 is: %s", fileSHA1);
ExistingFileAsync fileAsyncTask =
new ExistingFileAsync(fileSHA1, this, new ExistingFileAsync.Callback() {
@Override
public void onResult(ExistingFileAsync.Result result) {
Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
duplicateCheckPassed =
result == ExistingFileAsync.Result.DUPLICATE_PROCEED
|| result == ExistingFileAsync.Result.NO_DUPLICATE;
}
new ExistingFileAsync(fileSHA1, this, result -> {
Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
duplicateCheckPassed = (result == DUPLICATE_PROCEED
|| result == NO_DUPLICATE);
});
fileAsyncTask.execute();
} catch (IOException e) {
@ -401,17 +399,12 @@ public class ShareActivity
}
}
private Snackbar requestPermissionUsingSnackBar(
String rationale, final String[] perms, final int code) {
private Snackbar requestPermissionUsingSnackBar(String rationale,
final String[] perms,
final int code) {
Snackbar snackbar = Snackbar.make(findViewById(android.R.id.content), rationale,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
ActivityCompat.requestPermissions(ShareActivity.this,
perms, code);
}
});
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
view -> ActivityCompat.requestPermissions(ShareActivity.this, perms, code));
snackbar.show();
return snackbar;
}

View file

@ -86,12 +86,12 @@ public class UploadService extends HandlerService<Contribution> {
@Override
public void onProgress(long transferred, long total) {
Timber.d("Uploaded %d of %d", transferred, total);
if(!notificationTitleChanged) {
if (!notificationTitleChanged) {
curProgressNotification.setContentTitle(notificationProgressTitle);
notificationTitleChanged = true;
contribution.setState(Contribution.STATE_IN_PROGRESS);
}
if(transferred == total) {
if (transferred == total) {
// Completed!
curProgressNotification.setContentTitle(notificationFinishingTitle);
curProgressNotification.setProgress(0, 100, true);
@ -124,7 +124,7 @@ public class UploadService extends HandlerService<Contribution> {
@Override
protected void handle(int what, Contribution contribution) {
switch(what) {
switch (what) {
case ACTION_UPLOAD_FILE:
//FIXME: Google Photos bug
uploadContribution(contribution);
@ -162,7 +162,7 @@ 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 (intent.getAction().equals(ACTION_START_SERVICE) && freshStart) {
ContentValues failedValues = new ContentValues();
failedValues.put(Contribution.Table.COLUMN_STATE, Contribution.STATE_FAILED);
@ -189,7 +189,7 @@ public class UploadService extends HandlerService<Contribution> {
try {
//FIXME: Google Photos bug
file = this.getContentResolver().openInputStream(contribution.getLocalUri());
} catch(FileNotFoundException e) {
} catch (FileNotFoundException e) {
Timber.d("File not found");
Toast fileNotFound = Toast.makeText(this, R.string.upload_failed, Toast.LENGTH_LONG);
fileNotFound.show();
@ -220,9 +220,9 @@ public class UploadService extends HandlerService<Contribution> {
filename = findUniqueFilename(filename);
unfinishedUploads.add(filename);
}
if(!api.validateLogin()) {
if (!api.validateLogin()) {
// Need to revalidate!
if(app.revalidateAuthToken()) {
if (app.revalidateAuthToken()) {
Timber.d("Successfully revalidated token!");
} else {
Timber.d("Unable to revalidate :(");
@ -245,7 +245,7 @@ public class UploadService extends HandlerService<Contribution> {
curProgressNotification = null;
String resultStatus = uploadResult.getResultStatus();
if(!resultStatus.equals("Success")) {
if (!resultStatus.equals("Success")) {
showFailedNotification(contribution);
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT)
.param("username", app.getCurrentAccount().name)
@ -269,15 +269,15 @@ public class UploadService extends HandlerService<Contribution> {
.param("result", "success")
.log();
}
} catch(IOException e) {
} catch (IOException e) {
Timber.d("I have a network fuckup");
showFailedNotification(contribution);
} finally {
if ( filename != null ) {
if (filename != null) {
unfinishedUploads.remove(filename);
}
toUpload--;
if(toUpload == 0) {
if (toUpload == 0) {
// Sync modifications right after all uplaods are processed
ContentResolver.requestSync((CommonsApplication.getInstance()).getCurrentAccount(), ModificationsContentProvider.AUTHORITY, new Bundle());
stopForeground(true);
@ -287,7 +287,7 @@ public class UploadService extends HandlerService<Contribution> {
@SuppressLint("StringFormatInvalid")
private void showFailedNotification(Contribution contribution) {
Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true)
Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
.setAutoCancel(true)
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0))
@ -304,7 +304,7 @@ public class UploadService extends HandlerService<Contribution> {
private String findUniqueFilename(String fileName) throws IOException {
MediaWikiApi api = app.getMWApi();
String sequenceFileName;
for ( int sequenceNumber = 1; true; sequenceNumber++ ) {
for (int sequenceNumber = 1; true; sequenceNumber++) {
if (sequenceNumber == 1) {
sequenceFileName = fileName;
} else {
@ -318,9 +318,8 @@ public class UploadService extends HandlerService<Contribution> {
sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2");
}
}
if ( api.fileExistsWithName(sequenceFileName) || unfinishedUploads.contains(sequenceFileName) ) {
continue;
} else {
if (!api.fileExistsWithName(sequenceFileName)
&& !unfinishedUploads.contains(sequenceFileName)) {
break;
}
}

View file

@ -7,14 +7,11 @@ import java.util.concurrent.Executor;
public class ExecutorUtils {
private static final Executor uiExecutor = new Executor() {
@Override
public void execute(Runnable command) {
if (Looper.myLooper() == Looper.getMainLooper()) {
command.run();
} else {
new Handler(Looper.getMainLooper()).post(command);
}
private static final Executor uiExecutor = command -> {
if (Looper.myLooper() == Looper.getMainLooper()) {
command.run();
} else {
new Handler(Looper.getMainLooper()).post(command);
}
};

View file

@ -42,8 +42,8 @@ public class FileUtils {
if (file != null) {
if (file.isDirectory()) {
String[] children = file.list();
for (int i = 0; i < children.length; i++) {
deletedAll = deleteFile(new File(file, children[i])) && deletedAll;
for (String child : children) {
deletedAll = deleteFile(new File(file, child)) && deletedAll;
}
} else {
deletedAll = file.delete();

View file

@ -0,0 +1,22 @@
package fr.free.nrw.commons.utils;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.support.graphics.drawable.VectorDrawableCompat;
public class UiUtils {
/**
* Draws a vectorial image onto a bitmap.
* @param vectorDrawable vectorial image
* @return bitmap representation of the vectorial image
*/
public static Bitmap getBitmap(VectorDrawableCompat vectorDrawable) {
Bitmap bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(),
vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
vectorDrawable.draw(canvas);
return bitmap;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 590 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 993 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 B

Some files were not shown because too many files have changed in this diff Show more