From 01cb9ccd704f288a7d6f9ab0bcf3adc4fb1b097a Mon Sep 17 00:00:00 2001 From: Elliott Eggleston Date: Sat, 19 May 2018 15:00:06 -0500 Subject: [PATCH] Wmhack2018 (#1536) * Add new activity to manifest * Create review activity layout base * Add a new menu item to drawer for peer review * Add a top menu with randomizer icon to review activity * Add strings for review button * Add activity to ActivityBuilderModule for injection * Add a new drawer item to start review acitivty * Create base of the Review Activity * Add fragment pager * Add new fragment for injection * Create a fragment pager layout * Wikimedia hackathon 2018 (#1533) * First draft of fn to get random recent image * Use log entries for requests to beta, try to connect refresh button FIXME: runs http request on main thread, breaks * Tweak button connection * Add ReviewController class * Fix fragments * Wmhack2018 (#1534) * tiny fixes * Load pictures into activities * Re-use same class for all review fragments (#1537) And try to add pager indicator * [WIP] category check * [WIP] add on-click actions to ReviewActivity * [WIP] add SendThankTask * Make it beautiful * Use standalone category extraction code in MediaDataExtractor * Add categories to category review page --- app/src/main/AndroidManifest.xml | 4 + .../free/nrw/commons/MediaDataExtractor.java | 18 +- .../free/nrw/commons/delete/DeleteTask.java | 44 ++--- .../nrw/commons/di/ActivityBuilderModule.java | 4 + .../di/CommonsApplicationComponent.java | 8 +- .../nrw/commons/di/FragmentBuilderModule.java | 4 + .../commons/media/MediaDetailFragment.java | 6 + .../media/RecentChangesImageUtils.java | 32 ++++ .../mwapi/ApacheHttpClientMediaWikiApi.java | 87 ++++++++- .../free/nrw/commons/mwapi/MediaWikiApi.java | 7 + .../nrw/commons/review/CheckCategoryTask.java | 130 +++++++++++++ .../nrw/commons/review/ReviewActivity.java | 173 ++++++++++++++++++ .../nrw/commons/review/ReviewController.java | 52 ++++++ .../commons/review/ReviewImageFragment.java | 92 ++++++++++ .../commons/review/ReviewPagerAdapter.java | 51 ++++++ .../nrw/commons/review/SendThankTask.java | 140 ++++++++++++++ .../commons/theme/NavigationBaseActivity.java | 6 + .../commons/utils/MediaDataExtractorUtil.java | 28 +++ .../main/res/drawable/ic_check_black_24dp.xml | 9 + .../res/drawable/ic_refresh_black_24dp.xml | 9 + .../res/drawable/tab_indicator_default.xml | 12 ++ .../res/drawable/tab_indicator_selected.xml | 8 + app/src/main/res/drawable/tab_selector.xml | 8 + app/src/main/res/layout/activity_review.xml | 65 +++++++ .../main/res/layout/fragment_review_image.xml | 89 +++++++++ app/src/main/res/menu/drawer.xml | 5 + .../main/res/menu/review_randomizer_menu.xml | 10 + app/src/main/res/values/strings.xml | 30 +++ 28 files changed, 1086 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/media/RecentChangesImageUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/CheckCategoryTask.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/SendThankTask.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java create mode 100644 app/src/main/res/drawable/ic_check_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_refresh_black_24dp.xml create mode 100644 app/src/main/res/drawable/tab_indicator_default.xml create mode 100644 app/src/main/res/drawable/tab_indicator_selected.xml create mode 100644 app/src/main/res/drawable/tab_selector.xml create mode 100644 app/src/main/res/layout/activity_review.xml create mode 100644 app/src/main/res/layout/fragment_review_image.xml create mode 100644 app/src/main/res/menu/review_randomizer_menu.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 17f6770d2..a4c944a2d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -96,6 +96,10 @@ android:label="@string/title_activity_featured_images" android:parentActivityName=".contributions.ContributionsActivity" /> + + { notificationBuilder = new NotificationCompat.Builder(context); Toast toast = new Toast(context); toast.setGravity(Gravity.CENTER,0,0); - toast = Toast.makeText(context,"Trying to nominate "+media.getDisplayTitle()+ " for deletion",Toast.LENGTH_SHORT); + toast = Toast.makeText(context,"Trying to nominate "+media.getDisplayTitle()+ " for deletion", Toast.LENGTH_SHORT); toast.show(); } @@ -64,7 +64,7 @@ public class DeleteTask extends AsyncTask { String editToken; String authCookie; - String summary = "Nominating " + media.getFilename() +" for deletion."; + String summary = context.getString(R.string.nominating_file_for_deletion, media.getFilename()); authCookie = sessionManager.getAuthCookie(); mwApi.setAuthCookie(authCookie); @@ -97,19 +97,19 @@ public class DeleteTask extends AsyncTask { publishProgress(1); mwApi.prependEdit(editToken,fileDeleteString+"\n", - media.getFilename(),summary); + media.getFilename(), summary); publishProgress(2); mwApi.edit(editToken,subpageString+"\n", - "Commons:Deletion_requests/"+media.getFilename(),summary); + "Commons:Deletion_requests/"+media.getFilename(), summary); publishProgress(3); mwApi.appendEdit(editToken,logPageString+"\n", - "Commons:Deletion_requests/"+date,summary); + "Commons:Deletion_requests/"+date, summary); publishProgress(4); mwApi.appendEdit(editToken,userPageString+"\n", - "User_Talk:"+sessionManager.getCurrentAccount().name,summary); + "User_Talk:"+sessionManager.getCurrentAccount().name, summary); publishProgress(5); } catch (Exception e) { @@ -123,29 +123,21 @@ public class DeleteTask extends AsyncTask { protected void onProgressUpdate (Integer... values){ super.onProgressUpdate(values); + int[] messages = new int[]{ + R.string.getting_edit_token, + R.string.nominate_for_deletion_edit_file_page, + R.string.nominate_for_deletion_create_deletion_request, + R.string.nominate_for_deletion_edit_deletion_request_log, + R.string.nominate_for_deletion_notify_user, + R.string.nominate_for_deletion_done + }; + String message = ""; - switch (values[0]){ - case 0: - message = "Getting token"; - break; - case 1: - message = "Adding delete message to file"; - break; - case 2: - message = "Creating Delete requests sub-page"; - break; - case 3: - message = "Adding file to Delete requests log"; - break; - case 4: - message = "Notifying User on Talk page"; - break; - case 5: - message = "Done"; - break; + if (0 < values[0] && values[0] < messages.length) { + message = context.getString(messages[values[0]]); } - notificationBuilder.setContentTitle("Nominating "+media.getDisplayTitle()+" for deletion") + notificationBuilder.setContentTitle(context.getString(R.string.nominating_file_for_deletion, media.getFilename())) .setStyle(new NotificationCompat.BigTextStyle() .bigText(message)) .setSmallIcon(R.drawable.ic_launcher) diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java index 51aa85903..e062dbcc9 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -10,6 +10,7 @@ import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.nearby.NearbyActivity; import fr.free.nrw.commons.notification.NotificationActivity; +import fr.free.nrw.commons.review.ReviewActivity; import fr.free.nrw.commons.settings.SettingsActivity; import fr.free.nrw.commons.upload.MultipleShareActivity; import fr.free.nrw.commons.upload.ShareActivity; @@ -50,4 +51,7 @@ public abstract class ActivityBuilderModule { @ContributesAndroidInjector abstract CategoryImagesActivity bindFeaturedImagesActivity(); + + @ContributesAndroidInjector + abstract ReviewActivity bindReviewActivity(); } diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java index 91f6d4ccb..99ad9a346 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -9,11 +9,11 @@ import dagger.android.support.AndroidSupportInjectionModule; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.MediaWikiImageView; import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsSyncAdapter; import fr.free.nrw.commons.delete.DeleteTask; import fr.free.nrw.commons.modifications.ModificationsSyncAdapter; +import fr.free.nrw.commons.review.CheckCategoryTask; +import fr.free.nrw.commons.review.SendThankTask; import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.nearby.PlaceRenderer; @@ -40,6 +40,10 @@ public interface CommonsApplicationComponent extends AndroidInjector getNotifications() { @@ -616,11 +653,59 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { } private Date parseMWDate(String mwDate) { - SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC try { return isoFormat.parse(mwDate); } catch (ParseException e) { throw new RuntimeException(e); } } + + private String formatMWDate(Date date) { + return isoFormat.format(date); + } + + public Media getRecentRandomImage() throws IOException { + Media media = null; + int tries = 0; + Random r = new Random(); + + while (media == null && tries < MAX_RANDOM_TRIES) { + Date now = new Date(); + Date startDate = new Date(now.getTime() - r.nextInt(RANDOM_SECONDS) * 1000L); + ApiResult apiResult = null; + try { + MWApi.RequestBuilder requestBuilder = api.action("query") + .param("list", "recentchanges") + .param("rcstart", formatMWDate(startDate)) + .param("rcnamespace", FILE_NAMESPACE) + .param("rcprop", "title|ids") + .param("rctype", "new|log") + .param("rctoponly", "1"); + + apiResult = requestBuilder.get(); + } catch (IOException e) { + Timber.e("Failed to obtain recent random", e); + } + if (apiResult != null) { + ApiResult recentChangesNode = apiResult.getNode("/api/query/recentchanges"); + if (recentChangesNode != null + && recentChangesNode.getDocument() != null + && recentChangesNode.getDocument().getChildNodes() != null + && recentChangesNode.getDocument().getChildNodes().getLength() > 0) { + NodeList childNodes = recentChangesNode.getDocument().getChildNodes(); + String imageTitle = RecentChangesImageUtils.findImageInRecentChanges(childNodes); + if (imageTitle != null) { + boolean deletionStatus = pageExists("Commons:Deletion_requests/" + imageTitle); + if (!deletionStatus) { + // strip File: prefix + imageTitle = imageTitle.replace("File:", ""); + media = new Media(imageTitle); + } + } + } + } + tries++; + } + return media; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index c0bd2fd87..4046530d1 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -75,7 +75,14 @@ public interface MediaWikiApi { @NonNull Single getUploadCount(String userName); + boolean thank(String editToken, String revision) throws IOException; + + String firstRevisionOfFile(String filename) throws IOException; + interface ProgressListener { void onProgress(long transferred, long total); } + + @Nullable + Media getRecentRandomImage() throws IOException; } diff --git a/app/src/main/java/fr/free/nrw/commons/review/CheckCategoryTask.java b/app/src/main/java/fr/free/nrw/commons/review/CheckCategoryTask.java new file mode 100644 index 000000000..d2086e5bd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/CheckCategoryTask.java @@ -0,0 +1,130 @@ +package fr.free.nrw.commons.review; + +import android.app.NotificationManager; +import android.content.Context; +import android.os.AsyncTask; +import android.support.v4.app.NotificationCompat; +import android.view.Gravity; +import android.widget.Toast; + +import javax.inject.Inject; + +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.di.ApplicationlessInjection; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import timber.log.Timber; + +import static android.support.v4.app.NotificationCompat.DEFAULT_ALL; +import static android.support.v4.app.NotificationCompat.PRIORITY_HIGH; + +// Example code: +// CheckCategoryTask deleteTask = new CheckCategoryTask(getActivity(), media); + +public class CheckCategoryTask extends AsyncTask { + + @Inject + MediaWikiApi mwApi; + @Inject + SessionManager sessionManager; + + public static final int NOTIFICATION_CHECK_CATEGORY = 0x101; + + private NotificationManager notificationManager; + private NotificationCompat.Builder notificationBuilder; + private Context context; + private Media media; + + public CheckCategoryTask(Context context, Media media){ + this.context = context; + this.media = media; + } + + @Override + protected void onPreExecute(){ + ApplicationlessInjection + .getInstance(context.getApplicationContext()) + .getCommonsApplicationComponent() + .inject(this); + + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder(context); + Toast toast = new Toast(context); + toast.setGravity(Gravity.CENTER,0,0); + toast = Toast.makeText(context, context.getString(R.string.check_category_toast, media.getDisplayTitle()), Toast.LENGTH_SHORT); + toast.show(); + } + + @Override + protected Boolean doInBackground(Void ...voids) { + publishProgress(0); + + String editToken; + String authCookie; + String summary = context.getString(R.string.check_category_edit_summary); + + authCookie = sessionManager.getAuthCookie(); + mwApi.setAuthCookie(authCookie); + + try { + editToken = mwApi.getEditToken(); + if (editToken.equals("+\\")) { + return false; + } + publishProgress(1); + + mwApi.appendEdit(editToken, "\n{{subst:chc}}\n", media.getFilename(), summary); + publishProgress(2); + } + catch (Exception e) { + Timber.d(e.getMessage()); + return false; + } + return true; + } + + @Override + protected void onProgressUpdate (Integer... values){ + super.onProgressUpdate(values); + + int[] messages = new int[]{R.string.getting_edit_token, R.string.check_category_adding_template}; + String message = ""; + if (0 < values[0] && values[0] < messages.length) { + message = context.getString(messages[values[0]]); + } + + notificationBuilder.setContentTitle(context.getString(R.string.check_category_notification_title, media.getDisplayTitle())) + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(messages.length, values[0], false) + .setOngoing(true); + notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build()); + } + + @Override + protected void onPostExecute(Boolean result) { + String message = ""; + String title = ""; + + if (result){ + title = context.getString(R.string.check_category_success_title); + message = context.getString(R.string.check_category_success_message, media.getDisplayTitle()); + } + else { + title = context.getString(R.string.check_category_failure_title); + message = context.getString(R.string.check_category_failure_message, media.getDisplayTitle()); + } + + notificationBuilder.setDefaults(DEFAULT_ALL) + .setContentTitle(title) + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(0,0,false) + .setOngoing(false) + .setPriority(PRIORITY_HIGH); + notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build()); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java new file mode 100644 index 000000000..4d51b97f3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java @@ -0,0 +1,173 @@ +package fr.free.nrw.commons.review; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import android.os.Handler; +import android.support.design.widget.NavigationView; +import android.support.v4.view.ViewPager; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.widget.Toolbar; + +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.EditText; + +import com.viewpagerindicator.CirclePageIndicator; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.auth.AuthenticatedActivity; +import fr.free.nrw.commons.mwapi.MediaResult; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.utils.MediaDataExtractorUtil; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +/** + * Created by root on 18.05.2018. + */ + +public class ReviewActivity extends AuthenticatedActivity { + + @BindView(R.id.toolbar) + Toolbar toolbar; + @BindView(R.id.navigation_view) + NavigationView navigationView; + @BindView(R.id.drawer_layout) + DrawerLayout drawerLayout; + + @BindView(R.id.reviewPager) + ViewPager pager; + + @Inject MediaWikiApi mwApi; + + private ReviewPagerAdapter reviewPagerAdapter; + + //private ReviewCallback reviewCallback; + private ReviewController reviewController; + + @BindView(R.id.reviewPagerIndicator) + public CirclePageIndicator pagerIndicator; + + @Override + protected void onAuthCookieAcquired(String authCookie) { + + } + + @Override + protected void onAuthFailure() { + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_review); + ButterKnife.bind(this); + initDrawer(); + + reviewController = new ReviewController(); + + + reviewPagerAdapter = new ReviewPagerAdapter(getSupportFragmentManager()); + pager.setAdapter(reviewPagerAdapter); + reviewPagerAdapter.getItem(0); + pagerIndicator.setViewPager(pager); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.review_randomizer_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.action_review_randomizer) { + Observable.fromCallable(() -> { + Media result = null; + try { + result = mwApi.getRecentRandomImage(); + + //String thumBaseUrl = Utils.makeThumbBaseUrl(result.getFilename()); + //reviewPagerAdapter.currentThumbBasedUrl = thumBaseUrl; + + //Log.d("review", result.getWikiSource()); + + } catch (IOException e) { + Log.d("review", e.toString()); + } + return result; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::updateImage); + return true; + } + + return super.onOptionsItemSelected(item); + } + + private void updateImage(Media result) { + reviewController.onImageRefreshed(result.getFilename()); //file name is updated + reviewPagerAdapter.updateFilename(); + pager.setCurrentItem(0); + Observable.fromCallable(() -> { + MediaResult media = mwApi.fetchMediaByFilename("File:" + result.getFilename()); + return MediaDataExtractorUtil.extractCategories(media.getWikiSource()); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::updateCategories); + } + + private void updateCategories(ArrayList categories) { + reviewController.onCategoriesRefreshed(categories); + reviewPagerAdapter.updateCategories(); + } + + /** + * References ReviewPagerAdapter to null before the activity is destroyed + */ + @Override + public void onDestroy() { + //adapter.setCallback(null); + super.onDestroy(); + } + + /** + * Consumers should be simply using this method to use this activity. + * @param context + * @param title Page title + */ + public static void startYourself(Context context, String title) { + Intent reviewActivity = new Intent(context, ReviewActivity.class); + context.startActivity(reviewActivity); + } + + interface ReviewCallback { + void onImageRefreshed(String itemTitle); + void onQuestionChanged(); + void onSurveyFinished(); + void onImproperImageReported(); + void onLicenceViolationReported(); + void onWrongCategoryReported(); + void onThankSent(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java new file mode 100644 index 000000000..2bde33ecd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java @@ -0,0 +1,52 @@ +package fr.free.nrw.commons.review; + +import java.util.ArrayList; + +/** + * Created by root on 19.05.2018. + */ + +public class ReviewController implements ReviewActivity.ReviewCallback { + public static String fileName; + protected static ArrayList categories; + + @Override + public void onImageRefreshed(String fileName) { + ReviewController.fileName = fileName; + ReviewController.categories = new ArrayList<>(); + } + + public void onCategoriesRefreshed(ArrayList categories) { + ReviewController.categories = categories; + } + + @Override + public void onQuestionChanged() { + + } + + @Override + public void onSurveyFinished() { + + } + + @Override + public void onImproperImageReported() { + + } + + @Override + public void onLicenceViolationReported() { + + } + + @Override + public void onWrongCategoryReported() { + + } + + @Override + public void onThankSent() { + + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java new file mode 100644 index 000000000..bab8df6df --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java @@ -0,0 +1,92 @@ +package fr.free.nrw.commons.review; + +import android.app.AlertDialog; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.TextView; + +import com.facebook.drawee.view.SimpleDraweeView; + +import java.util.ArrayList; + +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; + +/** + * Created by root on 19.05.2018. + */ + +public class ReviewImageFragment extends CommonsDaggerSupportFragment { + + public static final int SPAM = 0; + public static final int COPYRIGHT = 1; + public static final int CATEGORY = 2; + + private int position; + private String fileName; + private String catString; + private View catsView; + private SimpleDraweeView simpleDraweeView; + + public void update(int position, String fileName) { + this.position = position; + this.fileName = fileName; + + if (simpleDraweeView!=null) { + simpleDraweeView.setImageURI(Utils.makeThumbBaseUrl(fileName)); + } + } + + public void updateCategories(Iterable categories) { + catString = TextUtils.join(", ", categories); + if (catsView != null) { + ((TextView) catsView).setText(catString); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + position = getArguments().getInt("position"); + View layoutView = inflater.inflate(R.layout.fragment_review_image, container, + false); + View textView = layoutView.findViewById(R.id.reviewQuestion); + catsView = layoutView.findViewById(R.id.reviewCategories); + String question; + switch(position) { + case COPYRIGHT: + question = getString(R.string.review_copyright); + break; + case CATEGORY: + question = getString(R.string.review_category); + catsView.setVisibility(View.VISIBLE); + break; + case SPAM: + question = getString(R.string.review_spam); + break; + default: + question = "How did we get here?"; + } + ((TextView) textView).setText(question); + simpleDraweeView = layoutView.findViewById(R.id.imageView); + + if (fileName != null) { + simpleDraweeView.setImageURI(Utils.makeThumbBaseUrl(fileName)); + } + if (catString != null) { + ((TextView) catsView).setText(catString); + } + return layoutView; + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java new file mode 100644 index 000000000..06d07c7c9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java @@ -0,0 +1,51 @@ +package fr.free.nrw.commons.review; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; + +/** + * Created by nes on 19.05.2018. + */ + +public class ReviewPagerAdapter extends FragmentStatePagerAdapter { + private int currentPosition; + ReviewImageFragment[] reviewImageFragments; + + + public ReviewPagerAdapter(FragmentManager fm) { + super(fm); + reviewImageFragments = new ReviewImageFragment[] { + new ReviewImageFragment(), + new ReviewImageFragment(), + new ReviewImageFragment() + }; + } + + @Override + public int getCount() { + return 3; + } + + public void updateFilename() { + for (int i = 0; i < getCount(); i++) { + ReviewImageFragment fragment = reviewImageFragments[i]; + fragment.update(i, ReviewController.fileName); + } + } + + public void updateCategories() { + ReviewImageFragment categoryFragment = reviewImageFragments[ReviewImageFragment.CATEGORY]; + categoryFragment.updateCategories(ReviewController.categories); + } + + @Override + public Fragment getItem(int position) { + Bundle bundle = new Bundle(); + bundle.putInt("position", position); + reviewImageFragments[position].setArguments(bundle); + return reviewImageFragments[position]; + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/SendThankTask.java b/app/src/main/java/fr/free/nrw/commons/review/SendThankTask.java new file mode 100644 index 000000000..0f723217f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/SendThankTask.java @@ -0,0 +1,140 @@ +package fr.free.nrw.commons.review; + +import android.app.NotificationManager; +import android.content.Context; +import android.os.AsyncTask; +import android.support.v4.app.NotificationCompat; +import android.view.Gravity; +import android.widget.Toast; + +import javax.inject.Inject; + +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.di.ApplicationlessInjection; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import timber.log.Timber; + +import static android.support.v4.app.NotificationCompat.DEFAULT_ALL; +import static android.support.v4.app.NotificationCompat.PRIORITY_HIGH; + +// example code: +// +// media = new Media("File:Iru.png"); +// Observable.fromCallable(() -> mwApi.firstRevisionOfFile(media.getFilename())) +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe(revision -> { +// SendThankTask task = new SendThankTask(getActivity(), media, revision); +// task.execute(); +// }); + +public class SendThankTask extends AsyncTask { + + @Inject + MediaWikiApi mwApi; + @Inject + SessionManager sessionManager; + + public static final int NOTIFICATION_SEND_THANK = 0x102; + + private NotificationManager notificationManager; + private NotificationCompat.Builder notificationBuilder; + private Context context; + private Media media; + private String revision; + + public SendThankTask(Context context, Media media, String revision){ + this.context = context; + this.media = media; + this.revision = revision; + } + + @Override + protected void onPreExecute(){ + ApplicationlessInjection + .getInstance(context.getApplicationContext()) + .getCommonsApplicationComponent() + .inject(this); + + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder(context); + Toast toast = new Toast(context); + toast.setGravity(Gravity.CENTER,0,0); + toast = Toast.makeText(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle()), Toast.LENGTH_SHORT); + toast.show(); + } + + @Override + protected Boolean doInBackground(Void ...voids) { + publishProgress(0); + + String editToken; + String authCookie; + + authCookie = sessionManager.getAuthCookie(); + mwApi.setAuthCookie(authCookie); + + try { + editToken = mwApi.getEditToken(); + if (editToken.equals("+\\")) { + return false; + } + publishProgress(1); + + mwApi.thank(editToken, revision); + + publishProgress(2); + } + catch (Exception e) { + Timber.d(e.getMessage()); + return false; + } + return true; + } + + @Override + protected void onProgressUpdate (Integer... values){ + super.onProgressUpdate(values); + + int[] messages = new int[]{R.string.getting_edit_token, R.string.send_thank_send}; + String message = ""; + if (0 < values[0] && values[0] < messages.length) { + message = context.getString(messages[values[0]]); + } + + notificationBuilder.setContentTitle(context.getString(R.string.send_thank_notification_title)) + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(messages.length, values[0], false) + .setOngoing(true); + notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build()); + } + + @Override + protected void onPostExecute(Boolean result) { + String message = ""; + String title = ""; + + if (result){ + title = context.getString(R.string.send_thank_success_title); + message = context.getString(R.string.send_thank_success_message, media.getDisplayTitle()); + } + else { + title = context.getString(R.string.send_thank_failure_title); + message = context.getString(R.string.send_thank_failure_message, media.getDisplayTitle()); + } + + notificationBuilder.setDefaults(DEFAULT_ALL) + .setContentTitle(title) + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(0,0,false) + .setOngoing(false) + .setPriority(PRIORITY_HIGH); + notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build()); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index 4a7322b57..8fc12d068 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -30,6 +30,7 @@ import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.nearby.NearbyActivity; import fr.free.nrw.commons.notification.NotificationActivity; +import fr.free.nrw.commons.review.ReviewActivity; import fr.free.nrw.commons.settings.SettingsActivity; import timber.log.Timber; @@ -160,6 +161,11 @@ public abstract class NavigationBaseActivity extends BaseActivity drawerLayout.closeDrawer(navigationView); CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_featured_images), FEATURED_IMAGES_CATEGORY); return true; + + case R.id.action_review: + drawerLayout.closeDrawer(navigationView); + ReviewActivity.startYourself(this, getString(R.string.title_activity_review)); + return true; default: Timber.e("Unknown option [%s] selected from the navigation menu", itemId); return false; diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java new file mode 100644 index 000000000..63421a8e4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java @@ -0,0 +1,28 @@ +package fr.free.nrw.commons.utils; + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MediaDataExtractorUtil { + + /** + * We could fetch all category links from API, but we actually only want the ones + * directly in the page source so they're editable. In the future this may change. + * + * @param source wikitext source code + */ + public static ArrayList extractCategories(String source) { + ArrayList categories = new ArrayList<>(); + Pattern regex = Pattern.compile("\\[\\[\\s*Category\\s*:([^]]*)\\s*\\]\\]", Pattern.CASE_INSENSITIVE); + Matcher matcher = regex.matcher(source); + while (matcher.find()) { + String cat = matcher.group(1).trim(); + categories.add(cat); + } + + return categories; + } + + +} diff --git a/app/src/main/res/drawable/ic_check_black_24dp.xml b/app/src/main/res/drawable/ic_check_black_24dp.xml new file mode 100644 index 000000000..3c728c59f --- /dev/null +++ b/app/src/main/res/drawable/ic_check_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_refresh_black_24dp.xml b/app/src/main/res/drawable/ic_refresh_black_24dp.xml new file mode 100644 index 000000000..8229a9a64 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/tab_indicator_default.xml b/app/src/main/res/drawable/tab_indicator_default.xml new file mode 100644 index 000000000..341f4d706 --- /dev/null +++ b/app/src/main/res/drawable/tab_indicator_default.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab_indicator_selected.xml b/app/src/main/res/drawable/tab_indicator_selected.xml new file mode 100644 index 000000000..41c1bcf73 --- /dev/null +++ b/app/src/main/res/drawable/tab_indicator_selected.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab_selector.xml b/app/src/main/res/drawable/tab_selector.xml new file mode 100644 index 000000000..001747c31 --- /dev/null +++ b/app/src/main/res/drawable/tab_selector.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_review.xml b/app/src/main/res/layout/activity_review.xml new file mode 100644 index 000000000..a0b813f55 --- /dev/null +++ b/app/src/main/res/layout/activity_review.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_review_image.xml b/app/src/main/res/layout/fragment_review_image.xml new file mode 100644 index 000000000..7abf88f01 --- /dev/null +++ b/app/src/main/res/layout/fragment_review_image.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + +