convert top level classes to kotlin (#6368)

* Converted welcome activity / pager to kotlin

* Removed unused interface

* Convert ViewPagerAdapter to kotlin and enforce that all tabs must have a title that comes from strings.xml

* Convert OkHttpConnectionFactory and remove an exception class nobody was using

* Convert MapController to kotlin along with fixing nullability in a few places
This commit is contained in:
Paul Hawke 2025-07-06 19:50:16 -05:00 committed by GitHub
parent 65f41beed8
commit 66395b9871
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 555 additions and 655 deletions

View file

@ -16,6 +16,7 @@
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" module="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />

View file

@ -1,30 +0,0 @@
package fr.free.nrw.commons;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import java.util.List;
public abstract class MapController {
/**
* We pass this variable as a group of placeList and boundaryCoordinates
*/
public class NearbyPlacesInfo {
public List<Place> placeList; // List of nearby places
public LatLng[] boundaryCoordinates; // Corners of nearby area
public LatLng currentLatLng; // Current location when this places are populated
public LatLng searchLatLng; // Search location for finding this places
public List<Media> mediaList; // Search location for finding this places
}
/**
* We pass this variable as a group of placeList and boundaryCoordinates
*/
public class ExplorePlacesInfo {
public List<Place> explorePlaceList; // List of nearby places
public LatLng[] boundaryCoordinates; // Corners of nearby area
public LatLng currentLatLng; // Current location when this places are populated
public LatLng searchLatLng; // Search location for finding this places
public List<Media> mediaList; // Search location for finding this places
}
}

View file

@ -0,0 +1,46 @@
package fr.free.nrw.commons
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.nearby.Place
abstract class MapController {
/**
* We pass this variable as a group of placeList and boundaryCoordinates
*/
inner class NearbyPlacesInfo {
@JvmField
var placeList: List<Place> = emptyList() // List of nearby places
@JvmField
var boundaryCoordinates: Array<LatLng> = emptyArray() // Corners of nearby area
@JvmField
var currentLatLng: LatLng? = null // Current location when this places are populated
@JvmField
var searchLatLng: LatLng? = null // Search location for finding this places
@JvmField
var mediaList: List<Media>? = null // Search location for finding this places
}
/**
* We pass this variable as a group of placeList and boundaryCoordinates
*/
inner class ExplorePlacesInfo {
@JvmField
var explorePlaceList: List<Place> = emptyList() // List of nearby places
@JvmField
var boundaryCoordinates: Array<LatLng> = emptyArray() // Corners of nearby area
@JvmField
var currentLatLng: LatLng? = null // Current location when this places are populated
@JvmField
var searchLatLng: LatLng? = null // Search location for finding this places
@JvmField
var mediaList: List<Media> = emptyList() // Search location for finding this places
}
}

View file

@ -1,154 +0,0 @@
package fr.free.nrw.commons;
import androidx.annotation.NonNull;
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.Cache;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.logging.HttpLoggingInterceptor;
import okhttp3.logging.HttpLoggingInterceptor.Level;
import timber.log.Timber;
public final class OkHttpConnectionFactory {
private static final String CACHE_DIR_NAME = "okhttp-cache";
private static final long NET_CACHE_SIZE = 64 * 1024 * 1024;
public static OkHttpClient CLIENT;
@NonNull public static OkHttpClient getClient(final CommonsCookieJar cookieJar) {
if (CLIENT == null) {
CLIENT = createClient(cookieJar);
}
return CLIENT;
}
@NonNull
private static OkHttpClient createClient(final CommonsCookieJar cookieJar) {
return new OkHttpClient.Builder()
.cookieJar(cookieJar)
.cache((CommonsApplication.getInstance()!=null) ? new Cache(new File(CommonsApplication.getInstance().getCacheDir(), CACHE_DIR_NAME), NET_CACHE_SIZE) : null)
.connectTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.addInterceptor(getLoggingInterceptor())
.addInterceptor(new UnsuccessfulResponseInterceptor())
.addInterceptor(new CommonHeaderRequestInterceptor())
.build();
}
private static HttpLoggingInterceptor getLoggingInterceptor() {
final HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor()
.setLevel(Level.BASIC);
httpLoggingInterceptor.redactHeader("Authorization");
httpLoggingInterceptor.redactHeader("Cookie");
return httpLoggingInterceptor;
}
private static class CommonHeaderRequestInterceptor implements Interceptor {
@Override
@NonNull
public Response intercept(@NonNull final Chain chain) throws IOException {
final Request request = chain.request().newBuilder()
.header("User-Agent", CommonsApplication.getInstance().getUserAgent())
.build();
return chain.proceed(request);
}
}
public static class UnsuccessfulResponseInterceptor implements Interceptor {
private static final String SUPPRESS_ERROR_LOG = "x-commons-suppress-error-log";
public static final String SUPPRESS_ERROR_LOG_HEADER = SUPPRESS_ERROR_LOG+": true";
private static final List<String> DO_NOT_INTERCEPT = Collections.singletonList(
"api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1");
private static final String ERRORS_PREFIX = "{\"error";
@Override
@NonNull
public Response intercept(@NonNull final Chain chain) throws IOException {
final Request rq = chain.request();
// If the request contains our special "suppress errors" header, make note of it
// but don't pass that on to the server.
final boolean suppressErrors = rq.headers().names().contains(SUPPRESS_ERROR_LOG);
final Request request = rq.newBuilder()
.removeHeader(SUPPRESS_ERROR_LOG)
.build();
final Response rsp = chain.proceed(request);
// Do not intercept certain requests and let the caller handle the errors
if(isExcludedUrl(chain.request())) {
return rsp;
}
if (rsp.isSuccessful()) {
try (final ResponseBody responseBody = rsp.peekBody(ERRORS_PREFIX.length())) {
if (ERRORS_PREFIX.equals(responseBody.string())) {
try (final ResponseBody body = rsp.body()) {
throw new IOException(body.string());
}
}
} catch (final IOException e) {
// Log the error as debug (and therefore, "expected") or at error level
if (suppressErrors) {
Timber.d(e, "Suppressed (known / expected) error");
} else {
Timber.e(e);
}
}
return rsp;
}
throw new HttpStatusException(rsp);
}
private boolean isExcludedUrl(final Request request) {
final String requestUrl = request.url().toString();
for(final String url: DO_NOT_INTERCEPT) {
if(requestUrl.contains(url)) {
return true;
}
}
return false;
}
}
private OkHttpConnectionFactory() {
}
public static class HttpStatusException extends IOException {
private final int code;
private final String url;
public HttpStatusException(@NonNull Response rsp) {
this.code = rsp.code();
this.url = rsp.request().url().uri().toString();
try {
if (rsp.body() != null && rsp.body().contentType() != null
&& rsp.body().contentType().toString().contains("json")) {
}
} catch (Exception e) {
// Log?
}
}
public int code() {
return code;
}
@Override
public String getMessage() {
String str = "Code: " + code + ", URL: " + url;
return str;
}
}
}

View file

@ -0,0 +1,122 @@
package fr.free.nrw.commons
import androidx.annotation.VisibleForTesting
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar
import okhttp3.Cache
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.util.concurrent.TimeUnit
object OkHttpConnectionFactory {
private const val CACHE_DIR_NAME = "okhttp-cache"
private const val NET_CACHE_SIZE = (64 * 1024 * 1024).toLong()
@VisibleForTesting
var CLIENT: OkHttpClient? = null
fun getClient(cookieJar: CommonsCookieJar): OkHttpClient {
if (CLIENT == null) {
CLIENT = createClient(cookieJar)
}
return CLIENT!!
}
private fun createClient(cookieJar: CommonsCookieJar): OkHttpClient {
return OkHttpClient.Builder()
.cookieJar(cookieJar)
.cache(
if (CommonsApplication.instance != null) Cache(
File(CommonsApplication.instance.cacheDir, CACHE_DIR_NAME),
NET_CACHE_SIZE
) else null
)
.connectTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
setLevel(HttpLoggingInterceptor.Level.BASIC)
redactHeader("Authorization")
redactHeader("Cookie")
})
.addInterceptor(UnsuccessfulResponseInterceptor())
.addInterceptor(CommonHeaderRequestInterceptor())
.build()
}
}
private class CommonHeaderRequestInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.header("User-Agent", CommonsApplication.instance.userAgent)
.build()
return chain.proceed(request)
}
}
private const val SUPPRESS_ERROR_LOG = "x-commons-suppress-error-log"
const val SUPPRESS_ERROR_LOG_HEADER: String = "$SUPPRESS_ERROR_LOG: true"
private class UnsuccessfulResponseInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val rq = chain.request()
// If the request contains our special "suppress errors" header, make note of it
// but don't pass that on to the server.
val suppressErrors = rq.headers.names().contains(SUPPRESS_ERROR_LOG)
val request = rq.newBuilder()
.removeHeader(SUPPRESS_ERROR_LOG)
.build()
val rsp = chain.proceed(request)
// Do not intercept certain requests and let the caller handle the errors
if (isExcludedUrl(chain.request())) {
return rsp
}
if (rsp.isSuccessful) {
try {
rsp.peekBody(ERRORS_PREFIX.length.toLong()).use { responseBody ->
if (ERRORS_PREFIX == responseBody.string()) {
rsp.body.use { body ->
throw IOException(body!!.string())
}
}
}
} catch (e: IOException) {
// Log the error as debug (and therefore, "expected") or at error level
if (suppressErrors) {
Timber.d(e, "Suppressed (known / expected) error")
} else {
Timber.e(e)
}
}
return rsp
}
throw IOException("Unsuccessful response")
}
private fun isExcludedUrl(request: Request): Boolean {
val requestUrl = request.url.toString()
for (url in DO_NOT_INTERCEPT) {
if (requestUrl.contains(url)) {
return true
}
}
return false
}
companion object {
val DO_NOT_INTERCEPT = listOf(
"api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1"
)
const val ERRORS_PREFIX = "{\"error"
}
}

