From 381f9eca0c73338332546ffb9c1f39797b242cef Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Tue, 26 Nov 2024 17:59:31 +0530 Subject: [PATCH 1/4] Migrated notification module from Java to Kotlin (#5955) * Rename .java to .kt * Migration of notification module from Java to Kotlin --- .../contributions/ContributionsFragment.java | 2 +- .../commons/contributions/MainActivity.java | 2 +- .../notification/NotificationActivity.java | 288 ------------------ .../notification/NotificationActivity.kt | 247 +++++++++++++++ ...catinAdapter.kt => NotificationAdapter.kt} | 2 +- .../notification/NotificationController.java | 33 -- .../notification/NotificationController.kt | 25 ++ .../notification/NotificationHelper.java | 73 ----- .../notification/NotificationHelper.kt | 73 +++++ .../NotificationWorkerFragment.java | 31 -- .../NotificationWorkerFragment.kt | 20 ++ .../notification/models/NotificationType.java | 28 -- .../notification/models/NotificationType.kt | 33 ++ 13 files changed, 401 insertions(+), 456 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt rename app/src/main/java/fr/free/nrw/commons/notification/{NotificatinAdapter.kt => NotificationAdapter.kt} (92%) delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationController.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.kt diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java index 1699f35f0..bffafaef1 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -289,7 +289,7 @@ public class ContributionsFragment }); } notification.setOnClickListener(view -> { - NotificationActivity.startYourself(getContext(), "unread"); + NotificationActivity.Companion.startYourself(getContext(), "unread"); }); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index 849ef3450..03027f287 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -414,7 +414,7 @@ public class MainActivity extends BaseActivity return true; case R.id.notifications: // Starts notification activity on click to notification icon - NotificationActivity.startYourself(this, "unread"); + NotificationActivity.Companion.startYourself(this, "unread"); return true; default: return super.onOptionsItemSelected(item); diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java deleted file mode 100644 index b57df4948..000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ /dev/null @@ -1,288 +0,0 @@ -package fr.free.nrw.commons.notification; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import com.google.android.material.snackbar.Snackbar; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.databinding.ActivityNotificationBinding; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; -import fr.free.nrw.commons.notification.models.Notification; -import fr.free.nrw.commons.notification.models.NotificationType; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.NetworkUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.Observable; -import io.reactivex.ObservableSource; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Callable; -import javax.inject.Inject; -import kotlin.Unit; -import timber.log.Timber; - -/** - * Created by root on 18.12.2017. - */ - -public class NotificationActivity extends BaseActivity { - private ActivityNotificationBinding binding; - - @Inject - NotificationController controller; - - @Inject - SessionManager sessionManager; - - private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment"; - private NotificationWorkerFragment mNotificationWorkerFragment; - private NotificatinAdapter adapter; - private List notificationList; - MenuItem notificationMenuItem; - /** - * Boolean isRead is true if this notification activity is for read section of notification. - */ - private boolean isRead; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - isRead = getIntent().getStringExtra("title").equals("read"); - binding = ActivityNotificationBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - mNotificationWorkerFragment = (NotificationWorkerFragment) getFragmentManager() - .findFragmentByTag(TAG_NOTIFICATION_WORKER_FRAGMENT); - initListView(); - setPageTitle(); - setSupportActionBar(binding.toolbar.toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - /** - * If this is unread section of the notifications, removeNotification method - * Marks the notification as read, - * Removes the notification from unread, - * Displays the Snackbar. - * - * Otherwise returns (read section). - * - * @param notification - */ - @SuppressLint("CheckResult") - public void removeNotification(Notification notification) { - if (isRead) { - return; - } - Disposable disposable = Observable.defer((Callable>) - () -> controller.markAsRead(notification)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - if (result) { - notificationList.remove(notification); - setItems(notificationList); - adapter.notifyDataSetChanged(); - ViewUtil.showLongSnackbar(binding.container,getString(R.string.notification_mark_read)); - if (notificationList.size() == 0) { - setEmptyView(); - binding.container.setVisibility(View.GONE); - binding.noNotificationBackground.setVisibility(View.VISIBLE); - } - } else { - adapter.notifyDataSetChanged(); - setItems(notificationList); - ViewUtil.showLongToast(this,getString(R.string.some_error)); - } - }, throwable -> { - if (throwable instanceof InvalidLoginTokenException) { - final String username = sessionManager.getUserName(); - final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener( - this, - getString(R.string.invalid_login_message), - username - ); - - CommonsApplication.getInstance().clearApplicationData( - this, logoutListener); - } else { - Timber.e(throwable, "Error occurred while loading notifications"); - throwable.printStackTrace(); - ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications); - binding.progressBar.setVisibility(View.GONE); - ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications); - } - binding.progressBar.setVisibility(View.GONE); - }); - getCompositeDisposable().add(disposable); - } - - - - private void initListView() { - binding.listView.setLayoutManager(new LinearLayoutManager(this)); - DividerItemDecoration itemDecor = new DividerItemDecoration(binding.listView.getContext(), DividerItemDecoration.VERTICAL); - binding.listView.addItemDecoration(itemDecor); - if (isRead) { - refresh(true); - } else { - refresh(false); - } - adapter = new NotificatinAdapter(item -> { - Timber.d("Notification clicked %s", item.getLink()); - if (item.getNotificationType() == NotificationType.EMAIL){ - ViewUtil.showLongSnackbar(binding.container,getString(R.string.check_your_email_inbox)); - } else { - handleUrl(item.getLink()); - } - removeNotification(item); - return Unit.INSTANCE; - }); - binding.listView.setAdapter(adapter); - } - - private void refresh(boolean archived) { - if (!NetworkUtils.isInternetConnectionEstablished(this)) { - binding.progressBar.setVisibility(View.GONE); - Snackbar.make(binding.container, R.string.no_internet, Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.retry, view -> refresh(archived)).show(); - } else { - addNotifications(archived); - } - binding.progressBar.setVisibility(View.VISIBLE); - binding.noNotificationBackground.setVisibility(View.GONE); - binding.container.setVisibility(View.VISIBLE); - } - - @SuppressLint("CheckResult") - private void addNotifications(boolean archived) { - Timber.d("Add notifications"); - if (mNotificationWorkerFragment == null) { - binding.progressBar.setVisibility(View.VISIBLE); - getCompositeDisposable().add(controller.getNotifications(archived) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(notificationList -> { - Collections.reverse(notificationList); - Timber.d("Number of notifications is %d", notificationList.size()); - this.notificationList = notificationList; - if (notificationList.size()==0){ - setEmptyView(); - binding.container.setVisibility(View.GONE); - binding.noNotificationBackground.setVisibility(View.VISIBLE); - } else { - setItems(notificationList); - } - binding.progressBar.setVisibility(View.GONE); - }, throwable -> { - Timber.e(throwable, "Error occurred while loading notifications "); - ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications); - binding.progressBar.setVisibility(View.GONE); - })); - } else { - notificationList = mNotificationWorkerFragment.getNotificationList(); - setItems(notificationList); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_notifications, menu); - notificationMenuItem = menu.findItem(R.id.archived); - setMenuItemTitle(); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.getItemId()) { - case R.id.archived: - if (item.getTitle().equals(getString(R.string.menu_option_read))) { - NotificationActivity.startYourself(NotificationActivity.this, "read"); - }else if (item.getTitle().equals(getString(R.string.menu_option_unread))) { - onBackPressed(); - } - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private void handleUrl(String url) { - if (url == null || url.equals("")) { - return; - } - Utils.handleWebUrl(this, Uri.parse(url)); - } - - private void setItems(List notificationList) { - if (notificationList == null || notificationList.isEmpty()) { - ViewUtil.showShortSnackbar(binding.container, R.string.no_notifications); - /*progressBar.setVisibility(View.GONE); - recyclerView.setVisibility(View.GONE);*/ - binding.container.setVisibility(View.GONE); - setEmptyView(); - binding.noNotificationBackground.setVisibility(View.VISIBLE); - return; - } - binding.container.setVisibility(View.VISIBLE); - binding.noNotificationBackground.setVisibility(View.GONE); - adapter.setItems(notificationList); - } - - public static void startYourself(Context context, String title) { - Intent intent = new Intent(context, NotificationActivity.class); - intent.putExtra("title", title); - - context.startActivity(intent); - } - - private void setPageTitle() { - if (getSupportActionBar() != null) { - if (isRead) { - getSupportActionBar().setTitle(R.string.read_notifications); - } else { - getSupportActionBar().setTitle(R.string.notifications); - } - } - } - - private void setEmptyView() { - if (isRead) { - binding.noNotificationText.setText(R.string.no_read_notification); - }else { - binding.noNotificationText.setText(R.string.no_notification); - } - } - - private void setMenuItemTitle() { - if (isRead) { - notificationMenuItem.setTitle(R.string.menu_option_unread); - - }else { - notificationMenuItem.setTitle(R.string.menu_option_read); - - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt new file mode 100644 index 000000000..1d87a8f82 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt @@ -0,0 +1,247 @@ +package fr.free.nrw.commons.notification + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException +import fr.free.nrw.commons.databinding.ActivityNotificationBinding +import fr.free.nrw.commons.notification.models.Notification +import fr.free.nrw.commons.notification.models.NotificationType +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.NetworkUtils +import fr.free.nrw.commons.utils.ViewUtil +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import javax.inject.Inject + +/** + * Created by root on 18.12.2017. + */ +class NotificationActivity : BaseActivity() { + + private lateinit var binding: ActivityNotificationBinding + + @Inject + lateinit var controller: NotificationController + + @Inject + lateinit var sessionManager: SessionManager + + private val tagNotificationWorkerFragment = "NotificationWorkerFragment" + private var mNotificationWorkerFragment: NotificationWorkerFragment? = null + private lateinit var adapter: NotificationAdapter + private var notificationList: MutableList = mutableListOf() + private var notificationMenuItem: MenuItem? = null + + /** + * Boolean isRead is true if this notification activity is for read section of notification. + */ + private var isRead: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + isRead = intent.getStringExtra("title") == "read" + binding = ActivityNotificationBinding.inflate(layoutInflater) + setContentView(binding.root) + mNotificationWorkerFragment = supportFragmentManager.findFragmentByTag( + tagNotificationWorkerFragment + ) as? NotificationWorkerFragment + initListView() + setPageTitle() + setSupportActionBar(binding.toolbar.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + @SuppressLint("CheckResult", "NotifyDataSetChanged") + fun removeNotification(notification: Notification) { + if (isRead) return + + val disposable = Observable.defer { controller.markAsRead(notification) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ result -> + if (result) { + notificationList.remove(notification) + setItems(notificationList) + adapter.notifyDataSetChanged() + ViewUtil.showLongSnackbar(binding.container, getString(R.string.notification_mark_read)) + if (notificationList.isEmpty()) { + setEmptyView() + binding.container.visibility = View.GONE + binding.noNotificationBackground.visibility = View.VISIBLE + } + } else { + adapter.notifyDataSetChanged() + setItems(notificationList) + ViewUtil.showLongToast(this, getString(R.string.some_error)) + } + }, { throwable -> + if (throwable is InvalidLoginTokenException) { + val username = sessionManager.getUserName() + val logoutListener = CommonsApplication.BaseLogoutListener( + this, + getString(R.string.invalid_login_message), + username + ) + CommonsApplication.instance.clearApplicationData(this, logoutListener) + } else { + Timber.e(throwable, "Error occurred while loading notifications") + ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications) + } + binding.progressBar.visibility = View.GONE + }) + compositeDisposable.add(disposable) + } + + private fun initListView() { + binding.listView.layoutManager = LinearLayoutManager(this) + val itemDecor = DividerItemDecoration(binding.listView.context, DividerItemDecoration.VERTICAL) + binding.listView.addItemDecoration(itemDecor) + refresh(isRead) + adapter = NotificationAdapter { item -> + Timber.d("Notification clicked %s", item.link) + if (item.notificationType == NotificationType.EMAIL) { + ViewUtil.showLongSnackbar(binding.container, getString(R.string.check_your_email_inbox)) + } else { + handleUrl(item.link) + } + removeNotification(item) + } + binding.listView.adapter = adapter + } + + private fun refresh(archived: Boolean) { + if (!NetworkUtils.isInternetConnectionEstablished(this)) { + binding.progressBar.visibility = View.GONE + Snackbar.make(binding.container, R.string.no_internet, Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.retry) { refresh(archived) } + .show() + } else { + addNotifications(archived) + } + binding.progressBar.visibility = View.VISIBLE + binding.noNotificationBackground.visibility = View.GONE + binding.container.visibility = View.VISIBLE + } + + @SuppressLint("CheckResult") + private fun addNotifications(archived: Boolean) { + Timber.d("Add notifications") + if (mNotificationWorkerFragment == null) { + binding.progressBar.visibility = View.VISIBLE + compositeDisposable.add(controller.getNotifications(archived) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ notificationList -> + notificationList.reversed() + Timber.d("Number of notifications is %d", notificationList.size) + this.notificationList = notificationList.toMutableList() + if (notificationList.isEmpty()) { + setEmptyView() + binding.container.visibility = View.GONE + binding.noNotificationBackground.visibility = View.VISIBLE + } else { + setItems(notificationList) + } + binding.progressBar.visibility = View.GONE + }, { throwable -> + Timber.e(throwable, "Error occurred while loading notifications") + ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications) + binding.progressBar.visibility = View.GONE + })) + } else { + notificationList = mNotificationWorkerFragment?.notificationList?.toMutableList() ?: mutableListOf() + setItems(notificationList) + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_notifications, menu) + notificationMenuItem = menu.findItem(R.id.archived) + setMenuItemTitle() + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.archived -> { + if (item.title == getString(R.string.menu_option_read)) { + startYourself(this, "read") + } else if (item.title == getString(R.string.menu_option_unread)) { + onBackPressed() + } + true + } + else -> super.onOptionsItemSelected(item) + } + } + + private fun handleUrl(url: String?) { + if (url.isNullOrEmpty()) return + Utils.handleWebUrl(this, Uri.parse(url)) + } + + private fun setItems(notificationList: List?) { + if (notificationList.isNullOrEmpty()) { + ViewUtil.showShortSnackbar(binding.container, R.string.no_notifications) + binding.container.visibility = View.GONE + setEmptyView() + binding.noNotificationBackground.visibility = View.VISIBLE + return + } + binding.container.visibility = View.VISIBLE + binding.noNotificationBackground.visibility = View.GONE + adapter.items = notificationList + } + + private fun setPageTitle() { + supportActionBar?.title = if (isRead) { + getString(R.string.read_notifications) + } else { + getString(R.string.notifications) + } + } + + private fun setEmptyView() { + binding.noNotificationText.text = if (isRead) { + getString(R.string.no_read_notification) + } else { + getString(R.string.no_notification) + } + } + + private fun setMenuItemTitle() { + notificationMenuItem?.title = if (isRead) { + getString(R.string.menu_option_unread) + } else { + getString(R.string.menu_option_read) + } + } + + companion object { + fun startYourself(context: Context, title: String) { + val intent = Intent(context, NotificationActivity::class.java) + intent.putExtra("title", title) + context.startActivity(intent) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificatinAdapter.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapter.kt similarity index 92% rename from app/src/main/java/fr/free/nrw/commons/notification/NotificatinAdapter.kt rename to app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapter.kt index 41d7d4883..637443ecf 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificatinAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapter.kt @@ -3,7 +3,7 @@ package fr.free.nrw.commons.notification import fr.free.nrw.commons.notification.models.Notification import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter -internal class NotificatinAdapter( +internal class NotificationAdapter( onNotificationClicked: (Notification) -> Unit, ) : BaseDelegateAdapter( notificationDelegate(onNotificationClicked), diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java deleted file mode 100644 index de1f372d2..000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java +++ /dev/null @@ -1,33 +0,0 @@ -package fr.free.nrw.commons.notification; - -import fr.free.nrw.commons.notification.models.Notification; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import io.reactivex.Observable; -import io.reactivex.Single; - -/** - * Created by root on 19.12.2017. - */ -@Singleton -public class NotificationController { - - private NotificationClient notificationClient; - - - @Inject - public NotificationController(NotificationClient notificationClient) { - this.notificationClient = notificationClient; - } - - public Single> getNotifications(boolean archived) { - return notificationClient.getNotifications(archived); - } - - Observable markAsRead(Notification notification) { - return notificationClient.markNotificationAsRead(notification.getNotificationId()); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.kt new file mode 100644 index 000000000..870d658cb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.notification + +import fr.free.nrw.commons.notification.models.Notification +import javax.inject.Inject +import javax.inject.Singleton + +import io.reactivex.Observable +import io.reactivex.Single + +/** + * Created by root on 19.12.2017. + */ +@Singleton +class NotificationController @Inject constructor( + private val notificationClient: NotificationClient +) { + + fun getNotifications(archived: Boolean): Single> { + return notificationClient.getNotifications(archived) + } + + fun markAsRead(notification: Notification): Observable { + return notificationClient.markNotificationAsRead(notification.notificationId) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java deleted file mode 100644 index b63d3a4c1..000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java +++ /dev/null @@ -1,73 +0,0 @@ -package fr.free.nrw.commons.notification; - -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import androidx.core.app.NotificationCompat; -import javax.inject.Inject; -import javax.inject.Singleton; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import static androidx.core.app.NotificationCompat.DEFAULT_ALL; -import static androidx.core.app.NotificationCompat.PRIORITY_HIGH; - -/** - * Helper class that can be used to build a generic notification - * Going forward all notifications should be built using this helper class - */ -@Singleton -public class NotificationHelper { - - public static final int NOTIFICATION_DELETE = 1; - public static final int NOTIFICATION_EDIT_CATEGORY = 2; - public static final int NOTIFICATION_EDIT_COORDINATES = 3; - public static final int NOTIFICATION_EDIT_DESCRIPTION = 4; - public static final int NOTIFICATION_EDIT_DEPICTIONS = 5; - - private final NotificationManager notificationManager; - private final NotificationCompat.Builder notificationBuilder; - - @Inject - public NotificationHelper(final Context context) { - notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationBuilder = new NotificationCompat - .Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL) - .setOnlyAlertOnce(true); - } - - /** - * Public interface to build and show a notification in the notification bar - * @param context passed context - * @param notificationTitle title of the notification - * @param notificationMessage message to be displayed in the notification - * @param notificationId the notificationID - * @param intent the intent to be fired when the notification is clicked - */ - public void showNotification( - final Context context, - final String notificationTitle, - final String notificationMessage, - final int notificationId, - final Intent intent - ) { - notificationBuilder.setDefaults(DEFAULT_ALL) - .setContentTitle(notificationTitle) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(notificationMessage)) - .setSmallIcon(R.drawable.ic_launcher) - .setProgress(0, 0, false) - .setOngoing(false) - .setPriority(PRIORITY_HIGH); - - int flags = PendingIntent.FLAG_UPDATE_CURRENT; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - flags |= PendingIntent.FLAG_IMMUTABLE; // This flag was introduced in API 23 - } - - final PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, flags); - notificationBuilder.setContentIntent(pendingIntent); - notificationManager.notify(notificationId, notificationBuilder.build()); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.kt new file mode 100644 index 000000000..101a8fccc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.kt @@ -0,0 +1,73 @@ +package fr.free.nrw.commons.notification + +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import javax.inject.Inject +import javax.inject.Singleton +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.R +import androidx.core.app.NotificationCompat.DEFAULT_ALL +import androidx.core.app.NotificationCompat.PRIORITY_HIGH + +/** + * Helper class that can be used to build a generic notification + * Going forward all notifications should be built using this helper class + */ +@Singleton +class NotificationHelper @Inject constructor( + context: Context +) { + + companion object { + const val NOTIFICATION_DELETE = 1 + const val NOTIFICATION_EDIT_CATEGORY = 2 + const val NOTIFICATION_EDIT_COORDINATES = 3 + const val NOTIFICATION_EDIT_DESCRIPTION = 4 + const val NOTIFICATION_EDIT_DEPICTIONS = 5 + } + + private val notificationManager: NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + private val notificationBuilder: NotificationCompat.Builder = NotificationCompat + .Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL) + .setOnlyAlertOnce(true) + + /** + * Public interface to build and show a notification in the notification bar + * @param context passed context + * @param notificationTitle title of the notification + * @param notificationMessage message to be displayed in the notification + * @param notificationId the notificationID + * @param intent the intent to be fired when the notification is clicked + */ + fun showNotification( + context: Context, + notificationTitle: String, + notificationMessage: String, + notificationId: Int, + intent: Intent + ) { + notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) + .setContentTitle(notificationTitle) + .setStyle(NotificationCompat.BigTextStyle().bigText(notificationMessage)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(0, 0, false) + .setOngoing(false) + .setPriority(NotificationCompat.PRIORITY_HIGH) + + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + + val pendingIntent = PendingIntent.getActivity(context, 1, intent, flags) + notificationBuilder.setContentIntent(pendingIntent) + notificationManager.notify(notificationId, notificationBuilder.build()) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.java deleted file mode 100644 index ffee5eac2..000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.java +++ /dev/null @@ -1,31 +0,0 @@ -package fr.free.nrw.commons.notification; - -import android.app.Fragment; -import android.os.Bundle; - -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.notification.models.Notification; -import java.util.List; - -/** - * Created by knightshade on 25/2/18. - */ - -public class NotificationWorkerFragment extends Fragment { - private List notificationList; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setRetainInstance(true); - } - - public void setNotificationList(List notificationList){ - this.notificationList = notificationList; - } - - public List getNotificationList(){ - return notificationList; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.kt new file mode 100644 index 000000000..928651b9a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.kt @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.notification + +import android.app.Fragment +import android.os.Bundle + +import fr.free.nrw.commons.notification.models.Notification + + +/** + * Created by knightshade on 25/2/18. + */ +class NotificationWorkerFragment : Fragment() { + + var notificationList: List? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + retainInstance = true + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.java b/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.java deleted file mode 100644 index fb9ae7e99..000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.java +++ /dev/null @@ -1,28 +0,0 @@ -package fr.free.nrw.commons.notification.models; - -public enum NotificationType { - THANK_YOU_EDIT("thank-you-edit"), - EDIT_USER_TALK("edit-user-talk"), - MENTION("mention"), - EMAIL("email"), - WELCOME("welcome"), - UNKNOWN("unknown"); - private String type; - - NotificationType(String type) { - this.type = type; - } - - public String getType() { - return type; - } - - public static NotificationType handledValueOf(String name) { - for (NotificationType e : values()) { - if (e.getType().equals(name)) { - return e; - } - } - return UNKNOWN; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.kt b/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.kt new file mode 100644 index 000000000..9034b3c59 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.kt @@ -0,0 +1,33 @@ +package fr.free.nrw.commons.notification.models + +enum class NotificationType(private val type: String) { + THANK_YOU_EDIT("thank-you-edit"), + + EDIT_USER_TALK("edit-user-talk"), + + MENTION("mention"), + + EMAIL("email"), + + WELCOME("welcome"), + + UNKNOWN("unknown"); + + // Getter for the type property + fun getType(): String { + return type + } + + companion object { + // Returns the corresponding NotificationType for a given name or UNKNOWN + // if no match is found + fun handledValueOf(name: String): NotificationType { + for (e in values()) { + if (e.type == name) { + return e + } + } + return UNKNOWN + } + } +} From 238023056fbc97ec95f98d1870b3e0e57bf53e5b Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Wed, 27 Nov 2024 19:28:46 +0530 Subject: [PATCH 2/4] Migrated navtab module from Java to Kotlin (#5965) * Rename .java to .kt * Migrated navtab module from Java to Kotlin * Migrated navtab module from Java to Kotlin --- .../navtab/MoreBottomSheetFragment.java | 238 ----------------- .../commons/navtab/MoreBottomSheetFragment.kt | 242 ++++++++++++++++++ .../MoreBottomSheetLoggedOutFragment.java | 142 ---------- .../MoreBottomSheetLoggedOutFragment.kt | 151 +++++++++++ .../fr/free/nrw/commons/navtab/NavTab.java | 95 ------- .../java/fr/free/nrw/commons/navtab/NavTab.kt | 79 ++++++ .../navtab/NavTabFragmentPagerAdapter.java | 38 --- .../navtab/NavTabFragmentPagerAdapter.kt | 36 +++ .../free/nrw/commons/navtab/NavTabLayout.java | 41 --- .../free/nrw/commons/navtab/NavTabLayout.kt | 47 ++++ .../nrw/commons/navtab/NavTabLoggedOut.java | 79 ------ .../nrw/commons/navtab/NavTabLoggedOut.kt | 65 +++++ 12 files changed, 620 insertions(+), 633 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTab.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.kt diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java deleted file mode 100644 index 9ea59488e..000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java +++ /dev/null @@ -1,238 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import fr.free.nrw.commons.AboutActivity; -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; -import fr.free.nrw.commons.feedback.FeedbackContentCreator; -import fr.free.nrw.commons.feedback.model.Feedback; -import fr.free.nrw.commons.feedback.FeedbackDialog; -import fr.free.nrw.commons.kvstore.BasicKvStore; -import fr.free.nrw.commons.kvstore.JsonKvStore; -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 io.reactivex.Single; -import io.reactivex.SingleSource; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import java.util.concurrent.Callable; -import javax.inject.Inject; -import javax.inject.Named; - -public class MoreBottomSheetFragment extends BottomSheetDialogFragment { - - @Inject - CommonsLogSender commonsLogSender; - - private TextView moreProfile; - - @Inject @Named("default_preferences") - JsonKvStore store; - - @Inject - @Named("commons-page-edit") - PageEditClient pageEditClient; - - private static final String GITHUB_ISSUES_URL = "https://github.com/commons-app/apps-android-commons/issues"; - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - final @NonNull FragmentMoreBottomSheetBinding binding = - FragmentMoreBottomSheetBinding.inflate(inflater, container, false); - moreProfile = binding.moreProfile; - - if(store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED)){ - binding.morePeerReview.setVisibility(View.GONE); - } - - binding.moreLogout.setOnClickListener(v -> onLogoutClicked()); - binding.moreFeedback.setOnClickListener(v -> onFeedbackClicked()); - binding.moreAbout.setOnClickListener(v -> onAboutClicked()); - binding.moreTutorial.setOnClickListener(v -> onTutorialClicked()); - binding.moreSettings.setOnClickListener(v -> onSettingsClicked()); - binding.moreProfile.setOnClickListener(v -> onProfileClicked()); - binding.morePeerReview.setOnClickListener(v -> onPeerReviewClicked()); - binding.moreFeedbackGithub.setOnClickListener(v -> onFeedbackGithubClicked()); - - setUserName(); - return binding.getRoot(); - } - - private void onFeedbackGithubClicked() { - final Intent intent; - intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(GITHUB_ISSUES_URL)); - startActivity(intent); - } - - @Override - public void onAttach(@NonNull final Context context) { - super.onAttach(context); - ApplicationlessInjection - .getInstance(requireActivity().getApplicationContext()) - .getCommonsApplicationComponent() - .inject(this); - } - - /** - * Set the username and user achievements level (if available) in navigationHeader. - */ - private void setUserName() { - BasicKvStore store = new BasicKvStore(this.getContext(), getUserName()); - String level = store.getString("userAchievementsLevel","0"); - if (level.equals("0")) { - moreProfile.setText(getUserName() + " (" + getString(R.string.see_your_achievements) + ")"); - } - else { - moreProfile.setText(getUserName() + " (" + getString(R.string.level) + " " + level + ")"); - } - } - - private String getUserName(){ - final AccountManager accountManager = AccountManager.get(getActivity()); - final Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE); - if (allAccounts.length != 0) { - return allAccounts[0].name; - } - return ""; - } - - - protected void onLogoutClicked() { - new AlertDialog.Builder(requireActivity()) - .setMessage(R.string.logout_verification) - .setCancelable(false) - .setPositiveButton(R.string.yes, (dialog, which) -> { - final CommonsApplication app = (CommonsApplication) - requireContext().getApplicationContext(); - app.clearApplicationData(requireContext(), new ActivityLogoutListener(requireActivity(), getContext())); - }) - .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) - .show(); - } - - protected void onFeedbackClicked() { - showFeedbackDialog(); - } - - /** - * Creates and shows a dialog asking feedback from users - */ - private void showFeedbackDialog() { - new FeedbackDialog(getContext(), this::uploadFeedback).show(); - } - - /** - * uploads feedback data on the server - */ - void uploadFeedback(final Feedback feedback) { - final FeedbackContentCreator feedbackContentCreator = new FeedbackContentCreator(getContext(), feedback); - - final Single single = - pageEditClient.createNewSection( - "Commons:Mobile_app/Feedback", - feedbackContentCreator.getSectionTitle(), - feedbackContentCreator.getSectionText(), - "New feedback on version " + feedback.getVersion() + " of the app" - ) - .flatMapSingle(Single::just) - .firstOrError(); - - Single.defer((Callable>) () -> single) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(aBoolean -> { - if (aBoolean) { - Toast.makeText(getContext(), getString(R.string.thanks_feedback), Toast.LENGTH_SHORT) - .show(); - } else { - Toast.makeText(getContext(), getString(R.string.error_feedback), - Toast.LENGTH_SHORT).show(); - } - }); - } - - /** - * This method shows the alert dialog when a user wants to send feedback about the app. - */ - private void showAlertDialog() { - new AlertDialog.Builder(requireActivity()) - .setMessage(R.string.feedback_sharing_data_alert) - .setCancelable(false) - .setPositiveButton(R.string.ok, (dialog, which) -> sendFeedback()) - .show(); - } - - /** - * This method collects the feedback message and starts the activity with implicit intent - * to available email client. - */ - private void sendFeedback() { - final String technicalInfo = commonsLogSender.getExtraInfo(); - - final Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO); - feedbackIntent.setType("message/rfc822"); - feedbackIntent.setData(Uri.parse("mailto:")); - feedbackIntent.putExtra(Intent.EXTRA_EMAIL, - new String[]{CommonsApplication.FEEDBACK_EMAIL}); - feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, - CommonsApplication.FEEDBACK_EMAIL_SUBJECT); - feedbackIntent.putExtra(Intent.EXTRA_TEXT, String.format( - "\n\n%s\n%s", CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER, technicalInfo)); - try { - startActivity(feedbackIntent); - } catch (final ActivityNotFoundException e) { - Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show(); - } - } - - protected void onAboutClicked() { - final Intent intent = new Intent(getActivity(), AboutActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - requireActivity().startActivity(intent); - } - - protected void onTutorialClicked() { - WelcomeActivity.startYourself(getActivity()); - } - - protected void onSettingsClicked() { - final Intent intent = new Intent(getActivity(), SettingsActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - requireActivity().startActivity(intent); - } - - protected void onProfileClicked() { - ProfileActivity.startYourself(getActivity(), getUserName(), false); - } - - protected void onPeerReviewClicked() { - ReviewActivity.Companion.startYourself(getActivity(), getString(R.string.title_activity_review)); - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt new file mode 100644 index 000000000..857e18ec3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt @@ -0,0 +1,242 @@ +package fr.free.nrw.commons.navtab + +import android.accounts.AccountManager +import android.annotation.SuppressLint +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import fr.free.nrw.commons.AboutActivity +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 +import fr.free.nrw.commons.feedback.FeedbackContentCreator +import fr.free.nrw.commons.feedback.FeedbackDialog +import fr.free.nrw.commons.feedback.model.Feedback +import fr.free.nrw.commons.kvstore.BasicKvStore +import fr.free.nrw.commons.kvstore.JsonKvStore +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 io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject +import javax.inject.Named + + +class MoreBottomSheetFragment : BottomSheetDialogFragment() { + + @Inject + lateinit var commonsLogSender: CommonsLogSender + + @Inject + @field: Named("default_preferences") + lateinit var store: JsonKvStore + + @Inject + @field: Named("commons-page-edit") + lateinit var pageEditClient: PageEditClient + + companion object { + private const val GITHUB_ISSUES_URL = + "https://github.com/commons-app/apps-android-commons/issues" + } + + private var binding: FragmentMoreBottomSheetBinding? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentMoreBottomSheetBinding.inflate(inflater, container, false) + + if (store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED)) { + binding?.morePeerReview?.visibility = View.GONE + } + + binding?.apply { + moreLogout.setOnClickListener { onLogoutClicked() } + moreFeedback.setOnClickListener { onFeedbackClicked() } + moreAbout.setOnClickListener { onAboutClicked() } + moreTutorial.setOnClickListener { onTutorialClicked() } + moreSettings.setOnClickListener { onSettingsClicked() } + moreProfile.setOnClickListener { onProfileClicked() } + morePeerReview.setOnClickListener { onPeerReviewClicked() } + moreFeedbackGithub.setOnClickListener { onFeedbackGithubClicked() } + } + + setUserName() + return binding?.root + } + + private fun onFeedbackGithubClicked() { + val intent = Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(GITHUB_ISSUES_URL) + } + startActivity(intent) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + ApplicationlessInjection + .getInstance(requireActivity().applicationContext) + .commonsApplicationComponent + .inject(this) + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } + + /** + * Set the username and user achievements level (if available) in navigationHeader. + */ + private fun setUserName() { + val store = BasicKvStore(requireContext(), getUserName()) + val level = store.getString("userAchievementsLevel", "0") + binding?.moreProfile?.text = if (level == "0") { + "${getUserName()} (${getString(R.string.see_your_achievements)})" + } else { + "${getUserName()} (${getString(R.string.level)} $level)" + } + } + + private fun getUserName(): String { + val accountManager = AccountManager.get(requireActivity()) + val allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE) + return if (allAccounts.isNotEmpty()) { + allAccounts[0].name + } else { + "" + } + } + + fun onLogoutClicked() { + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.logout_verification) + .setCancelable(false) + .setPositiveButton(R.string.yes) { _, _ -> + val app = requireContext().applicationContext as CommonsApplication + app.clearApplicationData(requireContext(), ActivityLogoutListener(requireActivity(), requireContext())) + } + .setNegativeButton(R.string.no) { dialog, _ -> dialog.cancel() } + .show() + } + + fun onFeedbackClicked() { + showFeedbackDialog() + } + + /** + * Creates and shows a dialog asking feedback from users + */ + private fun showFeedbackDialog() { + FeedbackDialog(requireContext()) { uploadFeedback(it) }.show() + } + + /** + * Uploads feedback data on the server + */ + @SuppressLint("CheckResult") + fun uploadFeedback(feedback: Feedback) { + val feedbackContentCreator = FeedbackContentCreator(requireContext(), feedback) + + val single = pageEditClient.createNewSection( + "Commons:Mobile_app/Feedback", + feedbackContentCreator.sectionTitle, + feedbackContentCreator.sectionText, + "New feedback on version ${feedback.version} of the app" + ) + .flatMapSingle { Single.just(it) } + .firstOrError() + + Single.defer { single } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { success -> + val messageResId = if (success) { + R.string.thanks_feedback + } else { + R.string.error_feedback + } + Toast.makeText(requireContext(), getString(messageResId), Toast.LENGTH_SHORT).show() + } + } + + /** + * This method shows the alert dialog when a user wants to send feedback about the app. + */ + private fun showAlertDialog() { + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.feedback_sharing_data_alert) + .setCancelable(false) + .setPositiveButton(R.string.ok) { _, _ -> sendFeedback() } + .show() + } + + /** + * This method collects the feedback message and starts the activity with implicit intent + * to the available email client. + */ + @SuppressLint("IntentReset") + private fun sendFeedback() { + val technicalInfo = commonsLogSender.getExtraInfo() + + val feedbackIntent = Intent(Intent.ACTION_SENDTO).apply { + type = "message/rfc822" + data = Uri.parse("mailto:") + putExtra(Intent.EXTRA_EMAIL, arrayOf(CommonsApplication.FEEDBACK_EMAIL)) + putExtra(Intent.EXTRA_SUBJECT, CommonsApplication.FEEDBACK_EMAIL_SUBJECT) + putExtra(Intent.EXTRA_TEXT, "\n\n${CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER}\n$technicalInfo") + } + + try { + startActivity(feedbackIntent) + } catch (e: ActivityNotFoundException) { + Toast.makeText(activity, R.string.no_email_client, Toast.LENGTH_SHORT).show() + } + } + + fun onAboutClicked() { + val intent = Intent(activity, AboutActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + requireActivity().startActivity(intent) + } + + fun onTutorialClicked() { + WelcomeActivity.startYourself(requireActivity()) + } + + fun onSettingsClicked() { + val intent = Intent(activity, SettingsActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + requireActivity().startActivity(intent) + } + + fun onProfileClicked() { + ProfileActivity.startYourself(requireActivity(), getUserName(), false) + } + + fun onPeerReviewClicked() { + ReviewActivity.startYourself(requireActivity(), getString(R.string.title_activity_review)) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java deleted file mode 100644 index 3537d7f7b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java +++ /dev/null @@ -1,142 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import fr.free.nrw.commons.AboutActivity; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetLoggedOutBinding; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.logging.CommonsLogSender; -import fr.free.nrw.commons.settings.SettingsActivity; -import javax.inject.Inject; -import javax.inject.Named; -import timber.log.Timber; - -public class MoreBottomSheetLoggedOutFragment extends BottomSheetDialogFragment { - - private FragmentMoreBottomSheetLoggedOutBinding binding; - @Inject - CommonsLogSender commonsLogSender; - @Inject - @Named("default_preferences") - JsonKvStore applicationKvStore; - - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { - binding = FragmentMoreBottomSheetLoggedOutBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - binding.moreLogin.setOnClickListener(v -> onLogoutClicked()); - binding.moreFeedback.setOnClickListener(v -> onFeedbackClicked()); - binding.moreAbout.setOnClickListener(v -> onAboutClicked()); - binding.moreSettings.setOnClickListener(v -> onSettingsClicked()); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - - @Override - public void onAttach(@NonNull final Context context) { - super.onAttach(context); - ApplicationlessInjection - .getInstance(requireActivity().getApplicationContext()) - .getCommonsApplicationComponent() - .inject(this); - } - - public void onLogoutClicked() { - applicationKvStore.putBoolean("login_skipped", false); - final Intent intent = new Intent(getContext(), LoginActivity.class); - requireActivity().finish(); //Kill the activity from which you will go to next activity - startActivity(intent); - } - - public void onFeedbackClicked() { - showAlertDialog(); - } - - /** - * This method shows the alert dialog when a user wants to send feedback about the app. - */ - private void showAlertDialog() { - new AlertDialog.Builder(requireActivity()) - .setMessage(R.string.feedback_sharing_data_alert) - .setCancelable(false) - .setPositiveButton(R.string.ok, (dialog, which) -> { - sendFeedback(); - }) - .show(); - } - - /** - * This method collects the feedback message and starts and activity with implicit intent to - * available email client. - */ - private void sendFeedback() { - final String technicalInfo = commonsLogSender.getExtraInfo(); - - final Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO); - feedbackIntent.setType("message/rfc822"); - feedbackIntent.setData(Uri.parse("mailto:")); - feedbackIntent.putExtra(Intent.EXTRA_EMAIL, - new String[]{CommonsApplication.FEEDBACK_EMAIL}); - feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, - CommonsApplication.FEEDBACK_EMAIL_SUBJECT); - feedbackIntent.putExtra(Intent.EXTRA_TEXT, String.format( - "\n\n%s\n%s", CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER, technicalInfo)); - try { - startActivity(feedbackIntent); - } catch (final ActivityNotFoundException e) { - Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show(); - } - } - - public void onAboutClicked() { - final Intent intent = new Intent(getActivity(), AboutActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - requireActivity().startActivity(intent); - } - - public void onSettingsClicked() { - final Intent intent = new Intent(getActivity(), SettingsActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - requireActivity().startActivity(intent); - } - - private class BaseLogoutListener implements CommonsApplication.LogoutListener { - - @Override - public void onLogoutComplete() { - Timber.d("Logout complete callback received."); - final Intent nearbyIntent = new Intent( - getContext(), LoginActivity.class); - nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(nearbyIntent); - requireActivity().finish(); - } - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.kt b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.kt new file mode 100644 index 000000000..96baf9e5e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.kt @@ -0,0 +1,151 @@ +package fr.free.nrw.commons.navtab + +import android.annotation.SuppressLint +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import fr.free.nrw.commons.AboutActivity +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.LoginActivity +import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetLoggedOutBinding +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.logging.CommonsLogSender +import fr.free.nrw.commons.settings.SettingsActivity +import javax.inject.Inject +import javax.inject.Named +import timber.log.Timber + + +class MoreBottomSheetLoggedOutFragment : BottomSheetDialogFragment() { + + private var binding: FragmentMoreBottomSheetLoggedOutBinding? = null + + @Inject + lateinit var commonsLogSender: CommonsLogSender + + @Inject + @field: Named("default_preferences") + lateinit var applicationKvStore: JsonKvStore + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentMoreBottomSheetLoggedOutBinding.inflate( + inflater, + container, + false + ) + return binding?.root + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle? + ) { + binding?.apply { + moreLogin.setOnClickListener { onLogoutClicked() } + moreFeedback.setOnClickListener { onFeedbackClicked() } + moreAbout.setOnClickListener { onAboutClicked() } + moreSettings.setOnClickListener { onSettingsClicked() } + } + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } + + override fun onAttach(context: Context) { + super.onAttach(context) + ApplicationlessInjection + .getInstance(requireActivity().applicationContext) + .commonsApplicationComponent + .inject(this) + } + + fun onLogoutClicked() { + applicationKvStore.putBoolean("login_skipped", false) + val intent = Intent(context, LoginActivity::class.java) + requireActivity().finish() // Kill the activity from which you will go to next activity + startActivity(intent) + } + + fun onFeedbackClicked() { + showAlertDialog() + } + + /** + * This method shows the alert dialog when a user wants to send feedback about the app. + */ + private fun showAlertDialog() { + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.feedback_sharing_data_alert) + .setCancelable(false) + .setPositiveButton(R.string.ok) { _, _ -> sendFeedback() } + .show() + } + + /** + * This method collects the feedback message and starts an activity with an implicit intent to + * the available email client. + */ + @SuppressLint("IntentReset") + private fun sendFeedback() { + val technicalInfo = commonsLogSender.getExtraInfo() + + val feedbackIntent = Intent(Intent.ACTION_SENDTO).apply { + type = "message/rfc822" + data = Uri.parse("mailto:") + putExtra(Intent.EXTRA_EMAIL, arrayOf(CommonsApplication.FEEDBACK_EMAIL)) + putExtra(Intent.EXTRA_SUBJECT, CommonsApplication.FEEDBACK_EMAIL_SUBJECT) + putExtra( + Intent.EXTRA_TEXT, + "\n\n${CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER}\n$technicalInfo" + ) + } + + try { + startActivity(feedbackIntent) + } catch (e: ActivityNotFoundException) { + Toast.makeText(activity, R.string.no_email_client, Toast.LENGTH_SHORT).show() + } + } + + fun onAboutClicked() { + val intent = Intent(activity, AboutActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + requireActivity().startActivity(intent) + } + + fun onSettingsClicked() { + val intent = Intent(activity, SettingsActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + requireActivity().startActivity(intent) + } + + private inner class BaseLogoutListener : CommonsApplication.LogoutListener { + + override fun onLogoutComplete() { + Timber.d("Logout complete callback received.") + val nearbyIntent = Intent(context, LoginActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + } + startActivity(nearbyIntent) + requireActivity().finish() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java deleted file mode 100644 index 0a3123c1c..000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java +++ /dev/null @@ -1,95 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.fragment.app.Fragment; - -import fr.free.nrw.commons.bookmarks.BookmarkFragment; -import fr.free.nrw.commons.contributions.ContributionsFragment; -import fr.free.nrw.commons.explore.ExploreFragment; -import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment; -import fr.free.nrw.commons.wikidata.model.EnumCode; -import fr.free.nrw.commons.wikidata.model.EnumCodeMap; - -import fr.free.nrw.commons.R; - - -public enum NavTab implements EnumCode { - CONTRIBUTIONS(R.string.contributions_fragment, R.drawable.ic_baseline_person_24) { - @NonNull - @Override - public Fragment newInstance() { - return ContributionsFragment.newInstance(); - } - }, - NEARBY(R.string.nearby_fragment, R.drawable.ic_location_on_black_24dp) { - @NonNull - @Override - public Fragment newInstance() { - return NearbyParentFragment.newInstance(); - } - }, - EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) { - @NonNull - @Override - public Fragment newInstance() { - return ExploreFragment.newInstance(); - } - }, - BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) { - @NonNull - @Override - public Fragment newInstance() { - return BookmarkFragment.newInstance(); - } - }, - MORE(R.string.more, R.drawable.ic_menu_black_24dp) { - @NonNull - @Override - public Fragment newInstance() { - return null; - } - }; - - private static final EnumCodeMap MAP = new EnumCodeMap<>(NavTab.class); - - @StringRes - private final int text; - @DrawableRes - private final int icon; - - @NonNull - public static NavTab of(int code) { - return MAP.get(code); - } - - public static int size() { - return MAP.size(); - } - - @StringRes - public int text() { - return text; - } - - @DrawableRes - public int icon() { - return icon; - } - - @NonNull - public abstract Fragment newInstance(); - - @Override - public int code() { - // This enumeration is not marshalled so tying declaration order to presentation order is - // convenient and consistent. - return ordinal(); - } - - NavTab(@StringRes int text, @DrawableRes int icon) { - this.text = text; - this.icon = icon; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.kt b/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.kt new file mode 100644 index 000000000..4573fccad --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.kt @@ -0,0 +1,79 @@ +package fr.free.nrw.commons.navtab + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment + +import fr.free.nrw.commons.bookmarks.BookmarkFragment +import fr.free.nrw.commons.contributions.ContributionsFragment +import fr.free.nrw.commons.explore.ExploreFragment +import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment +import fr.free.nrw.commons.wikidata.model.EnumCode +import fr.free.nrw.commons.wikidata.model.EnumCodeMap + +import fr.free.nrw.commons.R + + +enum class NavTab( + @StringRes private val text: Int, + @DrawableRes private val icon: Int +) : EnumCode { + + CONTRIBUTIONS(R.string.contributions_fragment, R.drawable.ic_baseline_person_24) { + override fun newInstance(): Fragment { + return ContributionsFragment.newInstance() + } + }, + NEARBY(R.string.nearby_fragment, R.drawable.ic_location_on_black_24dp) { + override fun newInstance(): Fragment { + return NearbyParentFragment.newInstance() + } + }, + EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) { + override fun newInstance(): Fragment { + return ExploreFragment.newInstance() + } + }, + BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) { + override fun newInstance(): Fragment { + return BookmarkFragment.newInstance() + } + }, + MORE(R.string.more, R.drawable.ic_menu_black_24dp) { + override fun newInstance(): Fragment? { + return null + } + }; + + companion object { + private val MAP: EnumCodeMap = EnumCodeMap(NavTab::class.java) + + @JvmStatic + fun of(code: Int): NavTab { + return MAP[code] + } + + @JvmStatic + fun size(): Int { + return MAP.size() + } + } + + @StringRes + fun text(): Int { + return text + } + + @DrawableRes + fun icon(): Int { + return icon + } + + abstract fun newInstance(): Fragment? + + override fun code(): Int { + // This enumeration is not marshalled so tying declaration order to presentation order is + // convenient and consistent. + return ordinal + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java deleted file mode 100644 index 5384f2e01..000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java +++ /dev/null @@ -1,38 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import android.view.ViewGroup; - -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentPagerAdapter; - -public class NavTabFragmentPagerAdapter extends FragmentPagerAdapter { - - private Fragment currentFragment; - - public NavTabFragmentPagerAdapter(FragmentManager mgr) { - super(mgr); - } - - @Nullable - public Fragment getCurrentFragment() { - return currentFragment; - } - - @Override - public Fragment getItem(int pos) { - return NavTab.of(pos).newInstance(); - } - - @Override - public int getCount() { - return NavTab.size(); - } - - @Override - public void setPrimaryItem(ViewGroup container, int position, Object object) { - currentFragment = ((Fragment) object); - super.setPrimaryItem(container, position, object); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.kt b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.kt new file mode 100644 index 000000000..369c39ed6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.kt @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.navtab + +import android.view.ViewGroup + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentPagerAdapter + + +class NavTabFragmentPagerAdapter( + mgr: FragmentManager +) : FragmentPagerAdapter(mgr, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + private var currentFragment: Fragment? = null + + fun getCurrentFragment(): Fragment? { + return currentFragment + } + + override fun getItem(pos: Int): Fragment { + return NavTab.of(pos).newInstance()!! + } + + override fun getCount(): Int { + return NavTab.size() + } + + override fun setPrimaryItem( + container: ViewGroup, + position: Int, + `object`: Any + ) { + currentFragment = `object` as Fragment + super.setPrimaryItem(container, position, `object`) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java deleted file mode 100644 index 399cbc789..000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java +++ /dev/null @@ -1,41 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.Menu; - -import com.google.android.material.bottomnavigation.BottomNavigationView; -import fr.free.nrw.commons.contributions.MainActivity; - - -public class NavTabLayout extends BottomNavigationView { - - public NavTabLayout(Context context) { - super(context); - setTabViews(); - } - - public NavTabLayout(Context context, AttributeSet attrs) { - super(context, attrs); - setTabViews(); - } - - public NavTabLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - setTabViews(); - } - - private void setTabViews() { - if (((MainActivity) getContext()).applicationKvStore.getBoolean("login_skipped") == true) { - for (int i = 0; i < NavTabLoggedOut.size(); i++) { - NavTabLoggedOut navTab = NavTabLoggedOut.of(i); - getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()); - } - } else { - for (int i = 0; i < NavTab.size(); i++) { - NavTab navTab = NavTab.of(i); - getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()); - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.kt b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.kt new file mode 100644 index 000000000..8d5298cac --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.kt @@ -0,0 +1,47 @@ +package fr.free.nrw.commons.navtab + +import android.content.Context +import android.util.AttributeSet +import android.view.Menu + +import com.google.android.material.bottomnavigation.BottomNavigationView +import fr.free.nrw.commons.contributions.MainActivity + + +class NavTabLayout : BottomNavigationView { + + constructor(context: Context) : super(context) { + setTabViews() + } + + constructor( + context: Context, + attrs: AttributeSet? + ) : super(context, attrs) { + setTabViews() + } + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int + ) : super(context, attrs, defStyleAttr) { + setTabViews() + } + + private fun setTabViews() { + val isLoginSkipped = (context as MainActivity) + .applicationKvStore.getBoolean("login_skipped") + if (isLoginSkipped) { + for (i in 0 until NavTabLoggedOut.size()) { + val navTab = NavTabLoggedOut.of(i) + menu.add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()) + } + } else { + for (i in 0 until NavTab.size()) { + val navTab = NavTab.of(i) + menu.add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java deleted file mode 100644 index dc1c7ce6b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java +++ /dev/null @@ -1,79 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.fragment.app.Fragment; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.bookmarks.BookmarkFragment; -import fr.free.nrw.commons.explore.ExploreFragment; -import fr.free.nrw.commons.wikidata.model.EnumCode; -import fr.free.nrw.commons.wikidata.model.EnumCodeMap; - - -public enum NavTabLoggedOut implements EnumCode { - - EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) { - @NonNull - @Override - public Fragment newInstance() { - return ExploreFragment.newInstance(); - } - }, - BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) { - @NonNull - @Override - public Fragment newInstance() { - return BookmarkFragment.newInstance(); - } - }, - MORE(R.string.more, R.drawable.ic_menu_black_24dp) { - @NonNull - @Override - public Fragment newInstance() { - return null; - } - }; - - private static final EnumCodeMap MAP = new EnumCodeMap<>( - NavTabLoggedOut.class); - - @StringRes - private final int text; - @DrawableRes - private final int icon; - - @NonNull - public static NavTabLoggedOut of(int code) { - return MAP.get(code); - } - - public static int size() { - return MAP.size(); - } - - @StringRes - public int text() { - return text; - } - - @DrawableRes - public int icon() { - return icon; - } - - @NonNull - public abstract Fragment newInstance(); - - @Override - public int code() { - // This enumeration is not marshalled so tying declaration order to presentation order is - // convenient and consistent. - return ordinal(); - } - - NavTabLoggedOut(@StringRes int text, @DrawableRes int icon) { - this.text = text; - this.icon = icon; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.kt b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.kt new file mode 100644 index 000000000..ad73f1bbd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.kt @@ -0,0 +1,65 @@ +package fr.free.nrw.commons.navtab + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import fr.free.nrw.commons.R +import fr.free.nrw.commons.bookmarks.BookmarkFragment +import fr.free.nrw.commons.explore.ExploreFragment +import fr.free.nrw.commons.wikidata.model.EnumCode +import fr.free.nrw.commons.wikidata.model.EnumCodeMap + + +enum class NavTabLoggedOut( + @StringRes private val text: Int, + @DrawableRes private val icon: Int +) : EnumCode { + + EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) { + override fun newInstance(): Fragment { + return ExploreFragment.newInstance() + } + }, + BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) { + override fun newInstance(): Fragment { + return BookmarkFragment.newInstance() + } + }, + MORE(R.string.more, R.drawable.ic_menu_black_24dp) { + override fun newInstance(): Fragment? { + return null + } + }; + + companion object { + private val MAP: EnumCodeMap = EnumCodeMap(NavTabLoggedOut::class.java) + + @JvmStatic + fun of(code: Int): NavTabLoggedOut { + return MAP[code] + } + + @JvmStatic + fun size(): Int { + return MAP.size() + } + } + + @StringRes + fun text(): Int { + return text + } + + @DrawableRes + fun icon(): Int { + return icon + } + + abstract fun newInstance(): Fragment? + + override fun code(): Int { + // This enumeration is not marshalled so tying declaration order to presentation order is + // convenient and consistent. + return ordinal + } +} \ No newline at end of file From 0c969c365bd7ed45e1ca08e9acb8af5b54382ae4 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Thu, 28 Nov 2024 02:09:25 -0600 Subject: [PATCH 3/4] Convert auth package to kotlin (#5966) * Convert SessionManager to kotlin along with other small fixes * Convert WikiAccountAuthenticator to kotlin * Migrate WikiAccountAuthenticatorService to kotlin * Converted AccountUtil to kotlin * Convert SignupActivity to kotlin * Convert LoginActivity to kotlin * Merge from main --- .../ui/PasteSensitiveTextInputEditTextTest.kt | 2 +- .../fr/free/nrw/commons/auth/AccountUtil.java | 44 -- .../fr/free/nrw/commons/auth/AccountUtil.kt | 24 + .../free/nrw/commons/auth/LoginActivity.java | 456 ------------------ .../fr/free/nrw/commons/auth/LoginActivity.kt | 404 ++++++++++++++++ .../free/nrw/commons/auth/SessionManager.java | 148 ------ .../free/nrw/commons/auth/SessionManager.kt | 95 ++++ .../free/nrw/commons/auth/SignupActivity.java | 82 ---- .../free/nrw/commons/auth/SignupActivity.kt | 75 +++ .../auth/WikiAccountAuthenticator.java | 141 ------ .../commons/auth/WikiAccountAuthenticator.kt | 108 +++++ .../auth/WikiAccountAuthenticatorService.java | 31 -- .../auth/WikiAccountAuthenticatorService.kt | 22 + .../commons/di/CommonsApplicationModule.java | 6 - .../feedback/FeedbackContentCreator.java | 4 +- .../commons/media/MediaDetailFragment.java | 12 +- .../notification/NotificationActivity.kt | 2 +- .../free/nrw/commons/review/ReviewActivity.kt | 4 +- .../commons/upload/FailedUploadsFragment.kt | 2 +- .../nrw/commons/utils/AbstractTextWatcher.kt | 2 +- .../nrw/commons/TestCommonsApplication.kt | 4 - .../nrw/commons/auth/AccountUtilUnitTest.kt | 16 +- .../commons/auth/LoginActivityUnitTests.kt | 11 - ...WikiAccountAuthenticatorServiceUnitTest.kt | 16 +- .../auth/WikiAccountAuthenticatorUnitTest.kt | 5 +- 25 files changed, 752 insertions(+), 964 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt diff --git a/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt index aedbcb133..647c5bbda 100644 --- a/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt +++ b/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt @@ -17,7 +17,7 @@ class PasteSensitiveTextInputEditTextTest { @Before fun setup() { context = ApplicationProvider.getApplicationContext() - textView = PasteSensitiveTextInputEditText(context) + textView = PasteSensitiveTextInputEditText(context!!) } // this test has no real value, just % for test code coverage diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java deleted file mode 100644 index 53903769d..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java +++ /dev/null @@ -1,44 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Context; - -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.BuildConfig; -import timber.log.Timber; - -public class AccountUtil { - - public static final String AUTH_TOKEN_TYPE = "CommonsAndroid"; - - public AccountUtil() { - } - - /** - * @return Account|null - */ - @Nullable - public static Account account(Context context) { - try { - Account[] accounts = accountManager(context).getAccountsByType(BuildConfig.ACCOUNT_TYPE); - if (accounts.length > 0) { - return accounts[0]; - } - } catch (SecurityException e) { - Timber.e(e); - } - return null; - } - - @Nullable - public static String getUserName(Context context) { - Account account = account(context); - return account == null ? null : account.name; - } - - private static AccountManager accountManager(Context context) { - return AccountManager.get(context); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt new file mode 100644 index 000000000..aa86cd0d8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt @@ -0,0 +1,24 @@ +package fr.free.nrw.commons.auth + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import androidx.annotation.VisibleForTesting +import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE +import timber.log.Timber + +const val AUTH_TOKEN_TYPE: String = "CommonsAndroid" + +fun getUserName(context: Context): String? { + return account(context)?.name +} + +@VisibleForTesting +fun account(context: Context): Account? = try { + val accountManager = AccountManager.get(context) + val accounts = accountManager.getAccountsByType(ACCOUNT_TYPE) + if (accounts.isNotEmpty()) accounts[0] else null +} catch (e: SecurityException) { + Timber.e(e) + null +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java deleted file mode 100644 index 3ff61e511..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ /dev/null @@ -1,456 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.AccountAuthenticatorActivity; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; - -import android.widget.TextView; -import androidx.annotation.ColorRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.core.app.NavUtils; -import androidx.core.content.ContextCompat; -import fr.free.nrw.commons.auth.login.LoginClient; -import fr.free.nrw.commons.auth.login.LoginResult; -import fr.free.nrw.commons.databinding.ActivityLoginBinding; -import fr.free.nrw.commons.utils.ActivityUtils; -import java.util.Locale; -import fr.free.nrw.commons.auth.login.LoginCallback; - -import java.util.Objects; -import javax.inject.Inject; -import javax.inject.Named; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.utils.SystemThemeUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.disposables.CompositeDisposable; -import timber.log.Timber; - -import static android.view.KeyEvent.KEYCODE_ENTER; -import static android.view.View.VISIBLE; -import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; -import static fr.free.nrw.commons.CommonsApplication.LOGIN_MESSAGE_INTENT_KEY; -import static fr.free.nrw.commons.CommonsApplication.LOGIN_USERNAME_INTENT_KEY; - -public class LoginActivity extends AccountAuthenticatorActivity { - - @Inject - SessionManager sessionManager; - - @Inject - @Named("default_preferences") - JsonKvStore applicationKvStore; - - @Inject - LoginClient loginClient; - - @Inject - SystemThemeUtils systemThemeUtils; - - private ActivityLoginBinding binding; - ProgressDialog progressDialog; - private AppCompatDelegate delegate; - private LoginTextWatcher textWatcher = new LoginTextWatcher(); - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - final String saveProgressDailog="ProgressDailog_state"; - final String saveErrorMessage ="errorMessage"; - final String saveUsername="username"; - final String savePassword="password"; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ApplicationlessInjection - .getInstance(this.getApplicationContext()) - .getCommonsApplicationComponent() - .inject(this); - - boolean isDarkTheme = systemThemeUtils.isDeviceInNightMode(); - setTheme(isDarkTheme ? R.style.DarkAppTheme : R.style.LightAppTheme); - getDelegate().installViewFactory(); - getDelegate().onCreate(savedInstanceState); - - binding = ActivityLoginBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - String message = getIntent().getStringExtra(LOGIN_MESSAGE_INTENT_KEY); - String username = getIntent().getStringExtra(LOGIN_USERNAME_INTENT_KEY); - - binding.loginUsername.addTextChangedListener(textWatcher); - binding.loginPassword.addTextChangedListener(textWatcher); - binding.loginTwoFactor.addTextChangedListener(textWatcher); - - binding.skipLogin.setOnClickListener(view -> skipLogin()); - binding.forgotPassword.setOnClickListener(view -> forgotPassword()); - binding.aboutPrivacyPolicy.setOnClickListener(view -> onPrivacyPolicyClicked()); - binding.signUpButton.setOnClickListener(view -> signUp()); - binding.loginButton.setOnClickListener(view -> performLogin()); - - binding.loginPassword.setOnEditorActionListener(this::onEditorAction); - binding.loginPassword.setOnFocusChangeListener(this::onPasswordFocusChanged); - - if (ConfigUtils.isBetaFlavour()) { - binding.loginCredentials.setText(getString(R.string.login_credential)); - } else { - binding.loginCredentials.setVisibility(View.GONE); - } - if (message != null) { - showMessage(message, R.color.secondaryDarkColor); - } - if (username != null) { - binding.loginUsername.setText(username); - } - } - /** - * Hides the keyboard if the user's focus is not on the password (hasFocus is false). - * @param view The keyboard - * @param hasFocus Set to true if the keyboard has focus - */ - void onPasswordFocusChanged(View view, boolean hasFocus) { - if (!hasFocus) { - ViewUtil.hideKeyboard(view); - } - } - - boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { - if (binding.loginButton.isEnabled()) { - if (actionId == IME_ACTION_DONE) { - performLogin(); - return true; - } else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) { - performLogin(); - return true; - } - } - return false; - } - - - protected void skipLogin() { - new AlertDialog.Builder(this).setTitle(R.string.skip_login_title) - .setMessage(R.string.skip_login_message) - .setCancelable(false) - .setPositiveButton(R.string.yes, (dialog, which) -> { - dialog.cancel(); - performSkipLogin(); - }) - .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) - .show(); - } - - protected void forgotPassword() { - Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)); - } - - protected void onPrivacyPolicyClicked() { - Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)); - } - - protected void signUp() { - Intent intent = new Intent(this, SignupActivity.class); - startActivity(intent); - } - - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - getDelegate().onPostCreate(savedInstanceState); - } - - @Override - protected void onResume() { - super.onResume(); - - if (sessionManager.getCurrentAccount() != null - && sessionManager.isUserLoggedIn()) { - applicationKvStore.putBoolean("login_skipped", false); - startMainActivity(); - } - - if (applicationKvStore.getBoolean("login_skipped", false)) { - performSkipLogin(); - } - - } - - @Override - protected void onDestroy() { - compositeDisposable.clear(); - try { - // To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - } catch (Exception e) { - e.printStackTrace(); - } - binding.loginUsername.removeTextChangedListener(textWatcher); - binding.loginPassword.removeTextChangedListener(textWatcher); - binding.loginTwoFactor.removeTextChangedListener(textWatcher); - delegate.onDestroy(); - if(null!=loginClient) { - loginClient.cancel(); - } - binding = null; - super.onDestroy(); - } - - public void performLogin() { - Timber.d("Login to start!"); - final String username = Objects.requireNonNull(binding.loginUsername.getText()).toString(); - final String password = Objects.requireNonNull(binding.loginPassword.getText()).toString(); - final String twoFactorCode = Objects.requireNonNull(binding.loginTwoFactor.getText()).toString(); - - showLoggingProgressBar(); - loginClient.doLogin(username, password, twoFactorCode, Locale.getDefault().getLanguage(), - new LoginCallback() { - @Override - public void success(@NonNull LoginResult loginResult) { - runOnUiThread(()->{ - Timber.d("Login Success"); - hideProgress(); - onLoginSuccess(loginResult); - }); - } - - @Override - public void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token) { - runOnUiThread(()->{ - Timber.d("Requesting 2FA prompt"); - hideProgress(); - askUserForTwoFactorAuth(); - }); - } - - @Override - public void passwordResetPrompt(@Nullable String token) { - runOnUiThread(()->{ - Timber.d("Showing password reset prompt"); - hideProgress(); - showPasswordResetPrompt(); - }); - } - - @Override - public void error(@NonNull Throwable caught) { - runOnUiThread(()->{ - Timber.e(caught); - hideProgress(); - showMessageAndCancelDialog(caught.getLocalizedMessage()); - }); - } - }); - } - - - - private void hideProgress() { - progressDialog.dismiss(); - } - - private void showPasswordResetPrompt() { - showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword)); - } - - - /** - * This function is called when user skips the login. - * It redirects the user to Explore Activity. - */ - private void performSkipLogin() { - applicationKvStore.putBoolean("login_skipped", true); - MainActivity.startYourself(this); - finish(); - } - - private void showLoggingProgressBar() { - progressDialog = new ProgressDialog(this); - progressDialog.setIndeterminate(true); - progressDialog.setTitle(getString(R.string.logging_in_title)); - progressDialog.setMessage(getString(R.string.logging_in_message)); - progressDialog.setCanceledOnTouchOutside(false); - progressDialog.show(); - } - - private void onLoginSuccess(LoginResult loginResult) { - compositeDisposable.clear(); - sessionManager.setUserLoggedIn(true); - sessionManager.updateAccount(loginResult); - progressDialog.dismiss(); - showSuccessAndDismissDialog(); - startMainActivity(); - } - - @Override - protected void onStart() { - super.onStart(); - delegate.onStart(); - } - - @Override - protected void onStop() { - super.onStop(); - delegate.onStop(); - } - - @Override - protected void onPostResume() { - super.onPostResume(); - getDelegate().onPostResume(); - } - - @Override - public void setContentView(View view, ViewGroup.LayoutParams params) { - getDelegate().setContentView(view, params); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - @NonNull - public MenuInflater getMenuInflater() { - return getDelegate().getMenuInflater(); - } - - public void askUserForTwoFactorAuth() { - progressDialog.dismiss(); - binding.twoFactorContainer.setVisibility(VISIBLE); - binding.loginTwoFactor.setVisibility(VISIBLE); - binding.loginTwoFactor.requestFocus(); - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY); - showMessageAndCancelDialog(R.string.login_failed_2fa_needed); - } - - public void showMessageAndCancelDialog(@StringRes int resId) { - showMessage(resId, R.color.secondaryDarkColor); - if (progressDialog != null) { - progressDialog.cancel(); - } - } - - public void showMessageAndCancelDialog(String error) { - showMessage(error, R.color.secondaryDarkColor); - if (progressDialog != null) { - progressDialog.cancel(); - } - } - - public void showSuccessAndDismissDialog() { - showMessage(R.string.login_success, R.color.primaryDarkColor); - progressDialog.dismiss(); - } - - public void startMainActivity() { - ActivityUtils.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP); - finish(); - } - - private void showMessage(@StringRes int resId, @ColorRes int colorResId) { - binding.errorMessage.setText(getString(resId)); - binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); - binding.errorMessageContainer.setVisibility(VISIBLE); - } - - private void showMessage(String message, @ColorRes int colorResId) { - binding.errorMessage.setText(message); - binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); - binding.errorMessageContainer.setVisibility(VISIBLE); - } - - private AppCompatDelegate getDelegate() { - if (delegate == null) { - delegate = AppCompatDelegate.create(this, null); - } - return delegate; - } - - private class LoginTextWatcher implements TextWatcher { - @Override - public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int start, int count, int after) { - } - - @Override - public void afterTextChanged(Editable editable) { - boolean enabled = binding.loginUsername.getText().length() != 0 && - binding.loginPassword.getText().length() != 0 && - (BuildConfig.DEBUG || binding.loginTwoFactor.getText().length() != 0 || - binding.loginTwoFactor.getVisibility() != VISIBLE); - binding.loginButton.setEnabled(enabled); - } - } - - public static void startYourself(Context context) { - Intent intent = new Intent(context, LoginActivity.class); - context.startActivity(intent); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - // if progressDialog is visible during the configuration change then store state as true else false so that - // we maintain visibility of progressDailog after configuration change - if(progressDialog!=null&&progressDialog.isShowing()) { - outState.putBoolean(saveProgressDailog,true); - } else { - outState.putBoolean(saveProgressDailog,false); - } - outState.putString(saveErrorMessage,binding.errorMessage.getText().toString()); //Save the errorMessage - outState.putString(saveUsername,getUsername()); // Save the username - outState.putString(savePassword,getPassword()); // Save the password - } - private String getUsername() { - return binding.loginUsername.getText().toString(); - } - private String getPassword(){ - return binding.loginPassword.getText().toString(); - } - - @Override - protected void onRestoreInstanceState(final Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - binding.loginUsername.setText(savedInstanceState.getString(saveUsername)); - binding.loginPassword.setText(savedInstanceState.getString(savePassword)); - if(savedInstanceState.getBoolean(saveProgressDailog)) { - performLogin(); - } - String errorMessage=savedInstanceState.getString(saveErrorMessage); - if(sessionManager.isUserLoggedIn()) { - showMessage(R.string.login_success, R.color.primaryDarkColor); - } else { - showMessage(errorMessage, R.color.secondaryDarkColor); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt new file mode 100644 index 000000000..3aa9b26d9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt @@ -0,0 +1,404 @@ +package fr.free.nrw.commons.auth + +import android.accounts.AccountAuthenticatorActivity +import android.app.ProgressDialog +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.KeyEvent +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.app.NavUtils +import androidx.core.content.ContextCompat +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.auth.login.LoginCallback +import fr.free.nrw.commons.auth.login.LoginClient +import fr.free.nrw.commons.auth.login.LoginResult +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.ActivityLoginBinding +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.utils.AbstractTextWatcher +import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.utils.SystemThemeUtils +import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard +import io.reactivex.disposables.CompositeDisposable +import timber.log.Timber +import java.util.Locale +import javax.inject.Inject +import javax.inject.Named + +class LoginActivity : AccountAuthenticatorActivity() { + @Inject + lateinit var sessionManager: SessionManager + + @Inject + @field:Named("default_preferences") + lateinit var applicationKvStore: JsonKvStore + + @Inject + lateinit var loginClient: LoginClient + + @Inject + lateinit var systemThemeUtils: SystemThemeUtils + + private var binding: ActivityLoginBinding? = null + private var progressDialog: ProgressDialog? = null + private val textWatcher = AbstractTextWatcher(::onTextChanged) + private val compositeDisposable = CompositeDisposable() + private val delegate: AppCompatDelegate by lazy { + AppCompatDelegate.create(this, null) + } + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ApplicationlessInjection + .getInstance(this.applicationContext) + .commonsApplicationComponent + .inject(this) + + val isDarkTheme = systemThemeUtils.isDeviceInNightMode() + setTheme(if (isDarkTheme) R.style.DarkAppTheme else R.style.LightAppTheme) + delegate.installViewFactory() + delegate.onCreate(savedInstanceState) + + binding = ActivityLoginBinding.inflate(layoutInflater) + with(binding!!) { + setContentView(root) + + loginUsername.addTextChangedListener(textWatcher) + loginPassword.addTextChangedListener(textWatcher) + loginTwoFactor.addTextChangedListener(textWatcher) + + skipLogin.setOnClickListener { skipLogin() } + forgotPassword.setOnClickListener { forgotPassword() } + aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() } + signUpButton.setOnClickListener { signUp() } + loginButton.setOnClickListener { performLogin() } + loginPassword.setOnEditorActionListener(::onEditorAction) + + loginPassword.onFocusChangeListener = + View.OnFocusChangeListener(::onPasswordFocusChanged) + + if (isBetaFlavour) { + loginCredentials.text = getString(R.string.login_credential) + } else { + loginCredentials.visibility = View.GONE + } + + intent.getStringExtra(CommonsApplication.LOGIN_MESSAGE_INTENT_KEY)?.let { + showMessage(it, R.color.secondaryDarkColor) + } + + intent.getStringExtra(CommonsApplication.LOGIN_USERNAME_INTENT_KEY)?.let { + loginUsername.setText(it) + } + } + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + delegate.onPostCreate(savedInstanceState) + } + + override fun onResume() { + super.onResume() + + if (sessionManager.currentAccount != null && sessionManager.isUserLoggedIn) { + applicationKvStore.putBoolean("login_skipped", false) + startMainActivity() + } + + if (applicationKvStore.getBoolean("login_skipped", false)) { + performSkipLogin() + } + } + + override fun onDestroy() { + compositeDisposable.clear() + try { + // To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method + if (progressDialog?.isShowing == true) { + progressDialog!!.dismiss() + } + } catch (e: Exception) { + e.printStackTrace() + } + with(binding!!) { + loginUsername.removeTextChangedListener(textWatcher) + loginPassword.removeTextChangedListener(textWatcher) + loginTwoFactor.removeTextChangedListener(textWatcher) + } + delegate.onDestroy() + loginClient?.cancel() + binding = null + super.onDestroy() + } + + override fun onStart() { + super.onStart() + delegate.onStart() + } + + override fun onStop() { + super.onStop() + delegate.onStop() + } + + override fun onPostResume() { + super.onPostResume() + delegate.onPostResume() + } + + override fun setContentView(view: View, params: ViewGroup.LayoutParams) { + delegate.setContentView(view, params) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + NavUtils.navigateUpFromSameTask(this) + return true + } + } + return super.onOptionsItemSelected(item) + } + + override fun onSaveInstanceState(outState: Bundle) { + // if progressDialog is visible during the configuration change then store state as true else false so that + // we maintain visibility of progressDailog after configuration change + if (progressDialog != null && progressDialog!!.isShowing) { + outState.putBoolean(saveProgressDailog, true) + } else { + outState.putBoolean(saveProgressDailog, false) + } + outState.putString( + saveErrorMessage, + binding!!.errorMessage.text.toString() + ) //Save the errorMessage + outState.putString( + saveUsername, + binding!!.loginUsername.text.toString() + ) // Save the username + outState.putString( + savePassword, + binding!!.loginPassword.text.toString() + ) // Save the password + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + binding!!.loginUsername.setText(savedInstanceState.getString(saveUsername)) + binding!!.loginPassword.setText(savedInstanceState.getString(savePassword)) + if (savedInstanceState.getBoolean(saveProgressDailog)) { + performLogin() + } + val errorMessage = savedInstanceState.getString(saveErrorMessage) + if (sessionManager.isUserLoggedIn) { + showMessage(R.string.login_success, R.color.primaryDarkColor) + } else { + showMessage(errorMessage, R.color.secondaryDarkColor) + } + } + + /** + * Hides the keyboard if the user's focus is not on the password (hasFocus is false). + * @param view The keyboard + * @param hasFocus Set to true if the keyboard has focus + */ + private fun onPasswordFocusChanged(view: View, hasFocus: Boolean) { + if (!hasFocus) { + hideKeyboard(view) + } + } + + private fun onEditorAction(textView: TextView, actionId: Int, keyEvent: KeyEvent?) = + if (binding!!.loginButton.isEnabled && isTriggerAction(actionId, keyEvent)) { + performLogin() + true + } else false + + private fun isTriggerAction(actionId: Int, keyEvent: KeyEvent?) = + actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER + + private fun skipLogin() { + AlertDialog.Builder(this) + .setTitle(R.string.skip_login_title) + .setMessage(R.string.skip_login_message) + .setCancelable(false) + .setPositiveButton(R.string.yes) { dialog: DialogInterface, which: Int -> + dialog.cancel() + performSkipLogin() + } + .setNegativeButton(R.string.no) { dialog: DialogInterface, which: Int -> + dialog.cancel() + } + .show() + } + + private fun forgotPassword() = + Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)) + + private fun onPrivacyPolicyClicked() = + Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)) + + private fun signUp() = + startActivity(Intent(this, SignupActivity::class.java)) + + @VisibleForTesting + fun performLogin() { + Timber.d("Login to start!") + val username = binding!!.loginUsername.text.toString() + val password = binding!!.loginPassword.text.toString() + val twoFactorCode = binding!!.loginTwoFactor.text.toString() + + showLoggingProgressBar() + loginClient.doLogin(username, + password, + twoFactorCode, + Locale.getDefault().language, + object : LoginCallback { + override fun success(loginResult: LoginResult) = runOnUiThread { + Timber.d("Login Success") + progressDialog!!.dismiss() + onLoginSuccess(loginResult) + } + + override fun twoFactorPrompt(caught: Throwable, token: String?) = runOnUiThread { + Timber.d("Requesting 2FA prompt") + progressDialog!!.dismiss() + askUserForTwoFactorAuth() + } + + override fun passwordResetPrompt(token: String?) = runOnUiThread { + Timber.d("Showing password reset prompt") + progressDialog!!.dismiss() + showPasswordResetPrompt() + } + + override fun error(caught: Throwable) = runOnUiThread { + Timber.e(caught) + progressDialog!!.dismiss() + showMessageAndCancelDialog(caught.localizedMessage ?: "") + } + } + ) + } + + private fun showPasswordResetPrompt() = + showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword)) + + /** + * This function is called when user skips the login. + * It redirects the user to Explore Activity. + */ + private fun performSkipLogin() { + applicationKvStore.putBoolean("login_skipped", true) + MainActivity.startYourself(this) + finish() + } + + private fun showLoggingProgressBar() { + progressDialog = ProgressDialog(this).apply { + isIndeterminate = true + setTitle(getString(R.string.logging_in_title)) + setMessage(getString(R.string.logging_in_message)) + setCanceledOnTouchOutside(false) + } + progressDialog!!.show() + } + + private fun onLoginSuccess(loginResult: LoginResult) { + compositeDisposable.clear() + sessionManager.setUserLoggedIn(true) + sessionManager.updateAccount(loginResult) + progressDialog!!.dismiss() + showSuccessAndDismissDialog() + startMainActivity() + } + + override fun getMenuInflater(): MenuInflater = + delegate.menuInflater + + @VisibleForTesting + fun askUserForTwoFactorAuth() { + progressDialog!!.dismiss() + with(binding!!) { + twoFactorContainer.visibility = View.VISIBLE + loginTwoFactor.visibility = View.VISIBLE + loginTwoFactor.requestFocus() + } + val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) + showMessageAndCancelDialog(R.string.login_failed_2fa_needed) + } + + @VisibleForTesting + fun showMessageAndCancelDialog(@StringRes resId: Int) { + showMessage(resId, R.color.secondaryDarkColor) + progressDialog?.cancel() + } + + @VisibleForTesting + fun showMessageAndCancelDialog(error: String) { + showMessage(error, R.color.secondaryDarkColor) + progressDialog?.cancel() + } + + @VisibleForTesting + fun showSuccessAndDismissDialog() { + showMessage(R.string.login_success, R.color.primaryDarkColor) + progressDialog!!.dismiss() + } + + @VisibleForTesting + fun startMainActivity() { + startActivityWithFlags(this, MainActivity::class.java, Intent.FLAG_ACTIVITY_SINGLE_TOP) + finish() + } + + private fun showMessage(@StringRes resId: Int, @ColorRes colorResId: Int) = with(binding!!) { + errorMessage.text = getString(resId) + errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId)) + errorMessageContainer.visibility = View.VISIBLE + } + + private fun showMessage(message: String?, @ColorRes colorResId: Int) = with(binding!!) { + errorMessage.text = message + errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId)) + errorMessageContainer.visibility = View.VISIBLE + } + + private fun onTextChanged(text: String) { + val enabled = + binding!!.loginUsername.text!!.length != 0 && binding!!.loginPassword.text!!.length != 0 && + (BuildConfig.DEBUG || binding!!.loginTwoFactor.text!!.length != 0 || binding!!.loginTwoFactor.visibility != View.VISIBLE) + binding!!.loginButton.isEnabled = enabled + } + + companion object { + fun startYourself(context: Context) = + context.startActivity(Intent(context, LoginActivity::class.java)) + + const val saveProgressDailog: String = "ProgressDailog_state" + const val saveErrorMessage: String = "errorMessage" + const val saveUsername: String = "username" + const val savePassword: String = "password" + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java deleted file mode 100644 index 7c2f4a334..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java +++ /dev/null @@ -1,148 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Context; -import android.os.Build; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.auth.login.LoginResult; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import io.reactivex.Completable; -import io.reactivex.Observable; - -/** - * Manage the current logged in user session. - */ -@Singleton -public class SessionManager { - private final Context context; - private Account currentAccount; // Unlike a savings account... ;-) - private JsonKvStore defaultKvStore; - - @Inject - public SessionManager(Context context, - @Named("default_preferences") JsonKvStore defaultKvStore) { - this.context = context; - this.currentAccount = null; - this.defaultKvStore = defaultKvStore; - } - - private boolean createAccount(@NonNull String userName, @NonNull String password) { - Account account = getCurrentAccount(); - if (account == null || TextUtils.isEmpty(account.name) || !account.name.equals(userName)) { - removeAccount(); - account = new Account(userName, BuildConfig.ACCOUNT_TYPE); - return accountManager().addAccountExplicitly(account, password, null); - } - return true; - } - - private void removeAccount() { - Account account = getCurrentAccount(); - if (account != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - accountManager().removeAccountExplicitly(account); - } else { - //noinspection deprecation - accountManager().removeAccount(account, null, null); - } - } - } - - public void updateAccount(LoginResult result) { - boolean accountCreated = createAccount(result.getUserName(), result.getPassword()); - if (accountCreated) { - setPassword(result.getPassword()); - } - } - - private void setPassword(@NonNull String password) { - Account account = getCurrentAccount(); - if (account != null) { - accountManager().setPassword(account, password); - } - } - - /** - * @return Account|null - */ - @Nullable - public Account getCurrentAccount() { - if (currentAccount == null) { - AccountManager accountManager = AccountManager.get(context); - Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE); - if (allAccounts.length != 0) { - currentAccount = allAccounts[0]; - } - } - return currentAccount; - } - - public boolean doesAccountExist() { - return getCurrentAccount() != null; - } - - @Nullable - public String getUserName() { - Account account = getCurrentAccount(); - return account == null ? null : account.name; - } - - @Nullable - public String getPassword() { - Account account = getCurrentAccount(); - return account == null ? null : accountManager().getPassword(account); - } - - private AccountManager accountManager() { - return AccountManager.get(context); - } - - public boolean isUserLoggedIn() { - return defaultKvStore.getBoolean("isUserLoggedIn", false); - } - - void setUserLoggedIn(boolean isLoggedIn) { - defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn); - } - - public void forceLogin(Context context) { - if (context != null) { - LoginActivity.startYourself(context); - } - } - - /** - * Returns a Completable that clears existing accounts from account manager - */ - public Completable logout() { - return Completable.fromObservable( - Observable.empty() - .doOnComplete( - () -> { - removeAccount(); - currentAccount = null; - } - ) - ); - } - - /** - * Return a corresponding boolean preference - * - * @param key - * @return - */ - public boolean getPreference(String key) { - return defaultKvStore.getBoolean(key); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt new file mode 100644 index 000000000..eba4a55f4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt @@ -0,0 +1,95 @@ +package fr.free.nrw.commons.auth + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.os.Build +import android.text.TextUtils +import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE +import fr.free.nrw.commons.auth.login.LoginResult +import fr.free.nrw.commons.kvstore.JsonKvStore +import io.reactivex.Completable +import io.reactivex.Observable +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +/** + * Manage the current logged in user session. + */ +@Singleton +class SessionManager @Inject constructor( + private val context: Context, + @param:Named("default_preferences") private val defaultKvStore: JsonKvStore +) { + private val accountManager: AccountManager get() = AccountManager.get(context) + + private var _currentAccount: Account? = null // Unlike a savings account... ;-) + val currentAccount: Account? get() { + if (_currentAccount == null) { + val allAccounts = AccountManager.get(context).getAccountsByType(ACCOUNT_TYPE) + if (allAccounts.isNotEmpty()) { + _currentAccount = allAccounts[0] + } + } + return _currentAccount + } + + val userName: String? + get() = currentAccount?.name + + var password: String? + get() = currentAccount?.let { accountManager.getPassword(it) } + private set(value) { + currentAccount?.let { accountManager.setPassword(it, value) } + } + + val isUserLoggedIn: Boolean + get() = defaultKvStore.getBoolean("isUserLoggedIn", false) + + fun updateAccount(result: LoginResult) { + if (createAccount(result.userName!!, result.password!!)) { + password = result.password + } + } + + fun doesAccountExist(): Boolean = + currentAccount != null + + fun setUserLoggedIn(isLoggedIn: Boolean) = + defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn) + + fun forceLogin(context: Context?) = + context?.let { LoginActivity.startYourself(it) } + + fun getPreference(key: String?): Boolean = + defaultKvStore.getBoolean(key) + + fun logout(): Completable = Completable.fromObservable( + Observable.empty() + .doOnComplete { + removeAccount() + _currentAccount = null + } + ) + + private fun createAccount(userName: String, password: String): Boolean { + var account = currentAccount + if (account == null || TextUtils.isEmpty(account.name) || account.name != userName) { + removeAccount() + account = Account(userName, ACCOUNT_TYPE) + return accountManager.addAccountExplicitly(account, password, null) + } + return true + } + + private fun removeAccount() { + currentAccount?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + accountManager.removeAccountExplicitly(it) + } else { + accountManager.removeAccount(it, null, null) + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java deleted file mode 100644 index be90bb4bb..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java +++ /dev/null @@ -1,82 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.content.res.Configuration; -import android.os.Build; -import android.os.Bundle; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.widget.Toast; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.theme.BaseActivity; -import timber.log.Timber; - -public class SignupActivity extends BaseActivity { - - private WebView webView; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Timber.d("Signup Activity started"); - - webView = new WebView(this); - setContentView(webView); - - webView.setWebViewClient(new MyWebViewClient()); - WebSettings webSettings = webView.getSettings(); - /*Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can - trust Wikimedia's site... right?*/ - webSettings.setJavaScriptEnabled(true); - - webView.loadUrl(BuildConfig.SIGNUP_LANDING_URL); - } - - private class MyWebViewClient extends WebViewClient { - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url.equals(BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL)) { - //Signup success, so clear cookies, notify user, and load LoginActivity again - Timber.d("Overriding URL %s", url); - - Toast toast = Toast.makeText(SignupActivity.this, - R.string.account_created, Toast.LENGTH_LONG); - toast.show(); - // terminate on task completion. - finish(); - return true; - } else { - //If user clicks any other links in the webview - Timber.d("Not overriding URL, URL is: %s", url); - return false; - } - } - } - - @Override - public void onBackPressed() { - if (webView.canGoBack()) { - webView.goBack(); - } else { - super.onBackPressed(); - } - } - - /** - * Known bug in androidx.appcompat library version 1.1.0 being tracked here - * https://issuetracker.google.com/issues/141132133 - * App tries to put light/dark theme to webview and crashes in the process - * This code tries to prevent applying the theme when sdk is between api 21 to 25 - * @param overrideConfiguration - */ - @Override - public void applyOverrideConfiguration(final Configuration overrideConfiguration) { - if (Build.VERSION.SDK_INT <= 25 && - (getResources().getConfiguration().uiMode == getApplicationContext().getResources().getConfiguration().uiMode)) { - return; - } - super.applyOverrideConfiguration(overrideConfiguration); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt new file mode 100644 index 000000000..5b48ecd8f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt @@ -0,0 +1,75 @@ +package fr.free.nrw.commons.auth + +import android.annotation.SuppressLint +import android.content.res.Configuration +import android.os.Build +import android.os.Bundle +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Toast +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.R +import fr.free.nrw.commons.theme.BaseActivity +import timber.log.Timber + +class SignupActivity : BaseActivity() { + private var webView: WebView? = null + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Timber.d("Signup Activity started") + + webView = WebView(this) + with(webView!!) { + setContentView(this) + webViewClient = MyWebViewClient() + // Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can + // trust Wikimedia's site... right? + settings.javaScriptEnabled = true + loadUrl(BuildConfig.SIGNUP_LANDING_URL) + } + } + + override fun onBackPressed() { + if (webView!!.canGoBack()) { + webView!!.goBack() + } else { + super.onBackPressed() + } + } + + /** + * Known bug in androidx.appcompat library version 1.1.0 being tracked here + * https://issuetracker.google.com/issues/141132133 + * App tries to put light/dark theme to webview and crashes in the process + * This code tries to prevent applying the theme when sdk is between api 21 to 25 + */ + override fun applyOverrideConfiguration(overrideConfiguration: Configuration) { + if (Build.VERSION.SDK_INT <= 25 && + (resources.configuration.uiMode == applicationContext.resources.configuration.uiMode) + ) return + super.applyOverrideConfiguration(overrideConfiguration) + } + + private inner class MyWebViewClient : WebViewClient() { + @Deprecated("Deprecated in Java") + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean = + if (url == BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL) { + //Signup success, so clear cookies, notify user, and load LoginActivity again + Timber.d("Overriding URL %s", url) + + Toast.makeText( + this@SignupActivity, R.string.account_created, Toast.LENGTH_LONG + ).show() + + // terminate on task completion. + finish() + true + } else { + //If user clicks any other links in the webview + Timber.d("Not overriding URL, URL is: %s", url) + false + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java deleted file mode 100644 index 643725604..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java +++ /dev/null @@ -1,141 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.AbstractAccountAuthenticator; -import android.accounts.Account; -import android.accounts.AccountAuthenticatorResponse; -import android.accounts.AccountManager; -import android.accounts.NetworkErrorException; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.BuildConfig; - -import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; - -/** - * Handles WikiMedia commons account Authentication - */ -public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { - private static final String[] SYNC_AUTHORITIES = {BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY}; - - @NonNull - private final Context context; - - public WikiAccountAuthenticator(@NonNull Context context) { - super(context); - this.context = context; - } - - /** - * Provides Bundle with edited Account Properties - */ - @Override - public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { - Bundle bundle = new Bundle(); - bundle.putString("test", "editProperties"); - return bundle; - } - - @Override - public Bundle addAccount(@NonNull AccountAuthenticatorResponse response, - @NonNull String accountType, @Nullable String authTokenType, - @Nullable String[] requiredFeatures, @Nullable Bundle options) - throws NetworkErrorException { - // account type not supported returns bundle without loginActivity Intent, it just contains "test" key - if (!supportedAccountType(accountType)) { - Bundle bundle = new Bundle(); - bundle.putString("test", "addAccount"); - return bundle; - } - - return addAccount(response); - } - - @Override - public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response, - @NonNull Account account, @Nullable Bundle options) - throws NetworkErrorException { - Bundle bundle = new Bundle(); - bundle.putString("test", "confirmCredentials"); - return bundle; - } - - @Override - public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response, - @NonNull Account account, @NonNull String authTokenType, - @Nullable Bundle options) - throws NetworkErrorException { - Bundle bundle = new Bundle(); - bundle.putString("test", "getAuthToken"); - return bundle; - } - - @Nullable - @Override - public String getAuthTokenLabel(@NonNull String authTokenType) { - return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null; - } - - @Nullable - @Override - public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response, - @NonNull Account account, @Nullable String authTokenType, - @Nullable Bundle options) - throws NetworkErrorException { - Bundle bundle = new Bundle(); - bundle.putString("test", "updateCredentials"); - return bundle; - } - - @Nullable - @Override - public Bundle hasFeatures(@NonNull AccountAuthenticatorResponse response, - @NonNull Account account, @NonNull String[] features) - throws NetworkErrorException { - Bundle bundle = new Bundle(); - bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); - return bundle; - } - - private boolean supportedAccountType(@Nullable String type) { - return BuildConfig.ACCOUNT_TYPE.equals(type); - } - - /** - * Provides a bundle containing a Parcel - * the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type) - */ - private Bundle addAccount(AccountAuthenticatorResponse response) { - Intent intent = new Intent(context, LoginActivity.class); - intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); - - Bundle bundle = new Bundle(); - bundle.putParcelable(AccountManager.KEY_INTENT, intent); - - return bundle; - } - - @Override - public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response, - Account account) throws NetworkErrorException { - Bundle result = super.getAccountRemovalAllowed(response, account); - - if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) - && !result.containsKey(AccountManager.KEY_INTENT)) { - boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); - - if (allowed) { - for (String auth : SYNC_AUTHORITIES) { - ContentResolver.cancelSync(account, auth); - } - } - } - - return result; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt new file mode 100644 index 000000000..367989f14 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt @@ -0,0 +1,108 @@ +package fr.free.nrw.commons.auth + +import android.accounts.AbstractAccountAuthenticator +import android.accounts.Account +import android.accounts.AccountAuthenticatorResponse +import android.accounts.AccountManager +import android.accounts.NetworkErrorException +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.os.bundleOf +import fr.free.nrw.commons.BuildConfig + +private val SYNC_AUTHORITIES = arrayOf( + BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY +) + +/** + * Handles WikiMedia commons account Authentication + */ +class WikiAccountAuthenticator( + private val context: Context +) : AbstractAccountAuthenticator(context) { + /** + * Provides Bundle with edited Account Properties + */ + override fun editProperties( + response: AccountAuthenticatorResponse, + accountType: String + ) = bundleOf("test" to "editProperties") + + // account type not supported returns bundle without loginActivity Intent, it just contains "test" key + @Throws(NetworkErrorException::class) + override fun addAccount( + response: AccountAuthenticatorResponse, + accountType: String, + authTokenType: String?, + requiredFeatures: Array?, + options: Bundle? + ) = if (BuildConfig.ACCOUNT_TYPE == accountType) { + addAccount(response) + } else { + bundleOf("test" to "addAccount") + } + + @Throws(NetworkErrorException::class) + override fun confirmCredentials( + response: AccountAuthenticatorResponse, account: Account, options: Bundle? + ) = bundleOf("test" to "confirmCredentials") + + @Throws(NetworkErrorException::class) + override fun getAuthToken( + response: AccountAuthenticatorResponse, + account: Account, + authTokenType: String, + options: Bundle? + ) = bundleOf("test" to "getAuthToken") + + override fun getAuthTokenLabel(authTokenType: String) = + if (BuildConfig.ACCOUNT_TYPE == authTokenType) AUTH_TOKEN_TYPE else null + + @Throws(NetworkErrorException::class) + override fun updateCredentials( + response: AccountAuthenticatorResponse, + account: Account, + authTokenType: String?, + options: Bundle? + ) = bundleOf("test" to "updateCredentials") + + @Throws(NetworkErrorException::class) + override fun hasFeatures( + response: AccountAuthenticatorResponse, + account: Account, features: Array + ) = bundleOf(AccountManager.KEY_BOOLEAN_RESULT to false) + + /** + * Provides a bundle containing a Parcel + * the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type) + */ + private fun addAccount(response: AccountAuthenticatorResponse): Bundle { + val intent = Intent(context, LoginActivity::class.java) + .putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response) + return bundleOf(AccountManager.KEY_INTENT to intent) + } + + @Throws(NetworkErrorException::class) + override fun getAccountRemovalAllowed( + response: AccountAuthenticatorResponse?, + account: Account? + ): Bundle { + val result = super.getAccountRemovalAllowed(response, account) + + if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) + && !result.containsKey(AccountManager.KEY_INTENT) + ) { + val allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT) + + if (allowed) { + for (auth in SYNC_AUTHORITIES) { + ContentResolver.cancelSync(account, auth) + } + } + } + + return result + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java deleted file mode 100644 index bb41f27aa..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java +++ /dev/null @@ -1,31 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.AbstractAccountAuthenticator; -import android.content.Intent; -import android.os.IBinder; - -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.di.CommonsDaggerService; - -/** - * Handles the Auth service of the App, see AndroidManifests for details - * (Uses Dagger 2 as injector) - */ -public class WikiAccountAuthenticatorService extends CommonsDaggerService { - - @Nullable - private AbstractAccountAuthenticator authenticator; - - @Override - public void onCreate() { - super.onCreate(); - authenticator = new WikiAccountAuthenticator(this); - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return authenticator == null ? null : authenticator.getIBinder(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt new file mode 100644 index 000000000..852536a48 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt @@ -0,0 +1,22 @@ +package fr.free.nrw.commons.auth + +import android.accounts.AbstractAccountAuthenticator +import android.content.Intent +import android.os.IBinder +import fr.free.nrw.commons.di.CommonsDaggerService + +/** + * Handles the Auth service of the App, see AndroidManifests for details + * (Uses Dagger 2 as injector) + */ +class WikiAccountAuthenticatorService : CommonsDaggerService() { + private var authenticator: AbstractAccountAuthenticator? = null + + override fun onCreate() { + super.onCreate() + authenticator = WikiAccountAuthenticator(this) + } + + override fun onBind(intent: Intent): IBinder? = + authenticator?.iBinder +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index cd7324c63..3f9344184 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -14,7 +14,6 @@ import dagger.Module; import dagger.Provides; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao; @@ -114,11 +113,6 @@ public class CommonsApplicationModule { return byName; } - @Provides - public AccountUtil providesAccountUtil(Context context) { - return new AccountUtil(); - } - /** * Provides an instance of CategoryContentProviderClient i.e. the categories * that are there in local storage diff --git a/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java b/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java index 1723da723..2a4b612c0 100644 --- a/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java +++ b/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java @@ -2,7 +2,7 @@ package fr.free.nrw.commons.feedback; import android.content.Context; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.AccountUtil; +import fr.free.nrw.commons.auth.AccountUtilKt; import fr.free.nrw.commons.feedback.model.Feedback; import fr.free.nrw.commons.utils.LangCodeUtils; import java.util.Locale; @@ -43,7 +43,7 @@ public class FeedbackContentCreator { sectionTitleBuilder = new StringBuilder(); sectionTitleBuilder.append("Feedback from "); - sectionTitleBuilder.append(AccountUtil.getUserName(context)); + sectionTitleBuilder.append(AccountUtilKt.getUserName(context)); sectionTitleBuilder.append(" for version "); sectionTitleBuilder.append(feedback.getVersion()); sectionTitleBuilder.append(" on "); diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index ed20809ac..66f2221b8 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -53,7 +53,7 @@ import fr.free.nrw.commons.MediaDataExtractor; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.actions.ThanksClient; -import fr.free.nrw.commons.auth.AccountUtil; +import fr.free.nrw.commons.auth.AccountUtilKt; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; import fr.free.nrw.commons.category.CategoryClient; @@ -382,8 +382,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements enableProgressBar(); } - if (AccountUtil.getUserName(getContext()) != null && media != null - && AccountUtil.getUserName(getContext()).equals(media.getAuthor())) { + if (AccountUtilKt.getUserName(getContext()) != null && media != null + && AccountUtilKt.getUserName(getContext()).equals(media.getAuthor())) { binding.sendThanks.setVisibility(GONE); } else { binding.sendThanks.setVisibility(VISIBLE); @@ -485,7 +485,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements } private void onDeletionPageExists(Boolean deletionPageExists) { - if (AccountUtil.getUserName(getContext()) == null && !AccountUtil.getUserName(getContext()).equals(media.getAuthor())) { + if (AccountUtilKt.getUserName(getContext()) == null && !AccountUtilKt.getUserName(getContext()).equals(media.getAuthor())) { binding.nominateDeletion.setVisibility(GONE); binding.nominatedDeletionBanner.setVisibility(GONE); } else if (deletionPageExists) { @@ -1079,7 +1079,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements @SuppressLint("StringFormatInvalid") public void onDeleteButtonClicked(){ - if (AccountUtil.getUserName(getContext()) != null && AccountUtil.getUserName(getContext()).equals(media.getAuthor())) { + if (AccountUtilKt.getUserName(getContext()) != null && AccountUtilKt.getUserName(getContext()).equals(media.getAuthor())) { final ArrayAdapter languageAdapter = new ArrayAdapter<>(getActivity(), R.layout.simple_spinner_dropdown_list, reasonList); final Spinner spinner = new Spinner(getActivity()); @@ -1105,7 +1105,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements //Reviewer correct me if i have misunderstood something over here //But how does this if (delete.getVisibility() == View.VISIBLE) { // enableDeleteButton(true); makes sense ? - else if (AccountUtil.getUserName(getContext()) != null) { + else if (AccountUtilKt.getUserName(getContext()) != null) { final EditText input = new EditText(getActivity()); input.requestFocus(); AlertDialog d = DialogUtil.showAlertDialog(getActivity(), diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt index 1d87a8f82..1547f89ad 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt @@ -96,7 +96,7 @@ class NotificationActivity : BaseActivity() { } }, { throwable -> if (throwable is InvalidLoginTokenException) { - val username = sessionManager.getUserName() + val username = sessionManager.userName val logoutListener = CommonsApplication.BaseLogoutListener( this, getString(R.string.invalid_login_message), diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt index 44b0f9bc1..cd2cbc8ad 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt @@ -11,7 +11,7 @@ import android.view.MotionEvent import android.view.View import fr.free.nrw.commons.Media import fr.free.nrw.commons.R -import fr.free.nrw.commons.auth.AccountUtil +import fr.free.nrw.commons.auth.getUserName import fr.free.nrw.commons.databinding.ActivityReviewBinding import fr.free.nrw.commons.delete.DeleteHelper import fr.free.nrw.commons.media.MediaDetailFragment @@ -183,7 +183,7 @@ class ReviewActivity : BaseActivity() { } //If The Media User and Current Session Username is same then Skip the Image - if (media.user == AccountUtil.getUserName(applicationContext)) { + if (media.user == getUserName(applicationContext)) { runRandomizer() return } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt index c0e5097c0..dbbab7359 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt @@ -63,7 +63,7 @@ class FailedUploadsFragment : } if (StringUtils.isEmpty(userName)) { - userName = sessionManager.getUserName() + userName = sessionManager.userName } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt index dd06452f9..7e7275049 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt @@ -19,7 +19,7 @@ class AbstractTextWatcher( // No-op } - interface TextChange { + fun interface TextChange { fun onTextChanged(value: String) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt index 84ec5a2cb..4c38a30ff 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt @@ -6,7 +6,6 @@ import android.content.Context import androidx.collection.LruCache import com.google.gson.Gson import com.nhaarman.mockitokotlin2.mock -import fr.free.nrw.commons.auth.AccountUtil import fr.free.nrw.commons.data.DBOpenHelper import fr.free.nrw.commons.di.CommonsApplicationComponent import fr.free.nrw.commons.di.CommonsApplicationModule @@ -41,7 +40,6 @@ class TestCommonsApplication : Application() { class MockCommonsApplicationModule( appContext: Context, ) : CommonsApplicationModule(appContext) { - val accountUtil: AccountUtil = mock() val defaultSharedPreferences: JsonKvStore = mock() val locationServiceManager: LocationServiceManager = mock() val mockDbOpenHelper: DBOpenHelper = mock() @@ -58,8 +56,6 @@ class MockCommonsApplicationModule( override fun provideModificationContentProviderClient(context: Context?): ContentProviderClient = modificationClient - override fun providesAccountUtil(context: Context): AccountUtil = accountUtil - override fun providesDefaultKvStore( context: Context, gson: Gson, diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/AccountUtilUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/AccountUtilUnitTest.kt index b45b844c0..566484a02 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/AccountUtilUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/AccountUtilUnitTest.kt @@ -15,25 +15,17 @@ import org.robolectric.annotation.Config @Config(sdk = [21], application = TestCommonsApplication::class) class AccountUtilUnitTest { private lateinit var context: FakeContextWrapper - private lateinit var accountUtil: AccountUtil @Before @Throws(Exception::class) fun setUp() { context = FakeContextWrapper(ApplicationProvider.getApplicationContext()) - accountUtil = AccountUtil() - } - - @Test - @Throws(Exception::class) - fun checkNotNull() { - Assert.assertNotNull(accountUtil) } @Test @Throws(Exception::class) fun testGetUserName() { - Assert.assertEquals(AccountUtil.getUserName(context), "test@example.com") + Assert.assertEquals(getUserName(context), "test@example.com") } @Test @@ -41,13 +33,13 @@ class AccountUtilUnitTest { fun testGetUserNameWithException() { val context = FakeContextWrapperWithException(ApplicationProvider.getApplicationContext()) - Assert.assertEquals(AccountUtil.getUserName(context), null) + Assert.assertEquals(getUserName(context), null) } @Test @Throws(Exception::class) fun testAccount() { - Assert.assertEquals(AccountUtil.account(context)?.name, "test@example.com") + Assert.assertEquals(account(context)?.name, "test@example.com") } @Test @@ -55,6 +47,6 @@ class AccountUtilUnitTest { fun testAccountWithException() { val context = FakeContextWrapperWithException(ApplicationProvider.getApplicationContext()) - Assert.assertEquals(AccountUtil.account(context), null) + Assert.assertEquals(account(context), null) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt index 162f50584..871613ca5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt @@ -218,17 +218,6 @@ class LoginActivityUnitTests { method.invoke(activity) } - @Test - @Throws(Exception::class) - fun testHideProgress() { - val method: Method = - LoginActivity::class.java.getDeclaredMethod( - "hideProgress", - ) - method.isAccessible = true - method.invoke(activity) - } - @Test @Throws(Exception::class) fun testOnResume() { diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorServiceUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorServiceUnitTest.kt index c1c209462..370c74a47 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorServiceUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorServiceUnitTest.kt @@ -3,18 +3,13 @@ package fr.free.nrw.commons.auth import org.junit.Assert import org.junit.Before import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations import java.lang.reflect.Field class WikiAccountAuthenticatorServiceUnitTest { - private lateinit var service: WikiAccountAuthenticatorService - - @Before - fun setUp() { - MockitoAnnotations.openMocks(this) - service = WikiAccountAuthenticatorService() - service.onBind(null) - } + private val service = WikiAccountAuthenticatorService() @Test fun checkNotNull() { @@ -23,10 +18,9 @@ class WikiAccountAuthenticatorServiceUnitTest { @Test fun testOnBindCaseNull() { - val field: Field = - WikiAccountAuthenticatorService::class.java.getDeclaredField("authenticator") + val field: Field = WikiAccountAuthenticatorService::class.java.getDeclaredField("authenticator") field.isAccessible = true field.set(service, null) - Assert.assertEquals(service.onBind(null), null) + Assert.assertEquals(service.onBind(mock()), null) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorUnitTest.kt index 856e5015d..d5e24790b 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorUnitTest.kt @@ -86,10 +86,7 @@ class WikiAccountAuthenticatorUnitTest { @Test fun testGetAuthTokenLabelCaseNonNull() { - Assert.assertEquals( - authenticator.getAuthTokenLabel(BuildConfig.ACCOUNT_TYPE), - AccountUtil.AUTH_TOKEN_TYPE, - ) + Assert.assertEquals(authenticator.getAuthTokenLabel(BuildConfig.ACCOUNT_TYPE), AUTH_TOKEN_TYPE) } @Test From 794dbb8f9273654ff0bf70ba83288b7ead7d2fd4 Mon Sep 17 00:00:00 2001 From: Rohit Verma <101377978+rohit9625@users.noreply.github.com> Date: Thu, 28 Nov 2024 14:07:56 +0530 Subject: [PATCH 4/4] Fix modification on bottom sheet's data when coming from Nearby Banner and clicked on other pins (#5937) * refactor getIconFor method and remove call to highlightNearestPlace() It ensures single responsibility on getIconFor method * refactor and fix icon issue when coming from nearby banner --------- Co-authored-by: Nicolas Raoul --- .../fragments/NearbyParentFragment.java | 43 +++++++++++-------- .../main/res/layout/bottom_sheet_details.xml | 2 +- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index fdbc727bc..5da4e5478 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -965,6 +965,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment lastPlaceToCenter.location.getLatitude() - cameraShift, lastPlaceToCenter.getLocation().getLongitude(), 0)); } + highlightNearestPlace(place); } @@ -2001,7 +2002,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment * * @param nearestPlace nearest place, which has to be highlighted */ - private void highlightNearestPlace(Place nearestPlace) { + private void highlightNearestPlace(final Place nearestPlace) { + binding.bottomSheetDetails.icon.setVisibility(View.VISIBLE); passInfoToSheet(nearestPlace); hideBottomSheet(); bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); @@ -2015,32 +2017,37 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment * @return returns the drawable of marker according to the place information */ private @DrawableRes int getIconFor(Place place, Boolean isBookmarked) { - if (nearestPlace != null) { - if (place.name.equals(nearestPlace.name)) { - // Highlight nearest place only when user clicks on the home nearby banner - highlightNearestPlace(place); - return (isBookmarked ? - R.drawable.ic_custom_map_marker_purple_bookmarked : - R.drawable.ic_custom_map_marker_purple); - } + if (nearestPlace != null && place.name.equals(nearestPlace.name)) { + // Highlight nearest place only when user clicks on the home nearby banner +// highlightNearestPlace(place); + return (isBookmarked ? + R.drawable.ic_custom_map_marker_purple_bookmarked : + R.drawable.ic_custom_map_marker_purple + ); } + if (place.isMonument()) { return R.drawable.ic_custom_map_marker_monuments; - } else if (!place.pic.trim().isEmpty()) { + } + if (!place.pic.trim().isEmpty()) { return (isBookmarked ? R.drawable.ic_custom_map_marker_green_bookmarked : - R.drawable.ic_custom_map_marker_green); - } else if (!place.exists) { // Means that the topic of the Wikidata item does not exist in the real world anymore, for instance it is a past event, or a place that was destroyed + R.drawable.ic_custom_map_marker_green + ); + } + if (!place.exists) { // Means that the topic of the Wikidata item does not exist in the real world anymore, for instance it is a past event, or a place that was destroyed return (R.drawable.ic_clear_black_24dp); - }else if (place.name == "") { + } + if (place.name.isEmpty()) { return (isBookmarked ? R.drawable.ic_custom_map_marker_grey_bookmarked : - R.drawable.ic_custom_map_marker_grey); - } else { - return (isBookmarked ? - R.drawable.ic_custom_map_marker_red_bookmarked : - R.drawable.ic_custom_map_marker_red); + R.drawable.ic_custom_map_marker_grey + ); } + return (isBookmarked ? + R.drawable.ic_custom_map_marker_red_bookmarked : + R.drawable.ic_custom_map_marker_red + ); } /** diff --git a/app/src/main/res/layout/bottom_sheet_details.xml b/app/src/main/res/layout/bottom_sheet_details.xml index f026528b6..77d31a967 100644 --- a/app/src/main/res/layout/bottom_sheet_details.xml +++ b/app/src/main/res/layout/bottom_sheet_details.xml @@ -32,7 +32,7 @@ android:layout_width="@dimen/dimen_40" android:layout_height="@dimen/dimen_40" android:layout_marginLeft="@dimen/standard_gap" - android:visibility="gone"> + android:visibility="gone" />