View file

@ -1,7 +0,0 @@
package fr.free.nrw.commons;
import android.content.Context;
public interface ViewHolder<T> {
void bindModel(Context context, T model);
}

View file

@ -1,68 +0,0 @@
package fr.free.nrw.commons;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import java.util.ArrayList;
import java.util.List;
/**
* This adapter will be used to display fragments in a ViewPager
*/
public class ViewPagerAdapter extends FragmentPagerAdapter {
private List<Fragment> fragmentList = new ArrayList<>();
private List<String> fragmentTitleList = new ArrayList<>();
public ViewPagerAdapter(FragmentManager manager) {
super(manager);
}
/**
* Constructs a ViewPagerAdapter with a specified Fragment Manager and Fragment resume behavior.
*
* @param manager The FragmentManager
* @param behavior An integer which represents the behavior of non visible fragments. See
* FragmentPagerAdapter.java for options.
*/
public ViewPagerAdapter(FragmentManager manager, int behavior) {
super(manager, behavior);
}
/**
* This method returns the fragment of the viewpager at a particular position
* @param position
*/
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
/**
* This method returns the total number of fragments in the viewpager.
* @return size
*/
@Override
public int getCount() {
return fragmentList.size();
}
/**
* This method sets the fragment and title list in the viewpager
* @param fragmentList List of all fragments to be displayed in the viewpager
* @param fragmentTitleList List of all titles of the fragments
*/
public void setTabData(List<Fragment> fragmentList, List<String> fragmentTitleList) {
this.fragmentList = fragmentList;
this.fragmentTitleList = fragmentTitleList;
}
/**
* This method returns the title of the page at a particular position
* @param position
*/
@Override
public CharSequence getPageTitle(int position) {
return fragmentTitleList.get(position);
}
}

View file

@ -0,0 +1,44 @@
package fr.free.nrw.commons
import android.content.Context
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import java.util.Locale
/**
* This adapter will be used to display fragments in a ViewPager
*/
class ViewPagerAdapter : FragmentPagerAdapter {
private val context: Context
private var fragmentList: List<Fragment> = emptyList()
private var fragmentTitleList: List<String> = emptyList()
constructor(context: Context, manager: FragmentManager) : super(manager) {
this.context = context
}
constructor(context: Context, manager: FragmentManager, behavior: Int) : super(manager, behavior) {
this.context = context
}
override fun getItem(position: Int): Fragment = fragmentList[position]
override fun getPageTitle(position: Int): CharSequence = fragmentTitleList[position]
override fun getCount(): Int = fragmentList.size
fun setTabs(vararg titlesToFragments: Pair<Int, Fragment>) {
// Enforce that every title must come from strings.xml and all will consistently be uppercase
fragmentTitleList = titlesToFragments.map {
context.getString(it.first).uppercase(Locale.ROOT)
}
fragmentList = titlesToFragments.map { it.second }
}
companion object {
// Convenience method for Java callers, can be removed when everything is migrated
@JvmStatic
fun pairOf(first: Int, second: Fragment) = first to second
}
}

View file

@ -1,109 +0,0 @@
package fr.free.nrw.commons;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import fr.free.nrw.commons.databinding.ActivityWelcomeBinding;
import fr.free.nrw.commons.databinding.PopupForCopyrightBinding;
import fr.free.nrw.commons.quiz.QuizActivity;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.ConfigUtils;
public class WelcomeActivity extends BaseActivity {
private ActivityWelcomeBinding binding;
private PopupForCopyrightBinding copyrightBinding;
private final WelcomePagerAdapter adapter = new WelcomePagerAdapter();
private boolean isQuiz;
private AlertDialog.Builder dialogBuilder;
private AlertDialog dialog;
/**
* Initialises exiting fields and dependencies
*
* @param savedInstanceState WelcomeActivity bundled data
*/
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityWelcomeBinding.inflate(getLayoutInflater());
final View view = binding.getRoot();
setContentView(view);
if (getIntent() != null) {
final Bundle bundle = getIntent().getExtras();
if (bundle != null) {
isQuiz = bundle.getBoolean("isQuiz");
}
} else {
isQuiz = false;
}
// Enable skip button if beta flavor
if (ConfigUtils.isBetaFlavour()) {
binding.finishTutorialButton.setVisibility(View.VISIBLE);
dialogBuilder = new AlertDialog.Builder(this);
copyrightBinding = PopupForCopyrightBinding.inflate(getLayoutInflater());
final View contactPopupView = copyrightBinding.getRoot();
dialogBuilder.setView(contactPopupView);
dialogBuilder.setCancelable(false);
dialog = dialogBuilder.create();
dialog.show();
copyrightBinding.buttonOk.setOnClickListener(v -> dialog.dismiss());
}
binding.welcomePager.setAdapter(adapter);
binding.welcomePagerIndicator.setViewPager(binding.welcomePager);
binding.finishTutorialButton.setOnClickListener(v -> finishTutorial());
}
/**
* References WelcomePageAdapter to null before the activity is destroyed
*/
@Override
public void onDestroy() {
if (isQuiz) {
final Intent i = new Intent(this, QuizActivity.class);
startActivity(i);
}
super.onDestroy();
}
/**
* Creates a way to change current activity to WelcomeActivity
*
* @param context Activity context
*/
public static void startYourself(final Context context) {
final Intent welcomeIntent = new Intent(context, WelcomeActivity.class);
context.startActivity(welcomeIntent);
}
/**
* Override onBackPressed() to go to previous tutorial 'pages' if not on first page
*/
@Override
public void onBackPressed() {
if (binding.welcomePager.getCurrentItem() != 0) {
binding.welcomePager.setCurrentItem(binding.welcomePager.getCurrentItem() - 1, true);
} else {
if (defaultKvStore.getBoolean("firstrun", true)) {
finishAffinity();
} else {
super.onBackPressed();
}
}
}
public void finishTutorial() {
defaultKvStore.putBoolean("firstrun", false);
finish();
}
}

View file

@ -0,0 +1,78 @@
package fr.free.nrw.commons
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import fr.free.nrw.commons.databinding.ActivityWelcomeBinding
import fr.free.nrw.commons.databinding.PopupForCopyrightBinding
import fr.free.nrw.commons.quiz.QuizActivity
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
class WelcomeActivity : BaseActivity() {
private var binding: ActivityWelcomeBinding? = null
private var isQuiz = false
/**
* Initialises exiting fields and dependencies
*
* @param savedInstanceState WelcomeActivity bundled data
*/
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityWelcomeBinding.inflate(layoutInflater)
setContentView(binding!!.root)
isQuiz = intent?.extras?.getBoolean("isQuiz", false) ?: false
// Enable skip button if beta flavor
if (isBetaFlavour) {
binding!!.finishTutorialButton.visibility = View.VISIBLE
val copyrightBinding = PopupForCopyrightBinding.inflate(layoutInflater)
val dialog = AlertDialog.Builder(this)
.setView(copyrightBinding.root)
.setCancelable(false)
.create()
dialog.show()
copyrightBinding.buttonOk.setOnClickListener { v: View? -> dialog.dismiss() }
}
val adapter = WelcomePagerAdapter()
binding!!.welcomePager.adapter = adapter
binding!!.welcomePagerIndicator.setViewPager(binding!!.welcomePager)
binding!!.finishTutorialButton.setOnClickListener { v: View? -> finishTutorial() }
}
public override fun onDestroy() {
if (isQuiz) {
startActivity(Intent(this, QuizActivity::class.java))
}
super.onDestroy()
}
override fun onBackPressed() {
if (binding!!.welcomePager.currentItem != 0) {
binding!!.welcomePager.setCurrentItem(binding!!.welcomePager.currentItem - 1, true)
} else {
if (defaultKvStore.getBoolean("firstrun", true)) {
finishAffinity()
} else {
super.onBackPressed()
}
}
}
fun finishTutorial() {
defaultKvStore.putBoolean("firstrun", false)
finish()
}
}
fun Context.startWelcome() {
startActivity(Intent(this, WelcomeActivity::class.java))
}

View file

@ -1,77 +0,0 @@
package fr.free.nrw.commons;
import static fr.free.nrw.commons.utils.UrlUtilsKt.handleWebUrl;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.viewpager.widget.PagerAdapter;
import fr.free.nrw.commons.utils.UnderlineUtils;
public class WelcomePagerAdapter extends PagerAdapter {
private static final int[] PAGE_LAYOUTS = new int[]{
R.layout.welcome_wikipedia,
R.layout.welcome_do_upload,
R.layout.welcome_dont_upload,
R.layout.welcome_image_example,
R.layout.welcome_final
};
/**
* Gets total number of layouts
* @return Number of layouts
*/
@Override
public int getCount() {
return PAGE_LAYOUTS.length;
}
/**
* Compares given view with provided object
* @param view Adapter view
* @param object Adapter object
* @return Equality between view and object
*/
@Override
public boolean isViewFromObject(View view, Object object) {
return (view == object);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
LayoutInflater inflater = LayoutInflater.from(container.getContext());
ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false);
// If final page
if (position == PAGE_LAYOUTS.length - 1) {
// Add link to more information
TextView moreInfo = layout.findViewById(R.id.welcomeInfo);
UnderlineUtils.setUnderlinedText(moreInfo, R.string.welcome_help_button_text);
moreInfo.setOnClickListener(view -> handleWebUrl(
container.getContext(),
Uri.parse("https://commons.wikimedia.org/wiki/Help:Contents")
));
// Handle click of finishTutorialButton ("YES!" button) inside layout
layout.findViewById(R.id.finishTutorialButton)
.setOnClickListener(view -> ((WelcomeActivity) container.getContext()).finishTutorial());
}
container.addView(layout);
return layout;
}
/**
* Provides a way to remove an item from container
* @param container Adapter view group container
* @param position Index of item
* @param obj Adapter object
*/
@Override
public void destroyItem(ViewGroup container, int position, Object obj) {
container.removeView((View) obj);
}
}

View file

@ -0,0 +1,70 @@
package fr.free.nrw.commons
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.net.toUri
import androidx.viewpager.widget.PagerAdapter
import fr.free.nrw.commons.utils.UnderlineUtils.setUnderlinedText
import fr.free.nrw.commons.utils.handleWebUrl
class WelcomePagerAdapter : PagerAdapter() {
/**
* Gets total number of layouts
* @return Number of layouts
*/
override fun getCount(): Int = PAGE_LAYOUTS.size
/**
* Compares given view with provided object
* @param view Adapter view
* @param obj Adapter object
* @return Equality between view and object
*/
override fun isViewFromObject(view: View, obj: Any): Boolean = (view === obj)
/**
* Provides a way to remove an item from container
* @param container Adapter view group container
* @param position Index of item
* @param obj Adapter object
*/
override fun destroyItem(container: ViewGroup, position: Int, obj: Any) =
container.removeView(obj as View)
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val inflater = LayoutInflater.from(container.context)
val layout = inflater.inflate(PAGE_LAYOUTS[position], container, false) as ViewGroup
// If final page
if (position == PAGE_LAYOUTS.size - 1) {
// Add link to more information
val moreInfo = layout.findViewById<TextView>(R.id.welcomeInfo)
setUnderlinedText(moreInfo, R.string.welcome_help_button_text)
moreInfo.setOnClickListener {
handleWebUrl(
container.context,
"https://commons.wikimedia.org/wiki/Help:Contents".toUri()
)
}
// Handle click of finishTutorialButton ("YES!" button) inside layout
layout.findViewById<View>(R.id.finishTutorialButton)
.setOnClickListener { view: View? -> (container.context as WelcomeActivity).finishTutorial() }
}
container.addView(layout)
return layout
}
companion object {
private val PAGE_LAYOUTS = intArrayOf(
R.layout.welcome_wikipedia,
R.layout.welcome_do_upload,
R.layout.welcome_dont_upload,
R.layout.welcome_image_example,
R.layout.welcome_final
)
}
}

View file

@ -59,7 +59,7 @@ class CategoryDetailsActivity : BaseActivity(),
val view = binding.root
setContentView(view)
supportFragmentManager = getSupportFragmentManager()
viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
viewPagerAdapter = ViewPagerAdapter(this, supportFragmentManager)
binding.viewPager.adapter = viewPagerAdapter
binding.viewPager.offscreenPageLimit = 2
binding.tabLayout.setupWithViewPager(binding.viewPager)
@ -83,8 +83,6 @@ class CategoryDetailsActivity : BaseActivity(),
* Set the fragments according to the tab selected in the viewPager.
*/
private fun setTabs() {
val fragmentList = mutableListOf<Fragment>()
val titleList = mutableListOf<String>()
categoriesMediaFragment = CategoriesMediaFragment()
val subCategoryListFragment = SubCategoriesFragment()
val parentCategoriesFragment = ParentCategoriesFragment()
@ -99,13 +97,12 @@ class CategoryDetailsActivity : BaseActivity(),
viewModel.onCheckIfBookmarked(categoryName!!)
}
fragmentList.add(categoriesMediaFragment)
titleList.add("MEDIA")
fragmentList.add(subCategoryListFragment)
titleList.add("SUBCATEGORIES")
fragmentList.add(parentCategoriesFragment)
titleList.add("PARENT CATEGORIES")
viewPagerAdapter.setTabData(fragmentList, titleList)
viewPagerAdapter.setTabs(
R.string.title_for_media to categoriesMediaFragment,
R.string.title_for_subcategories to subCategoryListFragment,
R.string.title_for_parent_categories to parentCategoriesFragment
)
viewPagerAdapter.notifyDataSetChanged()
}

View file

@ -12,7 +12,6 @@ import androidx.fragment.app.FragmentManager
import androidx.work.ExistingWorkPolicy
import com.google.android.material.bottomnavigation.BottomNavigationView
import fr.free.nrw.commons.R
import fr.free.nrw.commons.WelcomeActivity
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.bookmarks.BookmarkFragment
import fr.free.nrw.commons.contributions.ContributionsFragment.Companion.newInstance
@ -33,6 +32,7 @@ import fr.free.nrw.commons.notification.NotificationActivity.Companion.startYour
import fr.free.nrw.commons.notification.NotificationController
import fr.free.nrw.commons.quiz.QuizChecker
import fr.free.nrw.commons.settings.SettingsFragment
import fr.free.nrw.commons.startWelcome
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.upload.UploadProgressActivity
import fr.free.nrw.commons.upload.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest
@ -517,7 +517,7 @@ after opening the app.
(!applicationKvStore!!.getBoolean("login_skipped"))
) {
defaultKvStore.putBoolean("inAppCameraFirstRun", true)
WelcomeActivity.startYourself(this)
startWelcome()
}
retryAllFailedUploads()

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.explore;
import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE;
import static fr.free.nrw.commons.ViewPagerAdapter.pairOf;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -23,10 +24,12 @@ import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.ActivityUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
import kotlin.Pair;
public class ExploreFragment extends CommonsDaggerSupportFragment {
@ -70,7 +73,7 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
loadNearbyMapData();
binding = FragmentExploreBinding.inflate(inflater, container, false);
viewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager(),
viewPagerAdapter = new ViewPagerAdapter(requireContext(), getChildFragmentManager(),
FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
binding.viewPager.setAdapter(viewPagerAdapter);
@ -111,9 +114,6 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
* Sets the titles in the tabLayout and fragments in the viewPager
*/
public void setTabs() {
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", FEATURED_IMAGES_CATEGORY);
@ -133,19 +133,15 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
featuredRootFragment = new ExploreListRootFragment(featuredArguments);
mobileRootFragment = new ExploreListRootFragment(mobileArguments);
mapRootFragment = new ExploreMapRootFragment(mapArguments);
fragmentList.add(featuredRootFragment);
titleList.add(getString(R.string.explore_tab_title_featured).toUpperCase(Locale.ROOT));
fragmentList.add(mobileRootFragment);
titleList.add(getString(R.string.explore_tab_title_mobile).toUpperCase(Locale.ROOT));
fragmentList.add(mapRootFragment);
titleList.add(getString(R.string.explore_tab_title_map).toUpperCase(Locale.ROOT));
((MainActivity) getActivity()).showTabs();
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPagerAdapter.setTabs(
pairOf(R.string.explore_tab_title_featured, featuredRootFragment),
pairOf(R.string.explore_tab_title_mobile, mobileRootFragment),
pairOf(R.string.explore_tab_title_map, mapRootFragment)
);
viewPagerAdapter.notifyDataSetChanged();
}

View file

@ -1,5 +1,7 @@
package fr.free.nrw.commons.explore;
import static fr.free.nrw.commons.ViewPagerAdapter.pairOf;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@ -26,11 +28,13 @@ import fr.free.nrw.commons.utils.FragmentUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import kotlin.Pair;
import timber.log.Timber;
/**
@ -65,7 +69,7 @@ public class SearchActivity extends BaseActivity
binding.toolbarSearch.setNavigationOnClickListener(v->onBackPressed());
supportFragmentManager = getSupportFragmentManager();
setSearchHistoryFragment();
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
viewPagerAdapter = new ViewPagerAdapter(this, getSupportFragmentManager());
binding.viewPager.setAdapter(viewPagerAdapter);
binding.viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive
binding.tabLayout.setupWithViewPager(binding.viewPager);
@ -90,19 +94,15 @@ public class SearchActivity extends BaseActivity
* Sets the titles in the tabLayout and fragments in the viewPager
*/
public void setTabs() {
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
searchMediaFragment = new SearchMediaFragment();
searchDepictionsFragment = new SearchDepictionsFragment();
searchCategoryFragment= new SearchCategoryFragment();
fragmentList.add(searchMediaFragment);
titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase(Locale.ROOT));
fragmentList.add(searchCategoryFragment);
titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase(Locale.ROOT));
fragmentList.add(searchDepictionsFragment);
titleList.add(getResources().getString(R.string.search_tab_title_depictions).toUpperCase(Locale.ROOT));
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPagerAdapter.setTabs(
pairOf(R.string.search_tab_title_media, searchMediaFragment),
pairOf(R.string.search_tab_title_categories, searchCategoryFragment),
pairOf(R.string.search_tab_title_depictions, searchDepictionsFragment)
);
viewPagerAdapter.notifyDataSetChanged();
getCompositeDisposable().add(RxSearchView.queryTextChanges(binding.searchBox)
.takeUntil(RxView.detaches(binding.searchBox))

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.explore.depictions;
import static fr.free.nrw.commons.ViewPagerAdapter.pairOf;
import static fr.free.nrw.commons.utils.UrlUtilsKt.handleWebUrl;
import android.content.Context;
@ -31,8 +32,10 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import kotlin.Pair;
/**
* Activity to show depiction media, parent classes and child classes of depicted items in Explore
@ -66,7 +69,7 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe
setContentView(binding.getRoot());
compositeDisposable = new CompositeDisposable();
supportFragmentManager = getSupportFragmentManager();
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
viewPagerAdapter = new ViewPagerAdapter(this, getSupportFragmentManager());
binding.viewPager.setAdapter(viewPagerAdapter);
binding.viewPager.setOffscreenPageLimit(2);
binding.tabLayout.setupWithViewPager(binding.viewPager);
@ -105,8 +108,6 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe
* Set the fragments according to the tab selected in the viewPager.
*/
private void setTabs() {
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
depictionImagesListFragment = new DepictedImagesFragment();
ChildDepictionsFragment childDepictionsFragment = new ChildDepictionsFragment();
ParentDepictionsFragment parentDepictionsFragment = new ParentDepictionsFragment();
@ -120,13 +121,12 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe
parentDepictionsFragment.setArguments(arguments);
childDepictionsFragment.setArguments(arguments);
}
fragmentList.add(depictionImagesListFragment);
titleList.add(getResources().getString(R.string.title_for_media));
fragmentList.add(childDepictionsFragment);
titleList.add(getResources().getString(R.string.title_for_child_classes));
fragmentList.add(parentDepictionsFragment);
titleList.add(getResources().getString(R.string.title_for_parent_classes));
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPagerAdapter.setTabs(
pairOf(R.string.title_for_media, depictionImagesListFragment),
pairOf(R.string.title_for_subcategories, childDepictionsFragment),
pairOf(R.string.title_for_parent_categories, parentDepictionsFragment)
);
binding.viewPager.setOffscreenPageLimit(2);
viewPagerAdapter.notifyDataSetChanged();

View file

@ -1,8 +1,10 @@
package fr.free.nrw.commons.explore.map;
import androidx.annotation.NonNull;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.media.MediaClient;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -23,6 +25,7 @@ public class ExploreMapCalls {
* @param currentLatLng coordinates of search location
* @return list of places obtained
*/
@NonNull
List<Media> callCommonsQuery(final LatLng currentLatLng) {
String coordinates = currentLatLng.getLatitude() + "|" + currentLatLng.getLongitude();
return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet();

View file

@ -14,6 +14,7 @@ import fr.free.nrw.commons.explore.map.ExploreMapController.NearbyBaseMarkerThum
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
import fr.free.nrw.commons.nearby.Place;
import io.reactivex.Observable;
import java.lang.reflect.Proxy;
import java.util.List;
@ -182,7 +183,7 @@ public class ExploreMapPresenter
exploreMapController
.loadAttractionsFromLocationToBaseMarkerOptions(explorePlacesInfo.currentLatLng,
// Curlatlang will be used to calculate distances
explorePlacesInfo.explorePlaceList,
(List<Place>) explorePlacesInfo.explorePlaceList,
exploreMapFragmentView.getContext(),
this,
explorePlacesInfo);
@ -230,11 +231,7 @@ public class ExploreMapPresenter
mylocation.setLongitude(exploreMapFragmentView.getLastMapFocus().getLongitude());
Float distance = mylocation.distanceTo(dest_location);
if (distance > 2000.0 * 3 / 4) {
return false;
} else {
return true;
}
return !(distance > 2000.0 * 3 / 4);
}
}

View file

@ -1,6 +1,6 @@
package fr.free.nrw.commons.media
import fr.free.nrw.commons.OkHttpConnectionFactory.UnsuccessfulResponseInterceptor.SUPPRESS_ERROR_LOG_HEADER
import fr.free.nrw.commons.SUPPRESS_ERROR_LOG_HEADER
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import io.reactivex.Single
import retrofit2.http.GET

View file

@ -18,7 +18,6 @@ import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.CommonsApplication.ActivityLogoutListener
import fr.free.nrw.commons.R
import fr.free.nrw.commons.WelcomeActivity
import fr.free.nrw.commons.actions.PageEditClient
import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetBinding
import fr.free.nrw.commons.di.ApplicationlessInjection
@ -32,6 +31,7 @@ import fr.free.nrw.commons.logging.CommonsLogSender
import fr.free.nrw.commons.profile.ProfileActivity
import fr.free.nrw.commons.review.ReviewActivity
import fr.free.nrw.commons.settings.SettingsActivity
import fr.free.nrw.commons.startWelcome
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
@ -241,7 +241,7 @@ class MoreBottomSheetFragment : BottomSheetDialogFragment() {
}
fun onTutorialClicked() {
WelcomeActivity.startYourself(requireActivity())
requireContext().startWelcome()
}
fun onSettingsClicked() {

View file

@ -1,10 +1,14 @@
package fr.free.nrw.commons.nearby;
import static java.util.Collections.emptyList;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.nearby.model.NearbyQueryParams;
import fr.free.nrw.commons.nearby.model.NearbyQueryParams.Radial;
import fr.free.nrw.commons.nearby.model.NearbyQueryParams.Rectangular;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
@ -46,13 +50,14 @@ public class NearbyPlaces {
* @param customQuery
* @return list of places obtained
*/
@NonNull
List<Place> radiusExpander(final LatLng currentLatLng, final String lang,
final boolean returnClosestResult, @Nullable final String customQuery) throws Exception {
final int minResults;
final double maxRadius;
List<Place> places = Collections.emptyList();
List<Place> places = emptyList();
// If returnClosestResult is true, then this means that we are trying to get closest point
// to use in cardView in Contributions fragment
@ -113,6 +118,7 @@ public class NearbyPlaces {
* @return A list of places obtained from the Wikidata query.
* @throws Exception If an error occurs during the retrieval process.
*/
@NonNull
public List<Place> getFromWikidataQuery(
final fr.free.nrw.commons.location.LatLng centerPoint,
final fr.free.nrw.commons.location.LatLng screenTopRight,
@ -120,11 +126,11 @@ public class NearbyPlaces {
final boolean shouldQueryForMonuments,
@Nullable final String customQuery) throws Exception {
if (customQuery != null) {
return okHttpJsonApiClient
.getNearbyPlaces(
new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft), lang,
final List<Place> nearbyPlaces = okHttpJsonApiClient.getNearbyPlaces(
new Rectangular(screenTopRight, screenBottomLeft), lang,
shouldQueryForMonuments,
customQuery);
return nearbyPlaces != null ? nearbyPlaces : emptyList();
}
final int lowerLimit = 1000, upperLimit = 1500;
@ -141,9 +147,10 @@ public class NearbyPlaces {
final int itemCount = okHttpJsonApiClient.getNearbyItemCount(
new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft));
if (itemCount < upperLimit) {
return okHttpJsonApiClient.getNearbyPlaces(
new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft), lang,
final List<Place> nearbyPlaces = okHttpJsonApiClient.getNearbyPlaces(
new Rectangular(screenTopRight, screenBottomLeft), lang,
shouldQueryForMonuments, null);
return nearbyPlaces != null ? nearbyPlaces : emptyList();
}
}
@ -175,9 +182,10 @@ public class NearbyPlaces {
maxRadius = targetRadius - 1;
}
}
return okHttpJsonApiClient.getNearbyPlaces(
new NearbyQueryParams.Radial(centerPoint, targetRadius / 100f), lang, shouldQueryForMonuments,
final List<Place> nearbyPlaces = okHttpJsonApiClient.getNearbyPlaces(
new Radial(centerPoint, targetRadius / 100f), lang, shouldQueryForMonuments,
null);
return nearbyPlaces != null ? nearbyPlaces : emptyList();
}
/**

View file

@ -11,6 +11,7 @@ import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.FileProvider
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import fr.free.nrw.commons.R
import fr.free.nrw.commons.ViewPagerAdapter
@ -71,7 +72,7 @@ class ProfileActivity : BaseActivity() {
title = userName
shouldShowContributions = intent.getBooleanExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, false)
viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
viewPagerAdapter = ViewPagerAdapter(this, supportFragmentManager)
binding.viewPager.adapter = viewPagerAdapter
binding.tabLayout.setupWithViewPager(binding.viewPager)
setTabs()
@ -83,39 +84,23 @@ class ProfileActivity : BaseActivity() {
}
private fun setTabs() {
val fragmentList = mutableListOf<Fragment>()
val titleList = mutableListOf<String>()
// Add Achievements tab
achievementsFragment = AchievementsFragment().apply {
arguments = Bundle().apply {
putString(KEY_USERNAME, userName)
}
arguments = bundleOf(KEY_USERNAME to userName)
}
fragmentList.add(achievementsFragment)
titleList.add(resources.getString(R.string.achievements_tab_title).uppercase())
// Add Leaderboard tab
leaderboardFragment = LeaderboardFragment().apply {
arguments = Bundle().apply {
putString(KEY_USERNAME, userName)
}
arguments = bundleOf(KEY_USERNAME to userName)
}
fragmentList.add(leaderboardFragment)
titleList.add(resources.getString(R.string.leaderboard_tab_title).uppercase(Locale.ROOT))
// Add Contributions tab
contributionsFragment = ContributionsFragment().apply {
arguments = Bundle().apply {
putString(KEY_USERNAME, userName)
}
}
contributionsFragment?.let {
fragmentList.add(it)
titleList.add(getString(R.string.contributions_fragment).uppercase(Locale.ROOT))
arguments = bundleOf(KEY_USERNAME to userName)
}
viewPagerAdapter.setTabData(fragmentList, titleList)
viewPagerAdapter.setTabs(
R.string.achievements_tab_title to achievementsFragment,
R.string.leaderboard_tab_title to leaderboardFragment,
R.string.contributions_fragment to contributionsFragment!!
)
viewPagerAdapter.notifyDataSetChanged()
}

View file

@ -28,8 +28,6 @@ class UploadProgressActivity : BaseActivity() {
@Inject
lateinit var contributionDao: ContributionDao
val fragmentList: MutableList<Fragment> = ArrayList()
val titleList: MutableList<String> = ArrayList()
var isPaused = true
var isPendingIconsVisible = true
var isErrorIconsVisisble = false
@ -38,7 +36,7 @@ class UploadProgressActivity : BaseActivity() {
super.onCreate(savedInstanceState)
binding = ActivityUploadProgressBinding.inflate(layoutInflater)
setContentView(binding.root)
viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
viewPagerAdapter = ViewPagerAdapter(this, supportFragmentManager)
binding.uploadProgressViewPager.setAdapter(viewPagerAdapter)
binding.uploadProgressViewPager.setId(R.id.upload_progress_view_pager)
binding.uploadProgressTabLayout.setupWithViewPager(binding.uploadProgressViewPager)
@ -81,11 +79,10 @@ class UploadProgressActivity : BaseActivity() {
pendingUploadsFragment = PendingUploadsFragment()
failedUploadsFragment = FailedUploadsFragment()
fragmentList.add(pendingUploadsFragment!!)
titleList.add(getString(R.string.pending))
fragmentList.add(failedUploadsFragment!!)
titleList.add(getString(R.string.failed))
viewPagerAdapter!!.setTabData(fragmentList, titleList)
viewPagerAdapter!!.setTabs(
R.string.pending to pendingUploadsFragment!!,
R.string.failed to failedUploadsFragment!!
)
viewPagerAdapter!!.notifyDataSetChanged()
}

View file

@ -617,6 +617,8 @@ Upload your first media by tapping on the add button.</string>
<string name="title_for_media">MEDIA</string>
<string name="title_for_child_classes">CHILD CLASSES</string>
<string name="title_for_parent_classes">PARENT CLASSES</string>
<string name="title_for_subcategories">SUBCATEGORIES</string>
<string name="title_for_parent_categories">PARENT CATEGORIES</string>
<string name="upload_nearby_place_found_title">Nearby Place Found</string>
<string name="upload_nearby_place_found_description_plural">Are these pictures of %1$s?</string>

View file

@ -1,110 +0,0 @@
package fr.free.nrw.commons;
import static fr.free.nrw.commons.TestConnectionFactoryKt.createTestClient;
import androidx.annotation.NonNull;
import java.util.List;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import okhttp3.Dispatcher;
import okhttp3.OkHttpClient;
import okhttp3.mockwebserver.MockResponse;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import fr.free.nrw.commons.wikidata.GsonUtil;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
@RunWith(RobolectricTestRunner.class)
public abstract class MockWebServerTest {
private OkHttpClient okHttpClient;
private final TestWebServer server = new TestWebServer();
@Before public void setUp() throws Throwable {
OkHttpConnectionFactory.CLIENT = createTestClient();
okHttpClient = OkHttpConnectionFactory.CLIENT.newBuilder()
.dispatcher(new Dispatcher(new ImmediateExecutorService())).build();
server.setUp();
}
@After public void tearDown() throws Throwable {
server.tearDown();
}
@NonNull protected TestWebServer server() {
return server;
}
protected void enqueueFromFile(@NonNull String filename) throws Throwable {
String json = TestFileUtil.readRawFile(filename);
server.enqueue(json);
}
protected void enqueue404() {
final int code = 404;
server.enqueue(new MockResponse().setResponseCode(code).setBody("Not Found"));
}
protected void enqueueMalformed() {
server.enqueue("(╯°□°)╯︵ ┻━┻");
}
protected void enqueueEmptyJson() {
server.enqueue(new MockResponse().setBody("{}"));
}
@NonNull protected OkHttpClient okHttpClient() {
return okHttpClient;
}
@NonNull protected <T> T service(Class<T> clazz) {
return service(clazz, server().getUrl());
}
@NonNull protected <T> T service(Class<T> clazz, @NonNull String url) {
return new Retrofit.Builder()
.baseUrl(url)
.callbackExecutor(new ImmediateExecutor())
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(GsonUtil.INSTANCE.getDefaultGson()))
.build()
.create(clazz);
}
public final class ImmediateExecutorService extends AbstractExecutorService {
@Override public void shutdown() {
throw new UnsupportedOperationException();
}
@NonNull @Override public List<Runnable> shutdownNow() {
throw new UnsupportedOperationException();
}
@Override public boolean isShutdown() {
throw new UnsupportedOperationException();
}
@Override public boolean isTerminated() {
throw new UnsupportedOperationException();
}
@Override public boolean awaitTermination(long l, @NonNull TimeUnit timeUnit)
throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override public void execute(@NonNull Runnable runnable) {
runnable.run();
}
}
public class ImmediateExecutor implements Executor {
@Override
public void execute(@NonNull Runnable runnable) {
runnable.run();
}
}
}

View file

@ -0,0 +1,110 @@
package fr.free.nrw.commons
import fr.free.nrw.commons.wikidata.GsonUtil.defaultGson
import okhttp3.Dispatcher
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import org.junit.After
import org.junit.Before
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.AbstractExecutorService
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
@RunWith(RobolectricTestRunner::class)
abstract class MockWebServerTest {
private var okHttpClient: OkHttpClient? = null
private val server = TestWebServer()
@Before
@Throws(Throwable::class)
open fun setUp() {
OkHttpConnectionFactory.CLIENT = createTestClient()
okHttpClient = OkHttpConnectionFactory.CLIENT!!.newBuilder()
.dispatcher(Dispatcher(ImmediateExecutorService())).build()
server.setUp()
}
@After
@Throws(Throwable::class)
fun tearDown() {
server.tearDown()
}
protected fun server(): TestWebServer {
return server
}
@Throws(Throwable::class)
protected fun enqueueFromFile(filename: String) {
val json = TestFileUtil.readRawFile(filename)
server.enqueue(json)
}
protected fun enqueue404() {
val code = 404
server.enqueue(MockResponse().setResponseCode(code).setBody("Not Found"))
}
protected fun enqueueMalformed() {
server.enqueue("(╯°□°)╯︵ ┻━┻")
}
protected fun enqueueEmptyJson() {
server.enqueue(MockResponse().setBody("{}"))
}
protected fun okHttpClient(): OkHttpClient {
return okHttpClient!!
}
protected fun <T> service(clazz: Class<T>): T {
return service(clazz, server().url)
}
protected fun <T> service(clazz: Class<T>, url: String): T {
return Retrofit.Builder()
.baseUrl(url)
.callbackExecutor(ImmediateExecutor())
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(defaultGson))
.build()
.create(clazz)
}
inner class ImmediateExecutorService : AbstractExecutorService() {
override fun shutdown() {
throw UnsupportedOperationException()
}
override fun shutdownNow(): List<Runnable> {
throw UnsupportedOperationException()
}
override fun isShutdown(): Boolean {
throw UnsupportedOperationException()
}
override fun isTerminated(): Boolean {
throw UnsupportedOperationException()
}
@Throws(InterruptedException::class)
override fun awaitTermination(l: Long, timeUnit: TimeUnit): Boolean {
throw UnsupportedOperationException()
}
override fun execute(runnable: Runnable) {
runnable.run()
}
}
inner class ImmediateExecutor : Executor {
override fun execute(runnable: Runnable) {
runnable.run()
}
}
}

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons
import fr.free.nrw.commons.OkHttpConnectionFactory.HttpStatusException
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
@ -39,6 +38,6 @@ private class UnsuccessfulResponseInterceptor : Interceptor {
if (rsp.isSuccessful) {
return rsp
}
throw HttpStatusException(rsp)
throw IOException("Unsuccessful response")
}
}

View file

@ -2,7 +2,6 @@ package fr.free.nrw.commons.auth.csrf
import com.google.gson.stream.MalformedJsonException
import fr.free.nrw.commons.MockWebServerTest
import fr.free.nrw.commons.OkHttpConnectionFactory.HttpStatusException
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.auth.login.LoginClient
import fr.free.nrw.commons.wikidata.mwapi.MwException
@ -13,6 +12,7 @@ import org.mockito.ArgumentMatchers.isA
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import java.io.IOException
class CsrfTokenClientTest : MockWebServerTest() {
private val cb = mock(CsrfTokenClient.Callback::class.java)
@ -53,7 +53,7 @@ class CsrfTokenClientTest : MockWebServerTest() {
performRequest()
verify(cb, never()).success(any(String::class.java))
verify(cb).failure(isA(HttpStatusException::class.java))
verify(cb).failure(isA(IOException::class.java))
}
@Test