Initial QSfera import
@@ -0,0 +1,165 @@
|
|||||||
|
actionbar_sync = Refresh account
|
||||||
|
actionbar_upload_from_apps = Content from other apps
|
||||||
|
actionbar_upload_files = Files
|
||||||
|
actionbar_descending = Descending
|
||||||
|
drawer_item_all_files = All files
|
||||||
|
drawer_item_shared_by_link_files = Shared by link
|
||||||
|
drawer_item_settings = Settings
|
||||||
|
drawer_item_uploads_list = Uploads
|
||||||
|
drawer_exceeded_quota = Storage full
|
||||||
|
drawer_nearing_quota = Storage nearly full
|
||||||
|
drawer_critical_quota = Storage very nearly full
|
||||||
|
prefs_category_general = General
|
||||||
|
prefs_accounts = Accounts
|
||||||
|
prefs_manage_accounts = Manage accounts
|
||||||
|
prefs_camera_upload_on_charging_summary = Enable wifi only uploads to activate this option
|
||||||
|
prefs_automatic_uploads_not_available_users_light = Automatic uploads are not available for users light
|
||||||
|
prefs_log_title = Enable logging
|
||||||
|
prefs_log_summary = This is used to log problems.
|
||||||
|
prefs_log_title_history = Logging history
|
||||||
|
prefs_log_summary_history = This shows the recorded logs.
|
||||||
|
prefs_log_delete_history_button = Delete history
|
||||||
|
prefs_remember_last_share_location = Remember share location
|
||||||
|
prefs_remember_last_upload_location_summary = Remember last share upload location
|
||||||
|
pattern_error_set = An error occurred while setting the pattern lock.
|
||||||
|
pattern_error_remove = An error occurred while removing the pattern lock.
|
||||||
|
auth_host_url = Server address https://…
|
||||||
|
uploader_error_message_received_piece_of_text = %1$s cannot upload a piece of text as a file.
|
||||||
|
light_users_subtitle = Personal space is not available
|
||||||
|
file_list_loading = Loading…
|
||||||
|
local_file_list_empty = There are no files in this folder.
|
||||||
|
local_file_list_empty_just_folders = There are no folders in this folder.
|
||||||
|
file_list_folder = folder
|
||||||
|
file_list_folders = folders
|
||||||
|
file_list_file = file
|
||||||
|
file_list_files = files
|
||||||
|
filedetails_select_file = Tap on a file to display additional information.
|
||||||
|
filedetails_path = Path:
|
||||||
|
filedetails_renamed_in_upload_msg = File was renamed to %1$s during upload
|
||||||
|
list_layout = List layout
|
||||||
|
common_remove_upload = Remove upload
|
||||||
|
common_retry_upload = Retry upload
|
||||||
|
common_save_exit = Save & exit
|
||||||
|
common_error = Error
|
||||||
|
common_loading = Loading…
|
||||||
|
common_unknown = unknown
|
||||||
|
change_password = Change password
|
||||||
|
delete_account = Remove account
|
||||||
|
create_account = Create account
|
||||||
|
uploader_upload_in_progress_ticker = Uploading…
|
||||||
|
uploader_upload_in_progress_content = %1$d%% Uploading %2$s
|
||||||
|
uploader_upload_succeeded_ticker = Upload succeeded
|
||||||
|
uploader_upload_failed_ssl_certificate_not_trusted = Server certificate is not trusted.
|
||||||
|
uploader_upload_available_offline_files = Uploading available offline files
|
||||||
|
uploader_upload_requested_from_wifi_files = Uploading requested from wifi files
|
||||||
|
uploads_view_upload_status_paused = Paused
|
||||||
|
uploads_view_upload_status_failed_retry = Upload will be retried shortly
|
||||||
|
uploads_view_upload_status_failed_firewall_error = Forbidden due to a firewall rule
|
||||||
|
uploads_view_later_waiting_to_upload = Waiting to upload
|
||||||
|
downloader_download_in_progress_ticker = Downloading…
|
||||||
|
downloader_download_in_progress_content = %1$d%% Downloading %2$s
|
||||||
|
downloader_download_forbidden_permissions = to download this file
|
||||||
|
sync_conflicts_in_favourites_ticker = Conflicts found
|
||||||
|
sync_conflicts_in_favourites_content = %1$d kept-in-sync files could not be sync\'ed
|
||||||
|
sync_fail_in_favourites_ticker = Kept-in-sync files failed
|
||||||
|
sync_fail_in_favourites_content = Contents of %1$d files could not be sync\'ed (%2$d conflicts)
|
||||||
|
pass_code_error_set = An error occurred while setting the passcode lock.
|
||||||
|
pass_code_error_remove = An error occurred while removing the passcode lock.
|
||||||
|
biometric_not_available = Biometric unlock is not available because system could not provide
|
||||||
|
media_err_no_account = No account provided
|
||||||
|
media_err_not_in_qsfera = File not in a valid account
|
||||||
|
auth_getting_authorization = Getting authorization…
|
||||||
|
auth_trying_to_login = Trying to log in…
|
||||||
|
auth_no_net_conn_title = No network connection
|
||||||
|
auth_nossl_plain_ok_title = Secure connection unavailable.
|
||||||
|
auth_unknown_error_title = Unknown error occurred!
|
||||||
|
auth_wrong_connection_title = Couldn\'t establish connection
|
||||||
|
auth_wtf_reenter_URL = Unexpected state; please enter the server address again
|
||||||
|
auth_connecting_auth_server = Connecting to authentication server …
|
||||||
|
auth_fail_get_user_name = Your server is not returning a correct user ID. Please contact an administrator.
|
||||||
|
auth_account_does_not_exist = Account does not exist in the device yet
|
||||||
|
available_offline_inherited_msg = A folder containing this file is available offline.
|
||||||
|
sync_file_fail_msg = Remote file could not be checked
|
||||||
|
filedisplay_unexpected_bad_get_content = Unexpected problem; please select the file from a different app
|
||||||
|
oauth_check_onoff = Login with oAuth2
|
||||||
|
oauth_login_connection = Connecting to OAuth2 server…
|
||||||
|
placeholder_filetype = PNG Image
|
||||||
|
proper_pics_folder_warning_camera_upload = Please make sure the folder selected is where the camera you are using saves the pictures taken. Otherwise, the feature will not be able to detect your pictures. Keep in mind that pictures will be uploaded in at least 15 minutes after taking them.
|
||||||
|
proper_videos_folder_warning_camera_upload = Please make sure the folder selected is where the camera you are using saves the videos recorded. Otherwise, the feature will not be able to detect your videos. Keep in mind that videos will be uploaded in at least 15 minutes after recording them.
|
||||||
|
preview_image_error_unknown_format = This image cannot be shown.
|
||||||
|
share_link_no_support_share_api = Sorry, sharing is not enabled on your server. Please contact your administrator.
|
||||||
|
share_link_password_title = Enter a password
|
||||||
|
share_link_empty_password = You must enter a password.
|
||||||
|
error_cant_bind_to_operations_service = Critical error: cannot perform operations
|
||||||
|
forbidden_permissions_rename = to rename this file
|
||||||
|
forbidden_permissions_delete = to delete this file
|
||||||
|
share_link_forbidden_permissions = to share this file
|
||||||
|
unshare_link_forbidden_permissions = to unshare this file
|
||||||
|
update_link_forbidden_permissions = to update this share
|
||||||
|
forbidden_permissions_create = to create the file
|
||||||
|
uploader_upload_forbidden = Upload not allowed
|
||||||
|
downloader_download_file_not_found = The file is no longer available on the server.
|
||||||
|
prefs_category_accounts = Accounts
|
||||||
|
used_quota_title_manage_accounts = Used:
|
||||||
|
actionbar_logger = Logs
|
||||||
|
log_send_history_button = Send history
|
||||||
|
log_send_no_mail_app = No app for sending logs found. Please install a mail app.
|
||||||
|
log_send_mail_subject = %1$s Android app logs
|
||||||
|
log_progress_dialog_text = Loading data…
|
||||||
|
file_list_empty_moving = Nothing in here. You can add a folder!
|
||||||
|
move_file_not_found = Unable to move. Please check whether the file exists.
|
||||||
|
forbidden_permissions_move = to move this file
|
||||||
|
copy_file_not_found = Unable to copy. Please check whether the file exists.
|
||||||
|
forbidden_permissions_copy = to copy this file
|
||||||
|
prefs_category_camera_upload = Camera uploads
|
||||||
|
sync_folder_failed_content = Synchronization of %1$s folder could not be completed
|
||||||
|
shared_subject_header = shared
|
||||||
|
with_you_subject_header = with you
|
||||||
|
auth_host_address = Server address
|
||||||
|
common_error_out_memory = Not enough memory
|
||||||
|
upload_copy_files = Copy file
|
||||||
|
upload_move_files = Move file
|
||||||
|
share_file = Share %1$s
|
||||||
|
share_incompatible_version = To share with other users, your server needs to be at least version 8.2
|
||||||
|
share_add_user_or_group = Add user or group
|
||||||
|
share_via_link_password_title = Secured
|
||||||
|
share_get_public_link_button = Get link
|
||||||
|
share_sharee_unavailable = Sorry, your server version does not allow sharing with users within clients.\nPlease contact your administrator
|
||||||
|
edit_share_unshare = Stop sharing
|
||||||
|
edit_share_done = done
|
||||||
|
auth_forbidden = Forbidden authentication
|
||||||
|
previewing_video_not_supported = Video previewing is not supported in your Android version. File download will start automatically.
|
||||||
|
share_via_link_empty_password = The password is empty.
|
||||||
|
clicks_to_be_developer = You are %1$d clicks away to enable developer menus
|
||||||
|
no_browser_installed_alert = No browser installed. Please install a browser in order to allow a secure login.
|
||||||
|
prefs_logs_summary = Logs may contain sensitive information. Sharing logs with others is sole user responsibility
|
||||||
|
synchronizing_account = Synchronizing account
|
||||||
|
scoped_storage_wizard_not_enough_space_warning = ⚠️ Your free space is currently limited on your device. Some files may not be migrated, please check that all downloaded files keep downloaded after the process has been completed.
|
||||||
|
folder_upload_some_content_or_sync_devices = Upload some content or sync with your devices!
|
||||||
|
release_notes_footer = Thank you for using %1$s.\n♥️
|
||||||
|
release_notes_4_5_0_title_feedback_in_previews = Feedback when (un)setting av. offline in all previews
|
||||||
|
release_notes_4_5_0_subtitle_feedback_in_previews = Added feedback when (un)setting av. offline in all previews and updated options menu depending on file status
|
||||||
|
release_notes_4_5_0_title_quota_improvements = Improvements in storage occupation display
|
||||||
|
release_notes_4_5_0_subtitle_quota_improvements = Storage occupation refreshed frequently in file and refresh operations and available in accounts manager view, besides of drawer menu
|
||||||
|
release_notes_4_5_0_title_light_users = Users Light
|
||||||
|
release_notes_4_5_0_subtitle_light_users = Users Light (users without personal space) are now supported in the app
|
||||||
|
release_notes_4_5_1_title_strange_behaviour_apps_provider = Fix for files creation via app providers
|
||||||
|
release_notes_4_5_1_subtitle_strange_behaviour_apps_provider = Files are now opened correctly when they are created using app providers
|
||||||
|
release_notes_title = КуСфера Client
|
||||||
|
release_notes_initial_release = Welcome to the initial release
|
||||||
|
ic_action_open_in_web = Open in web
|
||||||
|
server_accounts_warning_title = Accounts warning
|
||||||
|
server_accounts_warning_message = Please remove the account and log in again to get the spaces feature.
|
||||||
|
server_accounts_warning_checkbox_message = Don\'t show again
|
||||||
|
server_accounts_warning_button = Understood
|
||||||
|
content_description_get_public_link = Get public link
|
||||||
|
content_description_delete_public_link = Delete public link
|
||||||
|
content_description_get_edit_public_link = Edit public link
|
||||||
|
content_description_remove_account = Remove account
|
||||||
|
content_description_clean_account_storage = Clean account storage
|
||||||
|
content_description_edit_share = Edit share
|
||||||
|
content_description_delete_share = Delete share
|
||||||
|
create_shortcut_dialog_url_information = The URL must start with https:// or http:// so that it can be opened in the browser
|
||||||
|
feedback_dialog_get_in_contact_description = Discuss in our <a href=\"%1$s\"><b>GitHub repo</b></a>
|
||||||
|
release_notes_title_enhanced_bottom_nav_bar = Added text labels on bottom bar
|
||||||
|
release_notes_subtitle_bottom_nav_bar = Text labels were added and default active indicator is used to show which section is selected on the bottom bar
|
||||||
@@ -0,0 +1,694 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- GLOBAL -->
|
||||||
|
<string name="global_name">Имя</string>
|
||||||
|
<string name="global_date">Дата</string>
|
||||||
|
<string name="global_size">Размер</string>
|
||||||
|
<string name="actionbar_search">Поиск в текущей папке</string>
|
||||||
|
<string name="actionbar_search_space">Поиск пространств</string>
|
||||||
|
<string name="actionbar_upload">Загрузить</string>
|
||||||
|
<string name="actionbar_open_with">Открыть с помощью</string>
|
||||||
|
<string name="actionbar_open_with_read_only">Открыть с помощью (только чтение)</string>
|
||||||
|
<string name="actionbar_mkdir">Новый каталог</string>
|
||||||
|
<string name="actionbar_settings">Настройки</string>
|
||||||
|
<string name="actionbar_see_details">Подробно</string>
|
||||||
|
<string name="actionbar_send_file">Отправить</string>
|
||||||
|
<string name="actionbar_sort_title">Упорядочить по</string>
|
||||||
|
<string name="fab_new_file">Новый документ</string>
|
||||||
|
<string name="fab_new_shortcut">Новый ярлык</string>
|
||||||
|
<string name="drawer_item_only_available_offline">Доступные оффлайн</string>
|
||||||
|
<string name="drawer_close">Закрыть</string>
|
||||||
|
<string name="drawer_open">Открыть</string>
|
||||||
|
<string name="drawer_loading_quota">Загрузка квоты</string>
|
||||||
|
<string name="drawer_quota">Использовано %1$s из %2$s (%3$s %%)</string>
|
||||||
|
<string name="drawer_unavailable_free_storage">Использовано %1$s</string>
|
||||||
|
<string name="drawer_unavailable_used_storage">Нет данных о занятом объёме</string>
|
||||||
|
<string name="drawer_feedback">Обратная связь</string>
|
||||||
|
<string name="drawer_help">Помощь</string>
|
||||||
|
<string name="prefs_subsection_logging">Журналирование</string>
|
||||||
|
<string name="prefs_subsection_more">Больше</string>
|
||||||
|
<string name="prefs_subsection_security">Безопасность</string>
|
||||||
|
<string name="prefs_subsection_whatsnew">Что нового в этой версии?</string>
|
||||||
|
<string name="prefs_subsection_security_summary">Установка блокировок на доступ к приложению и разрешение на соприкосновение с другими видимыми окнами</string>
|
||||||
|
<string name="prefs_subsection_logging_summary">Отслеживание журналов, создаваемых при выполнении приложения</string>
|
||||||
|
<string name="prefs_subsection_more_summary">Информация о приложении, обратная связь, и прочее</string>
|
||||||
|
<string name="prefs_passcode">Блокировка кодом</string>
|
||||||
|
<string name="prefs_biometric">Биометрическая блокировка</string>
|
||||||
|
<string name="prefs_biometric_summary">Чтобы использовать эту настройку, включите блокировку по коду или графическому ключу</string>
|
||||||
|
<string name="prefs_lock_application">Блокировка приложения</string>
|
||||||
|
<string name="prefs_lock_application_entries_immediately">Немедленно</string>
|
||||||
|
<string name="prefs_lock_application_entries_1minute">Через 1 минуту</string>
|
||||||
|
<string name="prefs_lock_application_entries_5minutes">Через 5 минут</string>
|
||||||
|
<string name="prefs_lock_application_entries_30minutes">Через 30 минут</string>
|
||||||
|
<string name="prefs_lock_access_from_document_provider">Блокировать доступ к файлам системе</string>
|
||||||
|
<string name="prefs_lock_access_from_document_provider_summary">Блокировка доступа к файлам в аккаунтах ownCloud другим приложениям через стандартный файловый менеджер.</string>
|
||||||
|
<string name="prefs_touches_with_other_visible_windows">Разрешить работу под прозрачными окнами других приложений</string>
|
||||||
|
<string name="prefs_touches_with_other_visible_windows_summary">Разрешает взаимодействие, когда есть перекрытие другим видимым или прозрачным окном. Это нужно, если вы используете приложения для фильтрации света от экрана. Перекрывающее приложение сможет получать информацию о взаимодействии.</string>
|
||||||
|
<string name="confirmation_touches_with_other_windows_title">Вы уверены, что хотите включить эту функцию?</string>
|
||||||
|
<string name="confirmation_touches_with_other_windows_message">Используйте эту функцию на свой страх и риск. Вредоносное приложение может попытаться обманом заставить вас выполнить какие-либо действия, используя другие представления.</string>
|
||||||
|
<string name="prefs_subsection_picture_uploads">Автоматическая загрузка изображений</string>
|
||||||
|
<string name="prefs_subsection_picture_uploads_summary">Управляйте местоположением и поведением автоматически загружаемых фотографий</string>
|
||||||
|
<string name="prefs_subsection_video_uploads">Автоматическая загрузка видео</string>
|
||||||
|
<string name="prefs_subsection_video_uploads_summary">Настройка размещения и поведения видеороликов, загружаемых автоматически</string>
|
||||||
|
<string name="prefs_camera_picture_upload">Загрузка изображений</string>
|
||||||
|
<string name="prefs_camera_picture_upload_summary">Автоматически загружать изображения, сделанные камерой</string>
|
||||||
|
<string name="prefs_camera_picture_upload_path_title">Путь загрузки изображений</string>
|
||||||
|
<string name="prefs_camera_picture_upload_conditions_title">Условия для загрузки</string>
|
||||||
|
<string name="prefs_camera_picture_upload_conditions_summary">Автозагрузка будет запущена только при удовлетворении выбранных ниже условий</string>
|
||||||
|
<string name="prefs_camera_picture_upload_on_wifi">Загрузка изображений только через wifi</string>
|
||||||
|
<string name="prefs_camera_picture_upload_on_charging">Загрузка изображений только во время зарядки</string>
|
||||||
|
<string name="prefs_camera_video_upload">Загрузка видео</string>
|
||||||
|
<string name="prefs_camera_video_upload_summary">Автоматически загружать видео, записанные камерой</string>
|
||||||
|
<string name="prefs_camera_video_upload_path_title">Путь загрузки видео</string>
|
||||||
|
<string name="prefs_camera_video_upload_on_wifi">Загрузка видео только через wifi</string>
|
||||||
|
<string name="prefs_camera_video_upload_on_charging">Загрузка видео только во время зарядки</string>
|
||||||
|
<string name="prefs_subsection_advanced">Дополнительно</string>
|
||||||
|
<string name="prefs_subsection_notifications">Управление уведомлениями</string>
|
||||||
|
<string name="prefs_show_hidden_files">Показать скрытые файлы</string>
|
||||||
|
<string name="prefs_log_open_logs_list_view">Логи</string>
|
||||||
|
<string name="prefs_log_no_logs_list_view">Пусто</string>
|
||||||
|
<string name="prefs_log_empty_subtitle">Включите ведение журнала, и здесь будут отображаться файлы журнала.</string>
|
||||||
|
<string name="prefs_help">Помощь</string>
|
||||||
|
<string name="prefs_sync_calendar_contacts">Синхронизировать ваши контакты, календари и задачи</string>
|
||||||
|
<string name="prefs_sync_calendar_contacts_summary">Установить DAVx⁵</string>
|
||||||
|
<string name="prefs_access_document_provider">Блокировать доступ к файлам системе</string>
|
||||||
|
<string name="prefs_access_document_provider_summary">Предлагаемое приложение для просмотра файлов на вашем устройстве с помощью встроенного в Android проводника файлов</string>
|
||||||
|
<string name="prefs_recommend">Рекомендовать другу</string>
|
||||||
|
<string name="prefs_feedback">Обратная связь</string>
|
||||||
|
<string name="prefs_imprint">Штамп</string>
|
||||||
|
<string name="prefs_pattern">Графический ключ</string>
|
||||||
|
<string name="prefs_enable_logging">Включить журналирование</string>
|
||||||
|
<string name="prefs_enable_logging_summary">Активная запись лог файлов может влиять на расход ресурсов. Также они могут содержать конфиденциальную информацию. Обмен лог-файлами подлежит исключительно ответственности пользователя. Автоматическая загрузка лог-файлов на серверы ownCloud исключена.</string>
|
||||||
|
<string name="prefs_send_feedback">Отправить свой отзыв</string>
|
||||||
|
<string name="prefs_app_version">Версия приложения</string>
|
||||||
|
<string name="prefs_app_version_summary">%1$s %2$s версии %3$s (%4$s)</string>
|
||||||
|
<string name="pattern_enter_pattern">Пожалуйста, введите графический ключ</string>
|
||||||
|
<string name="pattern_configure_pattern">Введите графический ключ</string>
|
||||||
|
<string name="pattern_remove_pattern">Удалить графический ключ</string>
|
||||||
|
<string name="pattern_incorrect_pattern">Неверный графический ключ</string>
|
||||||
|
<string name="pattern_reenter_pattern">Введите графический ключ снова</string>
|
||||||
|
<string name="pattern_not_same_pattern">Ключи не совпадают</string>
|
||||||
|
<string name="pattern_already_set">Сначала отключите графический ключ. Графический ключ и код не могут быть включены одновременно.</string>
|
||||||
|
<string name="passcode_already_set">Сначала отключите код. Графический ключ и код не могут быть включены одновременно.</string>
|
||||||
|
<string name="pattern_no_longer_required">Графический ключ не будет больше запрашиваться</string>
|
||||||
|
<string name="pattern_configure_your_pattern_explanation">Графический ключ будет запрашиваться каждый раз при запуске приложения</string>
|
||||||
|
<string name="document_provider_locked">Заблокировано</string>
|
||||||
|
<string name="illegal_argument_exception_message">Допустимое ДЕЙСТВИЕ требуется для передачи в намерениях</string>
|
||||||
|
|
||||||
|
<!-- Bottom navigation bar -->
|
||||||
|
<string name="bottom_nav_files">Файлы</string>
|
||||||
|
<string name="bottom_nav_personal">Личный</string>
|
||||||
|
<string name="bottom_nav_uploads">Загрузки</string>
|
||||||
|
<string name="bottom_nav_offline">Оффлайн</string>
|
||||||
|
<string name="bottom_nav_links">Ссылки</string>
|
||||||
|
<string name="bottom_nav_shares">События обмена файлами</string>
|
||||||
|
|
||||||
|
<string name="recommend_subject">Попробуйте %1$s на вашем смартфоне!</string>
|
||||||
|
<string name="recommend_text">Хочу предложить вам использовать %1$s на смартфоне!\nЗагрузить можно здесь: %2$s\n\t</string>
|
||||||
|
|
||||||
|
<string name="error_reason">поскольку</string>
|
||||||
|
<string name="auth_failure_snackbar">Не удалось выполнить аутентификацию. Войдите в систему еще раз, чтобы восстановить доступ.</string>
|
||||||
|
<string name="server_not_supported">Версия вашего сервера ниже 10й, и поэтому не поддерживается. Пожалуйста, обновите ваш сервер до последней версии.</string>
|
||||||
|
<string name="auth_failure_snackbar_action">Измените учётную запись</string>
|
||||||
|
<string name="auth_oauth_failure">Токен доступа устарел или стал недопустимым. Введите имя и пароль повторно, чтобы восстановить доступ</string>
|
||||||
|
<string name="auth_oauth_failure_snackbar_action">Войти</string>
|
||||||
|
<string name="auth_check_server">Проверить сервер</string>
|
||||||
|
<string name="auth_username">Имя пользователя</string>
|
||||||
|
<string name="auth_password">Пароль</string>
|
||||||
|
<string name="auth_register">Незнакомы с %1$s?</string>
|
||||||
|
<string name="sync_string_files">Файлы</string>
|
||||||
|
<string name="setup_btn_login">Войти</string>
|
||||||
|
<string name="uploader_btn_upload_text">Загрузить</string>
|
||||||
|
<string name="uploader_top_message">Выбрать каталог для загрузки</string>
|
||||||
|
<string name="choose_upload_space">Выбрать каталог для загрузки</string>
|
||||||
|
<string name="uploader_wrn_no_account_title">Учётная запись не найдена</string>
|
||||||
|
<string name="uploader_wrn_no_account_text">На вашем устройстве нет аккаунтов %1$s. Пожалуйста выполните настройку аккаунта.</string>
|
||||||
|
<string name="uploader_wrn_no_account_setup_btn_text">Настройка</string>
|
||||||
|
<string name="uploader_wrn_no_account_quit_btn_text">Выход</string>
|
||||||
|
<string name="uploader_error_title_no_file_to_upload">Не выбран файл для загрузки</string>
|
||||||
|
<string name="uploader_error_message_no_file_to_upload">Принятые данные не включают в себя никаких файлов.</string>
|
||||||
|
<string name="uploader_error_title_file_cannot_be_uploaded">Файл не может быть загружен</string>
|
||||||
|
<string name="uploader_error_message_read_permission_not_granted">%1$s не позволено читать принятый файл</string>
|
||||||
|
<string name="uploader_error_message_source_file_not_found">Файл для загрузки не был найден в положенном месте. Пожалуйста проверьте, существует ли файл.</string>
|
||||||
|
<string name="uploader_error_message_source_file_not_copied">Произошла ошибка во время копирования этого файла во временную папку. Пожалуйста попробуйте послать заново.</string>
|
||||||
|
<string name="file_list_seconds_ago">несколько секунд назад</string>
|
||||||
|
<string name="file_list_empty_title_all_files">Здесь нет файлов</string>
|
||||||
|
<string name="spaces_list_empty_title">Нет пространств</string>
|
||||||
|
<string name="space_label">пространство:</string>
|
||||||
|
<string name="file_list_empty_title_available_offline">Нет файлов</string>
|
||||||
|
<string name="file_list_empty_title_shared_by_links">Нет ссылок</string>
|
||||||
|
<string name="file_list_empty_subtitle_all_files">Загрузите что-нибудь или синхронизируйте со своими устройствами!</string>
|
||||||
|
<string name="shares_list_empty_title">Нет пробелов</string>
|
||||||
|
<string name="shares_list_empty_subtitle">Вы не используете стороние ресурсами</string>
|
||||||
|
<string name="spaces_list_empty_subtitle">У вас нет доступа ни к одному пространству!</string>
|
||||||
|
<string name="file_list_empty_subtitle_available_offline">Здесь будут файлы и каталоги, отмеченные для оффлайн-режима</string>
|
||||||
|
<string name="file_list_empty_subtitle_shared_by_links">Здесь появятся файлы и каталоги, ссылкой на которые вы поделитесь</string>
|
||||||
|
<string name="file_list_no_app_for_file_type">Не найдено приложение для этого типа файла</string>
|
||||||
|
<string name="file_list_no_app_for_perform_action">Нет приложений, поддерживающих это действие</string>
|
||||||
|
<string name="local_file_list_search_with_no_matches">Нет соответствий для этого поиска</string>
|
||||||
|
<string name="upload_list_empty">Нет файлов</string>
|
||||||
|
<string name="upload_list_empty_subtitle">Здесь будут показаны загруженные файлы</string>
|
||||||
|
<string name="upload_to">Закачать в %1$s</string>
|
||||||
|
<string name="pick_document_type">Выберите тип документа для создания:</string>
|
||||||
|
<string name="upload_files_title">Файлы</string>
|
||||||
|
<string name="filedetails_size">Размер:</string>
|
||||||
|
<string name="filedetails_type">Тип:</string>
|
||||||
|
<string name="filedetails_created">Создан:</string>
|
||||||
|
<string name="filedetails_last_sync">Последняя синхронизация:</string>
|
||||||
|
<string name="filedetails_modified">Изменён:</string>
|
||||||
|
<string name="filedetails_download">Скачать</string>
|
||||||
|
<string name="filedetails_file_image_content_description">Файл изображения</string>
|
||||||
|
<string name="filedetails_cancel_btn_content_description">Кнопка отмены загрузки</string>
|
||||||
|
<string name="filedetails_sync_file">Синхронизация</string>
|
||||||
|
<string name="action_share">Общий доступ</string>
|
||||||
|
<string name="login_account_preparing">Подготовка аккаунта к первому запуску</string>
|
||||||
|
<string name="common_yes">Да</string>
|
||||||
|
<string name="common_no">Нет</string>
|
||||||
|
<string name="common_cancel_sync">Отменить синхронизацию</string>
|
||||||
|
<string name="common_back">Назад</string>
|
||||||
|
<string name="common_error_unknown">неизвестная ошибка</string>
|
||||||
|
<string name="common_pending">Ожидается</string>
|
||||||
|
<string name="common_important">Важный</string>
|
||||||
|
<string name="upload_chooser_title">Загрузить из</string>
|
||||||
|
<string name="uploader_info_dirname">Имя каталога</string>
|
||||||
|
<string name="uploader_upload_succeeded_content_single">%1$s загружено</string>
|
||||||
|
<string name="uploader_upload_failed_ticker">Ошибка загрузки</string>
|
||||||
|
<string name="uploader_upload_failed_content_single">Загрузка %1$s не может быть завершена</string>
|
||||||
|
<string name="uploader_upload_failed_credentials_error">Загрузка не удалась, вам необходимо войти повторно.</string>
|
||||||
|
<string name="uploader_upload_text_dialog_title">Укажите имя для нового файла</string>
|
||||||
|
<string name="uploader_upload_text_dialog_filename_error_empty">Имя файла не должно быть пустым</string>
|
||||||
|
<string name="uploader_upload_text_dialog_filename_error_length_max">Имя файла должно быть не длиннее %d символов</string>
|
||||||
|
<string name="uploader_upload_text_filename_hint">Полное имя</string>
|
||||||
|
<string name="uploader_upload_camera_upload_files">Загрузка файлов, загруженных из камеры</string>
|
||||||
|
<string name="uploader_upload_picture_upload_files">Будет загружено %d новых изображений</string>
|
||||||
|
<string name="uploader_upload_video_upload_files">Будет загружено %d новых видео</string>
|
||||||
|
<string name="uploader_upload_camera_upload_source_path_error">Загрузка из камеры не удалась</string>
|
||||||
|
<string name="uploader_upload_picture_upload_error">Исходный путь для загрузки изображений больше не является допустимым</string>
|
||||||
|
<string name="uploader_upload_video_upload_error">Исходный путь для загрузки видео больше не является допустимым</string>
|
||||||
|
<string name="ssl_certificate_not_trusted">Нет доверия сертификату сервера</string>
|
||||||
|
<string name="uploads_view_title">Загрузки</string>
|
||||||
|
<string name="uploads_view_group_current_uploads">Текущие</string>
|
||||||
|
<string name="uploads_view_group_failed_uploads">Не удалось (тапните для повторения)</string>
|
||||||
|
<string name="uploads_view_group_finished_uploads">Загружено</string>
|
||||||
|
<string name="uploads_view_group_queued_uploads">В очереди</string>
|
||||||
|
<string name="uploads_view_group_file_count">%d файлов</string>
|
||||||
|
<string name="uploads_view_group_file_count_single">%d файл</string>
|
||||||
|
<string name="uploads_view_upload_status_succeeded">Завершённые</string>
|
||||||
|
<string name="uploads_view_upload_status_cancelled">Отменён</string>
|
||||||
|
<string name="uploads_view_upload_status_failed_connection_error">Ошибка соединения</string>
|
||||||
|
<string name="uploads_view_upload_status_failed_credentials_error">Ошибка учетных данных</string>
|
||||||
|
<string name="uploads_view_upload_status_failed_folder_error">Ошибка каталога</string>
|
||||||
|
<string name="uploads_view_upload_status_failed_file_error">Ошибка файла</string>
|
||||||
|
<string name="uploads_view_upload_status_failed_localfile_error">Локальный файл не найден</string>
|
||||||
|
<string name="uploads_view_upload_status_failed_permission_error">Ошибка доступа</string>
|
||||||
|
<string name="uploads_view_upload_status_conflict">Конфликт</string>
|
||||||
|
<string name="uploads_view_upload_status_service_interrupted">Работа приложения была прекращена</string>
|
||||||
|
<string name="uploads_view_upload_status_unknown_fail">Неизвестная ошибка</string>
|
||||||
|
<string name="uploads_view_upload_status_waiting_for_wifi">Ожидание соединения по wifi</string>
|
||||||
|
<string name="uploads_view_upload_status_queued">В очереди</string>
|
||||||
|
<string name="uploads_view_unsupported_media_type">Неподдерживаемый тип медиаданных</string>
|
||||||
|
<string name="uploader_upload_enqueued_ticker">Загрузка %1$s в очереди</string>
|
||||||
|
<string name="uploader_upload_canceled_ticker">Загрузка отменена</string>
|
||||||
|
<string name="downloader_download_canceled_ticker">Скачивание отменено</string>
|
||||||
|
<string name="downloader_download_enqueued_ticker">Скачивание %1$s в очереди</string>
|
||||||
|
<string name="downloader_download_succeeded_ticker">Скачивание завершено</string>
|
||||||
|
<string name="downloader_download_succeeded_content">%1$s скачан</string>
|
||||||
|
<string name="downloader_download_failed_ticker">Скачивание не удалось</string>
|
||||||
|
<string name="downloader_preview_failed_ticker">Не удалось отобразить файл</string>
|
||||||
|
<string name="downloader_download_failed_content">Скачивание %1$s не может быть завершено</string>
|
||||||
|
<string name="downloader_not_downloaded_yet">Ещё не скачано</string>
|
||||||
|
<string name="downloader_download_failed_credentials_error">Скачивание не удалось, вам необходимо войти повторно</string>
|
||||||
|
<string name="common_choose_account">Выберите учётную запись</string>
|
||||||
|
<string name="sync_fail_ticker">Синхронизация не удалась</string>
|
||||||
|
<string name="sync_fail_ticker_unauthorized">Синхронизация не удалась, вам нужно войти повторно</string>
|
||||||
|
<string name="sync_fail_content">Синхронизация %1$s не может быть завершена</string>
|
||||||
|
<string name="sync_fail_content_unauthorized">Неверный пароль для %1$s</string>
|
||||||
|
<string name="sync_current_folder_was_removed">Целевой каталог 1%1$s уже не существует</string>
|
||||||
|
|
||||||
|
<string name="pass_code_enter_pass_code">Пожалуйста, укажите ваш код</string>
|
||||||
|
<string name="pass_code_configure_your_pass_code">Укажите ваш код</string>
|
||||||
|
<string name="pass_code_configure_your_pass_code_migration">Введите новый %1$d-значный код</string>
|
||||||
|
<string name="pass_code_configure_your_pass_code_explanation">Код будет запрашиваться каждый раз при запуске приложения</string>
|
||||||
|
<string name="pass_code_reenter_your_pass_code">Пожалуйста, укажите ваш код еще раз</string>
|
||||||
|
<string name="pass_code_remove_your_pass_code">Убрать код</string>
|
||||||
|
<string name="pass_code_mismatch">Коды не совпадают</string>
|
||||||
|
<string name="pass_code_wrong">Некоррентный код</string>
|
||||||
|
<string name="numberkeyboard_backspace_button_content_description">Кнопка Backspace</string>
|
||||||
|
<string name="numberkeyboard_biometric_button_content_description">Биометрическая кнопка</string>
|
||||||
|
|
||||||
|
<string name="numberkeyboard_zero">0</string>
|
||||||
|
<string name="numberkeyboard_one">1</string>
|
||||||
|
<string name="numberkeyboard_two">2</string>
|
||||||
|
<string name="numberkeyboard_three">3</string>
|
||||||
|
<string name="numberkeyboard_four">4</string>
|
||||||
|
<string name="numberkeyboard_five">5</string>
|
||||||
|
<string name="numberkeyboard_six">6</string>
|
||||||
|
<string name="numberkeyboard_seven">7</string>
|
||||||
|
<string name="numberkeyboard_eight">8</string>
|
||||||
|
<string name="numberkeyboard_nine">9</string>
|
||||||
|
|
||||||
|
<string name="lock_time_try_again">Попробуйте ещё раз через %1$s</string>
|
||||||
|
|
||||||
|
<string name="biometric_not_enrolled">Чтобы пользоваться этой возможностью, зарегистрируйте хотя-бы одну биометрию</string>
|
||||||
|
<string name="biometric_prompt_title">Биометрический вход</string>
|
||||||
|
<string name="biometric_prompt_subtitle">Вход с использованием ваших индивидуальных биометрических данных</string>
|
||||||
|
<string name="biometric_dialog_title">Хотите дополнительно активировать биометрическую защиту?</string>
|
||||||
|
<string name="media_notif_ticker">%1$s аудиоплеер</string>
|
||||||
|
<string name="media_state_playing">%1$s (проигрывается)</string>
|
||||||
|
<string name="media_state_loading">%1$s (загружается)</string>
|
||||||
|
<string name="media_event_done">%1$s воспроизведение завершено</string>
|
||||||
|
<string name="media_err_nothing_to_play">Медиафайлы не найдены</string>
|
||||||
|
<string name="media_err_not_in_owncloud">Файл в неверной учётной записи</string>
|
||||||
|
<string name="media_err_unsupported">Неподдерживаемый кодек</string>
|
||||||
|
<string name="media_err_io">Медиафайл не может быть прочитан</string>
|
||||||
|
<string name="media_err_malformed">Медиафайл некорректно закодирован</string>
|
||||||
|
<string name="media_err_timeout">Истекло время попытки воспроизведения</string>
|
||||||
|
<string name="media_err_invalid_progressive_playback">Невозможно организовать потоковую передачу медиафайла</string>
|
||||||
|
<string name="media_err_unknown">Медиафайл не может быть проигран стандартным плеером</string>
|
||||||
|
<string name="media_err_security_ex">Ошибка безопасности при воспроизведении %1$s</string>
|
||||||
|
<string name="media_err_io_ex">Ошибка ввода при воспроизведении %1$s</string>
|
||||||
|
<string name="media_err_unexpected">Неожиданная ошибка при воспроизведении %1$s</string>
|
||||||
|
<string name="media_rewind_description">Перемотка назад</string>
|
||||||
|
<string name="media_play_pause_description">Воспроизведение или пауза</string>
|
||||||
|
<string name="media_forward_description">Перемотка вперед</string>
|
||||||
|
<string name="upload_from_camera_title">Изображение из камеры</string>
|
||||||
|
|
||||||
|
<string name="auth_connection_established">Соединение установлено</string>
|
||||||
|
<string name="auth_testing_connection">Проверка соединения</string>
|
||||||
|
<string name="auth_not_configured_title">Конфигурация сервера задана неверно</string>
|
||||||
|
<string name="auth_account_not_new">Учётная запись такого пользователя и сервера уже существует на устройстве</string>
|
||||||
|
<string name="auth_account_not_the_same">Указанный пользователь не соответствует этой учётной записи</string>
|
||||||
|
<string name="auth_incorrect_path_title">Сервер не найден</string>
|
||||||
|
<string name="auth_incorrect_address_title">Неправильный формат адреса сервера</string>
|
||||||
|
<string name="auth_ssl_general_error_title">Ошибка инициализации SSL</string>
|
||||||
|
<string name="auth_bad_oc_version_title">Неизвестная версия сервера</string>
|
||||||
|
<string name="auth_secure_connection">Защищённое соединение установлено</string>
|
||||||
|
<string name="auth_unauthorized">Неверное имя пользователя или пароль</string>
|
||||||
|
<string name="auth_oauth_error">Ошибка авторизации</string>
|
||||||
|
<string name="auth_oauth_error_access_denied">Сервер авторизации отказал в доступе</string>
|
||||||
|
<string name="auth_expired_oauth_token_toast">Время авторизации истекло. Пожалуйста, авторизуйтесь снова</string>
|
||||||
|
<string name="auth_expired_basic_auth_toast">Пожалуйста укажите текущий пароль</string>
|
||||||
|
<string name="auth_unsupported_auth_method">Сервер не поддерживает выбранный метод аутентификации</string>
|
||||||
|
<string name="auth_can_not_auth_against_server">Не удается авторизоваться на этом сервере</string>
|
||||||
|
|
||||||
|
<string name="set_available_offline">Сделать доступным автономно</string>
|
||||||
|
<string name="unset_available_offline">Убрать доступность автономно</string>
|
||||||
|
<string name="confirmation_set_available_offline">Файл был успешно установлен для работы в оффлайн-режиме</string>
|
||||||
|
<string name="confirmation_unset_available_offline">Файл был успешно снят с оффлайн-доступа</string>
|
||||||
|
<string name="common_rename">Переименовать</string>
|
||||||
|
<string name="common_remove">Удалить</string>
|
||||||
|
<string name="confirmation_remove_file_alert">Вы действительно хотите удалить %1$s?</string>
|
||||||
|
<string name="confirmation_remove_folder_alert">Вы действительно хотите удалить %1$s и его содержимое?</string>
|
||||||
|
<string name="confirmation_remove_local">Только локально</string>
|
||||||
|
<string name="confirmation_remove_account_alert">Вы действительно хотите удалить аккаунт %1$s?</string>
|
||||||
|
<string name="confirmation_remove_account_alert_camera_uploads">К этой учётной записи привязаны закачки из камеры; удаление записи отключит закачки из камеры. Вы действительно хотите удалить учётную запись %1$s?</string>
|
||||||
|
<string name="confirmation_remove_public_share_title">Удалить ссылку</string>
|
||||||
|
<string name="confirmation_remove_private_share_title">Удалить общий доступ</string>
|
||||||
|
<string name="confirmation_remove_share_message">Вы действительно хотите удалить общий доступ %1$s?</string>
|
||||||
|
<string name="remove_success_msg">Удаление завершено</string>
|
||||||
|
<string name="remove_fail_msg">Ошибка удаления</string>
|
||||||
|
<string name="rename_dialog_title">Введите новое имя</string>
|
||||||
|
<string name="rename_local_fail_msg">Локальная копия не может быть переименована; попробуйте другое имя</string>
|
||||||
|
<string name="rename_server_fail_msg">Переименование не может быть завершено</string>
|
||||||
|
<string name="file_already_exists">файл уже существует</string>
|
||||||
|
<string name="folder_already_exists">файл уже существует</string>
|
||||||
|
<string name="file_already_exists_description">Элемент с тем же именем 1%1$s уже существует</string>
|
||||||
|
<string name="folder_already_exists_description">Элемент с тем же именем 1%1$s уже существует</string>
|
||||||
|
<string name="sync_file_nothing_to_do_msg">Содержимое файла уже синхронизировано</string>
|
||||||
|
<string name="sync_file_not_found_msg">Файл не найден</string>
|
||||||
|
<string name="new_remote_version_found_msg">Найдена новая версия на сервере. Загрузка...</string>
|
||||||
|
<string name="download_enqueued_msg">Скачивание в очереди</string>
|
||||||
|
<string name="upload_enqueued_msg">Загрузка в очереди</string>
|
||||||
|
<string name="create_dir_fail_msg">Не удалось создать каталог</string>
|
||||||
|
<string name="create_file_fail_msg">Не удалось создать каталог</string>
|
||||||
|
<string name="filename_forbidden_characters">Запрещённые символы: / \\</string>
|
||||||
|
<string name="filename_forbidden_characters_from_server">Имя файла содержит по крайней мере один некорректный символ</string>
|
||||||
|
<string name="filename_empty">Имя файла не может быть пустым</string>
|
||||||
|
<string name="filename_too_long">Неверная длина имени файла</string>
|
||||||
|
<string name="wait_a_moment">Подождите немного</string>
|
||||||
|
<string name="wait_checking_credentials">Проверка сохранённых учётных данных</string>
|
||||||
|
<string name="filedisplay_no_file_selected">Файлы не выбраны</string>
|
||||||
|
<string name="activity_chooser_title">Отправка ссылки пользователю</string>
|
||||||
|
<string name="wait_for_tmp_copy_from_private_storage">Копирование файла из частного хранилища</string>
|
||||||
|
|
||||||
|
|
||||||
|
<string name="ssl_connection_not_secure">Соединение небезопасно, и http-трафик запрещен.</string>
|
||||||
|
<string name="ssl_validator_header">Не удалось проверить подлинность сервера</string>
|
||||||
|
<string name="ssl_validator_reason_cert_not_trusted">- Сертификат сервера не является доверенным</string>
|
||||||
|
<string name="ssl_validator_reason_cert_expired">- Срок действия сертификата сервера истёк</string>
|
||||||
|
<string name="ssl_validator_reason_cert_not_yet_valid">- Сертификат сервера слишком новый</string>
|
||||||
|
<string name="ssl_validator_reason_hostname_not_verified">- URL не совпадает с именем сервера в сертификате</string>
|
||||||
|
<string name="ssl_validator_question">Доверять этому сертификату в любом случае?</string>
|
||||||
|
<string name="ssl_validator_not_saved">Сертификат не может быть сохранён</string>
|
||||||
|
<string name="ssl_validator_btn_details_see">Подробно</string>
|
||||||
|
<string name="ssl_validator_btn_details_hide">Скрыть</string>
|
||||||
|
<string name="ssl_validator_label_subject">Кому выдан:</string>
|
||||||
|
<string name="ssl_validator_label_issuer">Кем выдан:</string>
|
||||||
|
<string name="ssl_validator_label_CN">Имя:</string>
|
||||||
|
<string name="ssl_validator_label_O">Организация:</string>
|
||||||
|
<string name="ssl_validator_label_OU">Подразделение:</string>
|
||||||
|
<string name="ssl_validator_label_C">Страна:</string>
|
||||||
|
<string name="ssl_validator_label_ST">Штат:</string>
|
||||||
|
<string name="ssl_validator_label_L">Местонахождение:</string>
|
||||||
|
<string name="ssl_validator_label_validity">Срок действия:</string>
|
||||||
|
<string name="ssl_validator_label_validity_from">С:</string>
|
||||||
|
<string name="ssl_validator_label_validity_to">По:</string>
|
||||||
|
<string name="ssl_validator_label_signature">Подпись:</string>
|
||||||
|
<string name="ssl_validator_label_signature_algorithm">Алгоритм:</string>
|
||||||
|
<string name="digest_algorithm_not_available">Этот хэш алгоритм не доступен на этом телефоне.</string>
|
||||||
|
<string name="ssl_validator_label_certificate_fingerprint">Отпечаток пальца:</string>
|
||||||
|
<string name="certificate_load_problem">Проблема при загрузке сертификата.</string>
|
||||||
|
<string name="ssl_validator_null_cert">Сертификат не может быть показан.</string>
|
||||||
|
<string name="ssl_validator_no_info_about_error">- Нет информации об ошибке</string>
|
||||||
|
|
||||||
|
<string name="placeholder_sentence">Это заполнитель</string>
|
||||||
|
<string name="placeholder_filename">placeholder.txt</string>
|
||||||
|
<string name="placeholder_filesize">389 КБ</string>
|
||||||
|
<string name="placeholder_timestamp">2012/05/18 12:23 PM</string>
|
||||||
|
<string name="placeholder_media_time">12:23:45</string>
|
||||||
|
|
||||||
|
<string name="confirmation_disable_camera_uploads_title">Подтвердите</string>
|
||||||
|
<string name="confirmation_disable_pictures_upload_message">Вы уверены, что хотите отключить эту возможность? Последующие изображения не будут загружены</string>
|
||||||
|
<string name="confirmation_disable_videos_upload_message">Вы уверены, что хотите отключить эту возможность? Последующие видеозаписи не будут загружены</string>
|
||||||
|
<string name="conflict_title">Файл в конфликте</string>
|
||||||
|
<string name="conflict_description">Конфликт в файле %1$s, нажмите чтобы разрешить его</string>
|
||||||
|
<string name="conflict_message">Какие файлы Вы хотите сохранить? При выборе обеих версий, к названию локального файла будет добавлена цифра</string>
|
||||||
|
<string name="conflict_keep_both">Сохранить оба</string>
|
||||||
|
<string name="conflict_use_local_version">локальная версия</string>
|
||||||
|
<string name="conflict_use_server_version">удаленная версия</string>
|
||||||
|
<string name="error_conflict">Произошла ошибка в папке назначения</string>
|
||||||
|
<string name="conflict_replace">отмена</string>
|
||||||
|
|
||||||
|
<string name="preview_image_description">Предпросмотр</string>
|
||||||
|
|
||||||
|
<string name="error__upload__local_file_not_copied">%1$s невозможно скопировать в локальный каталог %2$s </string>
|
||||||
|
|
||||||
|
<string name="get_shares_error">Не удалось запросить с сервера список ресурсов общего доступа</string>
|
||||||
|
<string name="get_sharees_error">Не удалось запросить с сервера список пользователей и групп</string>
|
||||||
|
<string name="get_capabilities_error">Не удалось запросить с сервера список возможностей</string>
|
||||||
|
<string name="share_link_file_error">Не удалось предоставить доступ к этому файлу или каталогу</string>
|
||||||
|
<string name="unshare_link_file_error">Не удалось убрать общий доступ с этого файла или каталога</string>
|
||||||
|
<string name="update_link_file_error">Не удалось обновить этот файл или каталог</string>
|
||||||
|
|
||||||
|
<string name="activity_chooser_send_file_title">Отправить</string>
|
||||||
|
|
||||||
|
<string name="copy_link">Копировать ссылку</string>
|
||||||
|
<string name="clipboard_text_copied">Скопировано в буфер обмена</string>
|
||||||
|
<string name="clipboard_no_text_to_copy">Не получен текст для копирования в буфер обмена</string>
|
||||||
|
<string name="clipboard_uxexpected_error">Неожиданная ошибка во время копирования в буфер обмена</string>
|
||||||
|
<string name="clipboard_label">Текст скопирован из %1$s</string>
|
||||||
|
|
||||||
|
|
||||||
|
<string name="network_error_socket_exception">При подключении к серверу возникла ошибка</string>
|
||||||
|
<string name="network_error_socket_timeout_exception">Ответ сервера занимает слишком много времени</string>
|
||||||
|
<string name="network_error_connect_timeout_exception">Слишком много времени ушло на подключение к серверу</string>
|
||||||
|
<string name="network_host_not_available">Сервер не доступен по сети</string>
|
||||||
|
<string name="network_error_message">Произошла ошибка в сети</string>
|
||||||
|
<string name="resource_locked_error_message">Ресурс не заблокирован</string>
|
||||||
|
|
||||||
|
<string name="forbidden_permissions">У вас нет прав %s</string>
|
||||||
|
|
||||||
|
<string name="uploader_upload_forbidden_permissions">для загрузки в этот каталог</string>
|
||||||
|
|
||||||
|
<string name="prefs_add_account">Добавить учетную запись</string>
|
||||||
|
<string name="drawer_manage_accounts">Управление аккаунтами</string>
|
||||||
|
<string name="manage_accounts_quota">%1$s из %2$s</string>
|
||||||
|
<string name="auth_redirect_non_secure_connection_title">Безопасное соединение перенаправлено через небезопасный маршрут.</string>
|
||||||
|
|
||||||
|
<string name="actionbar_select_all">Выделить все</string>
|
||||||
|
<string name="actionbar_select_inverse">Инвертировать выделение</string>
|
||||||
|
|
||||||
|
<string name="actionbar_move">Переместить</string>
|
||||||
|
<string name="folder_picker_choose_button_text">Выбрать</string>
|
||||||
|
<string name="folder_picker_move_here_button_text">Поместить сюда</string>
|
||||||
|
<string name="folder_picker_copy_here_button_text">Копировать сюда</string>
|
||||||
|
<string name="folder_picker_no_permissions_message_text">У Вас нет полномочий для добавления контактов</string>
|
||||||
|
|
||||||
|
<string name="move_file_invalid_into_descendent">Невозможно переместить каталог в его подкаталог</string>
|
||||||
|
<string name="move_file_invalid_overwrite">Файл уже существует в каталоге назначения</string>
|
||||||
|
<string name="move_file_error">Произошла ошибка при попытке перемещения этого файла или каталога</string>
|
||||||
|
|
||||||
|
<string name="copy_file_invalid_into_descendent">Нельзя произвести копирование папки в унаследованную папку</string>
|
||||||
|
<string name="copy_file_invalid_overwrite">Файл уже существует в каталоге назначения</string>
|
||||||
|
<string name="copy_file_error">Произошла ошибка во время копирования этого файла или папки</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<string name="subject_user_shared_with_you">%1$s предоставил вам доступ к \"%2$s\"</string>
|
||||||
|
<string name="subject_shared_with_you">Вам предоставлен доступ к \"%1$s\"</string>
|
||||||
|
|
||||||
|
<string name="auth_refresh_button">Обновить соединение</string>
|
||||||
|
|
||||||
|
<string name="username">Имя пользователя</string>
|
||||||
|
|
||||||
|
<string name="file_list__footer__folder">1 каталог</string>
|
||||||
|
<string name="file_list__footer__folders">%1$d каталогов</string>
|
||||||
|
<string name="file_list__footer__file">1 файл</string>
|
||||||
|
<string name="file_list__footer__file_and_folder">1 файл, 1 папка</string>
|
||||||
|
<string name="file_list__footer__file_and_folders">1 файл, %1$d каталогов</string>
|
||||||
|
<string name="file_list__footer__files">%1$d файлов</string>
|
||||||
|
<string name="file_list__footer__files_and_folder">%1$d файлов, 1 каталог</string>
|
||||||
|
<string name="file_list__footer__files_and_folders">%1$d файлов, %2$d каталогов</string>
|
||||||
|
<string name="prefs_picture_upload_account">Учётная запись для закачки изображений</string>
|
||||||
|
<string name="prefs_video_upload_account">Учётная запись для закачки видео</string>
|
||||||
|
<string name="prefs_camera_upload_source_path_title">Каталог камеры (%1$s)</string>
|
||||||
|
<string name="prefs_camera_upload_source_path_title_required">обязательно</string>
|
||||||
|
<string name="prefs_camera_upload_behaviour_dialog_title">Исходный файл будет</string>
|
||||||
|
<string name="prefs_camera_upload_behaviour_title">Исходный файл будет</string>
|
||||||
|
<string name="prefs_camera_upload_last_sync_title">Последняя синхронизация</string>
|
||||||
|
<string name="notifications_permission_denied">Вы можете обновить свои установки в Настройки</string>
|
||||||
|
|
||||||
|
<string name="pref_behaviour_entries_keep_file">остался в исходной папке</string>
|
||||||
|
<string name="pref_behaviour_entries_remove_original_file">удалено из исходной папки</string>
|
||||||
|
|
||||||
|
<string name="share_dialog_title">Общий доступ</string>
|
||||||
|
<string name="share_with_user_section_title">Пользователи и группы</string>
|
||||||
|
<string name="share_no_users">Нет данных используемых совместно с другими пользователями</string>
|
||||||
|
<string name="share_via_link_section_title">Общедоступные ссылки</string>
|
||||||
|
<string name="share_via_link_create_title">Создать доступ по ссылке</string>
|
||||||
|
<string name="share_via_link_edit_title">Изменить доступ по ссылке</string>
|
||||||
|
<string name="share_via_link_name_label">Имя ссылки</string>
|
||||||
|
<string name="share_cancel_public_link_button">Отмена</string>
|
||||||
|
<string name="share_confirm_public_link_button">Сохранить</string>
|
||||||
|
<string name="share_via_link_expiration_date_label">Срок действия</string>
|
||||||
|
<string name="share_via_link_password_label">Пароль</string>
|
||||||
|
<string name="share_via_link_generate_password_button">Общие</string>
|
||||||
|
<string name="share_via_link_copy_password_button">Копировать</string>
|
||||||
|
<string name="share_via_link_edit_permission_read_only_label">Скачивание / Просмотр</string>
|
||||||
|
<string name="share_via_link_edit_permission_read_and_write_label">Скачивание / Просмотр / Закачка</string>
|
||||||
|
<string name="share_via_link_edit_permission_upload_only_label">Только закачка (перетащить файл сюда)</string>
|
||||||
|
<string name="share_with_title">Поделиться с</string>
|
||||||
|
<string name="share_with_edit_title">Поделиться с %1$s</string>
|
||||||
|
|
||||||
|
<string name="share_search">Найти</string>
|
||||||
|
|
||||||
|
<string name="search_users_and_groups_hint">Поиск пользователей и групп</string>
|
||||||
|
<string name="share_group_clarification">%1$s (группа)</string>
|
||||||
|
<string name="share_remote_clarification">%1$s (в общем доступе)</string>
|
||||||
|
<string name="share_known_remote_clarification">%1$s ( в %2$s )</string>
|
||||||
|
|
||||||
|
<string name="share_privilege_can_share">можно поделиться</string>
|
||||||
|
<string name="share_privilege_can_edit">можно редактировать</string>
|
||||||
|
<string name="share_privilege_can_edit_create">создать</string>
|
||||||
|
<string name="share_privilege_can_edit_change">изменить</string>
|
||||||
|
<string name="share_privilege_can_edit_delete">удалить</string>
|
||||||
|
|
||||||
|
<string name="action_upload_retry">Повторить</string>
|
||||||
|
<string name="action_upload_clear">Очистить</string>
|
||||||
|
|
||||||
|
<string name="local_file_not_found_toast">Файл не был найден в локальной файловой системе</string>
|
||||||
|
<string name="confirmation_remove_files_alert">Вы действительно хотите удалить выбранные %s элементы?</string>
|
||||||
|
<string name="confirmation_remove_folders_alert">Вы действительно хотите удалить выбранные элементы вместе с их содержимым?</string>
|
||||||
|
<string name="failed_upload_quota_exceeded_text">Недостаточно места в учётной записи</string>
|
||||||
|
|
||||||
|
<string name="actionbar_privacy_policy">Политика конфиденциальности</string>
|
||||||
|
<string name="prefs_privacy_policy">Политика конфиденциальности</string>
|
||||||
|
<string name="privacy_policy_error">Произошла ошибка:</string>
|
||||||
|
<string name="service_unavailable">Сервер недоступен.</string>
|
||||||
|
|
||||||
|
<string name="streaming_certificate_error">Не удалось запустить потоковую передачу, так как сертификат вашего сервера не является доверенным. Скачивание запустится автоматически`</string>
|
||||||
|
<string name="streaming_unrecognized_input">Этот формат видео не поддерживается</string>
|
||||||
|
<string name="streaming_file_not_found_error">Этот видеофайл больше не доступен на сервере</string>
|
||||||
|
<string name="streaming_crossed_redirection">Потоковая передача не разрешена через перекрёстные перенаправления</string>
|
||||||
|
<string name="previewing_video_common_error">Это видео невозможно воспроизвести.</string>
|
||||||
|
|
||||||
|
<string name="streaming_position_not_available">"Позиция недоступна"</string>
|
||||||
|
<string name="error_no_network_connection">Устройство не подключено к сети</string>
|
||||||
|
<string name="share_no_public_links">Ещё не создано ни одной общедоступной ссылки.</string>
|
||||||
|
<string name="share_no_link_in_this_share">У этого общего ресурса нет ссылки.</string>
|
||||||
|
<string name="share_warning_about_forwarding_public_links">Каждый, у кого есть эта ссылка, имеет доступ к файлу/каталогу.</string>
|
||||||
|
<string name="share_via_link_default_password">*****</string>
|
||||||
|
<string name="share_via_link_password_enforced_label">Пароль *</string>
|
||||||
|
<string name="share_via_link_expiration_date_enforced_label">Срок действия</string>
|
||||||
|
<string name="share_via_link_expiration_date_explanation_label">Общедоступная ссылка устареет не позднее, чем через %1$d дней после создания.</string>
|
||||||
|
<string name="share_via_link_default_name_template">Ссылка %1$s</string>
|
||||||
|
<string name="private_link_info">Личная ссылка: её могут использовать только те, у кого есть доступ к файлу или каталогу. Используйте её в качестве постоянной ссылки для себя или чтобы указать другим на файлы в общем доступе</string>
|
||||||
|
<string name="file_private_link_error">Личная ссылка для этого файла недоступна</string>
|
||||||
|
<string name="download_notification_channel_name">Скачивания</string>
|
||||||
|
<string name="download_notification_channel_description">Просмотр имени скачиваемого файла и процесса</string>
|
||||||
|
<string name="upload_notification_channel_name">Загрузки</string>
|
||||||
|
<string name="upload_notification_channel_description">Просмотр имени закачиваемого файла и процесса</string>
|
||||||
|
<string name="media_service_notification_channel_name">Проигрыватель музыки</string>
|
||||||
|
<string name="media_service_notification_channel_description">Просмотр проигрывателя музыки</string>
|
||||||
|
<string name="file_sync_notification_channel_name">Синхронизация файлов</string>
|
||||||
|
<string name="file_sync_notification_channel_description">Просмотр результатов синхронизации файлов</string>
|
||||||
|
<string name="file_sync_conflict_notification_channel_name">Конфликты файлов</string>
|
||||||
|
<string name="file_sync_conflict_notification_channel_description">Смотрите конфликты файлов, когда они происходят</string>
|
||||||
|
|
||||||
|
<string name="rate_dialog_title">Оцените приложение %1$s!</string>
|
||||||
|
<string name="rate_dialog_description">Если вам нравится пользоваться этим приложением, не уделите ли вы минутку, чтобы оценить его? Ваш отзыв очень важен для нас.</string>
|
||||||
|
<string name="button_no_thanks_name">НЕТ, СПАСИБО</string>
|
||||||
|
<string name="button_later_name">ПОЗЖЕ</string>
|
||||||
|
<string name="button_rate_now_name">ОЦЕНИТЬ СЕЙЧАС</string>
|
||||||
|
<plurals name="items_selected_count">
|
||||||
|
<item quantity="one">Выбран %d</item>
|
||||||
|
<item quantity="few">Выбрано %d</item>
|
||||||
|
<item quantity="many">Выбрано %d</item>
|
||||||
|
<item quantity="other">Выбрано %d</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
|
<!-- Welcome to oC intro features -->
|
||||||
|
<string name="welcome_feature_skip_button">Пропустить</string>
|
||||||
|
<string name="welcome_feature_1_title">Управляйте всеми вашими синхронизированными файлами</string>
|
||||||
|
<string name="welcome_feature_1_text">Вы можете копировать, перемещать, удалять</string>
|
||||||
|
<string name="welcome_feature_2_title">Делитесь файлами и папками</string>
|
||||||
|
<string name="welcome_feature_2_text">Вы можете делиться лично или со всеми</string>
|
||||||
|
<string name="welcome_feature_3_title">Много учётных записей</string>
|
||||||
|
<string name="welcome_feature_3_text">Подключайтесь ко всем вашим серверам ownCloud</string>
|
||||||
|
<string name="welcome_feature_4_title">Загрузки камеры</string>
|
||||||
|
<string name="welcome_feature_4_text">Ваши снимки и видео загружаются автоматически</string>
|
||||||
|
<string name="welcome_feature_5_title"> Потоковое видео</string>
|
||||||
|
<string name="welcome_feature_5_text">Проигрывайте ваши ролики, не скачивая их</string>
|
||||||
|
<string name="common_not_found">не найдено</string>
|
||||||
|
|
||||||
|
<string name="supports_oauth2_error">Невозможно было узнать, поддерживается ли OAuth2</string>
|
||||||
|
<string name="get_base_url_error">Узнать базу сервера, было невозможно</string>
|
||||||
|
<string name="prefs_http_logs">Записывать в лог-файл HTTP запросы и ответы</string>
|
||||||
|
|
||||||
|
<string name="spaces_sync_failed">Пространства не могут быть обновлены</string>
|
||||||
|
|
||||||
|
<!-- Scoped storage migration wizard -->
|
||||||
|
<string name="scoped_storage_wizard_explanation">Найдено %1$s данных на вашем внешнем хранилище. Он будет перемещен в безопасное хранилище на вашем устройстве. Оставшиеся файлы будут удалены из вашего внешнего хранилища после переноса, чтобы избежать дублирования и уязвимости.</string>
|
||||||
|
<string name="scoped_storage_wizard_intro">Ваше приложение было обновлено. Это обновление приложения переносит ваши файлы в более безопасное место на вашем устройстве. Эта одноразовая миграция данных обязательна и может занять до минуты или больше.</string>
|
||||||
|
<string name="scoped_storage_wizard_intro_button">Давайте начнём</string>
|
||||||
|
<string name="scoped_storage_wizard_migrate_and_clean_button">Перенести сейчас</string>
|
||||||
|
<string name="scoped_storage_wizard_migration_completed">Перенос завершен успешно. Ваши файлы теперь в большей безопасности, чем когда-либо прежде.</string>
|
||||||
|
<string name="scoped_storage_wizard_migration_completed_access_files_button">Доступ к вашим файлам</string>
|
||||||
|
<string name="scoped_storage_wizard_migration_in_progress">Перенос ваших файлов. Пожалуйста, не выключайте устройство.</string>
|
||||||
|
<string name="scoped_storage_wizard_migration_in_progress_button">Работает… Пожалуйста, подождите</string>
|
||||||
|
<string name="scoped_storage_wizard_title">Больше безопасности для ваших файлов</string>
|
||||||
|
|
||||||
|
<!-- Security Enforced -->
|
||||||
|
<string name="security_enforced_title">Пожалуйста, выберите вариант блокировки приложения:</string>
|
||||||
|
<string name="security_enforced_first_option">Блокировка кодом</string>
|
||||||
|
<string name="security_enforced_second_option">Шаблон заблокирован</string>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Webfinger -->
|
||||||
|
<string name="global_proceed">Продолжить</string>
|
||||||
|
<string name="error_webfinger_username_empty">Имя пользователя пусто</string>
|
||||||
|
|
||||||
|
<!-- Release Notes -->
|
||||||
|
<string name="release_notes_header">Новое в %1$s</string>
|
||||||
|
<string name="release_notes_proceed">Продолжить</string>
|
||||||
|
<string name="release_notes_icon">Иконка к примечанию релиза</string>
|
||||||
|
<string name="release_notes_bugfixes_title">Незначительные исправления ошибок</string>
|
||||||
|
<string name="release_notes_bugfixes_subtitle">Некоторые ошибки были исправлены для улучшения работы приложения</string>
|
||||||
|
<!-- Open in web -->
|
||||||
|
<string name="ic_action_open_with_web">Открыть в %1$s (веб)</string>
|
||||||
|
<string name="open_in_web_error_generic">Не удалось открыть в сети</string>
|
||||||
|
<string name="open_in_web_error_not_supported">нет приложений, которые поддерживают этот тип файла</string>
|
||||||
|
<string name="open_in_web_error_too_early">Запрашиваемый файл в данный момент недоступен, пожалуйста, попробуйте позже </string>
|
||||||
|
|
||||||
|
<!-- oCIS accounts warning -->
|
||||||
|
<string name="ocis_accounts_warning_title">Предупреждение о бесконечной шкале счетов</string>
|
||||||
|
<string name="ocis_accounts_warning_message">Пожалуйста, удалите учетную запись и войдите снова, чтобы получить возможность использования пространств.</string>
|
||||||
|
<string name="ocis_accounts_warning_checkbox_message">Не показывать снова</string>
|
||||||
|
<string name="ocis_accounts_warning_button">понял</string>
|
||||||
|
|
||||||
|
<string name="tab_label_ascii">Текст</string>
|
||||||
|
|
||||||
|
<string name="apply_to_all_conflicts">Применить ко всем %1$s конфликтам </string>
|
||||||
|
<string name="log_file_downloaded">загружен файл журнала</string>
|
||||||
|
<string name="go_to_download_folder">Скачивания</string>
|
||||||
|
<string name="log_file_downloaded_description">Файл журнала %1$s был правильно загружен в папку \"Загрузки\".</string>
|
||||||
|
|
||||||
|
<!-- DeepLinks -->
|
||||||
|
<string name="invalid_deep_link_format">Недопустимый формат ссылки</string>
|
||||||
|
<string name="deep_link_user_no_access">Пользователь не имеет доступа к файлу</string>
|
||||||
|
<string name="deep_link_loading">Открытие файлов по ссылке</string>
|
||||||
|
|
||||||
|
<string name="fab_refresh_text">Обновить</string>
|
||||||
|
<string name="fab_refresh_sync_in_progress">Синхронизация уже выполняется</string>
|
||||||
|
|
||||||
|
<string name="password_policy_min_characters">иметь длину как минимум 1%1$d символов </string>
|
||||||
|
<string name="password_policy_max_characters">иметь длину как минимум 1%1$d символов </string>
|
||||||
|
<string name="password_policy_min_digits">Как минимум %1$d цифры</string>
|
||||||
|
<string name="password_policy_lowercase_characters">Как минимум %1$d специальных символов</string>
|
||||||
|
<string name="password_policy_uppercase_characters">Не менее %1$d символов верхнего регистра</string>
|
||||||
|
<string name="password_policy_intro">Пожалуйста, введите пароль, соответствующий следующим критериям: \n</string>
|
||||||
|
<string name="password_policy_min_special_character">Не менее %1$d специальных символов: \"%2$s\"</string>
|
||||||
|
<string name="password_policy_error_password_banned">К сожалению, ваш пароль часто используется. Пожалуйста, выберите более сложный для угадывания пароль для вашей безопасности.</string>
|
||||||
|
|
||||||
|
<string name="prefs_delete_local_files_entries_never">Никогда</string>
|
||||||
|
<string name="prefs_delete_local_files_entries_1hour">1 час</string>
|
||||||
|
<string name="prefs_delete_local_files_entries_12hours">12 часов</string>
|
||||||
|
<string name="prefs_delete_local_files_entries_1day">1 день</string>
|
||||||
|
<string name="prefs_delete_local_files_entries_30days">30 дней</string>
|
||||||
|
<string name="prefs_delete_local_files">Удалить локальные копии</string>
|
||||||
|
<string name="prefs_delete_local_files_summary">Удалите автоматически загруженные файлы, которые недоступны в автономном режиме, если время, прошедшее с момента их последнего использования, превышает выбранное время.\nВыбранное время: %1$s</string>
|
||||||
|
|
||||||
|
<string name="insecure_http_url_title_dialog">Небезопасный HTTP-URL</string>
|
||||||
|
<string name="insecure_http_url_message_dialog">Указанный вами URL-адрес использует протокол HTTP, а не зашифрованный протокол HTTPS. Если вы продолжите, ваше общение не будет зашифровано.</string>
|
||||||
|
<string name="insecure_http_url_continue_button">Продолжить</string>
|
||||||
|
|
||||||
|
<string name="clean_data_account_message">Все загруженные файлы, которые недоступны в оффлайн-режиме, будут удалены из аккаунта %1$s</string>
|
||||||
|
<string name="clean_data_account_title">удалить локальное хранилище</string>
|
||||||
|
<string name="clean_data_account_button_yes">Очистить данные</string>
|
||||||
|
|
||||||
|
<string name="content_description_menu">Меню</string>
|
||||||
|
<string name="content_description_manage_accounts">Управление аккаунтами</string>
|
||||||
|
<string name="content_description_search">Найти</string>
|
||||||
|
<string name="content_description_type_view">Тип просмотра</string>
|
||||||
|
<string name="content_description_image_previewed">Предварительный просмотр изображений</string>
|
||||||
|
<string name="content_description_add_public_link">Добавить публичную ссылку</string>
|
||||||
|
<string name="content_description_get_private_link">Получить приватную ссылку</string>
|
||||||
|
<string name="content_description_add_new_content">Добавить новый контент</string>
|
||||||
|
<string name="content_description_add_new_content_expanded">Добавить новый контент, развернутый</string>
|
||||||
|
<string name="content_description_logo">Лого</string>
|
||||||
|
<string name="content_description_add_share">Добавить долю</string>
|
||||||
|
<string name="content_description_file_operations">%1$s операции</string>
|
||||||
|
<string name="content_description_sort_by_name_ascending">Сортировать %1$s по возрастанию</string>
|
||||||
|
<string name="content_description_sort_by_name_descending">Сортировать %1$s по убыванию</string>
|
||||||
|
<string name="content_description_create_new_folder">Создать новую папку</string>
|
||||||
|
<string name="content_description_cancel_button">Отмена</string>
|
||||||
|
<string name="create_shortcut_dialog_title">Создать ярлык</string>
|
||||||
|
<string name="create_shortcut_dialog_url">URL</string>
|
||||||
|
<string name="create_shortcut_dialog_url_error_no_blanks">URL не должен содержать пробелы</string>
|
||||||
|
<string name="create_shortcut_dialog_url_extension">.url</string>
|
||||||
|
<string name="open_shortcut_description">Этот ярлык указывает на:</string>
|
||||||
|
<string name="open_shortcut_title">Открыть ссылку</string>
|
||||||
|
|
||||||
|
<string name="create_space_dialog_name">Имя</string>
|
||||||
|
<string name="create_space_dialog_subtitle">подзаголовок</string>
|
||||||
|
<string name="create_space_dialog_quota">Квота</string>
|
||||||
|
<string name="disabled_space_label">Отключена</string>
|
||||||
|
<string name="members_title">Участники</string>
|
||||||
|
<string name="link_role_accessibility">Ссылка</string>
|
||||||
|
<string name="button_role_accessibility">Кнопка</string>
|
||||||
|
|
||||||
|
<string name="folder_picker_label">Выбор папки</string>
|
||||||
|
<string name="passcode_label">Пароль</string>
|
||||||
|
<string name="pattern_label">Шаблон</string>
|
||||||
|
<string name="receive_external_files_label">Получить внешние файлы</string>
|
||||||
|
<string name="video_preview_label">Предварительный просмотр видео</string>
|
||||||
|
<string name="release_notes_label">Примечания к выпуску</string>
|
||||||
|
<string name="image_preview_label">Предпросмотр</string>
|
||||||
|
<string name="login_label">Логин</string>
|
||||||
|
<string name="whats_new_label">Что нового</string>
|
||||||
|
<string name="audio_preview_label">Предварительный просмотр аудио</string>
|
||||||
|
<string name="details_label">Подробно</string>
|
||||||
|
<string name="text_preview_label">Предварительный просмотр текст файла</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[*]
|
||||||
|
max_line_length = 150
|
||||||
|
|
||||||
|
[*.{kt, kts}]
|
||||||
|
disabled_rules=no-consecutive-blank-lines,no-wildcard-imports,import-ordering,max-line-length,import-ordering,no-blank-line-before-rbrace,final-newline,indent,no-multi-spaces,comment-spacing,parameter-list-wrapping
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
### Actual behaviour
|
||||||
|
-Tell us what happens
|
||||||
|
|
||||||
|
### Expected behaviour
|
||||||
|
-Tell us what should happen
|
||||||
|
|
||||||
|
### Steps to reproduce
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
|
||||||
|
Can this problem be reproduced with the QSfera server?
|
||||||
|
(url: https://<qsfera-server>)
|
||||||
|
|
||||||
|
|
||||||
|
### Environment data
|
||||||
|
Android version:
|
||||||
|
|
||||||
|
Device model:
|
||||||
|
|
||||||
|
Stock or customized system:
|
||||||
|
|
||||||
|
Android app version:
|
||||||
|
|
||||||
|
Server version:
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
#### Web server error log
|
||||||
|
```
|
||||||
|
Insert your webserver log here
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Server log
|
||||||
|
```
|
||||||
|
Insert your QSfera server log here
|
||||||
|
```
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gradle" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
labels:
|
||||||
|
- "Dependencies"
|
||||||
|
commit-message:
|
||||||
|
prefix: "feat"
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
commit-message:
|
||||||
|
prefix: "feat"
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: Android Unit Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unit_tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v6
|
||||||
|
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew assembleDebug
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./gradlew testDebugUnitTest --continue
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
name: Build unsigned QSfera APK
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
build_flavor:
|
||||||
|
description: "Flavor to build"
|
||||||
|
type: choice
|
||||||
|
required: true
|
||||||
|
default: "qa"
|
||||||
|
options:
|
||||||
|
- qa
|
||||||
|
build_type:
|
||||||
|
description: "Type to build"
|
||||||
|
type: choice
|
||||||
|
required: true
|
||||||
|
default: "Debug" # uppercase for Gradle build type
|
||||||
|
options:
|
||||||
|
- Debug
|
||||||
|
version:
|
||||||
|
description: "Version to build (commit, branch or tag)"
|
||||||
|
type: string
|
||||||
|
default: "main"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_apk:
|
||||||
|
name: Build unsigned QSfera APK
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
artifact_name: ${{ steps.set_artifact.outputs.artifact_name }}
|
||||||
|
commit_hash: ${{ steps.commit.outputs.commit_hash }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout current repo
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.version }}
|
||||||
|
|
||||||
|
- name: Get commit hash
|
||||||
|
id: commit
|
||||||
|
run: |
|
||||||
|
COMMIT_HASH=$(git rev-parse HEAD)
|
||||||
|
echo "Commit hash: $COMMIT_HASH"
|
||||||
|
echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT
|
||||||
|
echo "commit_hash=$COMMIT_HASH" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Setup JDK
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Cache Gradle
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gradle-
|
||||||
|
|
||||||
|
- name: Build APK
|
||||||
|
run: |
|
||||||
|
FLAVOR="${{ inputs.build_flavor }}"
|
||||||
|
BUILD_TYPE="${{ inputs.build_type }}"
|
||||||
|
FLAVOR_TASK="$(tr '[:lower:]' '[:upper:]' <<< "${FLAVOR:0:1}")${FLAVOR:1}"
|
||||||
|
./gradlew clean assemble${FLAVOR_TASK}${BUILD_TYPE} -PversionCode=${{ github.run_number }}
|
||||||
|
|
||||||
|
- name: Set artifact name
|
||||||
|
id: set_artifact
|
||||||
|
run: |
|
||||||
|
DATE=$(date +%Y%m%d_%H%M)
|
||||||
|
COMMIT=$(git rev-parse --short HEAD)
|
||||||
|
FLAVOR="${{ inputs.build_flavor }}"
|
||||||
|
BUILD_TYPE="$(echo "${{ inputs.build_type }}" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
NAME="QSfera-${FLAVOR}-${COMMIT}-${DATE}-${GITHUB_RUN_NUMBER}.apk"
|
||||||
|
|
||||||
|
APK_PATH=$(find ./qsferaApp/build/outputs/apk/$FLAVOR/$BUILD_TYPE/ -name "*.apk" -print -quit)
|
||||||
|
|
||||||
|
echo "$APK_PATH"
|
||||||
|
if [ -z "$APK_PATH" ]; then
|
||||||
|
echo "APK not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$APK_PATH" "./$NAME"
|
||||||
|
echo "$NAME"
|
||||||
|
|
||||||
|
# Publish as output
|
||||||
|
echo "artifact_name=$NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ steps.set_artifact.outputs.artifact_name }}
|
||||||
|
path: ./${{ steps.set_artifact.outputs.artifact_name }}
|
||||||
|
compression-level: 0
|
||||||
|
|
||||||
|
- name: Upload APK to release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: build-tester-apk-${{ github.run_number }}
|
||||||
|
target_commitish: ${{ env.commit_hash }}
|
||||||
|
name: build-tester-apk build ${{ github.run_number }}
|
||||||
|
files: ${{ steps.set_artifact.outputs.artifact_name }}
|
||||||
|
prerelease: true
|
||||||
|
generate_release_notes: false
|
||||||
|
body: Built from ${{ env.commit_hash }}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: Detekt
|
||||||
|
|
||||||
|
# Controls when the action will run. Workflow runs when manually triggered using the UI
|
||||||
|
# or API.
|
||||||
|
on:
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
|
jobs:
|
||||||
|
Detekt:
|
||||||
|
# The type of runner that the job will run on
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
# Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies.
|
||||||
|
# See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v6
|
||||||
|
- name: detekt execution
|
||||||
|
run: ./gradlew detekt
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
name: "Validate Gradle Wrapper"
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validation:
|
||||||
|
name: "Validation"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: gradle/wrapper-validation-action@v3
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: "Push source language to Transifex"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push-translations:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: l10n-push-source
|
||||||
|
uses: transifex/cli-action@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.TX_TOKEN }}
|
||||||
|
args: push -s
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# built application files
|
||||||
|
*.apk
|
||||||
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
|
||||||
|
# files for the dex VM
|
||||||
|
*.dex
|
||||||
|
|
||||||
|
# Java class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Local configuration files (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Mac .DS_Store files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Proguard README
|
||||||
|
proguard-project.txt
|
||||||
|
|
||||||
|
# Android Studio and Gradle specific entries
|
||||||
|
.gradle
|
||||||
|
*.iml
|
||||||
|
build
|
||||||
|
|
||||||
|
# android sdk captures folder
|
||||||
|
captures
|
||||||
|
|
||||||
|
# ignore lint html and xml output
|
||||||
|
lint-*ml
|
||||||
|
|
||||||
|
.idea/*
|
||||||
|
!.idea/codeStyles/
|
||||||
|
|
||||||
|
# Prevent exidental commits of build folders
|
||||||
|
opencloudApp/release
|
||||||
|
|
||||||
|
# fastlane stuff (some)
|
||||||
|
fastlane/README.md
|
||||||
|
fastlane/report.xml
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<option name="LINE_SEPARATOR" value=" " />
|
||||||
|
<option name="RIGHT_MARGIN" value="150" />
|
||||||
|
<option name="WRAP_WHEN_TYPING_REACHES_RIGHT_MARGIN" value="true" />
|
||||||
|
<JavaCodeStyleSettings>
|
||||||
|
<option name="FIELD_NAME_PREFIX" value="m" />
|
||||||
|
<option name="STATIC_FIELD_NAME_PREFIX" value="s" />
|
||||||
|
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99999" />
|
||||||
|
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99999" />
|
||||||
|
<option name="IMPORT_LAYOUT_TABLE">
|
||||||
|
<value>
|
||||||
|
<package name="android" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="javax" withSubpackages="true" static="false" />
|
||||||
|
<package name="java" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="" withSubpackages="true" static="true" />
|
||||||
|
<emptyLine />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</JavaCodeStyleSettings>
|
||||||
|
<JetCodeStyleSettings>
|
||||||
|
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||||
|
<value>
|
||||||
|
<package name="kotlinx.android.synthetic" withSubpackages="true" static="false" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||||
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<MarkdownNavigatorCodeStyleSettings>
|
||||||
|
<option name="RIGHT_MARGIN" value="72" />
|
||||||
|
</MarkdownNavigatorCodeStyleSettings>
|
||||||
|
<XML>
|
||||||
|
<option name="XML_ATTRIBUTE_WRAP" value="0" />
|
||||||
|
<option name="XML_KEEP_BLANK_LINES" value="1" />
|
||||||
|
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
|
||||||
|
</XML>
|
||||||
|
<codeStyleSettings language="JAVA">
|
||||||
|
<option name="RIGHT_MARGIN" value="150" />
|
||||||
|
<option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
|
||||||
|
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
|
||||||
|
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
|
||||||
|
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||||
|
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
|
||||||
|
<option name="IF_BRACE_FORCE" value="3" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WRAP_LONG_LINES" value="true" />
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<FINAL>true</FINAL>
|
||||||
|
<PUBLIC>true</PUBLIC>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<FINAL>true</FINAL>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<PUBLIC>true</PUBLIC>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<INITIALIZER_BLOCK>true</INITIALIZER_BLOCK>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<FINAL>true</FINAL>
|
||||||
|
<PUBLIC>true</PUBLIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<FINAL>true</FINAL>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<PUBLIC>true</PUBLIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<INITIALIZER_BLOCK>true</INITIALIZER_BLOCK>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<CONSTRUCTOR>true</CONSTRUCTOR>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<METHOD>true</METHOD>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<METHOD>true</METHOD>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<ENUM>true</ENUM>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<INTERFACE>true</INTERFACE>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<CLASS>true</CLASS>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<CLASS>true</CLASS>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_NAMESPACE>Namespace:</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_NAMESPACE>Namespace:</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:layout_width</NAME>
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:layout_height</NAME>
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:layout_.*</NAME>
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:width</NAME>
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:height</NAME>
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="kotlin">
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
|
||||||
|
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||||
|
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
|
||||||
|
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||||
|
<option name="FIELD_ANNOTATION_WRAP" value="0" />
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[main]
|
||||||
|
host = https://www.transifex.com
|
||||||
|
|
||||||
|
[o:opencloud-eu:p:opencloud-eu:r:android]
|
||||||
|
file_filter = opencloudApp/src/main/res/values-<lang>/strings.xml
|
||||||
|
source_file = opencloudApp/src/main/res/values/strings.xml
|
||||||
|
source_lang = en
|
||||||
|
lang_map = zh_HK: zh-rHK, ar_JO: ar-rJO, es_SV: es-rSV, fa_IR: fa-rIR, ky_KG: ky-rKG, ne_NP: ne-rNP, pt_PT: pt-rPT, arn_CL: arn-rCL, de_AT: de-rAT, es_ES: es-rES, es_US: es-rUS, lb_LU: lb-rLU, ta_IN: ta-rIN, tr_TR: tr-rTR, as_IN: as-rIN, az_AZ: az-rAZ, en_IE: en-rIE, es_CL: es-rCL, gu_IN: gu-rIN, ps_AF: ps-rAF, dsb_DE: dsb-rDE, prs_AF: prs-rAF, quz_EC: quz-rEC, se_FI: se-rFI, ug_CN: ug-rCN, es_VE: es-rVE, lo_LA: lo-rLA, nl_BE: nl-rBE, sq_AL: sq-rAL, yo_NG: yo-rNG, af_ZA: af-rZA, ar_MA: ar-rMA, es_GT: es-rGT, tzm_DZ: tzm-rDZ, bs_BA: bs-rBA, en_CA: en-rCA, en_IN: en-rIN, ja_JP: ja-rJP, lv_LV: lv-rLV, sr_ME: sr-rME, uk_UA: uk-rUA, de_CH: de-rCH, en_PH: en-rPH, es_MX: es-rMX, ga_IE: ga-rIE, sr@latin: sr-rSP, ar_YE: ar-rYE, be_BY: be-rBY, en_AU: en-rAU, en@pirate: en-rpirate, fy_NL: fy-rNL, hu_HU: hu-rHU, hy_AM: hy-rAM, rw_RW: rw-rRW, id_ID: id-rID, ig_NG: ig-rNG, iu_CA: iu-rCA, my_MM: my, oc_FR: oc-rFR, quz_BO: quz-rBO, se_NO: se-rNO, smj_SE: smj-rSE, ar_EG: ar-rEG, cs_CZ: cs-rCZ, kn_IN: kn-rIN, syr_SY: syr-rSY, zh_MO: zh-rMO, de_LU: de-rLU, kk_KZ: kk-rKZ, mk_MK: mk-rMK, sr_BA: sr-rBA, sr_RS: sr-rRS, vi_VN: vi-rVN, ar_SA: ar-rSA, ca_ES: ca-rES, cy_GB: cy-rGB, es_AR: es-rAR, fr_LU: fr-rLU, mn_MN: mn-rMN, mr_IN: mr-rIN, smj_NO: smj-rNO, br_FR: br-rFR, es_PE: es-rPE, eu_ES: eu-rES, fi_FI: fi-rFI, sw_KE: sw-rKE, tt_RU: tt-rRU, ar_AE: ar-rAE, ar_IQ: ar-rIQ, en_SG: en-rSG, es_EC: es-rEC, hsb_DE: hsb-rDE, sv_SE: sv-rSE, ur_PK: ur-rPK, am_ET: am-rET, es_DO: es-rDO, ku_IQ: ku-rIQ, mi_NZ: mi-rNZ, or_IN: or-rIN, zu_ZA: zu-rZA, es_CR: es-rCR, es_HN: es-rHN, es_PR: es-rPR, et_EE: et-rEE, hi_IN: hi-rIN, ms_BN: ms-rBN, se_SE: se-rSE, xh_ZA: xh-rZA, en_US: en-rUS, hr_BA: hr-rBA, is_IS: is-rIS, it_CH: it-rCH, nb_NO: nb-rNO, sa_IN: sa-rIN, sl_SI: sl-rSI, tg_TJ: tg-rTJ, ar_LB: ar-rLB, es_UY: es-rUY, ha_NG: ha-rNG, ii_CN: ii-rCN, ms_MY: ms-rMY, smn_FI: smn-rFI, ta_LK: ta-rLK, tn_ZA: tn-rZA, ar_BH: ar-rBH, de_DE: de-rDE, dv_MV: dv-rMV, en_BZ: en-rBZ, fr_CH: fr-rCH, he_IL: he-rIL, hr_HR: hr-rHR, nso_ZA: nso-rZA, es_NI: es-rNI, pa_IN: pa-rIN, pl_PL: pl-rPL, quz_PE: quz-rPE, sk_SK: sk-rSK, th_TH: th-rTH, uz_UZ: uz-rUZ, zh_CN: zh-rCN, de_LI: de-rLI, el_GR: el-rGR, fo_FO: fo-rFO, gsw_FR: gsw-rFR, tk_TM: tk-rTM, zh_CN.GB2312: zh-rBG, zh_TW: zh-rTW, en_GB: en-rGB, en_ZW: en-rZW, mn_CN: mn-rCN, ru_RU: ru-rRU, es_PA: es-rPA, es_PY: es-rPY, gl_ES: gl-rES, rm_CH: rm-rCH, si_LK: si-rLK, sr_CS: sr-rCS, bg_BG: bg-rBG, en_ZA: en-rZA, fr_CA: fr-rCA, ro_RO: ro-rRO, sv_FI: sv-rFI, ar_OM: ar-rOM, bn_BD: bn-rBD, bn_IN: bn-rIN, es_BO: es-rBO, fil_PH: fil-rPH, mt_MT: mt-rMT, qut_GT: qut-rGT, sms_FI: sms-rFI, ba_RU: ba-rRU, en_NZ: en-rNZ, en_TT: en-rTT, fr_BE: fr-rBE, it_IT: it-rIT, te_IN: te-rIN, co_FR: co-rFR, fr_FR: fr-rFR, gd_GB: gd-rGB, ko_KR: ko-rKR, lt_LT: lt-rLT, moh_CA: moh-rCA, nl_NL: nl-rNL, sma_SE: sma-rSE, ar_SY: ar-rSY, bo_CN: bo-rCN, es_CO: es-rCO, km_KH: km-rKH, wo_SN: wo-rSN, zh_SG: zh-rSG, ar_DZ: ar-rDZ, ar_LY: ar-rLY, ar_QA: ar-rQA, nn_NO: nn-rNO, sma_NO: sma-rNO, en_JM: en-rJM, en_MY: en-rMY, ka_GE: ka-rGE, kok_IN: kok-rIN, ml_IN: ml-rIN, pt_BR: pt-rBR, sah_RU: sah-rRU, ar_KW: ar-rKW, ar_TN: ar-rTN, da_DK: da-rDK, fr_MC: fr-rMC, kl_GL: kl-rGL
|
||||||
|
replace_edited_strings = false
|
||||||
|
keep_translations = false
|
||||||
|
type = ANDROID
|
||||||
|
minimum_perc = 20
|
||||||
|
resource_name = android
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## Contributing
|
||||||
|
|
||||||
|
Use short-lived branches from `main` and keep changes scoped to one feature, fix, or maintenance task.
|
||||||
|
|
||||||
|
Suggested branch prefixes:
|
||||||
|
|
||||||
|
* `feature/`
|
||||||
|
* `fix/`
|
||||||
|
* `improvement/`
|
||||||
|
* `technical/`
|
||||||
|
|
||||||
|
Before opening a pull request, run the relevant Gradle build and test tasks for the modules you changed.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\gradlew.bat :qsferaApp:assembleOriginalDebug
|
||||||
|
.\gradlew.bat :qsferaComLibrary:testDebugUnitTest :qsferaDomain:testDebugUnitTest :qsferaData:testDebugUnitTest :qsferaApp:testOriginalDebugUnitTest
|
||||||
|
```
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
this service if you wish), that you receive source code or can get it
|
||||||
|
if you want it, that you can change the software or use pieces of it
|
||||||
|
in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. You must make sure that they, too, receive or can get the
|
||||||
|
source code. And you must show them these terms so they know their
|
||||||
|
rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on, we
|
||||||
|
want its recipients to know that what they have is not the original, so
|
||||||
|
that any problems introduced by others will not reflect on the original
|
||||||
|
authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running the Program is not restricted, and the output from the Program
|
||||||
|
is covered only if its contents constitute a work based on the
|
||||||
|
Program (independent of having been made by running the Program).
|
||||||
|
Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's
|
||||||
|
source code as you receive it, in any medium, provided that you
|
||||||
|
conspicuously and appropriately publish on each copy an appropriate
|
||||||
|
copyright notice and disclaimer of warranty; keep intact all the
|
||||||
|
notices that refer to this License and to the absence of any warranty;
|
||||||
|
and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion
|
||||||
|
of it, thus forming a work based on the Program, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in
|
||||||
|
whole or in part contains or is derived from the Program or any
|
||||||
|
part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively
|
||||||
|
when run, you must cause it, when started running for such
|
||||||
|
interactive use in the most ordinary way, to print or display an
|
||||||
|
announcement including an appropriate copyright notice and a
|
||||||
|
notice that there is no warranty (or else, saying that you provide
|
||||||
|
a warranty) and that users may redistribute the program under
|
||||||
|
these conditions, and telling the user how to view a copy of this
|
||||||
|
License. (Exception: if the Program itself is interactive but
|
||||||
|
does not normally print such an announcement, your work based on
|
||||||
|
the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
under Section 2) in object code or executable form under the terms of
|
||||||
|
Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable
|
||||||
|
source code, which must be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three
|
||||||
|
years, to give any third party, for a charge no more than your
|
||||||
|
cost of physically performing source distribution, a complete
|
||||||
|
machine-readable copy of the corresponding source code, to be
|
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer
|
||||||
|
to distribute corresponding source code. (This alternative is
|
||||||
|
allowed only for noncommercial distribution and only if you
|
||||||
|
received the program in object code or executable form with such
|
||||||
|
an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source
|
||||||
|
code means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to
|
||||||
|
control compilation and installation of the executable. However, as a
|
||||||
|
special exception, the source code distributed need not include
|
||||||
|
anything that is normally distributed (in either source or binary
|
||||||
|
form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component
|
||||||
|
itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering
|
||||||
|
access to copy from a designated place, then offering equivalent
|
||||||
|
access to copy the source code from the same place counts as
|
||||||
|
distribution of the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
except as expressly provided under this License. Any attempt
|
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
void, and will automatically terminate your rights under this License.
|
||||||
|
However, parties who have received copies, or rights, from you under
|
||||||
|
this License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Program or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Program (or any work based on the
|
||||||
|
Program), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute or modify the Program subject to
|
||||||
|
these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system, which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding
|
||||||
|
those countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free Software
|
||||||
|
Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
of preserving the free status of all derivatives of our free software and
|
||||||
|
of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||||
|
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||||
|
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||||
|
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||||
|
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||||
|
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||||
|
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||||
|
REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||||
|
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||||
|
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||||
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License version 2,
|
||||||
|
as published by the Free Software Foundation.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1989
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
This General Public License does not permit incorporating your program into
|
||||||
|
proprietary programs. If your program is a subroutine library, you may
|
||||||
|
consider it more useful to permit linking proprietary applications with the
|
||||||
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# КуСфера Android app
|
||||||
|
|
||||||
|
Android client project branded as `КуСфера`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
The Gradle modules are:
|
||||||
|
|
||||||
|
* `:qsferaApp`
|
||||||
|
* `:qsferaDomain`
|
||||||
|
* `:qsferaData`
|
||||||
|
* `:qsferaComLibrary`
|
||||||
|
* `:qsferaTestUtil`
|
||||||
|
|
||||||
|
Typical local checks:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\gradlew.bat :qsferaApp:assembleOriginalDebug
|
||||||
|
.\gradlew.bat :qsferaComLibrary:testDebugUnitTest :qsferaDomain:testDebugUnitTest :qsferaData:testDebugUnitTest :qsferaApp:testOriginalDebugUnitTest
|
||||||
|
```
|
||||||
|
|
||||||
|
The Android application package is `eu.qsfera.android`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Public store, support, privacy-policy, and help URLs are intentionally not configured in app resources until verified QSfera URLs are available.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
### Setup Information
|
||||||
|
|
||||||
|
These instructions describe how to build the КуСфера Android project locally.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
* Git.
|
||||||
|
* Android Studio or the Android Gradle command line environment.
|
||||||
|
* JDK compatible with the Android Gradle Plugin used by this project.
|
||||||
|
* Android SDK with the compile SDK configured in `build.gradle`.
|
||||||
|
|
||||||
|
### Android Studio
|
||||||
|
|
||||||
|
Open this repository as an existing Gradle project. Android Studio should import the modules from `settings.gradle`.
|
||||||
|
|
||||||
|
### Command Line
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\gradlew.bat :qsferaApp:assembleOriginalDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
If Gradle cannot find Java or the Android SDK, set `JAVA_HOME`, `ANDROID_HOME`, and `ANDROID_SDK_ROOT` for your local machine.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
###################################################################
|
||||||
|
QSfera Android client
|
||||||
|
|
||||||
|
Copyright (C) 2025 QSfera
|
||||||
|
Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
Copyright (C) 2012 Bartek Przybylski
|
||||||
|
###################################################################
|
||||||
|
|
||||||
|
|
||||||
|
###########
|
||||||
|
# LICENSE #
|
||||||
|
###########
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License version 2,
|
||||||
|
as published by the Free Software Foundation.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
The source distribution of this program should include a full copy
|
||||||
|
of the GNU GPL version 2 license in the LICENSE.txt file located
|
||||||
|
in its root directory. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
########################
|
||||||
|
# THIRD PARTY LICENSES #
|
||||||
|
########################
|
||||||
|
|
||||||
|
Both the source and binary distributions of this software contain
|
||||||
|
some third party software. All the third party software included
|
||||||
|
or linked is redistributed under the terms and conditions of their
|
||||||
|
original licenses. These licenses are compatible the GPL license
|
||||||
|
that govern this software, for the purposes they are being used.
|
||||||
|
|
||||||
|
The third party software included and used by this project is:
|
||||||
|
|
||||||
|
* Transifex client.
|
||||||
|
Copyright (C) Transifex.
|
||||||
|
Licensed under GNU General Public License.
|
||||||
|
Placed at third_party/transifex-client.
|
||||||
|
Used as a helper tool, not included in the QSfera client APK.
|
||||||
|
Original license document included at third_party/transifex-client/LICENSE.
|
||||||
|
See http://help.transifex.com/features/client/
|
||||||
|
|
||||||
|
* floatingactionbutton 1.10.1.
|
||||||
|
Copyright (c) 2014 Jerzy Chalupski
|
||||||
|
Licensed under Apache License, Version 2.0.
|
||||||
|
placed at libs/com-getbase-floatingactionbutton-1-10-0-exploded-aar has been exploded by ownCloud GmbH.
|
||||||
|
See https://github.com/futuresimple/android-floating-action-button
|
||||||
|
|
||||||
|
* PatternLockView, version 1.0.0
|
||||||
|
Copyright (C) 2017 aritraroy
|
||||||
|
Licensed under Apache License, Version 2.0.
|
||||||
|
Included in the QSfera client APK.
|
||||||
|
See https://github.com/aritraroy/PatternLockView
|
||||||
|
|
||||||
|
* Glide, version 4.6.1
|
||||||
|
Copyright (C) 2014 Google, Inc.
|
||||||
|
Licensed under BSD, part MIT and Apache 2.0.
|
||||||
|
Included in the QSfera client APK.
|
||||||
|
See https://github.com/bumptech/glide
|
||||||
|
|
||||||
|
* PhotoView, version 2.0.0
|
||||||
|
Copyright (C) 2017 Chris Banes
|
||||||
|
Licensed under Apache License, Version 2.0.
|
||||||
|
Included in the QSfera client APK.
|
||||||
|
See https://github.com/chrisbanes/PhotoView
|
||||||
|
|
||||||
|
* OkHttp, version 4.6.0
|
||||||
|
Licensed under Apache License, Version 2.0.
|
||||||
|
Included in the QSfera client APK.
|
||||||
|
See https://github.com/square/okhttp
|
||||||
|
|
||||||
|
* Dav4Jvm
|
||||||
|
Licensed under Mozilla Public License, Version 2.0.
|
||||||
|
Included in the QSfera client APK.
|
||||||
|
See https://gitlab.com/bitfireAT/dav4jvm
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
No verified public QSfera troubleshooting URL is configured in this repository.
|
||||||
|
|
||||||
|
For local build setup, see `SETUP.md`.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
buildscript {
|
||||||
|
ext {
|
||||||
|
// SDK
|
||||||
|
sdkCompileVersion = 35
|
||||||
|
sdkMinVersion = 26
|
||||||
|
sdkTargetVersion = 35
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
maven { url "https://plugins.gradle.org/m2/" }
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath libs.android.gradlePlugin
|
||||||
|
classpath libs.kotlin.gradlePlugin
|
||||||
|
classpath libs.ktlint.gradlePlugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias libs.plugins.sonarqube
|
||||||
|
alias libs.plugins.ksp apply false
|
||||||
|
alias libs.plugins.detekt
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url "https://jitpack.io"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
apply plugin: "com.google.devtools.ksp"
|
||||||
|
//apply plugin: "org.jlleitschuh.gradle.ktlint"
|
||||||
|
//apply plugin: "org.sonarqube"
|
||||||
|
apply plugin: "io.gitlab.arturbosch.detekt"
|
||||||
|
}
|
||||||
|
|
||||||
|
//sonarqube {
|
||||||
|
// properties {
|
||||||
|
// property "sonar.projectKey", "qsfera_android"
|
||||||
|
// property "sonar.organization", "qsfera-1"
|
||||||
|
// property "sonar.projectVersion", getBranchName()
|
||||||
|
// property "sonar.host.url", "https://sonarcloud.io"
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
def getBranchName() {
|
||||||
|
def name = "git rev-parse --abbrev-ref HEAD".execute()
|
||||||
|
return name.text.toString().trim()
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
check_license_in_file() {
|
||||||
|
if ! head -n 20 $FILE | grep -q "This program is free software: you can redistribute it and/or modify"
|
||||||
|
then
|
||||||
|
if ! head -n 20 $FILE | grep -q "Licensed under the Apache License, Version 2.0"
|
||||||
|
then
|
||||||
|
echo "$FILE does not contain a current copyright header"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for DIRS in qsferaApp/src qsferaData/src qsferaDomain/src qsferaTestUtil/src
|
||||||
|
do
|
||||||
|
for FILE in $(find $DIRS -name "*.java" -o -name "*.kt")
|
||||||
|
do
|
||||||
|
check_license_in_file
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
./gradlew ktlintFormat
|
||||||
@@ -0,0 +1,927 @@
|
|||||||
|
build:
|
||||||
|
maxIssues: 0
|
||||||
|
excludeCorrectable: false
|
||||||
|
weights:
|
||||||
|
# complexity: 2
|
||||||
|
# LongParameterList: 1
|
||||||
|
# style: 1
|
||||||
|
# comments: 1
|
||||||
|
|
||||||
|
config:
|
||||||
|
validation: true
|
||||||
|
warningsAsErrors: false
|
||||||
|
# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
|
||||||
|
excludes: ''
|
||||||
|
|
||||||
|
processors:
|
||||||
|
active: true
|
||||||
|
exclude:
|
||||||
|
- 'DetektProgressListener'
|
||||||
|
# - 'KtFileCountProcessor'
|
||||||
|
# - 'PackageCountProcessor'
|
||||||
|
# - 'ClassCountProcessor'
|
||||||
|
# - 'FunctionCountProcessor'
|
||||||
|
# - 'PropertyCountProcessor'
|
||||||
|
# - 'ProjectComplexityProcessor'
|
||||||
|
# - 'ProjectCognitiveComplexityProcessor'
|
||||||
|
# - 'ProjectLLOCProcessor'
|
||||||
|
# - 'ProjectCLOCProcessor'
|
||||||
|
# - 'ProjectLOCProcessor'
|
||||||
|
# - 'ProjectSLOCProcessor'
|
||||||
|
# - 'LicenseHeaderLoaderExtension'
|
||||||
|
|
||||||
|
console-reports:
|
||||||
|
active: true
|
||||||
|
exclude:
|
||||||
|
- 'ProjectStatisticsReport'
|
||||||
|
- 'ComplexityReport'
|
||||||
|
- 'NotificationReport'
|
||||||
|
- 'FindingsReport'
|
||||||
|
- 'FileBasedFindingsReport'
|
||||||
|
# - 'LiteFindingsReport'
|
||||||
|
|
||||||
|
output-reports:
|
||||||
|
active: true
|
||||||
|
exclude:
|
||||||
|
# - 'TxtOutputReport'
|
||||||
|
# - 'XmlOutputReport'
|
||||||
|
# - 'HtmlOutputReport'
|
||||||
|
|
||||||
|
comments:
|
||||||
|
active: false
|
||||||
|
AbsentOrWrongFileLicense:
|
||||||
|
active: false
|
||||||
|
licenseTemplateFile: 'license.template'
|
||||||
|
licenseTemplateIsRegex: false
|
||||||
|
CommentOverPrivateFunction:
|
||||||
|
active: false
|
||||||
|
CommentOverPrivateProperty:
|
||||||
|
active: false
|
||||||
|
DeprecatedBlockTag:
|
||||||
|
active: false
|
||||||
|
EndOfSentenceFormat:
|
||||||
|
active: false
|
||||||
|
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
|
||||||
|
KDocReferencesNonPublicProperty:
|
||||||
|
active: false
|
||||||
|
OutdatedDocumentation:
|
||||||
|
active: false
|
||||||
|
matchTypeParameters: true
|
||||||
|
matchDeclarationsOrder: true
|
||||||
|
allowParamOnConstructorProperties: false
|
||||||
|
UndocumentedPublicClass:
|
||||||
|
active: false
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
searchInNestedClass: true
|
||||||
|
searchInInnerClass: true
|
||||||
|
searchInInnerObject: true
|
||||||
|
searchInInnerInterface: true
|
||||||
|
UndocumentedPublicFunction:
|
||||||
|
active: false
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
UndocumentedPublicProperty:
|
||||||
|
active: false
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
|
||||||
|
complexity:
|
||||||
|
active: true
|
||||||
|
CognitiveComplexMethod:
|
||||||
|
active: false
|
||||||
|
threshold: 20
|
||||||
|
ComplexCondition:
|
||||||
|
active: true
|
||||||
|
threshold: 6
|
||||||
|
ComplexInterface:
|
||||||
|
active: false
|
||||||
|
threshold: 10
|
||||||
|
includeStaticDeclarations: false
|
||||||
|
includePrivateDeclarations: false
|
||||||
|
CyclomaticComplexMethod:
|
||||||
|
active: false
|
||||||
|
threshold: 15
|
||||||
|
ignoreSingleWhenExpression: false
|
||||||
|
ignoreSimpleWhenEntries: false
|
||||||
|
ignoreNestingFunctions: false
|
||||||
|
nestingFunctions:
|
||||||
|
- 'also'
|
||||||
|
- 'apply'
|
||||||
|
- 'forEach'
|
||||||
|
- 'isNotNull'
|
||||||
|
- 'ifNull'
|
||||||
|
- 'let'
|
||||||
|
- 'run'
|
||||||
|
- 'use'
|
||||||
|
- 'with'
|
||||||
|
LabeledExpression:
|
||||||
|
active: false
|
||||||
|
ignoredLabels: []
|
||||||
|
LargeClass:
|
||||||
|
active: false
|
||||||
|
threshold: 600
|
||||||
|
LongMethod:
|
||||||
|
active: true
|
||||||
|
threshold: 100
|
||||||
|
LongParameterList:
|
||||||
|
active: false
|
||||||
|
functionThreshold: 6
|
||||||
|
constructorThreshold: 7
|
||||||
|
ignoreDefaultParameters: false
|
||||||
|
ignoreDataClasses: true
|
||||||
|
ignoreAnnotatedParameter: []
|
||||||
|
MethodOverloading:
|
||||||
|
active: true
|
||||||
|
threshold: 6
|
||||||
|
NamedArguments:
|
||||||
|
active: false
|
||||||
|
threshold: 3
|
||||||
|
ignoreArgumentsMatchingNames: false
|
||||||
|
NestedBlockDepth:
|
||||||
|
active: false
|
||||||
|
threshold: 5
|
||||||
|
NestedScopeFunctions:
|
||||||
|
active: false
|
||||||
|
threshold: 1
|
||||||
|
functions: ['kotlin.apply', 'kotlin.run', 'kotlin.with', 'kotlin.let', 'kotlin.also']
|
||||||
|
ReplaceSafeCallChainWithRun:
|
||||||
|
active: false
|
||||||
|
StringLiteralDuplication:
|
||||||
|
active: false
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
threshold: 3
|
||||||
|
ignoreAnnotation: true
|
||||||
|
excludeStringsWithLessThan5Characters: true
|
||||||
|
ignoreStringsRegex: '$^'
|
||||||
|
TooManyFunctions:
|
||||||
|
active: false
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
thresholdInFiles: 11
|
||||||
|
thresholdInClasses: 11
|
||||||
|
thresholdInInterfaces: 11
|
||||||
|
thresholdInObjects: 11
|
||||||
|
thresholdInEnums: 11
|
||||||
|
ignoreDeprecated: false
|
||||||
|
ignorePrivate: false
|
||||||
|
ignoreOverridden: false
|
||||||
|
|
||||||
|
coroutines:
|
||||||
|
active: true
|
||||||
|
GlobalCoroutineUsage:
|
||||||
|
active: true
|
||||||
|
InjectDispatcher:
|
||||||
|
active: false
|
||||||
|
dispatcherNames:
|
||||||
|
- 'IO'
|
||||||
|
- 'Default'
|
||||||
|
- 'Unconfined'
|
||||||
|
RedundantSuspendModifier:
|
||||||
|
active: true
|
||||||
|
SleepInsteadOfDelay:
|
||||||
|
active: true
|
||||||
|
SuspendFunWithCoroutineScopeReceiver:
|
||||||
|
active: true
|
||||||
|
SuspendFunWithFlowReturnType:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
empty-blocks:
|
||||||
|
active: true
|
||||||
|
EmptyCatchBlock:
|
||||||
|
active: true
|
||||||
|
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||||
|
EmptyClassBlock:
|
||||||
|
active: true
|
||||||
|
EmptyDefaultConstructor:
|
||||||
|
active: true
|
||||||
|
EmptyDoWhileBlock:
|
||||||
|
active: true
|
||||||
|
EmptyElseBlock:
|
||||||
|
active: true
|
||||||
|
EmptyFinallyBlock:
|
||||||
|
active: true
|
||||||
|
EmptyForBlock:
|
||||||
|
active: true
|
||||||
|
EmptyFunctionBlock:
|
||||||
|
active: true
|
||||||
|
ignoreOverridden: true
|
||||||
|
EmptyIfBlock:
|
||||||
|
active: true
|
||||||
|
EmptyInitBlock:
|
||||||
|
active: true
|
||||||
|
EmptyKtFile:
|
||||||
|
active: true
|
||||||
|
EmptySecondaryConstructor:
|
||||||
|
active: true
|
||||||
|
EmptyTryBlock:
|
||||||
|
active: true
|
||||||
|
EmptyWhenBlock:
|
||||||
|
active: true
|
||||||
|
EmptyWhileBlock:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
exceptions:
|
||||||
|
active: true
|
||||||
|
ExceptionRaisedInUnexpectedLocation:
|
||||||
|
active: false
|
||||||
|
methodNames:
|
||||||
|
- 'equals'
|
||||||
|
- 'finalize'
|
||||||
|
- 'hashCode'
|
||||||
|
- 'toString'
|
||||||
|
InstanceOfCheckForException:
|
||||||
|
active: true
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
NotImplementedDeclaration:
|
||||||
|
active: true
|
||||||
|
ObjectExtendsThrowable:
|
||||||
|
active: true
|
||||||
|
PrintStackTrace:
|
||||||
|
active: true
|
||||||
|
RethrowCaughtException:
|
||||||
|
active: true
|
||||||
|
ReturnFromFinally:
|
||||||
|
active: true
|
||||||
|
ignoreLabeled: false
|
||||||
|
SwallowedException:
|
||||||
|
active: true
|
||||||
|
ignoredExceptionTypes:
|
||||||
|
- 'InterruptedException'
|
||||||
|
- 'MalformedURLException'
|
||||||
|
- 'NumberFormatException'
|
||||||
|
- 'ParseException'
|
||||||
|
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||||
|
ThrowingExceptionFromFinally:
|
||||||
|
active: true
|
||||||
|
ThrowingExceptionInMain:
|
||||||
|
active: false
|
||||||
|
ThrowingExceptionsWithoutMessageOrCause:
|
||||||
|
active: true
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
exceptions:
|
||||||
|
- 'ArrayIndexOutOfBoundsException'
|
||||||
|
- 'Exception'
|
||||||
|
- 'IllegalArgumentException'
|
||||||
|
- 'IllegalMonitorStateException'
|
||||||
|
- 'IllegalStateException'
|
||||||
|
- 'IndexOutOfBoundsException'
|
||||||
|
- 'NullPointerException'
|
||||||
|
- 'RuntimeException'
|
||||||
|
- 'Throwable'
|
||||||
|
ThrowingNewInstanceOfSameException:
|
||||||
|
active: true
|
||||||
|
TooGenericExceptionCaught:
|
||||||
|
active: false
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
exceptionNames:
|
||||||
|
- 'ArrayIndexOutOfBoundsException'
|
||||||
|
- 'Error'
|
||||||
|
- 'Exception'
|
||||||
|
- 'IllegalMonitorStateException'
|
||||||
|
- 'IndexOutOfBoundsException'
|
||||||
|
- 'NullPointerException'
|
||||||
|
- 'RuntimeException'
|
||||||
|
- 'Throwable'
|
||||||
|
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||||
|
TooGenericExceptionThrown:
|
||||||
|
active: false
|
||||||
|
exceptionNames:
|
||||||
|
- 'Error'
|
||||||
|
- 'Exception'
|
||||||
|
- 'RuntimeException'
|
||||||
|
- 'Throwable'
|
||||||
|
|
||||||
|
formatting:
|
||||||
|
active: false
|
||||||
|
AnnotationOnSeparateLine:
|
||||||
|
active: true
|
||||||
|
indentSize: 4
|
||||||
|
AnnotationSpacing:
|
||||||
|
active: true
|
||||||
|
ArgumentListWrapping:
|
||||||
|
active: true
|
||||||
|
indentSize: 4
|
||||||
|
maxLineLength: 120
|
||||||
|
BlockCommentInitialStarAlignment:
|
||||||
|
active: true
|
||||||
|
ChainWrapping:
|
||||||
|
active: true
|
||||||
|
indentSize: 4
|
||||||
|
ClassName:
|
||||||
|
active: false
|
||||||
|
CommentSpacing:
|
||||||
|
active: true
|
||||||
|
CommentWrapping:
|
||||||
|
active: true
|
||||||
|
indentSize: 4
|
||||||
|
ContentReceiverMapping:
|
||||||
|
active: false
|
||||||
|
maxLineLength: 120
|
||||||
|
indentSize: 4
|
||||||
|
DiscouragedCommentLocation:
|
||||||
|
active: false
|
||||||
|
EnumEntryNameCase:
|
||||||
|
active: true
|
||||||
|
EnumWrapping:
|
||||||
|
active: false
|
||||||
|
intentSize: 4
|
||||||
|
Filename:
|
||||||
|
active: true
|
||||||
|
FinalNewline:
|
||||||
|
active: true
|
||||||
|
insertFinalNewLine: true
|
||||||
|
FunKeywordSpacing:
|
||||||
|
active: true
|
||||||
|
FunctionName:
|
||||||
|
active: false
|
||||||
|
FunctionReturnTypeSpacing:
|
||||||
|
active: true
|
||||||
|
maxLineLength: 120
|
||||||
|
FunctionSignature:
|
||||||
|
active: false
|
||||||
|
forceMultilineWhenParameterCountGreaterOrEqualThan: 2147483647
|
||||||
|
functionBodyExpressionWrapping: 'default'
|
||||||
|
maxLineLength: 120
|
||||||
|
indentSize: 4
|
||||||
|
FunctionStartOfBodySpacing:
|
||||||
|
active: true
|
||||||
|
FunctionTypeReferenceSpacing:
|
||||||
|
active: true
|
||||||
|
IfElseBracing:
|
||||||
|
active: false
|
||||||
|
IfElseWrapping:
|
||||||
|
active: false
|
||||||
|
indentSize: 4
|
||||||
|
ImportOrdering:
|
||||||
|
active: true
|
||||||
|
layout: '*,java.**,javax.**,kotlin.**,^'
|
||||||
|
Indentation:
|
||||||
|
active: true
|
||||||
|
indentSize: 4
|
||||||
|
KdocWrapping:
|
||||||
|
active: true
|
||||||
|
indentSize: 4
|
||||||
|
MaximumLineLength:
|
||||||
|
active: true
|
||||||
|
maxLineLength: 120
|
||||||
|
ignoreBackTickedIdentifier: false
|
||||||
|
ModifierListSpacing:
|
||||||
|
active: true
|
||||||
|
MultiLineIfElse:
|
||||||
|
active: true
|
||||||
|
indentSize: 4
|
||||||
|
MultilineExpressionWrapping:
|
||||||
|
active: false
|
||||||
|
indentSize: 4
|
||||||
|
NoBlankLineBeforeRbrace:
|
||||||
|
active: true
|
||||||
|
NoBlankLineInList:
|
||||||
|
active: false
|
||||||
|
NoBlankLinesInChainedMethodCalls:
|
||||||
|
active: true
|
||||||
|
NoConsecutiveBlankLines:
|
||||||
|
active: true
|
||||||
|
NoConsecutiveComments:
|
||||||
|
active: false
|
||||||
|
NoEmptyClassBody:
|
||||||
|
active: true
|
||||||
|
NoEmptyFirstLineInClassBody:
|
||||||
|
active: false
|
||||||
|
indentSize: 4
|
||||||
|
NoEmptyFirstLineInMethodBlock:
|
||||||
|
active: true
|
||||||
|
NoLineBreakAfterElse:
|
||||||
|
active: true
|
||||||
|
NoLineBreakBeforeAssignment:
|
||||||
|
active: true
|
||||||
|
NoMultipleSpaces:
|
||||||
|
active: true
|
||||||
|
NoSemicolons:
|
||||||
|
active: true
|
||||||
|
NoSingleLineBlockComment:
|
||||||
|
active: false
|
||||||
|
indentSize: 4
|
||||||
|
NoTrailingSpaces:
|
||||||
|
active: true
|
||||||
|
NoUnitReturn:
|
||||||
|
active: true
|
||||||
|
NoUnusedImports:
|
||||||
|
active: true
|
||||||
|
NoWildcardImports:
|
||||||
|
active: true
|
||||||
|
packagesToUseImportOnDemandProperty: 'java.util.*,kotlinx.android.synthetic.**'
|
||||||
|
NullableTypeSpacing:
|
||||||
|
active: true
|
||||||
|
PackageName:
|
||||||
|
active: true
|
||||||
|
ParameterListSpacing:
|
||||||
|
active: false
|
||||||
|
ParameterListWrapping:
|
||||||
|
active: true
|
||||||
|
maxLineLength: 120
|
||||||
|
indentSize: 4
|
||||||
|
ParameterWrapping:
|
||||||
|
active: true
|
||||||
|
indentSize: 4
|
||||||
|
MaxLineLength: 120
|
||||||
|
PropertyName:
|
||||||
|
active: false
|
||||||
|
PropertyWrapping:
|
||||||
|
active: true
|
||||||
|
indentSize: 4
|
||||||
|
maxLineLength: 120
|
||||||
|
SpacingAroundAngleBrackets:
|
||||||
|
active: true
|
||||||
|
SpacingAroundColon:
|
||||||
|
active: true
|
||||||
|
SpacingAroundComma:
|
||||||
|
active: true
|
||||||
|
SpacingAroundCurly:
|
||||||
|
active: true
|
||||||
|
SpacingAroundDot:
|
||||||
|
active: true
|
||||||
|
SpacingAroundDoubleColon:
|
||||||
|
active: true
|
||||||
|
SpacingAroundKeyword:
|
||||||
|
active: true
|
||||||
|
SpacingAroundOperators:
|
||||||
|
active: true
|
||||||
|
SpacingAroundParens:
|
||||||
|
active: true
|
||||||
|
SpacingAroundRangeOperator:
|
||||||
|
active: true
|
||||||
|
SpacingAroundUnaryOperator:
|
||||||
|
active: true
|
||||||
|
SpacingBetweenDeclarationsWithAnnotations:
|
||||||
|
active: true
|
||||||
|
SpacingBetweenDeclarationsWithComments:
|
||||||
|
active: true
|
||||||
|
SpacingBetweenFunctionNameAndOpeningParenthesis:
|
||||||
|
active: true
|
||||||
|
StringTemplate:
|
||||||
|
active: true
|
||||||
|
StringTemplateIndent:
|
||||||
|
active: false
|
||||||
|
indentSize: 4
|
||||||
|
TrailingCommaOnCallSite:
|
||||||
|
active: false
|
||||||
|
useTrailingCommaOnCallSite: true
|
||||||
|
TrailingCommaOnDeclarationSite:
|
||||||
|
active: false
|
||||||
|
useTrailingCommaOnDeclarationSite: true
|
||||||
|
TryCatchFinallySpacing:
|
||||||
|
active: false
|
||||||
|
indentSize: 4
|
||||||
|
TypeArgumentListSpacing:
|
||||||
|
active: false
|
||||||
|
indentSize: 4
|
||||||
|
TypeParameterListSpacing:
|
||||||
|
active: false
|
||||||
|
indentSize: 4
|
||||||
|
UnnecessaryParenthesesBeforeTrailingLambda:
|
||||||
|
active: true
|
||||||
|
Wrapping:
|
||||||
|
active: true
|
||||||
|
indentSize: 4
|
||||||
|
maxLineLength: 120
|
||||||
|
|
||||||
|
libraries:
|
||||||
|
active: false
|
||||||
|
ForbiddenPublicDataClass:
|
||||||
|
active: true
|
||||||
|
ignorePackages: ['*.internal', '*.internal.*']
|
||||||
|
LibraryCodeMustSpecifyReturnType:
|
||||||
|
active: true
|
||||||
|
allowOmitUnit: false
|
||||||
|
LibraryEntitiesShouldNotBePublic:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
naming:
|
||||||
|
active: true
|
||||||
|
BooleanPropertyNaming:
|
||||||
|
active: false
|
||||||
|
allowedPattern: '^(is|has|are)'
|
||||||
|
ClassNaming:
|
||||||
|
active: true
|
||||||
|
classPattern: '[A-Z][a-zA-Z0-9]*'
|
||||||
|
ConstructorParameterNaming:
|
||||||
|
active: true
|
||||||
|
parameterPattern: '[a-z][A-Za-z0-9]*'
|
||||||
|
privateParameterPattern: '[a-z][A-Za-z0-9]*'
|
||||||
|
excludeClassPattern: '$^'
|
||||||
|
EnumNaming:
|
||||||
|
active: true
|
||||||
|
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
|
||||||
|
ForbiddenClassName:
|
||||||
|
active: false
|
||||||
|
forbiddenName: []
|
||||||
|
FunctionMaxLength:
|
||||||
|
active: false
|
||||||
|
maximumFunctionNameLength: 30
|
||||||
|
FunctionMinLength:
|
||||||
|
active: false
|
||||||
|
minimumFunctionNameLength: 3
|
||||||
|
FunctionNaming:
|
||||||
|
active: true
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
functionPattern: '[a-z][a-zA-Z0-9]*'
|
||||||
|
excludeClassPattern: '$^'
|
||||||
|
FunctionParameterNaming:
|
||||||
|
active: true
|
||||||
|
parameterPattern: '[a-z][A-Za-z0-9]*'
|
||||||
|
excludeClassPattern: '$^'
|
||||||
|
InvalidPackageDeclaration:
|
||||||
|
active: true
|
||||||
|
rootPackage: ''
|
||||||
|
requireRootInDeclaration: false
|
||||||
|
LambdaParameterNaming:
|
||||||
|
active: true
|
||||||
|
parameterPattern: '[a-z][A-Za-z0-9]*|_'
|
||||||
|
MatchingDeclarationName:
|
||||||
|
active: true
|
||||||
|
mustBeFirst: false
|
||||||
|
MemberNameEqualsClassName:
|
||||||
|
active: true
|
||||||
|
ignoreOverridden: true
|
||||||
|
NoNameShadowing:
|
||||||
|
active: true
|
||||||
|
NonBooleanPropertyPrefixedWithIs:
|
||||||
|
active: false
|
||||||
|
ObjectPropertyNaming:
|
||||||
|
active: true
|
||||||
|
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||||
|
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||||
|
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
|
||||||
|
PackageNaming:
|
||||||
|
active: true
|
||||||
|
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
|
||||||
|
TopLevelPropertyNaming:
|
||||||
|
active: true
|
||||||
|
constantPattern: '[A-Z][_A-Z0-9]*'
|
||||||
|
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||||
|
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
|
||||||
|
VariableMaxLength:
|
||||||
|
active: false
|
||||||
|
maximumVariableNameLength: 64
|
||||||
|
VariableMinLength:
|
||||||
|
active: false
|
||||||
|
minimumVariableNameLength: 1
|
||||||
|
VariableNaming:
|
||||||
|
active: true
|
||||||
|
variablePattern: '[a-z][A-Za-z0-9]*'
|
||||||
|
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
|
||||||
|
excludeClassPattern: '$^'
|
||||||
|
|
||||||
|
performance:
|
||||||
|
active: true
|
||||||
|
ArrayPrimitive:
|
||||||
|
active: true
|
||||||
|
CouldBeSequence:
|
||||||
|
active: false
|
||||||
|
threshold: 3
|
||||||
|
ForEachOnRange:
|
||||||
|
active: true
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
SpreadOperator:
|
||||||
|
active: true
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
UnnecessaryPartOfBinaryExpression:
|
||||||
|
active: true
|
||||||
|
UnnecessaryTemporaryInstantiation:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
potential-bugs:
|
||||||
|
active: true
|
||||||
|
AvoidReferentialEquality:
|
||||||
|
active: true
|
||||||
|
forbiddenTypePatterns:
|
||||||
|
- 'kotlin.String'
|
||||||
|
CastNullableToNonNullableType:
|
||||||
|
active: true
|
||||||
|
CastToNullableType:
|
||||||
|
active: false
|
||||||
|
Deprecation:
|
||||||
|
active: false
|
||||||
|
DontDowncastCollectionTypes:
|
||||||
|
active: true
|
||||||
|
DoubleMutabilityForCollection:
|
||||||
|
active: true
|
||||||
|
mutableTypes:
|
||||||
|
- 'kotlin.collections.MutableList'
|
||||||
|
- 'kotlin.collections.MutableMap'
|
||||||
|
- 'kotlin.collections.MutableSet'
|
||||||
|
- 'java.util.ArrayList'
|
||||||
|
- 'java.util.LinkedHashSet'
|
||||||
|
- 'java.util.HashSet'
|
||||||
|
- 'java.util.LinkedHashMap'
|
||||||
|
- 'java.util.HashMap'
|
||||||
|
ElseCaseInsteadOfExhaustiveWhen:
|
||||||
|
active: true
|
||||||
|
ignoredSubjectTypes: []
|
||||||
|
EqualsAlwaysReturnsTrueOrFalse:
|
||||||
|
active: true
|
||||||
|
EqualsWithHashCodeExist:
|
||||||
|
active: true
|
||||||
|
ExitOutsideMain:
|
||||||
|
active: true
|
||||||
|
ExplicitGarbageCollectionCall:
|
||||||
|
active: true
|
||||||
|
HasPlatformType:
|
||||||
|
active: true
|
||||||
|
IgnoredReturnValue:
|
||||||
|
active: false
|
||||||
|
restrictToConfig: true
|
||||||
|
returnValueAnnotations:
|
||||||
|
- '*.CheckResult'
|
||||||
|
- '*.CheckReturnValue'
|
||||||
|
ignoreReturnValueAnnotations:
|
||||||
|
- '*.CanIgnoreReturnValue'
|
||||||
|
ignoreFunctionCall: []
|
||||||
|
ImplicitDefaultLocale:
|
||||||
|
active: true
|
||||||
|
ImplicitUnitReturnType:
|
||||||
|
active: true
|
||||||
|
allowExplicitReturnType: false
|
||||||
|
InvalidRange:
|
||||||
|
active: true
|
||||||
|
IteratorHasNextCallsNextMethod:
|
||||||
|
active: true
|
||||||
|
IteratorNotThrowingNoSuchElementException:
|
||||||
|
active: true
|
||||||
|
LateinitUsage:
|
||||||
|
active: false
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
ignoreOnClassesPattern: ''
|
||||||
|
MapGetWithNotNullAssertionOperator:
|
||||||
|
active: true
|
||||||
|
MissingPackageDeclaration:
|
||||||
|
active: true
|
||||||
|
NullCheckOnMutableProperty:
|
||||||
|
active: false
|
||||||
|
NullableToStringCall:
|
||||||
|
active: true
|
||||||
|
PropertyUsedBeforeDeclaration:
|
||||||
|
active: true
|
||||||
|
UnconditionalJumpStatementInLoop:
|
||||||
|
active: true
|
||||||
|
UnnecessaryNotNullCheck:
|
||||||
|
active: true
|
||||||
|
UnnecessaryNotNullOperator:
|
||||||
|
active: true
|
||||||
|
UnnecessarySafeCall:
|
||||||
|
active: true
|
||||||
|
UnreachableCatchBlock:
|
||||||
|
active: true
|
||||||
|
UnreachableCode:
|
||||||
|
active: true
|
||||||
|
UnsafeCallOnNullableType:
|
||||||
|
active: false
|
||||||
|
UnsafeCast:
|
||||||
|
active: true
|
||||||
|
UnusedUnaryOperator:
|
||||||
|
active: true
|
||||||
|
UselessPostfixExpression:
|
||||||
|
active: true
|
||||||
|
WrongEqualsTypeParameter:
|
||||||
|
active: true
|
||||||
|
|
||||||
|
style:
|
||||||
|
active: true
|
||||||
|
AlsoCouldBeApply:
|
||||||
|
active: true
|
||||||
|
BracesOnIfStatements:
|
||||||
|
active: true
|
||||||
|
singleLine: 'consistent'
|
||||||
|
multiLine: 'consistent'
|
||||||
|
BracesOnWhenStatements:
|
||||||
|
active: true
|
||||||
|
singleLine: 'consistent'
|
||||||
|
multiLine: 'consistent'
|
||||||
|
CanBeNonNullable:
|
||||||
|
active: true
|
||||||
|
CascadingCallWrapping:
|
||||||
|
active: false
|
||||||
|
includeElvis: true
|
||||||
|
ClassOrdering:
|
||||||
|
active: true
|
||||||
|
CollapsibleIfStatements:
|
||||||
|
active: true
|
||||||
|
DataClassContainsFunctions:
|
||||||
|
active: false
|
||||||
|
allowOperators: false
|
||||||
|
conversionFunctionPrefix: ['to']
|
||||||
|
DataClassShouldBeImmutable:
|
||||||
|
active: false
|
||||||
|
DestructuringDeclarationWithTooManyEntries:
|
||||||
|
active: false
|
||||||
|
maxDestructuringEntries: 3
|
||||||
|
DoubleNegativeLambda:
|
||||||
|
active: true
|
||||||
|
negativeFunctions: ['takeUnless', 'none']
|
||||||
|
negativeFunctionNameParts: ['not', 'non']
|
||||||
|
EqualsNullCall:
|
||||||
|
active: true
|
||||||
|
EqualsOnSignatureLine:
|
||||||
|
active: true
|
||||||
|
ExplicitCollectionElementAccessMethod:
|
||||||
|
active: true
|
||||||
|
ExplicitItLambdaParameter:
|
||||||
|
active: true
|
||||||
|
ExpressionBodySyntax:
|
||||||
|
active: true
|
||||||
|
includeLineWrapping: true
|
||||||
|
ForbiddenAnnotation:
|
||||||
|
active: false
|
||||||
|
annotations: ['java.lang.SuppressWarnings', 'java.lang.Deprecated', 'java.lang.annotation.Documented', 'java.lang.annotation.Target', 'java.lang.annotation.Retention', 'java.lang.annotation.Repeatable', 'java.lang.annotation.Inherited']
|
||||||
|
ForbiddenComment:
|
||||||
|
active: true
|
||||||
|
allowedPatterns: ''
|
||||||
|
#comments: ['FIXME:', 'STOPSHIP:', 'TODO:']
|
||||||
|
comments: ['STOPSHIP:']
|
||||||
|
ForbiddenImport:
|
||||||
|
active: false
|
||||||
|
imports: []
|
||||||
|
forbiddenPatterns: ''
|
||||||
|
ForbiddenMethodCall:
|
||||||
|
active: true
|
||||||
|
methods: ['kotlin.io.print', 'kotlin.io.println']
|
||||||
|
ForbiddenSuppress:
|
||||||
|
active: false
|
||||||
|
rules: []
|
||||||
|
ForbiddenVoid:
|
||||||
|
active: true
|
||||||
|
ignoreOverridden: false
|
||||||
|
ignoreUsageInGenerics: false
|
||||||
|
FunctionOnlyReturningConstant:
|
||||||
|
active: true
|
||||||
|
ignoreOverridableFunction: true
|
||||||
|
ignoreActualFunction: true
|
||||||
|
excludedFunctions: []
|
||||||
|
LoopWithTooManyJumpStatements:
|
||||||
|
active: false
|
||||||
|
maxJumpCount: 1
|
||||||
|
MagicNumber:
|
||||||
|
active: false
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
ignoreNumbers:
|
||||||
|
- '-1'
|
||||||
|
- '0'
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
ignoreHashCodeFunction: true
|
||||||
|
ignorePropertyDeclaration: false
|
||||||
|
ignoreLocalVariableDeclaration: false
|
||||||
|
ignoreConstantDeclaration: true
|
||||||
|
ignoreCompanionObjectPropertyDeclaration: true
|
||||||
|
ignoreAnnotation: false
|
||||||
|
ignoreNamedArgument: true
|
||||||
|
ignoreEnums: false
|
||||||
|
ignoreRanges: false
|
||||||
|
ignoreExtensionFunctions: true
|
||||||
|
MandatoryBracesLoops:
|
||||||
|
active: true
|
||||||
|
MaxLineLength:
|
||||||
|
active: true
|
||||||
|
maxLineLength: 150
|
||||||
|
excludePackageStatements: true
|
||||||
|
excludeImportStatements: true
|
||||||
|
excludeCommentStatements: false
|
||||||
|
MayBeConst:
|
||||||
|
active: true
|
||||||
|
ModifierOrder:
|
||||||
|
active: true
|
||||||
|
MultilineLambdaItParameter:
|
||||||
|
active: false
|
||||||
|
MultilineRawStringIndentation:
|
||||||
|
active: false
|
||||||
|
indentSize: 4
|
||||||
|
trimmingMethods: ['trimIndent', 'trimMargin']
|
||||||
|
NestedClassesVisibility:
|
||||||
|
active: true
|
||||||
|
NewLineAtEndOfFile:
|
||||||
|
active: true
|
||||||
|
NoTabs:
|
||||||
|
active: false
|
||||||
|
NullableBooleanCheck:
|
||||||
|
active: true
|
||||||
|
ObjectLiteralToLambda:
|
||||||
|
active: true
|
||||||
|
OptionalAbstractKeyword:
|
||||||
|
active: true
|
||||||
|
OptionalUnit:
|
||||||
|
active: true
|
||||||
|
PreferToOverPairSyntax:
|
||||||
|
active: true
|
||||||
|
ProtectedMemberInFinalClass:
|
||||||
|
active: true
|
||||||
|
RedundantExplicitType:
|
||||||
|
active: true
|
||||||
|
RedundantHigherOrderMapUsage:
|
||||||
|
active: true
|
||||||
|
RedundantVisibilityModifierRule:
|
||||||
|
active: true
|
||||||
|
ReturnCount:
|
||||||
|
active: false
|
||||||
|
max: 2
|
||||||
|
excludedFunctions: ['equals']
|
||||||
|
excludeLabeled: false
|
||||||
|
excludeReturnFromLambda: true
|
||||||
|
excludeGuardClauses: false
|
||||||
|
SafeCast:
|
||||||
|
active: true
|
||||||
|
SerialVersionUIDInSerializableClass:
|
||||||
|
active: true
|
||||||
|
SpacingBetweenPackageAndImports:
|
||||||
|
active: true
|
||||||
|
StringShouldBeRawString:
|
||||||
|
active: false
|
||||||
|
maxEscapedCharacterCount: 2
|
||||||
|
ignoredCharacters: []
|
||||||
|
ThrowsCount:
|
||||||
|
active: false
|
||||||
|
max: 2
|
||||||
|
excludeGuardClauses: false
|
||||||
|
TrailingWhitespace:
|
||||||
|
active: true
|
||||||
|
TrimMultilineRawString:
|
||||||
|
active: false
|
||||||
|
trimmingMethods: ['trimIndent', 'trimMargin']
|
||||||
|
UnderscoresInNumericLiterals:
|
||||||
|
active: false
|
||||||
|
acceptableLength: 4
|
||||||
|
allowNonStandardGrouping: false
|
||||||
|
UnnecessaryAbstractClass:
|
||||||
|
active: true
|
||||||
|
UnnecessaryAnnotationUseSiteTarget:
|
||||||
|
active: false
|
||||||
|
UnnecessaryApply:
|
||||||
|
active: true
|
||||||
|
UnnecessaryBackticks:
|
||||||
|
active: false
|
||||||
|
UnnecessaryBracesAroundTrailingLambda:
|
||||||
|
active: true
|
||||||
|
UnnecessaryFilter:
|
||||||
|
active: true
|
||||||
|
UnnecessaryInheritance:
|
||||||
|
active: true
|
||||||
|
UnnecessaryInnerClass:
|
||||||
|
active: true
|
||||||
|
UnnecessaryLet:
|
||||||
|
active: true
|
||||||
|
UnnecessaryParentheses:
|
||||||
|
active: false
|
||||||
|
allowForUnclearPrecedence: true
|
||||||
|
UntilInsteadOfRangeTo:
|
||||||
|
active: true
|
||||||
|
UnusedImports:
|
||||||
|
active: true
|
||||||
|
UnusedParameter:
|
||||||
|
active: true
|
||||||
|
allowedNames: 'ignored|expected'
|
||||||
|
UnusedPrivateClass:
|
||||||
|
active: true
|
||||||
|
UnusedPrivateMember:
|
||||||
|
active: true
|
||||||
|
allowedNames: '(_|ignored|expected|serialVersionUID)'
|
||||||
|
UnusedPrivateProperty:
|
||||||
|
active: true
|
||||||
|
allowedNames: '_|ignored|expected|serialVersionUID'
|
||||||
|
UseAnyOrNoneInsteadOfFind:
|
||||||
|
active: true
|
||||||
|
UseArrayLiteralsInAnnotations:
|
||||||
|
active: true
|
||||||
|
UseCheckNotNull:
|
||||||
|
active: false
|
||||||
|
UseCheckOrError:
|
||||||
|
active: false
|
||||||
|
UseDataClass:
|
||||||
|
active: true
|
||||||
|
allowVars: true
|
||||||
|
UseEmptyCounterpart:
|
||||||
|
active: true
|
||||||
|
UseIfEmptyOrIfBlank:
|
||||||
|
active: true
|
||||||
|
UseIfInsteadOfWhen:
|
||||||
|
active: true
|
||||||
|
ignoreWhenContainingVariableDeclaration: false
|
||||||
|
UseIsNullOrEmpty:
|
||||||
|
active: true
|
||||||
|
UseLet:
|
||||||
|
active: true
|
||||||
|
UseOrEmpty:
|
||||||
|
active: true
|
||||||
|
UseRequire:
|
||||||
|
active: false
|
||||||
|
UseRequireNotNull:
|
||||||
|
active: false
|
||||||
|
UseSumOfInsteadOfFlatMapSize:
|
||||||
|
active: false
|
||||||
|
UselessCallOnNotNull:
|
||||||
|
active: true
|
||||||
|
UtilityClassWithPublicConstructor:
|
||||||
|
active: true
|
||||||
|
VarCouldBeVal:
|
||||||
|
active: true
|
||||||
|
ignoreLateinitVar: false
|
||||||
|
WildcardImport:
|
||||||
|
active: true
|
||||||
|
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||||
|
excludeImports:
|
||||||
|
- 'java.util.*'
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
Welcome to Gradle 7.5!
|
||||||
|
|
||||||
|
Here are the highlights of this release:
|
||||||
|
- Support for Java 18
|
||||||
|
- Support for building with Groovy 4
|
||||||
|
- Much more responsive continuous builds
|
||||||
|
- Improved diagnostics for dependency resolution
|
||||||
|
|
||||||
|
For more details see https://docs.gradle.org/7.5/release-notes.html
|
||||||
|
|
||||||
|
Starting a Gradle Daemon (subsequent builds will be faster)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package_name("eu.qsfera.android")
|
||||||
|
|
||||||
|
# Credentials are read from environment, NOT a file in the repo:
|
||||||
|
# SUPPLY_JSON_KEY — absolute path to the service-account JSON (local dev)
|
||||||
|
# SUPPLY_JSON_KEY_DATA — raw JSON contents (CI; e.g. a GitHub Actions secret)
|
||||||
|
# Set whichever one fits your workflow.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
default_platform(:android)
|
||||||
|
|
||||||
|
def qsfera_env(primary, legacy)
|
||||||
|
ENV[primary].to_s.empty? ? ENV[legacy] : ENV[primary]
|
||||||
|
end
|
||||||
|
|
||||||
|
platform :android do
|
||||||
|
desc "Pull latest translations from Transifex"
|
||||||
|
lane :pull_translations do
|
||||||
|
Dir.chdir("..") do
|
||||||
|
sh("tx", "pull", "-a", "-f", "--skip")
|
||||||
|
#sh("tx" "pull" "--force" "--skip" "--all" "-w" "1" "-m" "onlytranslated")
|
||||||
|
end
|
||||||
|
files = Dir.glob("../qsferaApp/src/main/res/values-*/strings.xml")
|
||||||
|
if files.empty?
|
||||||
|
UI.important("No values-*/strings.xml present after tx pull — nothing was downloaded.")
|
||||||
|
else
|
||||||
|
UI.success("#{files.size} translation file(s) on disk:")
|
||||||
|
files.sort.each { |f| UI.message(" #{f}") }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Build the originalRelease AAB (pulls translations first)"
|
||||||
|
lane :build_release do
|
||||||
|
{
|
||||||
|
"QSFERA_RELEASE_KEYSTORE" => "OC_RELEASE_KEYSTORE",
|
||||||
|
"QSFERA_RELEASE_KEYSTORE_PASSWORD" => "OC_RELEASE_KEYSTORE_PASSWORD",
|
||||||
|
"QSFERA_RELEASE_KEY_ALIAS" => "OC_RELEASE_KEY_ALIAS",
|
||||||
|
"QSFERA_RELEASE_KEY_PASSWORD" => "OC_RELEASE_KEY_PASSWORD"
|
||||||
|
}.each do |primary, legacy|
|
||||||
|
next unless qsfera_env(primary, legacy).to_s.empty?
|
||||||
|
|
||||||
|
UI.user_error!(
|
||||||
|
"#{primary} is not set — release AAB would be unsigned and rejected by Play. " \
|
||||||
|
"#{legacy} is still accepted as a legacy fallback."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
pull_translations
|
||||||
|
gradle(
|
||||||
|
task: "bundle",
|
||||||
|
flavor: "Original",
|
||||||
|
build_type: "Release"
|
||||||
|
)
|
||||||
|
sh("ls", "-l", "../qsferaApp/build/outputs/bundle/originalRelease/qsferaApp-original-release.aab")
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Pull translations, build, and upload to Play Store. Override track with: fastlane deploy track:beta"
|
||||||
|
lane :deploy do |options|
|
||||||
|
track = options[:track] || "internal"
|
||||||
|
build_release
|
||||||
|
upload_to_play_store(
|
||||||
|
track: track,
|
||||||
|
aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
|
||||||
|
skip_upload_apk: true,
|
||||||
|
skip_upload_metadata: true,
|
||||||
|
skip_upload_changelogs: true,
|
||||||
|
skip_upload_images: true,
|
||||||
|
skip_upload_screenshots: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
Excellent file management and collaboration for public authorities, providers and business – or anyone who values ease of use and digital sovereignty.
|
||||||
|
|
||||||
|
File Sync & Share
|
||||||
|
Changes to documents are synchronized in real time, ensuring all team members always have access to the latest version.
|
||||||
|
|
||||||
|
Workspaces
|
||||||
|
Create data rooms that enable access for all team members and promote efficient collaboration. Files and folders can be organized, managed and securely shared in these central areas.
|
||||||
|
|
||||||
|
Smart Search
|
||||||
|
The full-text and metadata search allows you to browse through all documents and files, quickly discovering relevant information. Whether you’re searching for specific terms within the text or metadata such as creation date or author, finding what you need is effortless.
|
||||||
|
|
||||||
|
Web Office
|
||||||
|
With QSfera's integrated office applications, teams can work on documents in real time – whether it’s text, spreadsheets, or presentations.
|
||||||
|
After Width: | Height: | Size: 302 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 313 KiB |
|
After Width: | Height: | Size: 285 KiB |
|
After Width: | Height: | Size: 384 KiB |
|
After Width: | Height: | Size: 576 KiB |
|
After Width: | Height: | Size: 562 KiB |
|
After Width: | Height: | Size: 364 KiB |
@@ -0,0 +1 @@
|
|||||||
|
File Sync, Workspaces, Smart Search & Web Office
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
android.enableJetifier=true
|
||||||
|
android.nonFinalResIds=false
|
||||||
|
android.nonTransitiveRClass=false
|
||||||
|
android.useAndroidX=true
|
||||||
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.overridePathCheck=true
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
[versions]
|
||||||
|
androidGradlePlugin = "8.7.2"
|
||||||
|
androidxActivity = "1.8.0"
|
||||||
|
androidxAnnotation = "1.6.0"
|
||||||
|
androidxAppCompat = "1.6.1"
|
||||||
|
androidxArch = "2.2.0"
|
||||||
|
androidxBiometric = "1.1.0"
|
||||||
|
androidxBrowser = "1.5.0"
|
||||||
|
androidxContraintLayout = "2.1.4"
|
||||||
|
androidxCore = "1.10.1"
|
||||||
|
androidxEnterpriseFeedback = "1.1.0"
|
||||||
|
androidxEspresso = "3.6.1"
|
||||||
|
androidxFragment = "1.5.7"
|
||||||
|
androidxLegacy = "1.0.0"
|
||||||
|
androidxLifecycle = "2.5.1"
|
||||||
|
androidxLifecycleExtensions = "2.2.0"
|
||||||
|
androidxRoom = "2.5.1"
|
||||||
|
androidxSqlite = "2.3.1"
|
||||||
|
androidxTest = "1.6.1"
|
||||||
|
androidxTestExt = "1.2.1"
|
||||||
|
androidxTestMonitor = "1.7.2"
|
||||||
|
androidxTestUiAutomator ="2.3.0"
|
||||||
|
androidxWork = "2.8.1"
|
||||||
|
coil = "2.2.2"
|
||||||
|
detekt = "1.23.8"
|
||||||
|
dexopener = "2.0.5"
|
||||||
|
disklrucache = "2.0.2"
|
||||||
|
media3 ="1.1.1"
|
||||||
|
floatingactionbutton = "1.10.1"
|
||||||
|
glide = "4.15.1"
|
||||||
|
glideToVectorYou = "v2.0.0"
|
||||||
|
junit4 = "4.13.2"
|
||||||
|
kaspresso = "1.6.1"
|
||||||
|
koin = "3.3.3"
|
||||||
|
kotlin = "1.9.20"
|
||||||
|
kotlinxCoroutines = "1.6.4"
|
||||||
|
ksp = "1.9.20-1.0.14"
|
||||||
|
ktlint = "11.1.0"
|
||||||
|
markwon = "4.6.2"
|
||||||
|
material = "1.8.0"
|
||||||
|
mockk = "1.13.13"
|
||||||
|
moshi = "1.15.2"
|
||||||
|
patternlockview = "a90b0d4bf0"
|
||||||
|
photoView = "2.3.0"
|
||||||
|
preference = "1.2.0"
|
||||||
|
sonarqube = "4.0.0.2929"
|
||||||
|
stetho = "1.6.0"
|
||||||
|
timber = "5.0.1"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "androidxActivity" }
|
||||||
|
androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "androidxAnnotation" }
|
||||||
|
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" }
|
||||||
|
androidx-arch-core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "androidxArch" }
|
||||||
|
androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "androidxBiometric" }
|
||||||
|
androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidxBrowser" }
|
||||||
|
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidxContraintLayout" }
|
||||||
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" }
|
||||||
|
androidx-enterprise-feedback = { group = "androidx.enterprise", name = "enterprise-feedback", version.ref = "androidxEnterpriseFeedback" }
|
||||||
|
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "androidxFragment" }
|
||||||
|
androidx-fragment-testing = { group = "androidx.fragment", name = "fragment-testing", version.ref = "androidxFragment" }
|
||||||
|
androidx-legacy-support = { group = "androidx.legacy", name = "legacy-support-v4", version.ref = "androidxLegacy" }
|
||||||
|
androidx-lifecycle-common-java8 = { group = "androidx.lifecycle", name = "lifecycle-common-java8", version.ref = "androidxLifecycle" }
|
||||||
|
androidx-lifecycle-extensions = { group = "androidx.lifecycle", name = "lifecycle-extensions", version.ref = "androidxLifecycleExtensions" }
|
||||||
|
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "androidxLifecycle" }
|
||||||
|
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
|
||||||
|
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" }
|
||||||
|
androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference" }
|
||||||
|
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidxRoom" }
|
||||||
|
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "androidxRoom" }
|
||||||
|
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidxRoom" }
|
||||||
|
androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "androidxRoom" }
|
||||||
|
androidx-sqlite-ktx = { group = "androidx.sqlite", name = "sqlite-ktx", version.ref = "androidxSqlite" }
|
||||||
|
androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidxTest" }
|
||||||
|
androidx-test-espresso-contrib = { group = "androidx.test.espresso", name = "espresso-contrib", version.ref = "androidxEspresso" }
|
||||||
|
androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidxEspresso" }
|
||||||
|
androidx-test-espresso-intents = { group = "androidx.test.espresso", name = "espresso-intents", version.ref = "androidxEspresso" }
|
||||||
|
androidx-test-espresso-web = { group = "androidx.test.espresso", name = "espresso-web", version.ref = "androidxEspresso" }
|
||||||
|
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxTestExt" }
|
||||||
|
androidx-test-monitor = { group = "androidx.test", name = "monitor", version.ref = "androidxTestMonitor" }
|
||||||
|
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTest" }
|
||||||
|
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTest" }
|
||||||
|
androidx-test-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "androidxTestUiAutomator" }
|
||||||
|
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidxWork" }
|
||||||
|
coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
|
||||||
|
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
|
||||||
|
detekt-libraries = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" }
|
||||||
|
dexopener = { group = "com.github.tmurakami", name = "dexopener", version.ref = "dexopener" }
|
||||||
|
disklrucache = { group = "com.jakewharton", name = "disklrucache", version.ref = "disklrucache" }
|
||||||
|
media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }
|
||||||
|
media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
|
||||||
|
floatingactionbutton = { group = "com.getbase", name = "floatingactionbutton", version.ref = "floatingactionbutton" }
|
||||||
|
glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" }
|
||||||
|
glide-vector = { group = "com.github.2coffees1team", name = "GlideToVectorYou", version.ref = "glideToVectorYou" }
|
||||||
|
junit4 = { group = "junit", name = "junit", version.ref = "junit4" }
|
||||||
|
kaspresso = { group = "com.kaspersky.android-components", name = "kaspresso", version.ref = "kaspresso" }
|
||||||
|
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
|
||||||
|
koin-androidx-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin" }
|
||||||
|
koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" }
|
||||||
|
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
|
||||||
|
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
|
||||||
|
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
|
||||||
|
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
|
||||||
|
markwon-core = { group = "io.noties.markwon", name = "core", version.ref = "markwon" }
|
||||||
|
markwon-ext-strikethrough = { group = "io.noties.markwon", name = "ext-strikethrough", version.ref = "markwon" }
|
||||||
|
markwon-ext-tables = { group = "io.noties.markwon", name = "ext-tables", version.ref = "markwon" }
|
||||||
|
markwon-ext-tasklist = { group = "io.noties.markwon", name = "ext-tasklist", version.ref = "markwon" }
|
||||||
|
markwon-html = { group = "io.noties.markwon", name = "html", version.ref = "markwon" }
|
||||||
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
|
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
|
||||||
|
mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" }
|
||||||
|
moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin", version.ref = "moshi" }
|
||||||
|
moshi-kotlin-codegen = { group = "com.squareup.moshi", name = "moshi-kotlin-codegen", version.ref = "moshi" }
|
||||||
|
patternlockview = { group = "com.github.aritraroy.PatternLockView", name = "patternlockview", version.ref = "patternlockview" }
|
||||||
|
photoview = { group = "com.github.chrisbanes", name = "PhotoView", version.ref = "photoView" }
|
||||||
|
stetho = { group = "com.facebook.stetho", name = "stetho", version.ref = "stetho" }
|
||||||
|
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
|
||||||
|
|
||||||
|
# Dependencies of the included build-logic
|
||||||
|
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
|
||||||
|
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||||
|
ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
|
||||||
|
ktlint-gradlePlugin = { group = "org.jlleitschuh.gradle", name = "ktlint-gradle", version.ref = "ktlint" }
|
||||||
|
|
||||||
|
[bundles]
|
||||||
|
espresso = ["androidx-test-espresso-contrib", "androidx-test-espresso-core", "androidx-test-espresso-intents", "androidx-test-espresso-web"]
|
||||||
|
markwon = ["markwon-core", "markwon-ext-tables", "markwon-ext-strikethrough", "markwon-ext-tasklist", "markwon-html"]
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
|
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
|
||||||
|
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/.gradle
|
||||||
|
/.build
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
plugins {
|
||||||
|
`kotlin-dsl`
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
google()
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
dependencyResolutionManagement {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
versionCatalogs {
|
||||||
|
create("libs") {
|
||||||
|
from(files("../gradle/libs.versions.toml"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
-optimizationpasses 5
|
||||||
|
-dontusemixedcaseclassnames
|
||||||
|
-dontskipnonpubliclibraryclasses
|
||||||
|
-dontpreverify
|
||||||
|
-verbose
|
||||||
|
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
|
||||||
|
|
||||||
|
-keep public class * extends android.app.Activity
|
||||||
|
-keep public class * extends android.app.Application
|
||||||
|
-keep public class * extends android.app.Service
|
||||||
|
-keep public class * extends android.content.BroadcastReceiver
|
||||||
|
-keep public class * extends android.content.ContentProvider
|
||||||
|
-keep public class * extends android.app.backup.BackupAgentHelper
|
||||||
|
-keep public class * extends android.preference.Preference
|
||||||
|
-keep public class com.android.vending.licensing.ILicensingService
|
||||||
|
|
||||||
|
-keepclasseswithmembernames class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
public <init>(android.content.Context, android.util.AttributeSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
public <init>(android.content.Context, android.util.AttributeSet, int);
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers enum * {
|
||||||
|
public static **[] values();
|
||||||
|
public static ** valueOf(java.lang.String);
|
||||||
|
}
|
||||||
|
|
||||||
|
-keep class * implements android.os.Parcelable {
|
||||||
|
public static final android.os.Parcelable$Creator *;
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'com.google.devtools.ksp'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-parcelize'
|
||||||
|
|
||||||
|
def commitSHA1 = 'COMMIT_SHA1'
|
||||||
|
def gitRemote = 'GIT_REMOTE'
|
||||||
|
|
||||||
|
def envValue(String primary, String legacy = null) {
|
||||||
|
def value = System.getenv(primary)
|
||||||
|
if (value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return legacy ? System.getenv(legacy) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Data and domain modules
|
||||||
|
implementation project(':qsferaDomain')
|
||||||
|
implementation project(':qsferaData')
|
||||||
|
|
||||||
|
// Kotlin
|
||||||
|
implementation libs.kotlin.stdlib
|
||||||
|
implementation libs.kotlinx.coroutines.core
|
||||||
|
|
||||||
|
// Android X
|
||||||
|
implementation libs.androidx.activity
|
||||||
|
implementation libs.androidx.annotation
|
||||||
|
implementation libs.androidx.appcompat
|
||||||
|
implementation libs.androidx.biometric
|
||||||
|
implementation libs.androidx.constraintlayout
|
||||||
|
implementation libs.androidx.core.ktx
|
||||||
|
implementation libs.androidx.fragment.ktx
|
||||||
|
implementation libs.androidx.legacy.support
|
||||||
|
implementation libs.androidx.lifecycle.common.java8
|
||||||
|
implementation libs.androidx.lifecycle.extensions
|
||||||
|
implementation libs.androidx.lifecycle.livedata.ktx
|
||||||
|
implementation libs.androidx.lifecycle.runtime.ktx
|
||||||
|
implementation libs.androidx.lifecycle.viewmodel.ktx
|
||||||
|
implementation libs.androidx.preference.ktx
|
||||||
|
implementation libs.androidx.room.runtime
|
||||||
|
implementation libs.androidx.sqlite.ktx
|
||||||
|
implementation libs.androidx.work.runtime.ktx
|
||||||
|
implementation(libs.androidx.browser) { because "CustomTabs required for OAuth2 and OIDC" }
|
||||||
|
implementation(libs.androidx.enterprise.feedback) { because "MDM feedback" }
|
||||||
|
|
||||||
|
// Image loading
|
||||||
|
implementation libs.coil
|
||||||
|
implementation libs.glide
|
||||||
|
implementation libs.glide.vector
|
||||||
|
|
||||||
|
// Zooming Android ImageView.
|
||||||
|
implementation libs.photoview
|
||||||
|
|
||||||
|
// Koin dependency injector
|
||||||
|
implementation libs.koin.androidx.workmanager
|
||||||
|
implementation libs.koin.core
|
||||||
|
|
||||||
|
// Miscellaneous
|
||||||
|
implementation libs.disklrucache
|
||||||
|
implementation libs.media3.exoplayer
|
||||||
|
implementation libs.media3.ui
|
||||||
|
implementation libs.floatingactionbutton
|
||||||
|
implementation libs.material
|
||||||
|
implementation libs.patternlockview
|
||||||
|
|
||||||
|
// Markdown Preview
|
||||||
|
implementation libs.bundles.markwon
|
||||||
|
|
||||||
|
// Timber
|
||||||
|
implementation libs.timber
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
testImplementation project(":qsferaTestUtil")
|
||||||
|
testImplementation libs.androidx.arch.core.testing
|
||||||
|
testImplementation libs.junit4
|
||||||
|
testImplementation libs.kotlinx.coroutines.test
|
||||||
|
testImplementation libs.mockk
|
||||||
|
|
||||||
|
// Instrumented tests
|
||||||
|
androidTestImplementation project(":qsferaTestUtil")
|
||||||
|
androidTestImplementation libs.androidx.annotation
|
||||||
|
androidTestImplementation libs.androidx.arch.core.testing
|
||||||
|
androidTestImplementation libs.androidx.test.core
|
||||||
|
androidTestImplementation libs.androidx.test.ext.junit
|
||||||
|
androidTestImplementation libs.androidx.test.rules
|
||||||
|
androidTestImplementation libs.androidx.test.runner
|
||||||
|
androidTestImplementation libs.androidx.test.uiautomator
|
||||||
|
androidTestImplementation libs.bundles.espresso
|
||||||
|
androidTestImplementation libs.dexopener
|
||||||
|
androidTestImplementation(libs.mockk.android) { exclude module: "objenesis" }
|
||||||
|
|
||||||
|
// Kaspresso
|
||||||
|
androidTestImplementation libs.kaspresso
|
||||||
|
|
||||||
|
// Debug
|
||||||
|
debugImplementation libs.androidx.fragment.testing
|
||||||
|
debugImplementation libs.androidx.test.monitor
|
||||||
|
debugImplementation libs.stetho
|
||||||
|
|
||||||
|
// Detekt
|
||||||
|
detektPlugins libs.detekt.formatting
|
||||||
|
detektPlugins libs.detekt.libraries
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations.all {
|
||||||
|
resolutionStrategy {
|
||||||
|
force "androidx.test:core:1.6.1"
|
||||||
|
force "androidx.test:core-ktx:1.6.1"
|
||||||
|
force "androidx.test:monitor:1.7.2"
|
||||||
|
force "androidx.test:runner:1.6.2"
|
||||||
|
force "androidx.test:rules:1.6.1"
|
||||||
|
force "androidx.test.espresso:espresso-core:3.6.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion sdkCompileVersion
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion sdkMinVersion
|
||||||
|
targetSdkVersion sdkTargetVersion
|
||||||
|
|
||||||
|
testInstrumentationRunner "eu.qsfera.android.utils.OCTestAndroidJUnitRunner"
|
||||||
|
|
||||||
|
versionCode = 32
|
||||||
|
versionName = "1.3.4"
|
||||||
|
|
||||||
|
buildConfigField "String", gitRemote, "\"" + getGitOriginRemote() + "\""
|
||||||
|
buildConfigField "String", commitSHA1, "\"" + getLatestGitHash() + "\""
|
||||||
|
|
||||||
|
// 2. If the caller (GitHub) provided a specific number, overwrite the versionCode.
|
||||||
|
if (project.hasProperty('versionCode')) {
|
||||||
|
versionCode project.property('versionCode').toInteger()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
androidTest.java.srcDirs += "src/test-common/java"
|
||||||
|
test.java.srcDirs += "src/test-common/java"
|
||||||
|
|
||||||
|
androidTest {
|
||||||
|
java.srcDirs += ['src/integrationTest/java']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lint {
|
||||||
|
abortOnError false
|
||||||
|
htmlOutput file('../lint-app-report.html')
|
||||||
|
ignoreWarnings false
|
||||||
|
xmlReport false
|
||||||
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
def releaseKeystore = envValue('QSFERA_RELEASE_KEYSTORE', 'OC_RELEASE_KEYSTORE')
|
||||||
|
if (releaseKeystore) {
|
||||||
|
storeFile file(releaseKeystore) // use an absolute path
|
||||||
|
storePassword envValue('QSFERA_RELEASE_KEYSTORE_PASSWORD', 'OC_RELEASE_KEYSTORE_PASSWORD')
|
||||||
|
keyAlias envValue('QSFERA_RELEASE_KEY_ALIAS', 'OC_RELEASE_KEY_ALIAS')
|
||||||
|
keyPassword envValue('QSFERA_RELEASE_KEY_PASSWORD', 'OC_RELEASE_KEY_PASSWORD')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
|
||||||
|
release {
|
||||||
|
if (envValue('QSFERA_RELEASE_KEYSTORE', 'OC_RELEASE_KEYSTORE')) {
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug {
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flavorDimensions "management"
|
||||||
|
productFlavors {
|
||||||
|
original {
|
||||||
|
dimension "management"
|
||||||
|
}
|
||||||
|
//mdm {
|
||||||
|
// dimension "management"
|
||||||
|
//}
|
||||||
|
qa {
|
||||||
|
dimension "management"
|
||||||
|
applicationIdSuffix ".qa"
|
||||||
|
// replace stuff from setup.xml
|
||||||
|
resValue "string", "app_name", "КуСфера QA"
|
||||||
|
resValue "string", "account_type", "eu.qsfera.qa"
|
||||||
|
resValue "string", "authority", "eu.qsfera.qa"
|
||||||
|
resValue "string", "document_provider_authority", "eu.qsfera.qa.documents"
|
||||||
|
resValue "string", "file_provider_authority", "eu.qsfera.qa.files"
|
||||||
|
resValue "string", "search_suggest_authority", "eu.qsfera.qa.search.users_and_groups"
|
||||||
|
resValue "string", "search_suggest_intent_action", "eu.qsfera.qa.search.users_and_groups.action.SHARE_WITH"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationVariants.all { variant ->
|
||||||
|
def appName = envValue('QSFERA_APP_NAME', 'OC_APP_NAME')
|
||||||
|
setOutputFileName(variant, appName, project)
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
packagingOptions {
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unitTests.returnDefaultValues = true
|
||||||
|
animationsDisabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources.excludes.add("META-INF/*")
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace "eu.qsfera.android"
|
||||||
|
testNamespace "eu.qsfera.android.test"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates output file names of a given variant to format
|
||||||
|
// [appName].[variant.versionName].[QSFERA_BUILD_NUMBER]-[variant.name].apk.
|
||||||
|
//
|
||||||
|
// QSFERA_BUILD_NUMBER is an environment variable read directly in this method. If undefined, it's not added.
|
||||||
|
// OC_BUILD_NUMBER is still accepted as a legacy fallback.
|
||||||
|
//
|
||||||
|
// @param variant Build variant instance which output file name will be updated.
|
||||||
|
// @param appName String to use as first part of the new file name. May be undefined, the original
|
||||||
|
// project.archivesBaseName property will be used instead.
|
||||||
|
// @param callerProject Caller project.
|
||||||
|
|
||||||
|
def setOutputFileName(variant, appName, callerProject) {
|
||||||
|
logger.info("Setting new name for output of variant $variant.name")
|
||||||
|
|
||||||
|
def originalFile = variant.outputs[0].outputFile
|
||||||
|
def originalName = originalFile.name
|
||||||
|
println "originalName is $originalName"
|
||||||
|
|
||||||
|
def newName = ""
|
||||||
|
|
||||||
|
if (appName) {
|
||||||
|
newName += appName
|
||||||
|
} else {
|
||||||
|
newName += "QSfera"
|
||||||
|
}
|
||||||
|
|
||||||
|
def versionName = "$variant.mergedFlavor.versionName"
|
||||||
|
if (variant.mergedFlavor.manifestPlaceholders.versionName != null) {
|
||||||
|
versionName = "$variant.mergedFlavor.manifestPlaceholders.versionName"
|
||||||
|
}
|
||||||
|
if (variant.buildType.manifestPlaceholders.versionName != null) {
|
||||||
|
versionName = "$variant.buildType.manifestPlaceholders.versionName"
|
||||||
|
}
|
||||||
|
newName += "_$versionName"
|
||||||
|
|
||||||
|
def buildNumber = envValue('QSFERA_BUILD_NUMBER', 'OC_BUILD_NUMBER')
|
||||||
|
if (buildNumber) {
|
||||||
|
newName += "_$buildNumber"
|
||||||
|
}
|
||||||
|
|
||||||
|
newName += originalName.substring(callerProject.archivesBaseName.length())
|
||||||
|
|
||||||
|
println "$variant.name: newName is $newName"
|
||||||
|
|
||||||
|
variant.outputs.all {
|
||||||
|
outputFileName = newName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static def getLatestGitHash() {
|
||||||
|
def process = "git rev-parse --short HEAD".execute()
|
||||||
|
return process.text.toString().trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
static def getGitOriginRemote() {
|
||||||
|
def process = "git remote -v".execute()
|
||||||
|
def values = process.text.toString().trim().split("\\r\\n|\\n|\\r")
|
||||||
|
|
||||||
|
def found = values.find { it.startsWith("origin") && it.endsWith("(push)") }
|
||||||
|
return found.replace("origin", "").replace("(push)", "").replace(".git", "").trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(io.gitlab.arturbosch.detekt.Detekt).configureEach {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<lint>
|
||||||
|
<issue id="MissingTranslation" severity="ignore" />
|
||||||
|
<issue id="MissingQuantity" severity="ignore" />
|
||||||
|
</lint>
|
||||||
@@ -0,0 +1,826 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Abel García de Prada
|
||||||
|
* @author Juan Carlos Garrote Gascón
|
||||||
|
* @author Jorge Aguado Recio
|
||||||
|
*
|
||||||
|
* Copyright (C) 2024 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
* <p>
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.authentication
|
||||||
|
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.accounts.AccountManager.KEY_ACCOUNT_NAME
|
||||||
|
import android.accounts.AccountManager.KEY_ACCOUNT_TYPE
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.app.Instrumentation
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.espresso.Espresso.closeSoftKeyboard
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.intent.Intents
|
||||||
|
import androidx.test.espresso.intent.Intents.intended
|
||||||
|
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||||
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||||
|
import androidx.test.espresso.matcher.RootMatchers.isDialog
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.Visibility
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.domain.exceptions.NoNetworkConnectionException
|
||||||
|
import eu.qsfera.android.domain.exceptions.QSferaVersionNotSupportedException
|
||||||
|
import eu.qsfera.android.domain.exceptions.ServerNotReachableException
|
||||||
|
import eu.qsfera.android.domain.exceptions.UnauthorizedException
|
||||||
|
import eu.qsfera.android.domain.server.model.ServerInfo
|
||||||
|
import eu.qsfera.android.domain.utils.Event
|
||||||
|
import eu.qsfera.android.extensions.parseError
|
||||||
|
import eu.qsfera.android.presentation.authentication.ACTION_UPDATE_EXPIRED_TOKEN
|
||||||
|
import eu.qsfera.android.presentation.authentication.ACTION_UPDATE_TOKEN
|
||||||
|
import eu.qsfera.android.presentation.authentication.AuthenticationViewModel
|
||||||
|
import eu.qsfera.android.presentation.authentication.BASIC_TOKEN_TYPE
|
||||||
|
import eu.qsfera.android.presentation.authentication.EXTRA_ACCOUNT
|
||||||
|
import eu.qsfera.android.presentation.authentication.EXTRA_ACTION
|
||||||
|
import eu.qsfera.android.presentation.authentication.KEY_AUTH_TOKEN_TYPE
|
||||||
|
import eu.qsfera.android.presentation.authentication.LoginActivity
|
||||||
|
import eu.qsfera.android.presentation.authentication.OAUTH_TOKEN_TYPE
|
||||||
|
import eu.qsfera.android.presentation.authentication.oauth.OAuthViewModel
|
||||||
|
import eu.qsfera.android.presentation.common.UIResult
|
||||||
|
import eu.qsfera.android.presentation.settings.SettingsActivity
|
||||||
|
import eu.qsfera.android.presentation.settings.SettingsViewModel
|
||||||
|
import eu.qsfera.android.providers.ContextProvider
|
||||||
|
import eu.qsfera.android.providers.MdmProvider
|
||||||
|
import eu.qsfera.android.testutil.OC_ACCOUNT
|
||||||
|
import eu.qsfera.android.testutil.OC_AUTH_TOKEN_TYPE
|
||||||
|
import eu.qsfera.android.testutil.OC_BASIC_PASSWORD
|
||||||
|
import eu.qsfera.android.testutil.OC_BASIC_USERNAME
|
||||||
|
import eu.qsfera.android.testutil.OC_INSECURE_SERVER_INFO_BASIC_AUTH
|
||||||
|
import eu.qsfera.android.testutil.OC_SECURE_SERVER_INFO_BASIC_AUTH
|
||||||
|
import eu.qsfera.android.testutil.OC_SECURE_SERVER_INFO_BEARER_AUTH
|
||||||
|
import eu.qsfera.android.utils.CONFIGURATION_SERVER_URL
|
||||||
|
import eu.qsfera.android.utils.CONFIGURATION_SERVER_URL_INPUT_VISIBILITY
|
||||||
|
import eu.qsfera.android.utils.NO_MDM_RESTRICTION_YET
|
||||||
|
import eu.qsfera.android.utils.matchers.assertVisibility
|
||||||
|
import eu.qsfera.android.utils.matchers.isDisplayed
|
||||||
|
import eu.qsfera.android.utils.matchers.isEnabled
|
||||||
|
import eu.qsfera.android.utils.matchers.isFocusable
|
||||||
|
import eu.qsfera.android.utils.matchers.withText
|
||||||
|
import eu.qsfera.android.utils.mockIntentToComponent
|
||||||
|
import eu.qsfera.android.utils.replaceText
|
||||||
|
import eu.qsfera.android.utils.scrollAndClick
|
||||||
|
import eu.qsfera.android.utils.typeText
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.unmockkAll
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.hamcrest.Matchers.allOf
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
class LoginActivityTest {
|
||||||
|
|
||||||
|
private lateinit var activityScenario: ActivityScenario<LoginActivity>
|
||||||
|
|
||||||
|
private lateinit var authenticationViewModel: AuthenticationViewModel
|
||||||
|
private lateinit var oauthViewModel: OAuthViewModel
|
||||||
|
private lateinit var settingsViewModel: SettingsViewModel
|
||||||
|
private lateinit var ocContextProvider: ContextProvider
|
||||||
|
private lateinit var mdmProvider: MdmProvider
|
||||||
|
private lateinit var context: Context
|
||||||
|
|
||||||
|
private lateinit var loginResultLiveData: MutableLiveData<Event<UIResult<String>>>
|
||||||
|
private lateinit var serverInfoLiveData: MutableLiveData<Event<UIResult<ServerInfo>>>
|
||||||
|
private lateinit var supportsOauth2LiveData: MutableLiveData<Event<UIResult<Boolean>>>
|
||||||
|
private lateinit var baseUrlLiveData: MutableLiveData<Event<UIResult<String>>>
|
||||||
|
private lateinit var accountDiscoveryLiveData: MutableLiveData<Event<UIResult<Unit>>>
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
|
|
||||||
|
authenticationViewModel = mockk(relaxed = true)
|
||||||
|
oauthViewModel = mockk(relaxed = true)
|
||||||
|
settingsViewModel = mockk(relaxUnitFun = true)
|
||||||
|
ocContextProvider = mockk(relaxed = true)
|
||||||
|
mdmProvider = mockk(relaxed = true)
|
||||||
|
val accountManager = mockk<AccountManager>(relaxed = true)
|
||||||
|
every { accountManager.getUserData(any(), any()) } returns null
|
||||||
|
every { accountManager.getPassword(any()) } returns null
|
||||||
|
|
||||||
|
mockkStatic(AccountManager::class)
|
||||||
|
every { AccountManager.get(any()) } returns accountManager
|
||||||
|
|
||||||
|
loginResultLiveData = MutableLiveData()
|
||||||
|
serverInfoLiveData = MutableLiveData()
|
||||||
|
supportsOauth2LiveData = MutableLiveData()
|
||||||
|
baseUrlLiveData = MutableLiveData()
|
||||||
|
accountDiscoveryLiveData = MutableLiveData()
|
||||||
|
|
||||||
|
every { authenticationViewModel.loginResult } returns loginResultLiveData
|
||||||
|
every { authenticationViewModel.serverInfo } returns serverInfoLiveData
|
||||||
|
every { authenticationViewModel.supportsOAuth2 } returns supportsOauth2LiveData
|
||||||
|
every { authenticationViewModel.baseUrl } returns baseUrlLiveData
|
||||||
|
every { authenticationViewModel.accountDiscovery } returns accountDiscoveryLiveData
|
||||||
|
every { settingsViewModel.isThereAttachedAccount() } returns false
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
context
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
authenticationViewModel
|
||||||
|
}
|
||||||
|
viewModel {
|
||||||
|
oauthViewModel
|
||||||
|
}
|
||||||
|
viewModel {
|
||||||
|
settingsViewModel
|
||||||
|
}
|
||||||
|
factory {
|
||||||
|
ocContextProvider
|
||||||
|
}
|
||||||
|
factory {
|
||||||
|
mdmProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchTest(
|
||||||
|
showServerUrlInput: Boolean = true,
|
||||||
|
serverUrl: String = "",
|
||||||
|
showLoginBackGroundImage: Boolean = true,
|
||||||
|
showWelcomeLink: Boolean = true,
|
||||||
|
accountType: String = "qsfera",
|
||||||
|
loginWelcomeText: String = "",
|
||||||
|
webfingerLookupServer: String = "",
|
||||||
|
intent: Intent? = null
|
||||||
|
) {
|
||||||
|
every { mdmProvider.getBrandingBoolean(CONFIGURATION_SERVER_URL_INPUT_VISIBILITY, R.bool.show_server_url_input) } returns showServerUrlInput
|
||||||
|
every { mdmProvider.getBrandingString(CONFIGURATION_SERVER_URL, R.string.server_url) } returns serverUrl
|
||||||
|
every { mdmProvider.getBrandingString(NO_MDM_RESTRICTION_YET, R.string.webfinger_lookup_server) } returns webfingerLookupServer
|
||||||
|
every { ocContextProvider.getBoolean(R.bool.use_login_background_image) } returns showLoginBackGroundImage
|
||||||
|
every { ocContextProvider.getBoolean(R.bool.show_welcome_link) } returns showWelcomeLink
|
||||||
|
every { ocContextProvider.getString(R.string.account_type) } returns accountType
|
||||||
|
every { ocContextProvider.getString(R.string.login_welcome_text) } returns loginWelcomeText
|
||||||
|
every { ocContextProvider.getString(R.string.app_name) } returns BRANDED_APP_NAME
|
||||||
|
|
||||||
|
activityScenario = if (intent == null) {
|
||||||
|
ActivityScenario.launch(LoginActivity::class.java)
|
||||||
|
} else {
|
||||||
|
ActivityScenario.launch(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun initialViewStatus_notBrandedOptions() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
assertViewsDisplayed()
|
||||||
|
assertWebfingerFlowDisplayed(webfingerEnabled = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun initialViewStatus_brandedOptions_webfinger() {
|
||||||
|
launchTest(webfingerLookupServer = OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
|
||||||
|
|
||||||
|
assertWebfingerFlowDisplayed(webfingerEnabled = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun initialViewStatus_brandedOptions_serverInfoInSetup() {
|
||||||
|
launchTest(showServerUrlInput = false, serverUrl = OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
|
||||||
|
|
||||||
|
assertViewsDisplayed(
|
||||||
|
showHostUrlFrame = false,
|
||||||
|
showHostUrlInput = false,
|
||||||
|
showCenteredRefreshButton = true,
|
||||||
|
showEmbeddedCheckServerButton = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun initialViewStatus_brandedOptions_serverInfoInSetup_connectionFails() {
|
||||||
|
|
||||||
|
launchTest(showServerUrlInput = false, serverUrl = OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
|
||||||
|
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Error(NoNetworkConnectionException())))
|
||||||
|
|
||||||
|
R.id.centeredRefreshButton.isDisplayed(true)
|
||||||
|
R.id.centeredRefreshButton.scrollAndClick()
|
||||||
|
|
||||||
|
verify(exactly = 1) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) }
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
|
||||||
|
|
||||||
|
R.id.centeredRefreshButton.isDisplayed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun initialViewStatus_brandedOptions_dontUseLoginBackgroundImage() {
|
||||||
|
launchTest(showLoginBackGroundImage = false)
|
||||||
|
|
||||||
|
assertViewsDisplayed(showLoginBackGroundImage = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun initialViewStatus_brandedOptions_dontShowWelcomeLink() {
|
||||||
|
launchTest(showWelcomeLink = false)
|
||||||
|
|
||||||
|
assertViewsDisplayed(showWelcomeLink = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun initialViewStatus_brandedOptions_customWelcomeText() {
|
||||||
|
launchTest(showWelcomeLink = true, loginWelcomeText = CUSTOM_WELCOME_TEXT)
|
||||||
|
|
||||||
|
assertViewsDisplayed(showWelcomeLink = true)
|
||||||
|
|
||||||
|
R.id.welcome_link.withText(CUSTOM_WELCOME_TEXT)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun initialViewStatus_brandedOptions_defaultWelcomeText() {
|
||||||
|
launchTest(showWelcomeLink = true, loginWelcomeText = "")
|
||||||
|
|
||||||
|
assertViewsDisplayed(showWelcomeLink = true)
|
||||||
|
|
||||||
|
R.id.welcome_link.withText(String.format(ocContextProvider.getString(R.string.auth_register), BRANDED_APP_NAME))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkServerInfo_clickButton_callGetServerInfo() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
R.id.hostUrlInput.typeText(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
|
||||||
|
|
||||||
|
R.id.embeddedCheckServerButton.scrollAndClick()
|
||||||
|
|
||||||
|
verify(exactly = 1) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkServerInfo_clickLogo_callGetServerInfo() {
|
||||||
|
launchTest()
|
||||||
|
R.id.hostUrlInput.typeText(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
|
||||||
|
|
||||||
|
R.id.thumbnail.scrollAndClick()
|
||||||
|
|
||||||
|
verify(exactly = 1) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkServerInfo_isLoading_show() {
|
||||||
|
launchTest()
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Loading()))
|
||||||
|
|
||||||
|
with(R.id.server_status_text) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.auth_testing_connection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkServerInfo_isSuccess_updateUrlInput() {
|
||||||
|
launchTest()
|
||||||
|
R.id.hostUrlInput.typeText("demo.qsfera.eu")
|
||||||
|
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
|
||||||
|
|
||||||
|
R.id.hostUrlInput.withText(SECURE_SERVER_INFO_BASIC.baseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkServerInfo_isSuccess_Secure() {
|
||||||
|
launchTest()
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
|
||||||
|
|
||||||
|
with(R.id.server_status_text) {
|
||||||
|
isDisplayed(true)
|
||||||
|
assertVisibility(Visibility.VISIBLE)
|
||||||
|
withText(R.string.auth_secure_connection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkServerInfo_isSuccess_NotSecure() {
|
||||||
|
launchTest()
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Success(INSECURE_SERVER_INFO_BASIC)))
|
||||||
|
|
||||||
|
onView(withText(R.string.insecure_http_url_title_dialog)).check(matches(isDisplayed()))
|
||||||
|
onView(withText(R.string.insecure_http_url_message_dialog)).check(matches(isDisplayed()))
|
||||||
|
onView(withText(R.string.insecure_http_url_continue_button)).inRoot(isDialog()).check(matches(isDisplayed())).perform(click())
|
||||||
|
|
||||||
|
with(R.id.server_status_text) {
|
||||||
|
isDisplayed(true)
|
||||||
|
assertVisibility(Visibility.VISIBLE)
|
||||||
|
withText(R.string.auth_connection_established)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkServerInfo_isSuccess_Basic() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
|
||||||
|
|
||||||
|
checkBasicFieldsVisibility(loginButtonShouldBeVisible = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkServerInfo_isSuccess_Bearer() {
|
||||||
|
Intents.init()
|
||||||
|
launchTest()
|
||||||
|
avoidOpeningChromeCustomTab()
|
||||||
|
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BEARER)))
|
||||||
|
|
||||||
|
checkBearerFieldsVisibility()
|
||||||
|
Intents.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkServerInfo_isSuccess_basicModifyUrlInput() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
|
||||||
|
|
||||||
|
checkBasicFieldsVisibility()
|
||||||
|
|
||||||
|
R.id.account_username.typeText(OC_BASIC_USERNAME)
|
||||||
|
|
||||||
|
R.id.account_password.typeText(OC_BASIC_PASSWORD)
|
||||||
|
|
||||||
|
R.id.hostUrlInput.typeText("anything")
|
||||||
|
|
||||||
|
with(R.id.account_username) {
|
||||||
|
withText("")
|
||||||
|
assertVisibility(Visibility.GONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
with(R.id.account_password) {
|
||||||
|
withText("")
|
||||||
|
assertVisibility(Visibility.GONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.loginButton.assertVisibility(Visibility.GONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkServerInfo_isSuccess_bearerModifyUrlInput() {
|
||||||
|
Intents.init()
|
||||||
|
launchTest()
|
||||||
|
avoidOpeningChromeCustomTab()
|
||||||
|
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BEARER)))
|
||||||
|
|
||||||
|
checkBearerFieldsVisibility()
|
||||||
|
|
||||||
|
R.id.hostUrlInput.typeText("anything")
|
||||||
|
|
||||||
|
R.id.auth_status_text.assertVisibility(Visibility.GONE)
|
||||||
|
Intents.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkServerInfo_isError_emptyUrl() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
R.id.hostUrlInput.typeText("")
|
||||||
|
|
||||||
|
R.id.embeddedCheckServerButton.scrollAndClick()
|
||||||
|
|
||||||
|
with(R.id.server_status_text) {
|
||||||
|
isDisplayed(true)
|
||||||
|
assertVisibility(Visibility.VISIBLE)
|
||||||
|
withText(R.string.auth_can_not_auth_against_server)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(exactly = 0) { authenticationViewModel.getServerInfo(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkServerInfo_isError_qsferaVersionNotSupported() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Error(QSferaVersionNotSupportedException())))
|
||||||
|
|
||||||
|
with(R.id.server_status_text) {
|
||||||
|
isDisplayed(true)
|
||||||
|
assertVisibility(Visibility.VISIBLE)
|
||||||
|
withText(R.string.server_not_supported)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkServerInfo_isError_noNetworkConnection() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Error(NoNetworkConnectionException())))
|
||||||
|
|
||||||
|
with(R.id.server_status_text) {
|
||||||
|
isDisplayed(true)
|
||||||
|
assertVisibility(Visibility.VISIBLE)
|
||||||
|
withText(R.string.error_no_network_connection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkServerInfo_isError_otherExceptions() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Error(ServerNotReachableException())))
|
||||||
|
|
||||||
|
with(R.id.server_status_text) {
|
||||||
|
isDisplayed(true)
|
||||||
|
assertVisibility(Visibility.VISIBLE)
|
||||||
|
withText(R.string.network_host_not_available)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun loginBasic_callLoginBasic() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
|
||||||
|
|
||||||
|
R.id.account_username.typeText(OC_BASIC_USERNAME)
|
||||||
|
|
||||||
|
R.id.account_password.typeText(OC_BASIC_PASSWORD)
|
||||||
|
|
||||||
|
with(R.id.loginButton) {
|
||||||
|
isDisplayed(true)
|
||||||
|
scrollAndClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(exactly = 1) { authenticationViewModel.loginBasic(OC_BASIC_USERNAME, OC_BASIC_PASSWORD, null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun loginBasic_callLoginBasic_trimUsername() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
|
||||||
|
|
||||||
|
R.id.account_username.typeText(" $OC_BASIC_USERNAME ")
|
||||||
|
|
||||||
|
R.id.account_password.typeText(OC_BASIC_PASSWORD)
|
||||||
|
|
||||||
|
with(R.id.loginButton) {
|
||||||
|
isDisplayed(true)
|
||||||
|
scrollAndClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(exactly = 1) { authenticationViewModel.loginBasic(OC_BASIC_USERNAME, OC_BASIC_PASSWORD, null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loginBasic_showOrHideFields() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
|
||||||
|
|
||||||
|
R.id.account_username.typeText(OC_BASIC_USERNAME)
|
||||||
|
|
||||||
|
R.id.loginButton.isDisplayed(false)
|
||||||
|
|
||||||
|
R.id.account_password.typeText(OC_BASIC_PASSWORD)
|
||||||
|
|
||||||
|
R.id.loginButton.isDisplayed(true)
|
||||||
|
|
||||||
|
R.id.account_username.replaceText("")
|
||||||
|
|
||||||
|
R.id.loginButton.isDisplayed(false)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun login_isLoading() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
loginResultLiveData.postValue(Event(UIResult.Loading()))
|
||||||
|
|
||||||
|
with(R.id.auth_status_text) {
|
||||||
|
withText(R.string.auth_trying_to_login)
|
||||||
|
isDisplayed(true)
|
||||||
|
assertVisibility(Visibility.VISIBLE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun login_isSuccess_finishResultCode() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
loginResultLiveData.postValue(Event(UIResult.Success(data = "Account_name")))
|
||||||
|
accountDiscoveryLiveData.postValue(Event(UIResult.Success()))
|
||||||
|
|
||||||
|
assertEquals(activityScenario.result.resultCode, RESULT_OK)
|
||||||
|
val accountName: String? = activityScenario.result?.resultData?.extras?.getString(KEY_ACCOUNT_NAME)
|
||||||
|
val accountType: String? = activityScenario.result?.resultData?.extras?.getString(KEY_ACCOUNT_TYPE)
|
||||||
|
|
||||||
|
assertNotNull(accountName)
|
||||||
|
assertNotNull(accountType)
|
||||||
|
assertEquals("Account_name", accountName)
|
||||||
|
assertEquals("qsfera", accountType)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun login_isSuccess_finishResultCodeBrandedAccountType() {
|
||||||
|
launchTest(accountType = "notQSfera")
|
||||||
|
|
||||||
|
loginResultLiveData.postValue(Event(UIResult.Success(data = "Account_name")))
|
||||||
|
accountDiscoveryLiveData.postValue(Event(UIResult.Success()))
|
||||||
|
|
||||||
|
assertEquals(activityScenario.result.resultCode, RESULT_OK)
|
||||||
|
val accountName: String? = activityScenario.result?.resultData?.extras?.getString(KEY_ACCOUNT_NAME)
|
||||||
|
val accountType: String? = activityScenario.result?.resultData?.extras?.getString(KEY_ACCOUNT_TYPE)
|
||||||
|
|
||||||
|
assertNotNull(accountName)
|
||||||
|
assertNotNull(accountType)
|
||||||
|
assertEquals("Account_name", accountName)
|
||||||
|
assertEquals("notQSfera", accountType)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun login_isError_NoNetworkConnectionException() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
loginResultLiveData.postValue(Event(UIResult.Error(NoNetworkConnectionException())))
|
||||||
|
|
||||||
|
R.id.server_status_text.withText(R.string.error_no_network_connection)
|
||||||
|
|
||||||
|
checkBasicFieldsVisibility(fieldsShouldBeVisible = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun login_isError_ServerNotReachableException() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
loginResultLiveData.postValue(Event(UIResult.Error(ServerNotReachableException())))
|
||||||
|
|
||||||
|
R.id.server_status_text.withText(R.string.error_no_network_connection)
|
||||||
|
|
||||||
|
checkBasicFieldsVisibility(fieldsShouldBeVisible = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun login_isError_OtherException() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
val exception = UnauthorizedException()
|
||||||
|
|
||||||
|
loginResultLiveData.postValue(Event(UIResult.Error(exception)))
|
||||||
|
|
||||||
|
R.id.auth_status_text.withText(exception.parseError("", context.resources, true) as String)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun intent_withSavedAccount_viewModelCalls() {
|
||||||
|
val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
|
||||||
|
}
|
||||||
|
|
||||||
|
launchTest(intent = intentWithAccount)
|
||||||
|
|
||||||
|
verify(exactly = 1) { authenticationViewModel.supportsOAuth2(OC_ACCOUNT.name) }
|
||||||
|
verify(exactly = 1) { authenticationViewModel.getBaseUrl(OC_ACCOUNT.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun supportsOAuth_isSuccess_actionUpdateExpiredTokenOAuth() {
|
||||||
|
val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
|
||||||
|
putExtra(EXTRA_ACTION, ACTION_UPDATE_EXPIRED_TOKEN)
|
||||||
|
putExtra(KEY_AUTH_TOKEN_TYPE, OAUTH_TOKEN_TYPE)
|
||||||
|
}
|
||||||
|
|
||||||
|
launchTest(intent = intentWithAccount)
|
||||||
|
|
||||||
|
supportsOauth2LiveData.postValue(Event(UIResult.Success(true)))
|
||||||
|
|
||||||
|
with(R.id.instructions_message) {
|
||||||
|
isDisplayed(true)
|
||||||
|
assertVisibility(Visibility.VISIBLE)
|
||||||
|
withText(context.getString(R.string.auth_expired_oauth_token_toast))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun supportsOAuth_isSuccess_actionUpdateToken() {
|
||||||
|
val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
|
||||||
|
putExtra(EXTRA_ACTION, ACTION_UPDATE_TOKEN)
|
||||||
|
putExtra(KEY_AUTH_TOKEN_TYPE, OC_AUTH_TOKEN_TYPE)
|
||||||
|
}
|
||||||
|
|
||||||
|
launchTest(intent = intentWithAccount)
|
||||||
|
|
||||||
|
supportsOauth2LiveData.postValue(Event(UIResult.Success(false)))
|
||||||
|
|
||||||
|
R.id.instructions_message.assertVisibility(Visibility.GONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun supportsOAuth_isSuccess_actionUpdateExpiredTokenBasic() {
|
||||||
|
val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
|
||||||
|
putExtra(EXTRA_ACTION, ACTION_UPDATE_EXPIRED_TOKEN)
|
||||||
|
putExtra(KEY_AUTH_TOKEN_TYPE, BASIC_TOKEN_TYPE)
|
||||||
|
}
|
||||||
|
|
||||||
|
launchTest(intent = intentWithAccount)
|
||||||
|
|
||||||
|
supportsOauth2LiveData.postValue(Event(UIResult.Success(false)))
|
||||||
|
|
||||||
|
with(R.id.instructions_message) {
|
||||||
|
isDisplayed(true)
|
||||||
|
assertVisibility(Visibility.VISIBLE)
|
||||||
|
withText(context.getString(R.string.auth_expired_basic_auth_toast))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getBaseUrl_isSuccess_updatesBaseUrl() {
|
||||||
|
val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
|
||||||
|
}
|
||||||
|
|
||||||
|
launchTest(intent = intentWithAccount)
|
||||||
|
|
||||||
|
baseUrlLiveData.postValue(Event(UIResult.Success(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)))
|
||||||
|
|
||||||
|
with(R.id.hostUrlInput) {
|
||||||
|
withText(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
|
||||||
|
assertVisibility(Visibility.VISIBLE)
|
||||||
|
isDisplayed(true)
|
||||||
|
isEnabled(false)
|
||||||
|
isFocusable(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(exactly = 0) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getBaseUrlAndActionNotCreate_isSuccess_updatesBaseUrl() {
|
||||||
|
val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
|
||||||
|
putExtra(EXTRA_ACTION, ACTION_UPDATE_EXPIRED_TOKEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
launchTest(intent = intentWithAccount)
|
||||||
|
|
||||||
|
baseUrlLiveData.postValue(Event(UIResult.Success(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)))
|
||||||
|
|
||||||
|
with(R.id.hostUrlInput) {
|
||||||
|
withText(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
|
||||||
|
assertVisibility(Visibility.VISIBLE)
|
||||||
|
isDisplayed(true)
|
||||||
|
isEnabled(false)
|
||||||
|
isFocusable(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(exactly = 1) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun settingsLink() {
|
||||||
|
Intents.init()
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
closeSoftKeyboard()
|
||||||
|
|
||||||
|
mockIntentToComponent(RESULT_OK, SettingsActivity::class.java.name)
|
||||||
|
onView(withId(R.id.settings_link)).perform(click())
|
||||||
|
intended(hasComponent(SettingsActivity::class.java.name))
|
||||||
|
|
||||||
|
Intents.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun avoidOpeningChromeCustomTab() {
|
||||||
|
Intents.intending(allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW)))
|
||||||
|
.respondWith(Instrumentation.ActivityResult(RESULT_OK, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkBasicFieldsVisibility(
|
||||||
|
fieldsShouldBeVisible: Boolean = true,
|
||||||
|
loginButtonShouldBeVisible: Boolean = false
|
||||||
|
) {
|
||||||
|
val visibilityMatcherFields = if (fieldsShouldBeVisible) Visibility.VISIBLE else Visibility.GONE
|
||||||
|
val visibilityMatcherLoginButton = if (loginButtonShouldBeVisible) Visibility.VISIBLE else Visibility.GONE
|
||||||
|
|
||||||
|
with(R.id.account_username) {
|
||||||
|
isDisplayed(fieldsShouldBeVisible)
|
||||||
|
assertVisibility(visibilityMatcherFields)
|
||||||
|
withText("")
|
||||||
|
}
|
||||||
|
|
||||||
|
with(R.id.account_password) {
|
||||||
|
isDisplayed(fieldsShouldBeVisible)
|
||||||
|
assertVisibility(visibilityMatcherFields)
|
||||||
|
withText("")
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.loginButton.assertVisibility(visibilityMatcherLoginButton)
|
||||||
|
R.id.auth_status_text.assertVisibility(visibilityMatcherLoginButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkBearerFieldsVisibility() {
|
||||||
|
R.id.account_username.assertVisibility(Visibility.GONE)
|
||||||
|
R.id.account_password.assertVisibility(Visibility.GONE)
|
||||||
|
R.id.auth_status_text.assertVisibility(Visibility.GONE)
|
||||||
|
|
||||||
|
with(R.id.server_status_text) {
|
||||||
|
isDisplayed(true)
|
||||||
|
assertVisibility(Visibility.VISIBLE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertWebfingerFlowDisplayed(
|
||||||
|
webfingerEnabled: Boolean,
|
||||||
|
) {
|
||||||
|
R.id.webfinger_layout.isDisplayed(webfingerEnabled)
|
||||||
|
R.id.webfinger_username.isDisplayed(webfingerEnabled)
|
||||||
|
R.id.webfinger_button.isDisplayed(webfingerEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertViewsDisplayed(
|
||||||
|
showLoginBackGroundImage: Boolean = true,
|
||||||
|
showThumbnail: Boolean = true,
|
||||||
|
showCenteredRefreshButton: Boolean = false,
|
||||||
|
showInstructionsMessage: Boolean = false,
|
||||||
|
showHostUrlFrame: Boolean = true,
|
||||||
|
showHostUrlInput: Boolean = true,
|
||||||
|
showEmbeddedCheckServerButton: Boolean = true,
|
||||||
|
showEmbeddedRefreshButton: Boolean = false,
|
||||||
|
showServerStatusText: Boolean = false,
|
||||||
|
showAccountUsername: Boolean = false,
|
||||||
|
showAccountPassword: Boolean = false,
|
||||||
|
showAuthStatus: Boolean = false,
|
||||||
|
showLoginButton: Boolean = false,
|
||||||
|
showWelcomeLink: Boolean = true
|
||||||
|
) {
|
||||||
|
R.id.login_background_image.isDisplayed(displayed = showLoginBackGroundImage)
|
||||||
|
R.id.thumbnail.isDisplayed(displayed = showThumbnail)
|
||||||
|
R.id.centeredRefreshButton.isDisplayed(displayed = showCenteredRefreshButton)
|
||||||
|
R.id.instructions_message.isDisplayed(displayed = showInstructionsMessage)
|
||||||
|
R.id.hostUrlFrame.isDisplayed(displayed = showHostUrlFrame)
|
||||||
|
R.id.hostUrlInput.isDisplayed(displayed = showHostUrlInput)
|
||||||
|
R.id.embeddedCheckServerButton.isDisplayed(displayed = showEmbeddedCheckServerButton)
|
||||||
|
R.id.embeddedRefreshButton.isDisplayed(displayed = showEmbeddedRefreshButton)
|
||||||
|
R.id.server_status_text.isDisplayed(displayed = showServerStatusText)
|
||||||
|
R.id.account_username_container.isDisplayed(displayed = showAccountUsername)
|
||||||
|
R.id.account_username.isDisplayed(displayed = showAccountUsername)
|
||||||
|
R.id.account_password_container.isDisplayed(displayed = showAccountPassword)
|
||||||
|
R.id.account_password.isDisplayed(displayed = showAccountPassword)
|
||||||
|
R.id.auth_status_text.isDisplayed(displayed = showAuthStatus)
|
||||||
|
R.id.loginButton.isDisplayed(displayed = showLoginButton)
|
||||||
|
R.id.welcome_link.isDisplayed(displayed = showWelcomeLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val SECURE_SERVER_INFO_BASIC = OC_SECURE_SERVER_INFO_BASIC_AUTH
|
||||||
|
val INSECURE_SERVER_INFO_BASIC = OC_INSECURE_SERVER_INFO_BASIC_AUTH
|
||||||
|
val SECURE_SERVER_INFO_BEARER = OC_SECURE_SERVER_INFO_BEARER_AUTH
|
||||||
|
private const val CUSTOM_WELCOME_TEXT = "Welcome to this test"
|
||||||
|
private const val BRANDED_APP_NAME = "BrandedAppName"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Abel García de Prada
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
* <p>
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package eu.qsfera.android.files
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.testing.FragmentScenario
|
||||||
|
import androidx.fragment.app.testing.launchFragment
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.action.ViewActions
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.matcher.RootMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.presentation.files.SortBottomSheetFragment
|
||||||
|
import eu.qsfera.android.presentation.files.SortOrder
|
||||||
|
import eu.qsfera.android.presentation.files.SortType
|
||||||
|
import eu.qsfera.android.utils.matchers.bsfItemWithIcon
|
||||||
|
import eu.qsfera.android.utils.matchers.bsfItemWithTitle
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SortBottomSheetFragmentTest {
|
||||||
|
|
||||||
|
private lateinit var fragmentScenario: FragmentScenario<SortBottomSheetFragment>
|
||||||
|
private val fragmentListener = mockk<SortBottomSheetFragment.SortDialogListener>()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
val fragmentArgs = Bundle().apply {
|
||||||
|
putParcelable(SortBottomSheetFragment.ARG_SORT_TYPE, SortType.SORT_TYPE_BY_NAME)
|
||||||
|
putParcelable(SortBottomSheetFragment.ARG_SORT_ORDER, SortOrder.SORT_ORDER_ASCENDING)
|
||||||
|
}
|
||||||
|
fragmentScenario = launchFragment(fragmentArgs)
|
||||||
|
every { fragmentListener.onSortSelected(any()) } returns Unit
|
||||||
|
fragmentScenario.onFragment { it.sortDialogListener = fragmentListener }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_initial_view() {
|
||||||
|
onView(withId(R.id.title))
|
||||||
|
.inRoot(RootMatchers.isDialog())
|
||||||
|
.check(matches(ViewMatchers.withText(R.string.actionbar_sort_title)))
|
||||||
|
.check(matches(ViewMatchers.hasTextColor(R.color.bottom_sheet_fragment_title_color)))
|
||||||
|
|
||||||
|
with(R.id.sort_by_name) {
|
||||||
|
bsfItemWithTitle(R.string.global_name, R.color.primary)
|
||||||
|
bsfItemWithIcon(R.drawable.ic_sort_by_name, R.color.primary)
|
||||||
|
}
|
||||||
|
with(R.id.sort_by_size) {
|
||||||
|
bsfItemWithTitle(R.string.global_size, R.color.bottom_sheet_fragment_item_color)
|
||||||
|
bsfItemWithIcon(R.drawable.ic_sort_by_size, R.color.bottom_sheet_fragment_item_color)
|
||||||
|
}
|
||||||
|
with(R.id.sort_by_date) {
|
||||||
|
bsfItemWithTitle(R.string.global_date, R.color.bottom_sheet_fragment_item_color)
|
||||||
|
bsfItemWithIcon(R.drawable.ic_sort_by_date, R.color.bottom_sheet_fragment_item_color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_sort_by_name_click() {
|
||||||
|
onView(withId(R.id.sort_by_name)).inRoot(RootMatchers.isDialog()).perform(ViewActions.click())
|
||||||
|
verify { fragmentListener.onSortSelected(SortType.SORT_TYPE_BY_NAME) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_sort_by_date_click() {
|
||||||
|
onView(withId(R.id.sort_by_date)).inRoot(RootMatchers.isDialog()).perform(ViewActions.click())
|
||||||
|
verify { fragmentListener.onSortSelected(SortType.SORT_TYPE_BY_DATE) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_sort_by_size_click() {
|
||||||
|
onView(withId(R.id.sort_by_size)).inRoot(RootMatchers.isDialog()).perform(ViewActions.click())
|
||||||
|
verify { fragmentListener.onSortSelected(SortType.SORT_TYPE_BY_SIZE) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
package eu.qsfera.android.files.details
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.core.app.ActivityScenario.launch
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.domain.files.model.OCFileWithSyncInfo
|
||||||
|
import eu.qsfera.android.presentation.files.details.FileDetailsFragment
|
||||||
|
import eu.qsfera.android.presentation.files.details.FileDetailsViewModel
|
||||||
|
import eu.qsfera.android.presentation.files.operations.FileOperationsViewModel
|
||||||
|
import eu.qsfera.android.sharing.shares.ui.TestShareFileActivity
|
||||||
|
import eu.qsfera.android.testutil.OC_ACCOUNT
|
||||||
|
import eu.qsfera.android.testutil.OC_FILE
|
||||||
|
import eu.qsfera.android.testutil.OC_FILE_WITH_SYNC_INFO_AVAILABLE_OFFLINE
|
||||||
|
import eu.qsfera.android.testutil.OC_FILE_WITH_SYNC_INFO
|
||||||
|
import eu.qsfera.android.testutil.OC_FILE_WITH_SYNC_INFO_AND_SPACE
|
||||||
|
import eu.qsfera.android.testutil.OC_FILE_WITH_SYNC_INFO_AND_WITHOUT_PERSONAL_SPACE
|
||||||
|
import eu.qsfera.android.utils.DisplayUtils
|
||||||
|
import eu.qsfera.android.utils.matchers.assertVisibility
|
||||||
|
import eu.qsfera.android.utils.matchers.isDisplayed
|
||||||
|
import eu.qsfera.android.utils.matchers.withDrawable
|
||||||
|
import eu.qsfera.android.utils.matchers.withText
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
class FileDetailsFragmentTest {
|
||||||
|
|
||||||
|
private lateinit var fileDetailsViewModel: FileDetailsViewModel
|
||||||
|
private lateinit var fileOperationsViewModel: FileOperationsViewModel
|
||||||
|
private lateinit var context: Context
|
||||||
|
|
||||||
|
private var currentFile: MutableStateFlow<OCFileWithSyncInfo?> = MutableStateFlow(OC_FILE_WITH_SYNC_INFO_AND_SPACE)
|
||||||
|
private var currentFileWithoutPersonalSpace: MutableStateFlow<OCFileWithSyncInfo?> =
|
||||||
|
MutableStateFlow(OC_FILE_WITH_SYNC_INFO_AND_WITHOUT_PERSONAL_SPACE)
|
||||||
|
private var currentFileSyncInfo: MutableStateFlow<OCFileWithSyncInfo?> = MutableStateFlow(OC_FILE_WITH_SYNC_INFO)
|
||||||
|
private var currentFileAvailableOffline: MutableStateFlow<OCFileWithSyncInfo?> = MutableStateFlow(OC_FILE_WITH_SYNC_INFO_AVAILABLE_OFFLINE)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
|
fileDetailsViewModel = mockk(relaxed = true)
|
||||||
|
fileOperationsViewModel = mockk(relaxed = true)
|
||||||
|
every { fileDetailsViewModel.currentFile } returns currentFile
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
context
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
fileDetailsViewModel
|
||||||
|
}
|
||||||
|
viewModel {
|
||||||
|
fileOperationsViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileDetailsFragment = FileDetailsFragment.newInstance(
|
||||||
|
OC_FILE,
|
||||||
|
OC_ACCOUNT,
|
||||||
|
syncFileAtOpen = false
|
||||||
|
)
|
||||||
|
|
||||||
|
launch(TestShareFileActivity::class.java).onActivity {
|
||||||
|
it.startFragment(fileDetailsFragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun display_visibility_of_detail_view_when_it_is_displayed() {
|
||||||
|
assertViewsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun show_space_personal_when_it_has_value() {
|
||||||
|
R.id.fdSpace.assertVisibility(ViewMatchers.Visibility.VISIBLE)
|
||||||
|
R.id.fdSpaceLabel.assertVisibility(ViewMatchers.Visibility.VISIBLE)
|
||||||
|
R.id.fdIconSpace.assertVisibility(ViewMatchers.Visibility.VISIBLE)
|
||||||
|
|
||||||
|
R.id.fdSpace.withText(R.string.bottom_nav_personal)
|
||||||
|
R.id.fdSpaceLabel.withText(R.string.space_label)
|
||||||
|
onView(withId(R.id.fdIconSpace))
|
||||||
|
.check(matches(withDrawable(R.drawable.ic_spaces)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hide_space_when_it_has_no_value() {
|
||||||
|
every { fileDetailsViewModel.currentFile } returns currentFileSyncInfo
|
||||||
|
|
||||||
|
R.id.fdSpace.assertVisibility(ViewMatchers.Visibility.GONE)
|
||||||
|
R.id.fdSpaceLabel.assertVisibility(ViewMatchers.Visibility.GONE)
|
||||||
|
R.id.fdIconSpace.assertVisibility(ViewMatchers.Visibility.GONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun show_space_not_personal_when_it_has_value() {
|
||||||
|
every { fileDetailsViewModel.currentFile } returns currentFileWithoutPersonalSpace
|
||||||
|
|
||||||
|
R.id.fdSpace.assertVisibility(ViewMatchers.Visibility.VISIBLE)
|
||||||
|
R.id.fdSpaceLabel.assertVisibility(ViewMatchers.Visibility.VISIBLE)
|
||||||
|
R.id.fdIconSpace.assertVisibility(ViewMatchers.Visibility.VISIBLE)
|
||||||
|
|
||||||
|
R.id.fdSpace.withText(currentFileWithoutPersonalSpace.value?.space?.name.toString())
|
||||||
|
R.id.fdSpaceLabel.withText(R.string.space_label)
|
||||||
|
onView(withId(R.id.fdIconSpace))
|
||||||
|
.check(matches(withDrawable(R.drawable.ic_spaces)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun show_last_sync_when_it_has_value() {
|
||||||
|
currentFile.value?.file?.lastSyncDateForData = 1212121212212
|
||||||
|
R.id.fdLastSync.assertVisibility(ViewMatchers.Visibility.VISIBLE)
|
||||||
|
R.id.fdLastSyncLabel.assertVisibility(ViewMatchers.Visibility.VISIBLE)
|
||||||
|
|
||||||
|
R.id.fdLastSyncLabel.withText(R.string.filedetails_last_sync)
|
||||||
|
R.id.fdLastSync.withText(DisplayUtils.unixTimeToHumanReadable(currentFile.value?.file?.lastSyncDateForData!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hide_last_sync_when_it_has_no_value() {
|
||||||
|
every { fileDetailsViewModel.currentFile } returns currentFile
|
||||||
|
|
||||||
|
R.id.fdLastSync.assertVisibility(ViewMatchers.Visibility.GONE)
|
||||||
|
R.id.fdLastSyncLabel.assertVisibility(ViewMatchers.Visibility.GONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun verifyTests() {
|
||||||
|
R.id.fdCreatedLabel.withText(R.string.filedetails_created)
|
||||||
|
R.id.fdCreated.withText(DisplayUtils.unixTimeToHumanReadable(currentFile.value?.file?.creationTimestamp!!))
|
||||||
|
|
||||||
|
R.id.fdModifiedLabel.withText(R.string.filedetails_modified)
|
||||||
|
R.id.fdModified.withText(DisplayUtils.unixTimeToHumanReadable(currentFile.value?.file?.modificationTimestamp!!))
|
||||||
|
|
||||||
|
R.id.fdPathLabel.withText(R.string.ssl_validator_label_L)
|
||||||
|
R.id.fdPath.withText(currentFile.value?.file?.getParentRemotePath()!!)
|
||||||
|
|
||||||
|
R.id.fdname.withText(currentFile.value?.file?.fileName!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun badge_available_offline_in_image_is_not_viewed_when_file_does_not_change_state() {
|
||||||
|
every { fileDetailsViewModel.currentFile } returns currentFileAvailableOffline
|
||||||
|
|
||||||
|
R.id.badgeDetailFile.assertVisibility(ViewMatchers.Visibility.VISIBLE)
|
||||||
|
onView(withId(R.id.badgeDetailFile))
|
||||||
|
.check(matches(withDrawable(R.drawable.offline_available_pin)))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun show_badge_isAvailableLocally_in_image_when_file_change_state() {
|
||||||
|
currentFile.value?.file?.etagInConflict = "error"
|
||||||
|
|
||||||
|
R.id.badgeDetailFile.assertVisibility(ViewMatchers.Visibility.VISIBLE)
|
||||||
|
onView(withId(R.id.badgeDetailFile))
|
||||||
|
.check(matches(withDrawable(R.drawable.error_pin)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertViewsDisplayed(
|
||||||
|
showImage: Boolean = true,
|
||||||
|
showFdName: Boolean = true,
|
||||||
|
showFdProgressText: Boolean = false,
|
||||||
|
showFdProgressBar: Boolean = false,
|
||||||
|
showFdCancelBtn: Boolean = false,
|
||||||
|
showDivider: Boolean = true,
|
||||||
|
showDivider2: Boolean = true,
|
||||||
|
showFdTypeLabel: Boolean = true,
|
||||||
|
showFdType: Boolean = true,
|
||||||
|
showFdSizeLabel: Boolean = true,
|
||||||
|
showFdSize: Boolean = true,
|
||||||
|
showFdModifiedLabel: Boolean = true,
|
||||||
|
showFdModified: Boolean = true,
|
||||||
|
showFdCreatedLabel: Boolean = true,
|
||||||
|
showFdCreated: Boolean = true,
|
||||||
|
showDivider3: Boolean = true,
|
||||||
|
showFdPathLabel: Boolean = true,
|
||||||
|
showFdPath: Boolean = true
|
||||||
|
) {
|
||||||
|
R.id.fdImageDetailFile.isDisplayed(displayed = showImage)
|
||||||
|
R.id.fdname.isDisplayed(displayed = showFdName)
|
||||||
|
R.id.fdProgressText.isDisplayed(displayed = showFdProgressText)
|
||||||
|
R.id.fdProgressBar.isDisplayed(displayed = showFdProgressBar)
|
||||||
|
R.id.fdCancelBtn.isDisplayed(displayed = showFdCancelBtn)
|
||||||
|
R.id.divider.isDisplayed(displayed = showDivider)
|
||||||
|
R.id.fdTypeLabel.isDisplayed(displayed = showFdTypeLabel)
|
||||||
|
R.id.fdType.isDisplayed(displayed = showFdType)
|
||||||
|
R.id.fdSizeLabel.isDisplayed(displayed = showFdSizeLabel)
|
||||||
|
R.id.fdSize.isDisplayed(displayed = showFdSize)
|
||||||
|
R.id.divider2.isDisplayed(displayed = showDivider2)
|
||||||
|
R.id.fdModifiedLabel.isDisplayed(displayed = showFdModifiedLabel)
|
||||||
|
R.id.fdModified.isDisplayed(displayed = showFdModified)
|
||||||
|
R.id.fdCreatedLabel.isDisplayed(displayed = showFdCreatedLabel)
|
||||||
|
R.id.fdCreated.isDisplayed(displayed = showFdCreated)
|
||||||
|
R.id.divider3.isDisplayed(displayed = showDivider3)
|
||||||
|
R.id.fdPathLabel.isDisplayed(displayed = showFdPathLabel)
|
||||||
|
R.id.fdPath.isDisplayed(displayed = showFdPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Fernando Sanz Velasco
|
||||||
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.logging
|
||||||
|
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.presentation.logging.LogsListActivity
|
||||||
|
import eu.qsfera.android.presentation.logging.LogListViewModel
|
||||||
|
import eu.qsfera.android.utils.matchers.assertChildCount
|
||||||
|
import eu.qsfera.android.utils.matchers.isDisplayed
|
||||||
|
import eu.qsfera.android.utils.matchers.withText
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.unmockkAll
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
class LogsListActivityTest {
|
||||||
|
|
||||||
|
private lateinit var activityScenario: ActivityScenario<LogsListActivity>
|
||||||
|
|
||||||
|
private lateinit var logListViewModel: LogListViewModel
|
||||||
|
|
||||||
|
private fun launchTest(logs: List<File>) {
|
||||||
|
every { logListViewModel.getLogsFiles() } returns logs
|
||||||
|
activityScenario = ActivityScenario.launch(LogsListActivity::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
logListViewModel = mockk(relaxed = true)
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
logListViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_visibility_toolbar() {
|
||||||
|
launchTest(logs = emptyList())
|
||||||
|
R.id.toolbar_activity_logs_list.isDisplayed(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_isRecyclerViewEmpty_show_label() {
|
||||||
|
launchTest(logs = emptyList())
|
||||||
|
R.id.logs_list_empty.isDisplayed(true)
|
||||||
|
R.id.list_empty_dataset_title.withText(R.string.prefs_log_no_logs_list_view)
|
||||||
|
R.id.list_empty_dataset_sub_title.withText(R.string.prefs_log_empty_subtitle)
|
||||||
|
R.id.recyclerView_activity_logs_list.isDisplayed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_isRecyclerViewNotEmpty_hide_label() {
|
||||||
|
launchTest(logs = listOf(File("path")))
|
||||||
|
R.id.logs_list_empty.isDisplayed(false)
|
||||||
|
R.id.recyclerView_activity_logs_list.isDisplayed(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_childCount() {
|
||||||
|
launchTest(logs = listOf(File("qsfera.2021-01.01.log"), File("qsfera.2021-01-02.log")))
|
||||||
|
R.id.recyclerView_activity_logs_list.assertChildCount(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Juan Carlos Garrote Gascón
|
||||||
|
*
|
||||||
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
|
* <p>
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
* <p>
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.settings
|
||||||
|
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.fragment.app.testing.FragmentScenario
|
||||||
|
import androidx.fragment.app.testing.launchFragmentInContainer
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.intent.Intents
|
||||||
|
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import eu.qsfera.android.BuildConfig
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.presentation.releasenotes.ReleaseNotesActivity
|
||||||
|
import eu.qsfera.android.presentation.settings.privacypolicy.PrivacyPolicyActivity
|
||||||
|
import eu.qsfera.android.presentation.settings.SettingsFragment
|
||||||
|
import eu.qsfera.android.presentation.releasenotes.ReleaseNotesViewModel
|
||||||
|
import eu.qsfera.android.presentation.settings.more.SettingsMoreViewModel
|
||||||
|
import eu.qsfera.android.presentation.settings.SettingsViewModel
|
||||||
|
import eu.qsfera.android.utils.matchers.verifyPreference
|
||||||
|
import eu.qsfera.android.utils.releaseNotesList
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.unmockkAll
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
class SettingsFragmentTest {
|
||||||
|
|
||||||
|
private lateinit var fragmentScenario: FragmentScenario<SettingsFragment>
|
||||||
|
|
||||||
|
private var subsectionSecurity: Preference? = null
|
||||||
|
private var subsectionLogging: Preference? = null
|
||||||
|
private var subsectionPictureUploads: Preference? = null
|
||||||
|
private var subsectionVideoUploads: Preference? = null
|
||||||
|
private var subsectionMore: Preference? = null
|
||||||
|
private var prefPrivacyPolicy: Preference? = null
|
||||||
|
private var subsectionWhatsNew: Preference? = null
|
||||||
|
private var prefAboutApp: Preference? = null
|
||||||
|
|
||||||
|
private lateinit var settingsViewModel: SettingsViewModel
|
||||||
|
private lateinit var moreViewModel: SettingsMoreViewModel
|
||||||
|
private lateinit var releaseNotesViewModel: ReleaseNotesViewModel
|
||||||
|
private lateinit var context: Context
|
||||||
|
|
||||||
|
private lateinit var version: String
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
settingsViewModel = mockk(relaxed = true)
|
||||||
|
moreViewModel = mockk(relaxed = true)
|
||||||
|
releaseNotesViewModel = mockk(relaxed = true)
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
context
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
settingsViewModel
|
||||||
|
}
|
||||||
|
viewModel {
|
||||||
|
moreViewModel
|
||||||
|
}
|
||||||
|
viewModel {
|
||||||
|
releaseNotesViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
version = String.format(
|
||||||
|
context.getString(R.string.prefs_app_version_summary),
|
||||||
|
context.getString(R.string.app_name),
|
||||||
|
BuildConfig.BUILD_TYPE,
|
||||||
|
BuildConfig.VERSION_NAME,
|
||||||
|
BuildConfig.COMMIT_SHA1
|
||||||
|
)
|
||||||
|
|
||||||
|
Intents.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Intents.release()
|
||||||
|
unmockkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchTest(
|
||||||
|
attachedAccount: Boolean,
|
||||||
|
moreSectionVisible: Boolean = true,
|
||||||
|
privacyPolicyEnabled: Boolean = true,
|
||||||
|
whatsNewSectionVisible: Boolean = true
|
||||||
|
) {
|
||||||
|
every { settingsViewModel.isThereAttachedAccount() } returns attachedAccount
|
||||||
|
every { moreViewModel.shouldMoreSectionBeVisible() } returns moreSectionVisible
|
||||||
|
every { moreViewModel.isPrivacyPolicyEnabled() } returns privacyPolicyEnabled
|
||||||
|
every { releaseNotesViewModel.shouldWhatsNewSectionBeVisible() } returns whatsNewSectionVisible
|
||||||
|
|
||||||
|
fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_qsfera)
|
||||||
|
fragmentScenario.onFragment { fragment ->
|
||||||
|
subsectionSecurity = fragment.findPreference(SUBSECTION_SECURITY)
|
||||||
|
subsectionLogging = fragment.findPreference(SUBSECTION_LOGGING)
|
||||||
|
subsectionPictureUploads = fragment.findPreference(SUBSECTION_PICTURE_UPLOADS)
|
||||||
|
subsectionVideoUploads = fragment.findPreference(SUBSECTION_VIDEO_UPLOADS)
|
||||||
|
subsectionMore = fragment.findPreference(SUBSECTION_MORE)
|
||||||
|
prefPrivacyPolicy = fragment.findPreference(PREFERENCE_PRIVACY_POLICY)
|
||||||
|
subsectionWhatsNew = fragment.findPreference(SUBSECTION_WHATSNEW)
|
||||||
|
prefAboutApp = fragment.findPreference(PREFERENCE_ABOUT_APP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun settingsViewCommon() {
|
||||||
|
launchTest(attachedAccount = false)
|
||||||
|
|
||||||
|
subsectionSecurity?.verifyPreference(
|
||||||
|
keyPref = SUBSECTION_SECURITY,
|
||||||
|
titlePref = context.getString(R.string.prefs_subsection_security),
|
||||||
|
summaryPref = context.getString(R.string.prefs_subsection_security_summary),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
subsectionLogging?.verifyPreference(
|
||||||
|
keyPref = SUBSECTION_LOGGING,
|
||||||
|
titlePref = context.getString(R.string.prefs_subsection_logging),
|
||||||
|
summaryPref = context.getString(R.string.prefs_subsection_logging_summary),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
subsectionMore?.verifyPreference(
|
||||||
|
keyPref = SUBSECTION_MORE,
|
||||||
|
titlePref = context.getString(R.string.prefs_subsection_more),
|
||||||
|
summaryPref = context.getString(R.string.prefs_subsection_more_summary),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
prefPrivacyPolicy?.verifyPreference(
|
||||||
|
keyPref = PREFERENCE_PRIVACY_POLICY,
|
||||||
|
titlePref = context.getString(R.string.prefs_privacy_policy),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
subsectionWhatsNew?.verifyPreference(
|
||||||
|
keyPref = SUBSECTION_WHATSNEW,
|
||||||
|
titlePref = context.getString(R.string.prefs_subsection_whatsnew),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
prefAboutApp?.verifyPreference(
|
||||||
|
keyPref = PREFERENCE_ABOUT_APP,
|
||||||
|
titlePref = context.getString(R.string.prefs_app_version),
|
||||||
|
summaryPref = version,
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun settingsViewNoAccountAttached() {
|
||||||
|
launchTest(attachedAccount = false)
|
||||||
|
|
||||||
|
subsectionPictureUploads?.verifyPreference(
|
||||||
|
keyPref = SUBSECTION_PICTURE_UPLOADS,
|
||||||
|
titlePref = context.getString(R.string.prefs_subsection_picture_uploads),
|
||||||
|
summaryPref = context.getString(R.string.prefs_subsection_picture_uploads_summary),
|
||||||
|
visible = false
|
||||||
|
)
|
||||||
|
|
||||||
|
subsectionVideoUploads?.verifyPreference(
|
||||||
|
keyPref = SUBSECTION_VIDEO_UPLOADS,
|
||||||
|
titlePref = context.getString(R.string.prefs_subsection_video_uploads),
|
||||||
|
summaryPref = context.getString(R.string.prefs_subsection_video_uploads_summary),
|
||||||
|
visible = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun settingsViewAccountAttached() {
|
||||||
|
launchTest(attachedAccount = true)
|
||||||
|
|
||||||
|
subsectionPictureUploads?.verifyPreference(
|
||||||
|
keyPref = SUBSECTION_PICTURE_UPLOADS,
|
||||||
|
titlePref = context.getString(R.string.prefs_subsection_picture_uploads),
|
||||||
|
summaryPref = context.getString(R.string.prefs_subsection_picture_uploads_summary),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
subsectionVideoUploads?.verifyPreference(
|
||||||
|
keyPref = SUBSECTION_VIDEO_UPLOADS,
|
||||||
|
titlePref = context.getString(R.string.prefs_subsection_video_uploads),
|
||||||
|
summaryPref = context.getString(R.string.prefs_subsection_video_uploads_summary),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun settingsMoreSectionHidden() {
|
||||||
|
launchTest(attachedAccount = false, moreSectionVisible = false)
|
||||||
|
|
||||||
|
subsectionMore?.verifyPreference(
|
||||||
|
keyPref = SUBSECTION_MORE,
|
||||||
|
titlePref = context.getString(R.string.prefs_subsection_more),
|
||||||
|
summaryPref = context.getString(R.string.prefs_subsection_more_summary),
|
||||||
|
visible = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun settingsWhatsNewSectionHidden() {
|
||||||
|
launchTest(attachedAccount = false, whatsNewSectionVisible = false)
|
||||||
|
|
||||||
|
subsectionWhatsNew?.verifyPreference(
|
||||||
|
keyPref = SUBSECTION_WHATSNEW,
|
||||||
|
titlePref = context.getString(R.string.prefs_subsection_whatsnew),
|
||||||
|
visible = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun privacyPolicyOpensPrivacyPolicyActivity() {
|
||||||
|
launchTest(attachedAccount = false)
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_privacy_policy)).perform(click())
|
||||||
|
Intents.intended(IntentMatchers.hasComponent(PrivacyPolicyActivity::class.java.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Flaky test")
|
||||||
|
@Test
|
||||||
|
fun whatsnewOpensReleaseNotesActivity() {
|
||||||
|
launchTest(attachedAccount = false)
|
||||||
|
every { releaseNotesViewModel.getReleaseNotes() } returns releaseNotesList
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_subsection_whatsnew)).perform(click())
|
||||||
|
Intents.intended(IntentMatchers.hasComponent(ReleaseNotesActivity::class.java.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clickOnAppVersion() {
|
||||||
|
launchTest(attachedAccount = false)
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_app_version)).perform(click())
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
|
||||||
|
|
||||||
|
onView(withText(R.string.clipboard_text_copied)).check(matches(isEnabled()))
|
||||||
|
assertEquals(version, clipboard?.primaryClip?.getItemAt(0)?.coerceToText(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SUBSECTION_SECURITY = "security_subsection"
|
||||||
|
private const val SUBSECTION_LOGGING = "logging_subsection"
|
||||||
|
private const val SUBSECTION_PICTURE_UPLOADS = "picture_uploads_subsection"
|
||||||
|
private const val SUBSECTION_VIDEO_UPLOADS = "video_uploads_subsection"
|
||||||
|
private const val SUBSECTION_MORE = "more_subsection"
|
||||||
|
private const val PREFERENCE_PRIVACY_POLICY = "privacyPolicy"
|
||||||
|
private const val PREFERENCE_ABOUT_APP = "about_app"
|
||||||
|
private const val SUBSECTION_WHATSNEW = "whatsNew"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author David Crespo Ríos
|
||||||
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.settings.advanced
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.fragment.app.testing.FragmentScenario
|
||||||
|
import androidx.fragment.app.testing.launchFragmentInContainer
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.presentation.settings.advanced.SettingsAdvancedFragment
|
||||||
|
import eu.qsfera.android.presentation.settings.advanced.SettingsAdvancedFragment.Companion.PREF_SHOW_HIDDEN_FILES
|
||||||
|
import eu.qsfera.android.presentation.settings.advanced.SettingsAdvancedViewModel
|
||||||
|
import eu.qsfera.android.utils.matchers.verifyPreference
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.unmockkAll
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
class SettingsAdvancedFragmentTest {
|
||||||
|
|
||||||
|
private lateinit var fragmentScenario: FragmentScenario<SettingsAdvancedFragment>
|
||||||
|
|
||||||
|
private var prefShowHiddenFiles: SwitchPreferenceCompat? = null
|
||||||
|
|
||||||
|
private lateinit var advancedViewModel: SettingsAdvancedViewModel
|
||||||
|
private lateinit var context: Context
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
advancedViewModel = mockk(relaxed = true)
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
context
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
advancedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
every { advancedViewModel.isHiddenFilesShown() } returns true
|
||||||
|
|
||||||
|
fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_qsfera)
|
||||||
|
|
||||||
|
fragmentScenario.onFragment { fragment ->
|
||||||
|
prefShowHiddenFiles = fragment.findPreference(PREF_SHOW_HIDDEN_FILES)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun advancedView() {
|
||||||
|
assertNotNull(prefShowHiddenFiles)
|
||||||
|
prefShowHiddenFiles?.verifyPreference(
|
||||||
|
keyPref = PREF_SHOW_HIDDEN_FILES,
|
||||||
|
titlePref = context.getString(R.string.prefs_show_hidden_files),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun disableShowHiddenFiles() {
|
||||||
|
prefShowHiddenFiles?.isChecked = advancedViewModel.isHiddenFilesShown()
|
||||||
|
|
||||||
|
onView(withText(context.getString(R.string.prefs_show_hidden_files))).perform(click())
|
||||||
|
|
||||||
|
prefShowHiddenFiles?.isChecked?.let { assertFalse(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Juan Carlos Garrote Gascón
|
||||||
|
*
|
||||||
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
|
* <p>
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
* <p>
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.settings.logs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.fragment.app.testing.FragmentScenario
|
||||||
|
import androidx.fragment.app.testing.launchFragmentInContainer
|
||||||
|
import androidx.preference.CheckBoxPreference
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.intent.Intents
|
||||||
|
import androidx.test.espresso.intent.Intents.intended
|
||||||
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.presentation.logging.LogsListActivity
|
||||||
|
import eu.qsfera.android.presentation.settings.logging.SettingsLogsFragment
|
||||||
|
import eu.qsfera.android.presentation.logging.LogListViewModel
|
||||||
|
import eu.qsfera.android.presentation.settings.logging.SettingsLogsViewModel
|
||||||
|
import eu.qsfera.android.utils.matchers.verifyPreference
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.unmockkAll
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
class SettingsLogsFragmentTest {
|
||||||
|
|
||||||
|
private lateinit var fragmentScenario: FragmentScenario<SettingsLogsFragment>
|
||||||
|
|
||||||
|
private lateinit var prefEnableLogging: SwitchPreferenceCompat
|
||||||
|
private lateinit var prefHttpLogs: CheckBoxPreference
|
||||||
|
private lateinit var prefLogsListActivity: Preference
|
||||||
|
|
||||||
|
private lateinit var logsViewModel: SettingsLogsViewModel
|
||||||
|
private lateinit var logListViewModel: LogListViewModel
|
||||||
|
private lateinit var context: Context
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
logsViewModel = mockk(relaxed = true)
|
||||||
|
logListViewModel = mockk(relaxed = true)
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
context
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
logsViewModel
|
||||||
|
}
|
||||||
|
viewModel {
|
||||||
|
logListViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Intents.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Intents.release()
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit()
|
||||||
|
unmockkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchTest(enabledLogging: Boolean) {
|
||||||
|
every { logsViewModel.isLoggingEnabled() } returns enabledLogging
|
||||||
|
|
||||||
|
fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_qsfera)
|
||||||
|
fragmentScenario.onFragment { fragment ->
|
||||||
|
prefEnableLogging = fragment.findPreference(SettingsLogsFragment.PREFERENCE_ENABLE_LOGGING)!!
|
||||||
|
prefHttpLogs = fragment.findPreference(SettingsLogsFragment.PREFERENCE_LOG_HTTP)!!
|
||||||
|
prefLogsListActivity = fragment.findPreference(SettingsLogsFragment.PREFERENCE_LOGS_LIST)!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun logsViewLoggingDisabled() {
|
||||||
|
launchTest(enabledLogging = false)
|
||||||
|
|
||||||
|
prefEnableLogging.verifyPreference(
|
||||||
|
keyPref = SettingsLogsFragment.PREFERENCE_ENABLE_LOGGING,
|
||||||
|
titlePref = context.getString(R.string.prefs_enable_logging),
|
||||||
|
summaryPref = context.getString(R.string.prefs_enable_logging_summary),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
prefHttpLogs.verifyPreference(
|
||||||
|
keyPref = SettingsLogsFragment.PREFERENCE_LOG_HTTP,
|
||||||
|
titlePref = context.getString(R.string.prefs_http_logs),
|
||||||
|
visible = true,
|
||||||
|
enabled = false
|
||||||
|
)
|
||||||
|
|
||||||
|
prefLogsListActivity.verifyPreference(
|
||||||
|
keyPref = SettingsLogsFragment.PREFERENCE_LOGS_LIST,
|
||||||
|
titlePref = context.getString(R.string.prefs_log_open_logs_list_view),
|
||||||
|
visible = true,
|
||||||
|
enabled = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun logsViewLoggingEnabled() {
|
||||||
|
launchTest(enabledLogging = true)
|
||||||
|
|
||||||
|
prefEnableLogging.verifyPreference(
|
||||||
|
keyPref = SettingsLogsFragment.PREFERENCE_ENABLE_LOGGING,
|
||||||
|
titlePref = context.getString(R.string.prefs_enable_logging),
|
||||||
|
summaryPref = context.getString(R.string.prefs_enable_logging_summary),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
prefHttpLogs.verifyPreference(
|
||||||
|
keyPref = SettingsLogsFragment.PREFERENCE_LOG_HTTP,
|
||||||
|
titlePref = context.getString(R.string.prefs_http_logs),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
prefLogsListActivity.verifyPreference(
|
||||||
|
keyPref = SettingsLogsFragment.PREFERENCE_LOGS_LIST,
|
||||||
|
titlePref = context.getString(R.string.prefs_log_open_logs_list_view),
|
||||||
|
visible = true,
|
||||||
|
enabled = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun enableLoggingMakesSettingsEnable() {
|
||||||
|
launchTest(enabledLogging = false)
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_enable_logging)).perform(click())
|
||||||
|
assertTrue(prefHttpLogs.isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun disableLoggingMakesSettingsDisable() {
|
||||||
|
launchTest(enabledLogging = false)
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_enable_logging)).perform(click())
|
||||||
|
onView(withText(R.string.prefs_enable_logging)).perform(click())
|
||||||
|
assertFalse(prefHttpLogs.isEnabled)
|
||||||
|
assertTrue(prefLogsListActivity.isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkHttpLogs() {
|
||||||
|
launchTest(enabledLogging = true)
|
||||||
|
onView(withText(R.string.prefs_http_logs)).perform(click())
|
||||||
|
assertTrue(prefHttpLogs.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun disableLoggingMakesHttpLogsNotChecked() {
|
||||||
|
launchTest(enabledLogging = false)
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_enable_logging)).perform(click())
|
||||||
|
onView(withText(R.string.prefs_http_logs)).perform(click())
|
||||||
|
onView(withText(R.string.prefs_enable_logging)).perform(click())
|
||||||
|
assertFalse(prefHttpLogs.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loggerOpen() {
|
||||||
|
launchTest(enabledLogging = true)
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_log_open_logs_list_view)).perform(click())
|
||||||
|
intended(hasComponent(LogsListActivity::class.java.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Juan Carlos Garrote Gascón
|
||||||
|
*
|
||||||
|
* Copyright (C) 2023 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.settings.more
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.Intent.EXTRA_SUBJECT
|
||||||
|
import android.content.Intent.EXTRA_TEXT
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.fragment.app.testing.FragmentScenario
|
||||||
|
import androidx.fragment.app.testing.launchFragmentInContainer
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions
|
||||||
|
import androidx.test.espresso.intent.Intents
|
||||||
|
import androidx.test.espresso.intent.Intents.intended
|
||||||
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||||
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasData
|
||||||
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
|
||||||
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasFlag
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import eu.qsfera.android.BuildConfig
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.presentation.settings.more.SettingsMoreFragment
|
||||||
|
import eu.qsfera.android.presentation.settings.more.SettingsMoreViewModel
|
||||||
|
import eu.qsfera.android.utils.matchers.verifyPreference
|
||||||
|
import eu.qsfera.android.utils.mockIntent
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.unmockkAll
|
||||||
|
import org.hamcrest.Matchers.allOf
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
class SettingsMoreFragmentTest {
|
||||||
|
|
||||||
|
private lateinit var fragmentScenario: FragmentScenario<SettingsMoreFragment>
|
||||||
|
|
||||||
|
private var prefHelp: Preference? = null
|
||||||
|
private var prefSync: Preference? = null
|
||||||
|
private var prefAccessDocProvider: Preference? = null
|
||||||
|
private var prefRecommend: Preference? = null
|
||||||
|
private var prefFeedback: Preference? = null
|
||||||
|
private var prefImprint: Preference? = null
|
||||||
|
|
||||||
|
private lateinit var moreViewModel: SettingsMoreViewModel
|
||||||
|
private lateinit var context: Context
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
moreViewModel = mockk(relaxed = true)
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
context
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
moreViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Intents.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Intents.release()
|
||||||
|
unmockkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPreference(key: String): Preference? {
|
||||||
|
var preference: Preference? = null
|
||||||
|
fragmentScenario.onFragment { fragment ->
|
||||||
|
preference = fragment.findPreference(key)
|
||||||
|
}
|
||||||
|
return preference
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchTest(
|
||||||
|
helpEnabled: Boolean = true,
|
||||||
|
syncEnabled: Boolean = true,
|
||||||
|
docProviderAppEnabled: Boolean = true,
|
||||||
|
recommendEnabled: Boolean = true,
|
||||||
|
feedbackEnabled: Boolean = true,
|
||||||
|
imprintEnabled: Boolean = true
|
||||||
|
) {
|
||||||
|
every { moreViewModel.isHelpEnabled() } returns helpEnabled
|
||||||
|
every { moreViewModel.isSyncEnabled() } returns syncEnabled
|
||||||
|
every { moreViewModel.isDocProviderAppEnabled() } returns docProviderAppEnabled
|
||||||
|
every { moreViewModel.isRecommendEnabled() } returns recommendEnabled
|
||||||
|
every { moreViewModel.isFeedbackEnabled() } returns feedbackEnabled
|
||||||
|
every { moreViewModel.isImprintEnabled() } returns imprintEnabled
|
||||||
|
|
||||||
|
fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_qsfera)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun moreView() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
prefHelp = getPreference(PREFERENCE_HELP)
|
||||||
|
assertNotNull(prefHelp)
|
||||||
|
prefHelp?.verifyPreference(
|
||||||
|
keyPref = PREFERENCE_HELP,
|
||||||
|
titlePref = context.getString(R.string.prefs_help),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
prefSync = getPreference(PREFERENCE_SYNC_CALENDAR_CONTACTS)
|
||||||
|
assertNotNull(prefSync)
|
||||||
|
prefSync?.verifyPreference(
|
||||||
|
keyPref = PREFERENCE_SYNC_CALENDAR_CONTACTS,
|
||||||
|
titlePref = context.getString(R.string.prefs_sync_calendar_contacts),
|
||||||
|
summaryPref = context.getString(R.string.prefs_sync_calendar_contacts_summary),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
prefAccessDocProvider = getPreference(PREFERENCE_ACCESS_DOCUMENT_PROVIDER)
|
||||||
|
assertNotNull(prefAccessDocProvider)
|
||||||
|
prefAccessDocProvider?.verifyPreference(
|
||||||
|
keyPref = PREFERENCE_ACCESS_DOCUMENT_PROVIDER,
|
||||||
|
titlePref = context.getString(R.string.prefs_access_document_provider),
|
||||||
|
summaryPref = context.getString(R.string.prefs_access_document_provider_summary),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
prefRecommend = getPreference(PREFERENCE_RECOMMEND)
|
||||||
|
assertNotNull(prefRecommend)
|
||||||
|
prefRecommend?.verifyPreference(
|
||||||
|
keyPref = PREFERENCE_RECOMMEND,
|
||||||
|
titlePref = context.getString(R.string.prefs_recommend),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
prefFeedback = getPreference(PREFERENCE_FEEDBACK)
|
||||||
|
assertNotNull(prefFeedback)
|
||||||
|
prefFeedback?.verifyPreference(
|
||||||
|
keyPref = PREFERENCE_FEEDBACK,
|
||||||
|
titlePref = context.getString(R.string.prefs_send_feedback),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
prefImprint = getPreference(PREFERENCE_IMPRINT)
|
||||||
|
assertNotNull(prefImprint)
|
||||||
|
prefImprint?.verifyPreference(
|
||||||
|
keyPref = PREFERENCE_IMPRINT,
|
||||||
|
titlePref = context.getString(R.string.prefs_imprint),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun helpNotEnabledView() {
|
||||||
|
launchTest(helpEnabled = false)
|
||||||
|
prefHelp = getPreference(PREFERENCE_HELP)
|
||||||
|
|
||||||
|
assertNull(prefHelp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun syncNotEnabledView() {
|
||||||
|
launchTest(syncEnabled = false)
|
||||||
|
prefSync = getPreference(PREFERENCE_SYNC_CALENDAR_CONTACTS)
|
||||||
|
|
||||||
|
assertNull(prefSync)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun accessDocumentProviderNotEnabledView() {
|
||||||
|
launchTest(docProviderAppEnabled = false)
|
||||||
|
prefAccessDocProvider = getPreference(PREFERENCE_ACCESS_DOCUMENT_PROVIDER)
|
||||||
|
|
||||||
|
assertNull(prefAccessDocProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun recommendNotEnabledView() {
|
||||||
|
launchTest(recommendEnabled = false)
|
||||||
|
prefRecommend = getPreference(PREFERENCE_RECOMMEND)
|
||||||
|
|
||||||
|
assertNull(prefRecommend)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun feedbackNotEnabledView() {
|
||||||
|
launchTest(feedbackEnabled = false)
|
||||||
|
prefFeedback = getPreference(PREFERENCE_FEEDBACK)
|
||||||
|
|
||||||
|
assertNull(prefFeedback)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun imprintNotEnabledView() {
|
||||||
|
launchTest(imprintEnabled = false)
|
||||||
|
prefImprint = getPreference(PREFERENCE_IMPRINT)
|
||||||
|
|
||||||
|
assertNull(prefImprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun helpOpensNotEmptyUrl() {
|
||||||
|
every { moreViewModel.getHelpUrl() } returns context.getString(R.string.url_help)
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
mockIntent(action = Intent.ACTION_VIEW)
|
||||||
|
onView(withText(R.string.prefs_help)).perform(click())
|
||||||
|
|
||||||
|
intended(hasData(context.getString(R.string.url_help)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun syncOpensNotEmptyUrl() {
|
||||||
|
every { moreViewModel.getSyncUrl() } returns context.getString(R.string.url_sync_calendar_contacts)
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
mockIntent(action = Intent.ACTION_VIEW)
|
||||||
|
onView(withText(R.string.prefs_sync_calendar_contacts)).perform(click())
|
||||||
|
|
||||||
|
intended(hasData(context.getString(R.string.url_sync_calendar_contacts)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun accessDocumentProviderOpensNotEmptyUrl() {
|
||||||
|
every { moreViewModel.getDocProviderAppUrl() } returns context.getString(R.string.url_document_provider_app)
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
mockIntent(action = Intent.ACTION_VIEW)
|
||||||
|
onView(withText(R.string.prefs_access_document_provider)).perform(click())
|
||||||
|
|
||||||
|
intended(hasData(context.getString(R.string.url_document_provider_app)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun recommendOpensSender() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
mockIntent(action = Intent.ACTION_SENDTO)
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_recommend)).perform(click())
|
||||||
|
// Delay needed since depending on the performance of the device where tests are executed,
|
||||||
|
// sender can interfere with the subsequent tests
|
||||||
|
Thread.sleep(1000)
|
||||||
|
intended(
|
||||||
|
allOf(
|
||||||
|
hasAction(Intent.ACTION_SENDTO), hasExtra(
|
||||||
|
EXTRA_SUBJECT, String.format(
|
||||||
|
context.getString(R.string.recommend_subject),
|
||||||
|
context.getString(R.string.app_name)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
hasExtra(
|
||||||
|
EXTRA_TEXT,
|
||||||
|
String.format(
|
||||||
|
context.getString(R.string.recommend_text),
|
||||||
|
context.getString(R.string.app_name),
|
||||||
|
context.getString(R.string.url_app_download)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
hasFlag(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun feedbackOpensSenderIfFeedbackMailExists() {
|
||||||
|
launchTest()
|
||||||
|
every { moreViewModel.getFeedbackMail() } returns FEEDBACK_MAIL
|
||||||
|
mockIntent(action = Intent.ACTION_SENDTO)
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_send_feedback)).perform(click())
|
||||||
|
// Delay needed since depending on the performance of the device where tests are executed,
|
||||||
|
// sender can interfere with the subsequent tests
|
||||||
|
Thread.sleep(1000)
|
||||||
|
intended(
|
||||||
|
allOf(
|
||||||
|
hasAction(Intent.ACTION_SENDTO),
|
||||||
|
hasExtra(
|
||||||
|
EXTRA_SUBJECT,
|
||||||
|
"Android v" + BuildConfig.VERSION_NAME + " - " + context.getText(R.string.prefs_feedback)
|
||||||
|
),
|
||||||
|
hasData(Uri.parse(FEEDBACK_MAIL)),
|
||||||
|
hasFlag(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun feedbackOpensAlertDialogIfFeedbackMailIsEmpty() {
|
||||||
|
launchTest()
|
||||||
|
every { moreViewModel.getFeedbackMail() } returns ""
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_send_feedback)).perform(click())
|
||||||
|
|
||||||
|
onView(withText(R.string.drawer_feedback)).check(ViewAssertions.matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun imprintOpensUrl() {
|
||||||
|
every { moreViewModel.getImprintUrl() } returns "https://qsfera.eu/mobile"
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
mockIntent(action = Intent.ACTION_VIEW)
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_imprint)).perform(click())
|
||||||
|
intended(hasData("https://qsfera.eu/mobile"))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREFERENCE_HELP = "help"
|
||||||
|
private const val PREFERENCE_SYNC_CALENDAR_CONTACTS = "syncCalendarContacts"
|
||||||
|
private const val PREFERENCE_ACCESS_DOCUMENT_PROVIDER = "accessDocumentProvider"
|
||||||
|
private const val PREFERENCE_RECOMMEND = "recommend"
|
||||||
|
private const val PREFERENCE_FEEDBACK = "feedback"
|
||||||
|
private const val PREFERENCE_IMPRINT = "imprint"
|
||||||
|
private const val FEEDBACK_MAIL = "mailto:mail@qsfera.eu"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
/*
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Jesus Recio (@jesmrec)
|
||||||
|
* @author Christian Schabesberger (@theScrabi)
|
||||||
|
* @author Juan Carlos Garrote Gascón (@JuancaG05)
|
||||||
|
* @author David Crespo Ríos (@davcres)
|
||||||
|
*
|
||||||
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.settings.security
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.db.PreferenceManager
|
||||||
|
import eu.qsfera.android.domain.utils.Event
|
||||||
|
import eu.qsfera.android.presentation.security.passcode.PassCodeActivity
|
||||||
|
import eu.qsfera.android.presentation.security.passcode.PasscodeAction
|
||||||
|
import eu.qsfera.android.presentation.security.passcode.PasscodeType
|
||||||
|
import eu.qsfera.android.presentation.security.passcode.Status
|
||||||
|
import eu.qsfera.android.presentation.security.biometric.BiometricViewModel
|
||||||
|
import eu.qsfera.android.presentation.security.passcode.PassCodeViewModel
|
||||||
|
import eu.qsfera.android.testutil.security.OC_PASSCODE_4_DIGITS
|
||||||
|
import eu.qsfera.android.utils.matchers.isDisplayed
|
||||||
|
import eu.qsfera.android.utils.matchers.withChildCountAndId
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
class PassCodeActivityTest {
|
||||||
|
|
||||||
|
private lateinit var activityScenario: ActivityScenario<PassCodeActivity>
|
||||||
|
|
||||||
|
private lateinit var context: Context
|
||||||
|
|
||||||
|
private lateinit var timeToUnlockLiveData: MutableLiveData<Event<String>>
|
||||||
|
private lateinit var finishTimeToUnlockLiveData: MutableLiveData<Event<Boolean>>
|
||||||
|
private lateinit var statusLiveData: MutableLiveData<Status>
|
||||||
|
private lateinit var passcodeLiveData: MutableLiveData<String>
|
||||||
|
|
||||||
|
private lateinit var passCodeViewModel: PassCodeViewModel
|
||||||
|
private lateinit var biometricViewModel: BiometricViewModel
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
|
passCodeViewModel = mockk(relaxed = true)
|
||||||
|
biometricViewModel = mockk(relaxed = true)
|
||||||
|
|
||||||
|
timeToUnlockLiveData = MutableLiveData()
|
||||||
|
finishTimeToUnlockLiveData = MutableLiveData()
|
||||||
|
statusLiveData = MutableLiveData()
|
||||||
|
passcodeLiveData = MutableLiveData()
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
allowOverride(override = true)
|
||||||
|
context
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
passCodeViewModel
|
||||||
|
}
|
||||||
|
viewModel {
|
||||||
|
biometricViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
every { passCodeViewModel.getPassCode() } returns OC_PASSCODE_4_DIGITS
|
||||||
|
every { passCodeViewModel.getNumberOfPassCodeDigits() } returns 4
|
||||||
|
every { passCodeViewModel.getNumberOfAttempts() } returns 0
|
||||||
|
every { passCodeViewModel.getTimeToUnlockLiveData } returns timeToUnlockLiveData
|
||||||
|
every { passCodeViewModel.getFinishedTimeToUnlockLiveData } returns finishTimeToUnlockLiveData
|
||||||
|
every { passCodeViewModel.status } returns statusLiveData
|
||||||
|
every { passCodeViewModel.passcode } returns passcodeLiveData
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
// Clean preferences
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun passcodeCheckNotLockedView() {
|
||||||
|
// Open Activity in passcode check mode
|
||||||
|
openPasscodeActivity(PassCodeActivity.ACTION_CHECK)
|
||||||
|
|
||||||
|
with(R.id.header) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pass_code_enter_pass_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.explanation.isDisplayed(false)
|
||||||
|
|
||||||
|
// Check if required amount of input fields are actually displayed
|
||||||
|
with(R.id.layout_code) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withChildCountAndId(passCodeViewModel.getNumberOfPassCodeDigits(), R.id.passCodeEditText)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.lock_time.isDisplayed(false)
|
||||||
|
|
||||||
|
R.id.numberKeyboard.isDisplayed(true)
|
||||||
|
R.id.key0.isDisplayed(true)
|
||||||
|
R.id.key1.isDisplayed(true)
|
||||||
|
R.id.key2.isDisplayed(true)
|
||||||
|
R.id.key3.isDisplayed(true)
|
||||||
|
R.id.key4.isDisplayed(true)
|
||||||
|
R.id.key5.isDisplayed(true)
|
||||||
|
R.id.key6.isDisplayed(true)
|
||||||
|
R.id.key7.isDisplayed(true)
|
||||||
|
R.id.key8.isDisplayed(true)
|
||||||
|
R.id.key9.isDisplayed(true)
|
||||||
|
R.id.backspaceBtn.isDisplayed(true)
|
||||||
|
R.id.biometricBtn.isDisplayed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun passcodeCheckLockedView() {
|
||||||
|
every { passCodeViewModel.getNumberOfAttempts() } returns 3
|
||||||
|
every { passCodeViewModel.getTimeToUnlockLeft() } returns 3000
|
||||||
|
|
||||||
|
timeToUnlockLiveData.postValue(Event("00:03"))
|
||||||
|
|
||||||
|
// Open Activity in passcode check mode
|
||||||
|
openPasscodeActivity(PassCodeActivity.ACTION_CHECK)
|
||||||
|
|
||||||
|
with(R.id.header) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pass_code_enter_pass_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.explanation.isDisplayed(false)
|
||||||
|
|
||||||
|
// Check if required amount of input fields are actually displayed
|
||||||
|
with(R.id.layout_code) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withChildCountAndId(passCodeViewModel.getNumberOfPassCodeDigits(), R.id.passCodeEditText)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.lock_time.isDisplayed(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun passcodeView() {
|
||||||
|
// Open Activity in passcode creation mode
|
||||||
|
openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
|
||||||
|
|
||||||
|
with(R.id.header) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pass_code_configure_your_pass_code)
|
||||||
|
}
|
||||||
|
with(R.id.explanation) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pass_code_configure_your_pass_code_explanation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if required amount of input fields are actually displayed
|
||||||
|
with(R.id.layout_code) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withChildCountAndId(passCodeViewModel.getNumberOfPassCodeDigits(), R.id.passCodeEditText)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.lock_time.isDisplayed(false)
|
||||||
|
|
||||||
|
R.id.error.isDisplayed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun firstTry() {
|
||||||
|
// Open Activity in passcode creation mode
|
||||||
|
openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
|
||||||
|
|
||||||
|
statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.NO_CONFIRM))
|
||||||
|
|
||||||
|
with(R.id.header) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pass_code_reenter_your_pass_code)
|
||||||
|
}
|
||||||
|
onView(withText(R.string.pass_code_configure_your_pass_code)).check(doesNotExist())
|
||||||
|
|
||||||
|
R.id.error.isDisplayed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun secondTryCorrect() {
|
||||||
|
every { biometricViewModel.isBiometricLockAvailable() } returns true
|
||||||
|
|
||||||
|
// Open Activity in passcode creation mode
|
||||||
|
openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
|
||||||
|
|
||||||
|
statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM))
|
||||||
|
|
||||||
|
// Click dialog's enable option
|
||||||
|
onView(withText(R.string.common_yes)).perform(click())
|
||||||
|
|
||||||
|
// Checking that the result returned is OK
|
||||||
|
assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun secondTryIncorrect() {
|
||||||
|
// Open Activity in passcode creation mode
|
||||||
|
openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
|
||||||
|
|
||||||
|
statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.ERROR))
|
||||||
|
|
||||||
|
with(R.id.header) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pass_code_configure_your_pass_code)
|
||||||
|
}
|
||||||
|
with(R.id.explanation) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pass_code_configure_your_pass_code_explanation)
|
||||||
|
}
|
||||||
|
with(R.id.error) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pass_code_mismatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.lock_time.isDisplayed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deletePasscodeView() {
|
||||||
|
// Open Activity in passcode deletion mode
|
||||||
|
openPasscodeActivity(PassCodeActivity.ACTION_REMOVE)
|
||||||
|
|
||||||
|
with(R.id.header) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pass_code_remove_your_pass_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.explanation.isDisplayed(false)
|
||||||
|
|
||||||
|
R.id.error.isDisplayed(false)
|
||||||
|
|
||||||
|
R.id.lock_time.isDisplayed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun deletePasscodeCorrect() {
|
||||||
|
// Open Activity in passcode deletion mode
|
||||||
|
openPasscodeActivity(PassCodeActivity.ACTION_REMOVE)
|
||||||
|
|
||||||
|
statusLiveData.postValue(Status(PasscodeAction.REMOVE, PasscodeType.OK))
|
||||||
|
|
||||||
|
assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deletePasscodeIncorrect() {
|
||||||
|
// Open Activity in passcode deletion mode
|
||||||
|
openPasscodeActivity(PassCodeActivity.ACTION_REMOVE)
|
||||||
|
|
||||||
|
statusLiveData.postValue(Status(PasscodeAction.REMOVE, PasscodeType.ERROR))
|
||||||
|
|
||||||
|
with(R.id.header) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pass_code_enter_pass_code)
|
||||||
|
}
|
||||||
|
with(R.id.error) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pass_code_wrong)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.explanation.isDisplayed(false)
|
||||||
|
|
||||||
|
R.id.lock_time.isDisplayed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun checkEnableBiometricDialogIsVisible() {
|
||||||
|
every { biometricViewModel.isBiometricLockAvailable() } returns true
|
||||||
|
|
||||||
|
// Open Activity in passcode creation mode
|
||||||
|
openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
|
||||||
|
|
||||||
|
statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM))
|
||||||
|
|
||||||
|
onView(withText(R.string.biometric_dialog_title)).check(matches(isDisplayed()))
|
||||||
|
onView(withText(R.string.common_yes)).check(matches(isDisplayed()))
|
||||||
|
onView(withText(R.string.common_no)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun checkEnableBiometricDialogYesOption() {
|
||||||
|
every { biometricViewModel.isBiometricLockAvailable() } returns true
|
||||||
|
|
||||||
|
// Open Activity in passcode creation mode
|
||||||
|
openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
|
||||||
|
|
||||||
|
statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM))
|
||||||
|
|
||||||
|
onView(withText(R.string.common_yes)).perform(click())
|
||||||
|
|
||||||
|
// Checking that the result returned is OK
|
||||||
|
assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun checkEnableBiometricDialogNoOption() {
|
||||||
|
every { biometricViewModel.isBiometricLockAvailable() } returns true
|
||||||
|
|
||||||
|
// Open Activity in passcode creation mode
|
||||||
|
openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
|
||||||
|
|
||||||
|
statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM))
|
||||||
|
|
||||||
|
onView(withText(R.string.common_no)).perform(click())
|
||||||
|
|
||||||
|
// Checking that the result returned is OK
|
||||||
|
assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openPasscodeActivity(mode: String) {
|
||||||
|
val intent = Intent(context, PassCodeActivity::class.java).apply {
|
||||||
|
action = mode
|
||||||
|
}
|
||||||
|
activityScenario = ActivityScenario.launch(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Jesus Recio (@jesmrec)
|
||||||
|
* @author Juan Carlos Garrote Gascón (@JuancaG05)
|
||||||
|
*
|
||||||
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.settings.security
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.db.PreferenceManager
|
||||||
|
import eu.qsfera.android.presentation.security.pattern.PatternActivity
|
||||||
|
import eu.qsfera.android.presentation.security.pattern.PatternViewModel
|
||||||
|
import eu.qsfera.android.testutil.security.OC_PATTERN
|
||||||
|
import eu.qsfera.android.utils.matchers.isDisplayed
|
||||||
|
import eu.qsfera.android.utils.matchers.withText
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
class PatternActivityTest {
|
||||||
|
|
||||||
|
private lateinit var activityScenario: ActivityScenario<PatternActivity>
|
||||||
|
|
||||||
|
private lateinit var context: Context
|
||||||
|
|
||||||
|
private lateinit var patternViewModel: PatternViewModel
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
|
patternViewModel = mockk(relaxUnitFun = true)
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
context
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
patternViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
// Clean preferences
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patternLockView() {
|
||||||
|
// Open Activity in pattern creation mode
|
||||||
|
openPatternActivity(PatternActivity.ACTION_REQUEST_WITH_RESULT)
|
||||||
|
|
||||||
|
with(R.id.header_pattern) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pattern_configure_pattern)
|
||||||
|
}
|
||||||
|
with(R.id.explanation_pattern) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pattern_configure_your_pattern_explanation)
|
||||||
|
}
|
||||||
|
R.id.pattern_lock_view.isDisplayed(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removePatternLock() {
|
||||||
|
// Save a pattern in Preferences
|
||||||
|
storePattern()
|
||||||
|
|
||||||
|
// Open Activity in pattern deletion mode
|
||||||
|
openPatternActivity(PatternActivity.ACTION_CHECK_WITH_RESULT)
|
||||||
|
|
||||||
|
with(R.id.header_pattern) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pattern_remove_pattern)
|
||||||
|
}
|
||||||
|
with(R.id.explanation_pattern) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(R.string.pattern_no_longer_required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun storePattern() {
|
||||||
|
val appPrefs = PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||||
|
appPrefs.apply {
|
||||||
|
putString(PatternActivity.PREFERENCE_PATTERN, OC_PATTERN)
|
||||||
|
putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, true)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openPatternActivity(mode: String) {
|
||||||
|
val intent = Intent(context, PatternActivity::class.java).apply {
|
||||||
|
action = mode
|
||||||
|
}
|
||||||
|
activityScenario = ActivityScenario.launch(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,499 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Juan Carlos Garrote Gascón
|
||||||
|
*
|
||||||
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
|
* <p>
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
* <p>
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.settings.security
|
||||||
|
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.biometric.BiometricViewModel
|
||||||
|
import androidx.fragment.app.testing.FragmentScenario
|
||||||
|
import androidx.fragment.app.testing.launchFragmentInContainer
|
||||||
|
import androidx.preference.CheckBoxPreference
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.intent.Intents
|
||||||
|
import androidx.test.espresso.intent.Intents.intended
|
||||||
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.presentation.security.biometric.BiometricActivity
|
||||||
|
import eu.qsfera.android.presentation.security.biometric.BiometricManager
|
||||||
|
import eu.qsfera.android.presentation.security.PREFERENCE_LOCK_TIMEOUT
|
||||||
|
import eu.qsfera.android.presentation.security.pattern.PatternActivity
|
||||||
|
import eu.qsfera.android.presentation.security.passcode.PassCodeActivity
|
||||||
|
import eu.qsfera.android.presentation.settings.security.SettingsSecurityFragment
|
||||||
|
import eu.qsfera.android.presentation.settings.security.SettingsSecurityFragment.Companion.PREFERENCE_LOCK_ACCESS_FROM_DOCUMENT_PROVIDER
|
||||||
|
import eu.qsfera.android.presentation.settings.security.SettingsSecurityViewModel
|
||||||
|
import eu.qsfera.android.testutil.security.OC_PATTERN
|
||||||
|
import eu.qsfera.android.utils.matchers.verifyPreference
|
||||||
|
import eu.qsfera.android.utils.mockIntent
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkObject
|
||||||
|
import org.hamcrest.Matchers.not
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
class SettingsSecurityFragmentTest {
|
||||||
|
|
||||||
|
private lateinit var fragmentScenario: FragmentScenario<SettingsSecurityFragment>
|
||||||
|
|
||||||
|
private lateinit var prefPasscode: CheckBoxPreference
|
||||||
|
private lateinit var prefPattern: CheckBoxPreference
|
||||||
|
private var prefBiometric: CheckBoxPreference? = null
|
||||||
|
private lateinit var prefLockApplication: ListPreference
|
||||||
|
private lateinit var prefLockAccessDocumentProvider: CheckBoxPreference
|
||||||
|
private lateinit var prefTouchesWithOtherVisibleWindows: CheckBoxPreference
|
||||||
|
|
||||||
|
private lateinit var securityViewModel: SettingsSecurityViewModel
|
||||||
|
private lateinit var biometricViewModel: BiometricViewModel
|
||||||
|
private lateinit var context: Context
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
securityViewModel = mockk(relaxed = true)
|
||||||
|
biometricViewModel = mockk(relaxed = true)
|
||||||
|
mockkObject(BiometricManager)
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
context
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
securityViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
every { securityViewModel.isSecurityEnforcedEnabled() } returns false
|
||||||
|
every { securityViewModel.getBiometricsState() } returns false
|
||||||
|
every { securityViewModel.isLockDelayEnforcedEnabled() } returns false
|
||||||
|
|
||||||
|
Intents.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Intents.release()
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchTest(withBiometrics: Boolean = true) {
|
||||||
|
every { BiometricManager.isHardwareDetected() } returns withBiometrics
|
||||||
|
|
||||||
|
fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_qsfera)
|
||||||
|
fragmentScenario.onFragment { fragment ->
|
||||||
|
prefPasscode = fragment.findPreference(PassCodeActivity.PREFERENCE_SET_PASSCODE)!!
|
||||||
|
prefPattern = fragment.findPreference(PatternActivity.PREFERENCE_SET_PATTERN)!!
|
||||||
|
prefBiometric = fragment.findPreference(BiometricActivity.PREFERENCE_SET_BIOMETRIC)
|
||||||
|
prefLockApplication = fragment.findPreference(PREFERENCE_LOCK_TIMEOUT)!!
|
||||||
|
prefLockAccessDocumentProvider = fragment.findPreference(PREFERENCE_LOCK_ACCESS_FROM_DOCUMENT_PROVIDER)!!
|
||||||
|
prefTouchesWithOtherVisibleWindows =
|
||||||
|
fragment.findPreference(SettingsSecurityFragment.PREFERENCE_TOUCHES_WITH_OTHER_VISIBLE_WINDOWS)!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkCommonPreferences() {
|
||||||
|
prefPasscode.verifyPreference(
|
||||||
|
keyPref = PassCodeActivity.PREFERENCE_SET_PASSCODE,
|
||||||
|
titlePref = context.getString(R.string.prefs_passcode),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
assertFalse(prefPasscode.isChecked)
|
||||||
|
|
||||||
|
prefPattern.verifyPreference(
|
||||||
|
keyPref = PatternActivity.PREFERENCE_SET_PATTERN,
|
||||||
|
titlePref = context.getString(R.string.prefs_pattern),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
assertFalse(prefPattern.isChecked)
|
||||||
|
|
||||||
|
prefLockApplication.verifyPreference(
|
||||||
|
keyPref = PREFERENCE_LOCK_TIMEOUT,
|
||||||
|
titlePref = context.getString(R.string.prefs_lock_application),
|
||||||
|
visible = true,
|
||||||
|
enabled = false
|
||||||
|
)
|
||||||
|
|
||||||
|
prefLockAccessDocumentProvider.verifyPreference(
|
||||||
|
keyPref = PREFERENCE_LOCK_ACCESS_FROM_DOCUMENT_PROVIDER,
|
||||||
|
titlePref = context.getString(R.string.prefs_lock_access_from_document_provider),
|
||||||
|
summaryPref = context.getString(R.string.prefs_lock_access_from_document_provider_summary),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
assertFalse(prefLockAccessDocumentProvider.isChecked)
|
||||||
|
|
||||||
|
prefTouchesWithOtherVisibleWindows.verifyPreference(
|
||||||
|
keyPref = SettingsSecurityFragment.PREFERENCE_TOUCHES_WITH_OTHER_VISIBLE_WINDOWS,
|
||||||
|
titlePref = context.getString(R.string.prefs_touches_with_other_visible_windows),
|
||||||
|
summaryPref = context.getString(R.string.prefs_touches_with_other_visible_windows_summary),
|
||||||
|
visible = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
assertFalse(prefTouchesWithOtherVisibleWindows.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun securityViewDeviceWithBiometrics() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
checkCommonPreferences()
|
||||||
|
|
||||||
|
assertNotNull(prefBiometric)
|
||||||
|
prefBiometric?.run {
|
||||||
|
verifyPreference(
|
||||||
|
keyPref = BiometricActivity.PREFERENCE_SET_BIOMETRIC,
|
||||||
|
titlePref = context.getString(R.string.prefs_biometric),
|
||||||
|
summaryPref = context.getString(R.string.prefs_biometric_summary),
|
||||||
|
visible = true,
|
||||||
|
enabled = false
|
||||||
|
)
|
||||||
|
assertFalse(isChecked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun securityViewDeviceWithNoBiometrics() {
|
||||||
|
launchTest(withBiometrics = false)
|
||||||
|
|
||||||
|
checkCommonPreferences()
|
||||||
|
|
||||||
|
assertNull(prefBiometric)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun passcodeOpen() {
|
||||||
|
every { securityViewModel.isPatternSet() } returns false
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
mockIntent(RESULT_OK, PassCodeActivity.ACTION_CREATE)
|
||||||
|
onView(withText(R.string.prefs_passcode)).perform(click())
|
||||||
|
intended(hasComponent(PassCodeActivity::class.java.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patternOpen() {
|
||||||
|
every { securityViewModel.isPasscodeSet() } returns false
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_pattern)).perform(click())
|
||||||
|
intended(hasComponent(PatternActivity::class.java.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun passcodeLockEnabledOk() {
|
||||||
|
every { securityViewModel.isPatternSet() } returns false
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
mockIntent(
|
||||||
|
action = PassCodeActivity.ACTION_CREATE
|
||||||
|
)
|
||||||
|
onView(withText(R.string.prefs_passcode)).perform(click())
|
||||||
|
assertTrue(prefPasscode.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patternLockEnabledOk() {
|
||||||
|
every { securityViewModel.isPasscodeSet() } returns false
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
mockIntent(
|
||||||
|
extras = Pair(PatternActivity.PREFERENCE_PATTERN, OC_PATTERN),
|
||||||
|
action = PatternActivity.ACTION_REQUEST_WITH_RESULT
|
||||||
|
)
|
||||||
|
onView(withText(R.string.prefs_pattern)).perform(click())
|
||||||
|
assertTrue(prefPattern.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun enablePasscodeEnablesBiometricLockAndLockApplication() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
firstEnablePasscode()
|
||||||
|
onView(withText(R.string.prefs_biometric)).check(matches(isEnabled()))
|
||||||
|
assertTrue(prefBiometric!!.isEnabled)
|
||||||
|
assertFalse(prefBiometric!!.isChecked)
|
||||||
|
assertTrue(prefLockApplication.isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun enablePatternEnablesBiometricLockAndLockApplication() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
firstEnablePattern()
|
||||||
|
onView(withText(R.string.prefs_biometric)).check(matches(isEnabled()))
|
||||||
|
assertTrue(prefBiometric!!.isEnabled)
|
||||||
|
assertFalse(prefBiometric!!.isChecked)
|
||||||
|
assertTrue(prefLockApplication.isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun onlyOneMethodEnabledPattern() {
|
||||||
|
every { securityViewModel.isPatternSet() } returns true
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
firstEnablePattern()
|
||||||
|
onView(withText(R.string.prefs_passcode)).perform(click())
|
||||||
|
onView(withText(R.string.pattern_already_set)).check(matches(isEnabled()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onlyOneMethodEnabledPasscode() {
|
||||||
|
every { securityViewModel.isPasscodeSet() } returns true
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
firstEnablePasscode()
|
||||||
|
onView(withText(R.string.prefs_pattern)).perform(click())
|
||||||
|
onView(withText(R.string.passcode_already_set)).check(matches(isEnabled()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun disablePasscodeOk() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
firstEnablePasscode()
|
||||||
|
mockIntent(
|
||||||
|
action = PassCodeActivity.ACTION_REMOVE
|
||||||
|
)
|
||||||
|
onView(withText(R.string.prefs_passcode)).perform(click())
|
||||||
|
assertFalse(prefPasscode.isChecked)
|
||||||
|
onView(withText(R.string.prefs_biometric)).check(matches(not(isEnabled())))
|
||||||
|
assertFalse(prefBiometric!!.isEnabled)
|
||||||
|
assertFalse(prefBiometric!!.isChecked)
|
||||||
|
assertFalse(prefLockApplication.isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun disablePatternOk() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
firstEnablePattern()
|
||||||
|
mockIntent(
|
||||||
|
action = PatternActivity.ACTION_CHECK_WITH_RESULT
|
||||||
|
)
|
||||||
|
onView(withText(R.string.prefs_pattern)).perform(click())
|
||||||
|
assertFalse(prefPattern.isChecked)
|
||||||
|
onView(withText(R.string.prefs_biometric)).check(matches(not(isEnabled())))
|
||||||
|
assertFalse(prefBiometric!!.isEnabled)
|
||||||
|
assertFalse(prefBiometric!!.isChecked)
|
||||||
|
assertFalse(prefLockApplication.isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun enableBiometricLockWithPasscodeEnabled() {
|
||||||
|
every { BiometricManager.hasEnrolledBiometric() } returns true
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
firstEnablePasscode()
|
||||||
|
onView(withText(R.string.prefs_biometric)).perform(click())
|
||||||
|
assertTrue(prefBiometric!!.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun enableBiometricLockWithPatternEnabled() {
|
||||||
|
every { BiometricManager.hasEnrolledBiometric() } returns true
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
firstEnablePattern()
|
||||||
|
onView(withText(R.string.prefs_biometric)).perform(click())
|
||||||
|
assertTrue(prefBiometric!!.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun enableBiometricLockNoEnrolledBiometric() {
|
||||||
|
every { BiometricManager.hasEnrolledBiometric() } returns false
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
firstEnablePasscode()
|
||||||
|
onView(withText(R.string.prefs_biometric)).perform(click())
|
||||||
|
assertFalse(prefBiometric!!.isChecked)
|
||||||
|
onView(withText(R.string.biometric_not_enrolled)).check(matches(isEnabled()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun disableBiometricLock() {
|
||||||
|
every { BiometricManager.hasEnrolledBiometric() } returns true
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
firstEnablePasscode()
|
||||||
|
onView(withText(R.string.prefs_biometric)).perform(click())
|
||||||
|
onView(withText(R.string.prefs_biometric)).perform(click())
|
||||||
|
assertFalse(prefBiometric!!.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun lockAccessFromDocumentProviderEnable() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_lock_access_from_document_provider)).perform(click())
|
||||||
|
assertTrue(prefLockAccessDocumentProvider.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun lockAccessFromDocumentProviderDisable() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_lock_access_from_document_provider)).perform(click())
|
||||||
|
onView(withText(R.string.prefs_lock_access_from_document_provider)).perform(click())
|
||||||
|
assertFalse(prefLockAccessDocumentProvider.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun touchesDialog() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
|
||||||
|
onView(withText(R.string.confirmation_touches_with_other_windows_title)).check(matches(isDisplayed()))
|
||||||
|
onView(withText(R.string.confirmation_touches_with_other_windows_message)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun touchesEnable() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
|
||||||
|
onView(withText(R.string.common_yes)).perform(click())
|
||||||
|
assertTrue(prefTouchesWithOtherVisibleWindows.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun touchesRefuse() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
|
||||||
|
onView(withText(R.string.common_no)).perform(click())
|
||||||
|
assertFalse(prefTouchesWithOtherVisibleWindows.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun touchesDisable() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
|
||||||
|
onView(withText(R.string.common_yes)).perform(click())
|
||||||
|
onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
|
||||||
|
assertFalse(prefTouchesWithOtherVisibleWindows.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun passcodeLockNotVisible() {
|
||||||
|
every { securityViewModel.isSecurityEnforcedEnabled() } returns true
|
||||||
|
launchTest()
|
||||||
|
assertFalse(prefPasscode.isVisible)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patternLockNotVisible() {
|
||||||
|
every { securityViewModel.isSecurityEnforcedEnabled() } returns true
|
||||||
|
launchTest()
|
||||||
|
assertFalse(prefPattern.isVisible)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun passcodeLockVisible() {
|
||||||
|
launchTest()
|
||||||
|
assertTrue(prefPasscode.isVisible)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patternLockVisible() {
|
||||||
|
launchTest()
|
||||||
|
assertTrue(prefPattern.isVisible)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun checkIfUserEnabledBiometricRecommendation() {
|
||||||
|
every { securityViewModel.getBiometricsState() } returns true
|
||||||
|
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
firstEnablePasscode()
|
||||||
|
|
||||||
|
assertTrue(prefBiometric!!.isChecked)
|
||||||
|
assertTrue(prefBiometric!!.isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkIfUserNotEnabledBiometricRecommendation() {
|
||||||
|
launchTest()
|
||||||
|
|
||||||
|
firstEnablePasscode()
|
||||||
|
|
||||||
|
assertFalse(prefBiometric!!.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun firstEnablePasscode() {
|
||||||
|
every { securityViewModel.isPatternSet() } returns false
|
||||||
|
|
||||||
|
mockIntent(
|
||||||
|
action = PassCodeActivity.ACTION_CREATE
|
||||||
|
)
|
||||||
|
onView(withText(R.string.prefs_passcode)).perform(click())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun firstEnablePattern() {
|
||||||
|
every { securityViewModel.isPasscodeSet() } returns false
|
||||||
|
|
||||||
|
mockIntent(
|
||||||
|
action = PatternActivity.ACTION_REQUEST_WITH_RESULT
|
||||||
|
)
|
||||||
|
onView(withText(R.string.prefs_pattern)).perform(click())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author David González Verdugo
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.sharing.sharees.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.domain.sharing.shares.model.OCShare
|
||||||
|
import eu.qsfera.android.domain.sharing.shares.model.ShareType
|
||||||
|
import eu.qsfera.android.domain.utils.Event
|
||||||
|
import eu.qsfera.android.presentation.common.UIResult
|
||||||
|
import eu.qsfera.android.presentation.sharing.sharees.SearchShareesFragment
|
||||||
|
import eu.qsfera.android.presentation.sharing.ShareViewModel
|
||||||
|
import eu.qsfera.android.sharing.shares.ui.TestShareFileActivity
|
||||||
|
import eu.qsfera.android.testutil.OC_SHARE
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockkClass
|
||||||
|
import org.hamcrest.CoreMatchers
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
class SearchShareesFragmentTest {
|
||||||
|
private val shareViewModel = mockkClass(ShareViewModel::class, relaxed = true)
|
||||||
|
private val sharesLiveData = MutableLiveData<Event<UIResult<List<OCShare>>>>()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
every { shareViewModel.shares } returns sharesLiveData
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
androidContext(ApplicationProvider.getApplicationContext())
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
shareViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
|
||||||
|
val searchShareesFragment = SearchShareesFragment()
|
||||||
|
it.startFragment(searchShareesFragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showSearchBar() {
|
||||||
|
onView(withId(R.id.search_mag_icon)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.search_plate)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showUserShares() {
|
||||||
|
sharesLiveData.postValue(
|
||||||
|
Event(
|
||||||
|
UIResult.Success(
|
||||||
|
listOf(
|
||||||
|
OC_SHARE.copy(sharedWithDisplayName = "Sheldon"),
|
||||||
|
OC_SHARE.copy(sharedWithDisplayName = "Penny")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withText("Sheldon"))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
.check(matches(hasSibling(withId(R.id.unshareButton))))
|
||||||
|
.check(matches(hasSibling(withId(R.id.editShareButton))))
|
||||||
|
onView(withText("Penny")).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showGroupShares() {
|
||||||
|
sharesLiveData.postValue(
|
||||||
|
Event(
|
||||||
|
UIResult.Success(
|
||||||
|
listOf(
|
||||||
|
OC_SHARE.copy(
|
||||||
|
shareType = ShareType.GROUP,
|
||||||
|
sharedWithDisplayName = "Friends"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withText("Friends (group)"))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
.check(matches(hasSibling(withId(R.id.icon))))
|
||||||
|
onView(ViewMatchers.withTagValue(CoreMatchers.equalTo(R.drawable.ic_group))).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author David González Verdugo
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.sharing.shares.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isChecked
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.domain.sharing.shares.model.OCShare
|
||||||
|
import eu.qsfera.android.domain.utils.Event
|
||||||
|
import eu.qsfera.android.presentation.common.UIResult
|
||||||
|
import eu.qsfera.android.presentation.sharing.sharees.EditPrivateShareFragment
|
||||||
|
import eu.qsfera.android.presentation.sharing.ShareViewModel
|
||||||
|
import eu.qsfera.android.testutil.OC_ACCOUNT
|
||||||
|
import eu.qsfera.android.testutil.OC_FILE
|
||||||
|
import eu.qsfera.android.testutil.OC_FOLDER
|
||||||
|
import eu.qsfera.android.testutil.OC_SHARE
|
||||||
|
import eu.qsfera.android.utils.Permissions
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.hamcrest.CoreMatchers.not
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
class EditPrivateShareFragmentTest {
|
||||||
|
private val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
private val defaultSharedWithDisplayName = "user"
|
||||||
|
private val shareViewModel = mockk<ShareViewModel>(relaxed = true)
|
||||||
|
private val privateShareAsLiveData = MutableLiveData<Event<UIResult<OCShare>>>()
|
||||||
|
|
||||||
|
private lateinit var activityScenario: ActivityScenario<TestShareFileActivity>
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
every { shareViewModel.privateShare } returns privateShareAsLiveData
|
||||||
|
every { shareViewModel.isResharingAllowed() } returns true
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
androidContext(ApplicationProvider.getApplicationContext())
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
shareViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showDialogTitle() {
|
||||||
|
loadEditPrivateShareFragment()
|
||||||
|
onView(withId(R.id.editShareTitle))
|
||||||
|
.check(
|
||||||
|
matches(
|
||||||
|
withText(
|
||||||
|
targetContext.getString(R.string.share_with_edit_title, defaultSharedWithDisplayName)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun closeDialog() {
|
||||||
|
loadEditPrivateShareFragment()
|
||||||
|
onView(withId(R.id.closeButton)).perform(click())
|
||||||
|
activityScenario.onActivity { Assert.assertNull(it.getTestFragment()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showToggles() {
|
||||||
|
loadEditPrivateShareFragment()
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showFileShareWithNoPermissions() {
|
||||||
|
loadEditPrivateShareFragment()
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isNotChecked()))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showFileShareWithEditPermissions() {
|
||||||
|
loadEditPrivateShareFragment(permissions = Permissions.EDIT_PERMISSIONS.value)
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showFileShareWithSharePermissions() {
|
||||||
|
loadEditPrivateShareFragment(permissions = Permissions.SHARE_PERMISSIONS.value)
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isNotChecked()))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isChecked()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showFileShareWithAllPermissions() {
|
||||||
|
loadEditPrivateShareFragment(permissions = Permissions.ALL_PERMISSIONS.value)
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isChecked()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showFolderShareWithCreatePermissions() {
|
||||||
|
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CREATE_PERMISSIONS.value)
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isNotChecked()))
|
||||||
|
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isNotChecked()))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showFolderShareWithCreateChangePermissions() {
|
||||||
|
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CREATE_CHANGE_PERMISSIONS.value)
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isNotChecked()))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showFolderShareWithCreateDeletePermissions() {
|
||||||
|
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CREATE_DELETE_PERMISSIONS.value)
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isNotChecked()))
|
||||||
|
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showFolderShareWithCreateChangeDeletePermissions() {
|
||||||
|
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CREATE_CHANGE_DELETE_PERMISSIONS.value)
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showFolderShareWithChangePermissions() {
|
||||||
|
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CHANGE_PERMISSIONS.value)
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isNotChecked()))
|
||||||
|
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isNotChecked()))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showFolderShareWithChangeDeletePermissions() {
|
||||||
|
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CHANGE_DELETE_PERMISSIONS.value)
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isNotChecked()))
|
||||||
|
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showFolderShareWithDeletePermissions() {
|
||||||
|
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_DELETE_PERMISSIONS.value)
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isNotChecked()))
|
||||||
|
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isNotChecked()))
|
||||||
|
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun disableEditPermissionWithFile() {
|
||||||
|
loadEditPrivateShareFragment(permissions = Permissions.EDIT_PERMISSIONS.value)
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
|
||||||
|
|
||||||
|
onView(withId(R.id.canEditSwitch)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isNotChecked())) // "Can edit" changes
|
||||||
|
onView(withId(R.id.canEditCreateCheckBox)).check(matches(not(isDisplayed()))) // No suboptions
|
||||||
|
onView(withId(R.id.canEditChangeCheckBox)).check(matches(not(isDisplayed())))
|
||||||
|
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(not(isDisplayed())))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked())) // "Can share" does not change
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun disableEditPermissionWithFolder() {
|
||||||
|
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_PERMISSIONS.value)
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
|
||||||
|
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isDisplayed())) // Suboptions appear
|
||||||
|
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
|
||||||
|
|
||||||
|
onView(withId(R.id.canEditSwitch)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.canEditSwitch)).check(matches(isNotChecked())) // "Can edit" changes
|
||||||
|
onView(withId(R.id.canEditCreateCheckBox)).check(matches(not(isDisplayed()))) // Suboptions hidden
|
||||||
|
onView(withId(R.id.canEditChangeCheckBox)).check(matches(not(isDisplayed())))
|
||||||
|
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(not(isDisplayed())))
|
||||||
|
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked())) // "Can share" does not change
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadEditPrivateShareFragment(
|
||||||
|
isFolder: Boolean = false,
|
||||||
|
permissions: Int = Permissions.READ_PERMISSIONS.value
|
||||||
|
) {
|
||||||
|
val shareToEdit = OC_SHARE.copy(
|
||||||
|
sharedWithDisplayName = defaultSharedWithDisplayName,
|
||||||
|
permissions = permissions
|
||||||
|
)
|
||||||
|
|
||||||
|
val sharedFile = if (isFolder) OC_FOLDER else OC_FILE
|
||||||
|
|
||||||
|
val editPrivateShareFragment = EditPrivateShareFragment.newInstance(
|
||||||
|
shareToEdit,
|
||||||
|
sharedFile,
|
||||||
|
OC_ACCOUNT
|
||||||
|
)
|
||||||
|
|
||||||
|
activityScenario = ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
|
||||||
|
it.startFragment(editPrivateShareFragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
privateShareAsLiveData.postValue(
|
||||||
|
Event(
|
||||||
|
UIResult.Success(
|
||||||
|
OC_SHARE.copy(
|
||||||
|
shareWith = "user",
|
||||||
|
sharedWithDisplayName = "User",
|
||||||
|
path = "/Videos",
|
||||||
|
isFolder = isFolder,
|
||||||
|
permissions = permissions
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,493 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author David González Verdugo
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.sharing.shares.ui
|
||||||
|
|
||||||
|
import android.text.InputType.TYPE_CLASS_TEXT
|
||||||
|
import android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.action.ViewActions.scrollTo
|
||||||
|
import androidx.test.espresso.action.ViewActions.typeText
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withInputType
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.domain.capabilities.model.CapabilityBooleanType
|
||||||
|
import eu.qsfera.android.domain.capabilities.model.OCCapability
|
||||||
|
import eu.qsfera.android.domain.utils.Event
|
||||||
|
import eu.qsfera.android.presentation.common.UIResult
|
||||||
|
import eu.qsfera.android.presentation.sharing.shares.PublicShareDialogFragment
|
||||||
|
import eu.qsfera.android.presentation.capabilities.CapabilityViewModel
|
||||||
|
import eu.qsfera.android.presentation.sharing.ShareViewModel
|
||||||
|
import eu.qsfera.android.testutil.OC_ACCOUNT
|
||||||
|
import eu.qsfera.android.testutil.OC_CAPABILITY
|
||||||
|
import eu.qsfera.android.testutil.OC_FILE
|
||||||
|
import eu.qsfera.android.testutil.OC_FOLDER
|
||||||
|
import eu.qsfera.android.utils.DateUtils
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.hamcrest.CoreMatchers.not
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class PublicShareCreationDialogFragmentTest {
|
||||||
|
private val capabilityViewModel = mockk<CapabilityViewModel>(relaxed = true)
|
||||||
|
private val capabilitiesLiveData = MutableLiveData<Event<UIResult<OCCapability>>>()
|
||||||
|
private val shareViewModel = mockk<ShareViewModel>(relaxed = true)
|
||||||
|
private val publicShareCreationStatus = MutableLiveData<Event<UIResult<Unit>>>()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
every { capabilityViewModel.capabilities } returns capabilitiesLiveData
|
||||||
|
every { shareViewModel.publicShareCreationStatus } returns publicShareCreationStatus
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
androidContext(ApplicationProvider.getApplicationContext())
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
capabilityViewModel
|
||||||
|
}
|
||||||
|
viewModel {
|
||||||
|
shareViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showDialogTitle() {
|
||||||
|
loadPublicShareDialogFragment()
|
||||||
|
onView(withId(R.id.publicShareDialogTitle)).check(matches(withText(R.string.share_via_link_create_title)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showMandatoryFields() {
|
||||||
|
loadPublicShareDialogFragment()
|
||||||
|
onView(withId(R.id.shareViaLinkNameSection)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordSection)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationSection)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showDialogButtons() {
|
||||||
|
loadPublicShareDialogFragment()
|
||||||
|
onView(withId(R.id.cancelButton)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.saveButton)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showFolderAdditionalFields() {
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
isFolder = true,
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.0.1",
|
||||||
|
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionGroup)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showDefaultLinkName() {
|
||||||
|
loadPublicShareDialogFragment()
|
||||||
|
onView(withId(R.id.shareViaLinkNameValue)).check(matches(withText("DOC_12112018.jpg link")))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun enablePasswordSwitch() {
|
||||||
|
loadPublicShareDialogFragment()
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordSwitch)).perform(click())
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordValue)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkPasswordNotVisible() {
|
||||||
|
loadPublicShareDialogFragment()
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordSwitch)).perform(click())
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordValue)).perform(typeText("supersecure"))
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordValue)).check(
|
||||||
|
matches(
|
||||||
|
withInputType(
|
||||||
|
TYPE_CLASS_TEXT or TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkPasswordEnforced() {
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordLabel)).check(
|
||||||
|
matches(withText(R.string.share_via_link_password_enforced_label))
|
||||||
|
)
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordSwitch))
|
||||||
|
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordValue))
|
||||||
|
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
||||||
|
onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkExpireDateEnforced() {
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
filesSharingPublicExpireDateEnforced = CapabilityBooleanType.TRUE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationLabel))
|
||||||
|
.check(matches(withText(R.string.share_via_link_expiration_date_enforced_label)))
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationSwitch))
|
||||||
|
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationExplanationLabel))
|
||||||
|
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkExpireDateNotEnforced() {
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
filesSharingPublicExpireDateEnforced = CapabilityBooleanType.FALSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationLabel))
|
||||||
|
.check(matches(withText(R.string.share_via_link_expiration_date_label)))
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationSwitch))
|
||||||
|
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationExplanationLabel))
|
||||||
|
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun enableExpirationSwitch() {
|
||||||
|
loadPublicShareDialogFragment()
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationSwitch)).perform(click())
|
||||||
|
onView(withId(android.R.id.button1)).perform(click())
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationValue))
|
||||||
|
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
||||||
|
//TODO: check the date form the picker
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cancelExpirationSwitch() {
|
||||||
|
loadPublicShareDialogFragment()
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationSwitch)).perform(click())
|
||||||
|
onView(withId(android.R.id.button2)).perform(click())
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationValue))
|
||||||
|
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.INVISIBLE)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showError() {
|
||||||
|
loadPublicShareDialogFragment()
|
||||||
|
|
||||||
|
onView(withId(R.id.saveButton)).perform(click())
|
||||||
|
|
||||||
|
publicShareCreationStatus.postValue(
|
||||||
|
Event(
|
||||||
|
UIResult.Error(
|
||||||
|
error = Throwable("It was not possible to share this file or folder")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.public_link_error_message)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.public_link_error_message)).check(
|
||||||
|
matches(
|
||||||
|
withText(R.string.share_link_file_error)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun uploadPermissionsWithFolderDisplayed() {
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
isFolder = true,
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.1.1",
|
||||||
|
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicUpload = CapabilityBooleanType.TRUE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionGroup)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun uploadPermissionsWithFolderNotDisplayed() {
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
isFolder = true,
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.1.1",
|
||||||
|
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicUpload = CapabilityBooleanType.FALSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionGroup)).check(matches(not(isDisplayed())))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun expirationDateDays() {
|
||||||
|
val daysToTest = 15
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.1.1",
|
||||||
|
filesSharingPublicExpireDateDays = daysToTest
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val formattedDate = SimpleDateFormat.getDateInstance().format(
|
||||||
|
DateUtils.addDaysToDate(
|
||||||
|
Date(),
|
||||||
|
daysToTest
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationSwitch))
|
||||||
|
.check(matches(isEnabled()))
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationValue))
|
||||||
|
.check(matches(withText(formattedDate)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun passwordNotEnforced() {
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.1.1",
|
||||||
|
filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordLabel))
|
||||||
|
.check(matches(withText(R.string.share_via_link_password_label)))
|
||||||
|
onView(withId(R.id.saveButton)).check(matches(isEnabled()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun passwordEnforced() {
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.1.1",
|
||||||
|
filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordLabel))
|
||||||
|
.check(matches(withText(R.string.share_via_link_password_enforced_label)))
|
||||||
|
onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun passwordEnforcedReadOnlyFolders() {
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
isFolder = true,
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.1.1",
|
||||||
|
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicPasswordEnforcedReadOnly = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).perform(scrollTo())
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).perform(click())
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordLabel))
|
||||||
|
.check(matches(withText(R.string.share_via_link_password_enforced_label)))
|
||||||
|
onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun passwordNotEnforcedReadOnlyFolders() {
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
isFolder = true,
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.1.1",
|
||||||
|
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicPasswordEnforcedReadOnly = CapabilityBooleanType.FALSE,
|
||||||
|
filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).perform(click())
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordLabel))
|
||||||
|
.check(matches(withText(R.string.share_via_link_password_label)))
|
||||||
|
onView(withId(R.id.saveButton)).check(matches(isEnabled()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun passwordEnforcedReadWriteFolders() {
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
isFolder = true,
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.1.1",
|
||||||
|
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicPasswordEnforcedReadWrite = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionReadAndWrite)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionReadAndWrite)).perform(click())
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordLabel))
|
||||||
|
.check(matches(withText(R.string.share_via_link_password_enforced_label)))
|
||||||
|
onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun passwordNotEnforcedReadWriteFolders() {
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
isFolder = true,
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.1.1",
|
||||||
|
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicPasswordEnforcedReadWrite = CapabilityBooleanType.FALSE,
|
||||||
|
filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionReadAndWrite)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionReadAndWrite)).perform(click())
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordLabel))
|
||||||
|
.check(matches(withText(R.string.share_via_link_password_label)))
|
||||||
|
onView(withId(R.id.saveButton)).check(matches(isEnabled()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun passwordEnforcedUploadOnlyFolders() {
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
isFolder = true,
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.1.1",
|
||||||
|
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicPasswordEnforcedUploadOnly = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).perform(click())
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordLabel))
|
||||||
|
.check(matches(withText(R.string.share_via_link_password_enforced_label)))
|
||||||
|
onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun passwordNotEnforcedUploadOnlyFolders() {
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
isFolder = true,
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.1.1",
|
||||||
|
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicPasswordEnforcedUploadOnly = CapabilityBooleanType.FALSE,
|
||||||
|
filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).perform(click())
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordLabel))
|
||||||
|
.check(matches(withText(R.string.share_via_link_password_label)))
|
||||||
|
onView(withId(R.id.saveButton)).check(matches(isEnabled()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun passwordEnforcedClearErrorMessageIfSwitchesToNotEnforced() {
|
||||||
|
val commonError = "Common error"
|
||||||
|
|
||||||
|
//One permission with password enforced. Error is cleaned after switching permission
|
||||||
|
//to a non-forced one
|
||||||
|
loadPublicShareDialogFragment(
|
||||||
|
isFolder = true,
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.1.1",
|
||||||
|
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
|
||||||
|
filesSharingPublicPasswordEnforcedUploadOnly = CapabilityBooleanType.FALSE,
|
||||||
|
filesSharingPublicPasswordEnforcedReadOnly = CapabilityBooleanType.FALSE,
|
||||||
|
filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.saveButton)).perform(scrollTo())
|
||||||
|
onView(withId(R.id.saveButton)).perform(click())
|
||||||
|
|
||||||
|
publicShareCreationStatus.postValue(
|
||||||
|
Event(
|
||||||
|
UIResult.Error(
|
||||||
|
error = Throwable(commonError)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.public_link_error_message)).perform(scrollTo())
|
||||||
|
onView(withText(commonError)).check(matches(isDisplayed()))
|
||||||
|
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).perform(scrollTo(), click())
|
||||||
|
|
||||||
|
onView(withText(commonError)).check(matches(not(isDisplayed())))
|
||||||
|
onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadPublicShareDialogFragment(
|
||||||
|
isFolder: Boolean = false,
|
||||||
|
capabilities: OCCapability = OC_CAPABILITY
|
||||||
|
) {
|
||||||
|
val file = if (isFolder) OC_FOLDER else OC_FILE
|
||||||
|
|
||||||
|
val publicShareDialogFragment = PublicShareDialogFragment.newInstanceToCreate(
|
||||||
|
file,
|
||||||
|
OC_ACCOUNT,
|
||||||
|
"DOC_12112018.jpg link"
|
||||||
|
)
|
||||||
|
|
||||||
|
ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
|
||||||
|
it.startFragment(publicShareDialogFragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
capabilitiesLiveData.postValue(
|
||||||
|
Event(UIResult.Success(capabilities))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author David González Verdugo
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.sharing.shares.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isChecked
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withHint
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.domain.capabilities.model.OCCapability
|
||||||
|
import eu.qsfera.android.domain.sharing.shares.model.ShareType
|
||||||
|
import eu.qsfera.android.domain.utils.Event
|
||||||
|
import eu.qsfera.android.lib.resources.shares.RemoteShare
|
||||||
|
import eu.qsfera.android.presentation.common.UIResult
|
||||||
|
import eu.qsfera.android.presentation.sharing.shares.PublicShareDialogFragment
|
||||||
|
import eu.qsfera.android.presentation.capabilities.CapabilityViewModel
|
||||||
|
import eu.qsfera.android.presentation.sharing.ShareViewModel
|
||||||
|
import eu.qsfera.android.testutil.OC_ACCOUNT
|
||||||
|
import eu.qsfera.android.testutil.OC_FILE
|
||||||
|
import eu.qsfera.android.testutil.OC_SHARE
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.GregorianCalendar
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
class PublicShareEditionDialogFragmentTest {
|
||||||
|
private val capabilityViewModel = mockk<CapabilityViewModel>(relaxed = true)
|
||||||
|
private val capabilitiesLiveData = MutableLiveData<Event<UIResult<OCCapability>>>()
|
||||||
|
private val shareViewModel = mockk<ShareViewModel>(relaxed = true)
|
||||||
|
|
||||||
|
private val expirationDate = 1556575200000 // GMT: Monday, April 29, 2019 10:00:00 PM
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
every { capabilityViewModel.capabilities } returns capabilitiesLiveData
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
androidContext(ApplicationProvider.getApplicationContext())
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
capabilityViewModel
|
||||||
|
}
|
||||||
|
viewModel {
|
||||||
|
shareViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val publicShareDialogFragment = PublicShareDialogFragment.newInstanceToUpdate(
|
||||||
|
OC_FILE,
|
||||||
|
OC_ACCOUNT,
|
||||||
|
OC_SHARE.copy(
|
||||||
|
shareType = ShareType.PUBLIC_LINK,
|
||||||
|
shareWith = "user",
|
||||||
|
name = "Docs link",
|
||||||
|
permissions = RemoteShare.CREATE_PERMISSION_FLAG,
|
||||||
|
expirationDate = expirationDate,
|
||||||
|
isFolder = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
|
||||||
|
it.startFragment(publicShareDialogFragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showEditionDialogTitle() {
|
||||||
|
onView(withId(R.id.publicShareDialogTitle)).check(matches(withText(R.string.share_via_link_edit_title)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkLinkNameSet() {
|
||||||
|
onView(withText(R.string.share_via_link_name_label)).check(matches(ViewMatchers.isDisplayed()))
|
||||||
|
onView(withId(R.id.shareViaLinkNameValue)).check(matches(withText("Docs link")))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkUploadOnly() {
|
||||||
|
onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).check(matches(isChecked()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkPasswordSet() {
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordLabel)).check(matches(withText(R.string.share_via_link_password_label)))
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordSwitch)).check(matches(withEffectiveVisibility(VISIBLE)))
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordValue)).check(matches(withEffectiveVisibility(VISIBLE)))
|
||||||
|
onView(withId(R.id.shareViaLinkPasswordValue)).check(matches(withHint(R.string.share_via_link_default_password)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkExpirationDateSet() {
|
||||||
|
val calendar = GregorianCalendar()
|
||||||
|
calendar.timeInMillis = expirationDate
|
||||||
|
|
||||||
|
val formatter: DateFormat = SimpleDateFormat.getDateInstance()
|
||||||
|
formatter.timeZone = TimeZone.getDefault()
|
||||||
|
|
||||||
|
val time = formatter.format(calendar.time)
|
||||||
|
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationLabel)).check(matches(withText(R.string.share_via_link_expiration_date_label)))
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationSwitch)).check(matches(withEffectiveVisibility(VISIBLE)))
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationValue)).check(matches(withEffectiveVisibility(VISIBLE)))
|
||||||
|
onView(withId(R.id.shareViaLinkExpirationValue)).check(matches(withText(time)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author David González Verdugo
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.sharing.shares.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withTagValue
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.domain.capabilities.model.CapabilityBooleanType
|
||||||
|
import eu.qsfera.android.domain.capabilities.model.OCCapability
|
||||||
|
import eu.qsfera.android.domain.sharing.shares.model.OCShare
|
||||||
|
import eu.qsfera.android.domain.sharing.shares.model.ShareType
|
||||||
|
import eu.qsfera.android.domain.utils.Event
|
||||||
|
import eu.qsfera.android.presentation.common.UIResult
|
||||||
|
import eu.qsfera.android.presentation.sharing.ShareFileFragment
|
||||||
|
import eu.qsfera.android.presentation.capabilities.CapabilityViewModel
|
||||||
|
import eu.qsfera.android.presentation.sharing.ShareViewModel
|
||||||
|
import eu.qsfera.android.testutil.OC_ACCOUNT
|
||||||
|
import eu.qsfera.android.testutil.OC_CAPABILITY
|
||||||
|
import eu.qsfera.android.testutil.OC_FILE
|
||||||
|
import eu.qsfera.android.testutil.OC_SHARE
|
||||||
|
import eu.qsfera.android.utils.matchers.assertVisibility
|
||||||
|
import eu.qsfera.android.utils.matchers.isDisplayed
|
||||||
|
import eu.qsfera.android.utils.matchers.withText
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.hamcrest.CoreMatchers
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
class ShareFileFragmentTest {
|
||||||
|
private val capabilityViewModel = mockk<CapabilityViewModel>(relaxed = true)
|
||||||
|
private val capabilitiesLiveData = MutableLiveData<Event<UIResult<OCCapability>>>()
|
||||||
|
private val shareViewModel = mockk<ShareViewModel>(relaxed = true)
|
||||||
|
private val sharesLiveData = MutableLiveData<Event<UIResult<List<OCShare>>>>()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
every { capabilityViewModel.capabilities } returns capabilitiesLiveData
|
||||||
|
every { shareViewModel.shares } returns sharesLiveData
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
androidContext(ApplicationProvider.getApplicationContext())
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
capabilityViewModel
|
||||||
|
}
|
||||||
|
viewModel {
|
||||||
|
shareViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showHeader() {
|
||||||
|
loadShareFileFragment()
|
||||||
|
onView(withId(R.id.shareFileName)).check(matches(withText(OC_FILE.fileName)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fileSizeVisible() {
|
||||||
|
loadShareFileFragment()
|
||||||
|
R.id.shareFileSize.isDisplayed(displayed = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showPrivateLink() {
|
||||||
|
loadShareFileFragment()
|
||||||
|
R.id.getPrivateLinkButton.isDisplayed(displayed = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hidePrivateLink() {
|
||||||
|
loadShareFileFragment(capabilities = OC_CAPABILITY.copy(filesPrivateLinks = CapabilityBooleanType.FALSE))
|
||||||
|
R.id.getPrivateLinkButton.isDisplayed(displayed = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************************************
|
||||||
|
******************************************* PRIVATE SHARES *******************************************
|
||||||
|
******************************************************************************************************/
|
||||||
|
|
||||||
|
private var userSharesList = listOf(
|
||||||
|
OC_SHARE.copy(sharedWithDisplayName = "Batman"),
|
||||||
|
OC_SHARE.copy(sharedWithDisplayName = "Joker")
|
||||||
|
)
|
||||||
|
|
||||||
|
private var groupSharesList = listOf(
|
||||||
|
OC_SHARE.copy(
|
||||||
|
shareType = ShareType.GROUP,
|
||||||
|
sharedWithDisplayName = "Suicide Squad"
|
||||||
|
),
|
||||||
|
OC_SHARE.copy(
|
||||||
|
shareType = ShareType.GROUP,
|
||||||
|
sharedWithDisplayName = "Avengers"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showUsersAndGroupsSectionTitle() {
|
||||||
|
loadShareFileFragment(shares = userSharesList)
|
||||||
|
onView(withText(R.string.share_with_user_section_title)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showNoPrivateShares() {
|
||||||
|
loadShareFileFragment(shares = listOf())
|
||||||
|
onView(withText(R.string.share_no_users)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showUserShares() {
|
||||||
|
loadShareFileFragment(shares = userSharesList)
|
||||||
|
onView(withText("Batman")).check(matches(isDisplayed()))
|
||||||
|
onView(withText("Batman")).check(matches(hasSibling(withId(R.id.unshareButton))))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
onView(withText("Batman")).check(matches(hasSibling(withId(R.id.editShareButton))))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
onView(withText("Joker")).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showGroupShares() {
|
||||||
|
loadShareFileFragment(shares = listOf(groupSharesList.first()))
|
||||||
|
onView(withText("Suicide Squad (group)")).check(matches(isDisplayed()))
|
||||||
|
onView(withText("Suicide Squad (group)")).check(matches(hasSibling(withId(R.id.icon))))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
onView(withTagValue(CoreMatchers.equalTo(R.drawable.ic_group))).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************************************
|
||||||
|
******************************************* PUBLIC SHARES ********************************************
|
||||||
|
******************************************************************************************************/
|
||||||
|
|
||||||
|
private var publicShareList = listOf(
|
||||||
|
OC_SHARE.copy(
|
||||||
|
shareType = ShareType.PUBLIC_LINK,
|
||||||
|
path = "/Photos/image.jpg",
|
||||||
|
isFolder = false,
|
||||||
|
name = "Image link",
|
||||||
|
shareLink = "http://server:port/s/1"
|
||||||
|
),
|
||||||
|
OC_SHARE.copy(
|
||||||
|
shareType = ShareType.PUBLIC_LINK,
|
||||||
|
path = "/Photos/image.jpg",
|
||||||
|
isFolder = false,
|
||||||
|
name = "Image link 2",
|
||||||
|
shareLink = "http://server:port/s/2"
|
||||||
|
),
|
||||||
|
OC_SHARE.copy(
|
||||||
|
shareType = ShareType.PUBLIC_LINK,
|
||||||
|
path = "/Photos/image.jpg",
|
||||||
|
isFolder = false,
|
||||||
|
name = "Image link 3",
|
||||||
|
shareLink = "http://server:port/s/3"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showNoPublicShares() {
|
||||||
|
loadShareFileFragment(shares = listOf())
|
||||||
|
onView(withText(R.string.share_no_public_links)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showPublicShares() {
|
||||||
|
loadShareFileFragment(shares = publicShareList)
|
||||||
|
onView(withText("Image link")).check(matches(isDisplayed()))
|
||||||
|
onView(withText("Image link")).check(matches(hasSibling(withId(R.id.getPublicLinkButton))))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
onView(withText("Image link")).check(matches(hasSibling(withId(R.id.deletePublicLinkButton))))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
onView(withText("Image link")).check(matches(hasSibling(withId(R.id.editPublicLinkButton))))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
onView(withText("Image link 2")).check(matches(isDisplayed()))
|
||||||
|
onView(withText("Image link 3")).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showPublicSharesSharingEnabled() {
|
||||||
|
loadShareFileFragment(
|
||||||
|
capabilities = OC_CAPABILITY.copy(filesSharingPublicEnabled = CapabilityBooleanType.TRUE),
|
||||||
|
shares = publicShareList
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withText("Image link")).check(matches(isDisplayed()))
|
||||||
|
onView(withText("Image link 2")).check(matches(isDisplayed()))
|
||||||
|
onView(withText("Image link 3")).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hidePublicSharesSharingDisabled() {
|
||||||
|
loadShareFileFragment(
|
||||||
|
capabilities = OC_CAPABILITY.copy(filesSharingPublicEnabled = CapabilityBooleanType.FALSE),
|
||||||
|
shares = publicShareList
|
||||||
|
)
|
||||||
|
|
||||||
|
R.id.shareViaLinkSection.assertVisibility(ViewMatchers.Visibility.GONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createPublicShareMultipleCapability() {
|
||||||
|
loadShareFileFragment(
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.1.1",
|
||||||
|
filesSharingPublicMultiple = CapabilityBooleanType.TRUE
|
||||||
|
),
|
||||||
|
shares = listOf(publicShareList[0])
|
||||||
|
)
|
||||||
|
|
||||||
|
R.id.addPublicLinkButton.assertVisibility(ViewMatchers.Visibility.VISIBLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cannotCreatePublicShareMultipleCapability() {
|
||||||
|
loadShareFileFragment(
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "10.1.1",
|
||||||
|
filesSharingPublicMultiple = CapabilityBooleanType.FALSE
|
||||||
|
),
|
||||||
|
shares = listOf(publicShareList[0])
|
||||||
|
)
|
||||||
|
|
||||||
|
R.id.addPublicLinkButton.assertVisibility(ViewMatchers.Visibility.INVISIBLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cannotCreatePublicShareServerCapability() {
|
||||||
|
loadShareFileFragment(
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
versionString = "9.3.1"
|
||||||
|
),
|
||||||
|
shares = listOf(publicShareList[0])
|
||||||
|
)
|
||||||
|
|
||||||
|
R.id.addPublicLinkButton.assertVisibility(ViewMatchers.Visibility.INVISIBLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************************************
|
||||||
|
*********************************************** COMMON ***********************************************
|
||||||
|
******************************************************************************************************/
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hideSharesSharingApiDisabled() {
|
||||||
|
loadShareFileFragment(
|
||||||
|
capabilities = OC_CAPABILITY.copy(
|
||||||
|
filesSharingApiEnabled = CapabilityBooleanType.FALSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
R.id.shareWithUsersSection.assertVisibility(ViewMatchers.Visibility.GONE)
|
||||||
|
|
||||||
|
R.id.shareViaLinkSection.assertVisibility(ViewMatchers.Visibility.GONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showError() {
|
||||||
|
loadShareFileFragment(
|
||||||
|
sharesUIResult = UIResult.Error(
|
||||||
|
error = Throwable("It was not possible to retrieve the shares from the server")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
com.google.android.material.R.id.snackbar_text.withText(R.string.get_shares_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadShareFileFragment(
|
||||||
|
capabilities: OCCapability = OC_CAPABILITY,
|
||||||
|
capabilitiesEvent: Event<UIResult<OCCapability>> = Event(UIResult.Success(capabilities)),
|
||||||
|
shares: List<OCShare> = listOf(OC_SHARE),
|
||||||
|
sharesUIResult: UIResult<List<OCShare>> = UIResult.Success(shares)
|
||||||
|
) {
|
||||||
|
val shareFileFragment = ShareFileFragment.newInstance(
|
||||||
|
OC_FILE,
|
||||||
|
OC_ACCOUNT
|
||||||
|
)
|
||||||
|
|
||||||
|
ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
|
||||||
|
it.startFragment(shareFileFragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
capabilitiesLiveData.postValue(capabilitiesEvent)
|
||||||
|
sharesLiveData.postValue(Event(sharesUIResult))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author David González Verdugo
|
||||||
|
* @author Jesus Recio Rincon
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.sharing.shares.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.domain.capabilities.model.OCCapability
|
||||||
|
import eu.qsfera.android.domain.sharing.shares.model.OCShare
|
||||||
|
import eu.qsfera.android.domain.utils.Event
|
||||||
|
import eu.qsfera.android.presentation.common.UIResult
|
||||||
|
import eu.qsfera.android.presentation.sharing.ShareFileFragment
|
||||||
|
import eu.qsfera.android.presentation.capabilities.CapabilityViewModel
|
||||||
|
import eu.qsfera.android.presentation.sharing.ShareViewModel
|
||||||
|
import eu.qsfera.android.testutil.OC_ACCOUNT
|
||||||
|
import eu.qsfera.android.testutil.OC_CAPABILITY
|
||||||
|
import eu.qsfera.android.testutil.OC_FOLDER
|
||||||
|
import eu.qsfera.android.testutil.OC_SHARE
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.hamcrest.CoreMatchers.not
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
class ShareFolderFragmentTest {
|
||||||
|
private val capabilityViewModel = mockk<CapabilityViewModel>(relaxed = true)
|
||||||
|
private val capabilitiesLiveData = MutableLiveData<Event<UIResult<OCCapability>>>()
|
||||||
|
private val shareViewModel = mockk<ShareViewModel>(relaxed = true)
|
||||||
|
private val sharesLiveData = MutableLiveData<Event<UIResult<List<OCShare>>>>()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
every { capabilityViewModel.capabilities } returns capabilitiesLiveData
|
||||||
|
every { shareViewModel.shares } returns sharesLiveData
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
androidContext(ApplicationProvider.getApplicationContext())
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
capabilityViewModel
|
||||||
|
}
|
||||||
|
viewModel {
|
||||||
|
shareViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val shareFileFragment = ShareFileFragment.newInstance(
|
||||||
|
OC_FOLDER.copy(privateLink = null),
|
||||||
|
OC_ACCOUNT
|
||||||
|
)
|
||||||
|
|
||||||
|
ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
|
||||||
|
it.startFragment(shareFileFragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
capabilitiesLiveData.postValue(Event(UIResult.Success(OC_CAPABILITY)))
|
||||||
|
|
||||||
|
sharesLiveData.postValue(Event(UIResult.Success(listOf(OC_SHARE))))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun folderSizeVisible() {
|
||||||
|
onView(withId(R.id.shareFileSize)).check(matches(not(isDisplayed())))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hidePrivateLink() {
|
||||||
|
onView(withId(R.id.getPrivateLinkButton)).check(matches(not(isDisplayed())))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author David González Verdugo
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.sharing.shares.ui
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.domain.files.model.OCFile
|
||||||
|
import eu.qsfera.android.domain.sharing.shares.model.OCShare
|
||||||
|
import eu.qsfera.android.presentation.sharing.ShareFragmentListener
|
||||||
|
import eu.qsfera.android.services.OperationsService
|
||||||
|
import eu.qsfera.android.testing.SingleFragmentActivity
|
||||||
|
import eu.qsfera.android.ui.fragment.FileFragment.ContainerActivity
|
||||||
|
import eu.qsfera.android.ui.helpers.FileOperationsHelper
|
||||||
|
|
||||||
|
class TestShareFileActivity : SingleFragmentActivity(), ShareFragmentListener, ContainerActivity {
|
||||||
|
fun startFragment(fragment: Fragment) {
|
||||||
|
supportFragmentManager.commit(allowStateLoss = true) {
|
||||||
|
add(R.id.container, fragment, TEST_FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTestFragment(): Fragment? = supportFragmentManager.findFragmentByTag(TEST_FRAGMENT_TAG)
|
||||||
|
|
||||||
|
override fun copyOrSendPrivateLink(file: OCFile) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteShare(remoteId: String) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showLoading() {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dismissLoading() {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showAddPublicShare(defaultLinkName: String) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showEditPublicShare(share: OCShare) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showRemoveShare(share: OCShare) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun copyOrSendPublicLink(share: OCShare) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showSearchUsersAndGroups() {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showEditPrivateShare(share: OCShare) {
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TEST_FRAGMENT_TAG = "TEST FRAGMENT"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOperationsServiceBinder(): OperationsService.OperationsServiceBinder {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFileOperationsHelper(): FileOperationsHelper {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showDetails(file: OCFile?) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author David Crespo Ríos
|
||||||
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.ui.activity
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.presentation.releasenotes.ReleaseNotesActivity
|
||||||
|
import eu.qsfera.android.presentation.releasenotes.ReleaseNotesViewModel
|
||||||
|
import eu.qsfera.android.utils.click
|
||||||
|
import eu.qsfera.android.utils.matchers.assertChildCount
|
||||||
|
import eu.qsfera.android.utils.matchers.isDisplayed
|
||||||
|
import eu.qsfera.android.utils.matchers.withText
|
||||||
|
import eu.qsfera.android.utils.releaseNotesList
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
class ReleaseNotesActivityTest {
|
||||||
|
private lateinit var activityScenario: ActivityScenario<ReleaseNotesActivity>
|
||||||
|
private lateinit var context: Context
|
||||||
|
|
||||||
|
private lateinit var releaseNotesViewModel: ReleaseNotesViewModel
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
|
releaseNotesViewModel = mockk(relaxed = true)
|
||||||
|
|
||||||
|
stopKoin()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
context
|
||||||
|
allowOverride(override = true)
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
viewModel {
|
||||||
|
releaseNotesViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
every { releaseNotesViewModel.getReleaseNotes() } returns releaseNotesList
|
||||||
|
|
||||||
|
val intent = Intent(context, ReleaseNotesActivity::class.java)
|
||||||
|
activityScenario = ActivityScenario.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun releaseNotesView() {
|
||||||
|
val header = String.format(
|
||||||
|
context.getString(R.string.release_notes_header),
|
||||||
|
context.getString(R.string.app_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
val footer = String.format(
|
||||||
|
context.getString(R.string.release_notes_footer),
|
||||||
|
context.getString(R.string.app_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
with(R.id.txtHeader) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.releaseNotes.isDisplayed(true)
|
||||||
|
|
||||||
|
with(R.id.txtFooter) {
|
||||||
|
isDisplayed(true)
|
||||||
|
withText(footer)
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.btnProceed.isDisplayed(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun releaseNotesProceedButton() {
|
||||||
|
R.id.btnProceed.click()
|
||||||
|
|
||||||
|
assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun test_childCount() {
|
||||||
|
R.id.releaseNotes.assertChildCount(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Abel García de Prada
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
* <p>
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.utils
|
||||||
|
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.action.ViewActions
|
||||||
|
import androidx.test.espresso.action.ViewActions.scrollTo
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
|
||||||
|
fun Int.typeText(text: String) {
|
||||||
|
onView(withId(this)).perform(scrollTo(), ViewActions.typeText(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.replaceText(text: String) {
|
||||||
|
onView(withId(this)).perform(scrollTo(), ViewActions.replaceText(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.scrollAndClick() {
|
||||||
|
onView(withId(this)).perform(scrollTo(), ViewActions.click())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.click() {
|
||||||
|
onView(withId(this)).perform(ViewActions.click())
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Juan Carlos Garrote Gascón
|
||||||
|
*
|
||||||
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
|
* <p>
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
* <p>
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.utils
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Instrumentation
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.test.espresso.intent.Intents.intending
|
||||||
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||||
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||||
|
|
||||||
|
fun mockIntent(
|
||||||
|
extras: Pair<String, String>,
|
||||||
|
resultCode: Int = Activity.RESULT_OK,
|
||||||
|
action: String
|
||||||
|
) {
|
||||||
|
val result = Intent()
|
||||||
|
result.putExtra(extras.first, extras.second)
|
||||||
|
val intentResult = Instrumentation.ActivityResult(resultCode, result)
|
||||||
|
intending(hasAction(action)).respondWith(intentResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmName("mockIntentNoExtras")
|
||||||
|
fun mockIntent(
|
||||||
|
resultCode: Int = Activity.RESULT_OK,
|
||||||
|
action: String
|
||||||
|
) {
|
||||||
|
val result = Intent()
|
||||||
|
val intentResult = Instrumentation.ActivityResult(resultCode, result)
|
||||||
|
intending(hasAction(action)).respondWith(intentResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mockIntentToComponent(
|
||||||
|
resultCode: Int = Activity.RESULT_OK,
|
||||||
|
packageName: String
|
||||||
|
) {
|
||||||
|
val result = Intent()
|
||||||
|
val intentResult = Instrumentation.ActivityResult(resultCode, result)
|
||||||
|
intending(hasComponent(packageName)).respondWith(intentResult)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.utils
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.test.runner.AndroidJUnitRunner
|
||||||
|
import com.github.tmurakami.dexopener.DexOpener
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to use DexOpener for executing instrumented tests on <P Android devices,
|
||||||
|
* as Mockk documentation suggests https://mockk.io/ANDROID.html
|
||||||
|
*/
|
||||||
|
class OCTestAndroidJUnitRunner : AndroidJUnitRunner() {
|
||||||
|
override fun newApplication(cl: ClassLoader, className: String, context: Context): Application {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
|
DexOpener.install(this)
|
||||||
|
}
|
||||||
|
return super.newApplication(cl, className, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author David González Verdugo
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.utils
|
||||||
|
|
||||||
|
enum class Permissions(val value: Int) {
|
||||||
|
READ_PERMISSIONS(1),
|
||||||
|
EDIT_PERMISSIONS(3),
|
||||||
|
SHARE_PERMISSIONS(17),
|
||||||
|
ALL_PERMISSIONS(19),
|
||||||
|
|
||||||
|
// FOLDERS
|
||||||
|
EDIT_CREATE_PERMISSIONS(5),
|
||||||
|
EDIT_CREATE_CHANGE_PERMISSIONS(7),
|
||||||
|
EDIT_CREATE_DELETE_PERMISSIONS(13),
|
||||||
|
EDIT_CREATE_CHANGE_DELETE_PERMISSIONS(15),
|
||||||
|
EDIT_CHANGE_PERMISSIONS(3),
|
||||||
|
EDIT_CHANGE_DELETE_PERMISSIONS(11),
|
||||||
|
EDIT_DELETE_PERMISSIONS(9),
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author David Crespo Ríos
|
||||||
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.utils
|
||||||
|
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.presentation.releasenotes.ReleaseNote
|
||||||
|
import eu.qsfera.android.presentation.releasenotes.ReleaseNoteType
|
||||||
|
|
||||||
|
val releaseNotesList = listOf(
|
||||||
|
ReleaseNote(
|
||||||
|
title = R.string.release_notes_header,
|
||||||
|
subtitle = R.string.release_notes_footer,
|
||||||
|
type = ReleaseNoteType.BUGFIX
|
||||||
|
),
|
||||||
|
ReleaseNote(
|
||||||
|
title = R.string.release_notes_header,
|
||||||
|
subtitle = R.string.release_notes_footer,
|
||||||
|
type = ReleaseNoteType.BUGFIX
|
||||||
|
),
|
||||||
|
ReleaseNote(
|
||||||
|
title = R.string.release_notes_header,
|
||||||
|
subtitle = R.string.release_notes_footer,
|
||||||
|
type = ReleaseNoteType.ENHANCEMENT
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Abel García de Prada
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
* <p>
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package eu.qsfera.android.utils.matchers
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.test.espresso.Espresso
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions
|
||||||
|
import androidx.test.espresso.matcher.BoundedMatcher
|
||||||
|
import androidx.test.espresso.matcher.RootMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.presentation.common.BottomSheetFragmentItemView
|
||||||
|
import org.hamcrest.Description
|
||||||
|
import org.hamcrest.Matcher
|
||||||
|
|
||||||
|
fun Int.bsfItemWithTitle(@StringRes title: Int, @ColorRes tintColor: Int?) {
|
||||||
|
Espresso.onView(ViewMatchers.withId(this)).inRoot(RootMatchers.isDialog())
|
||||||
|
.check(ViewAssertions.matches(withTitle(title, tintColor)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.bsfItemWithIcon(@DrawableRes drawable: Int, @ColorRes tintColor: Int?) {
|
||||||
|
Espresso.onView(ViewMatchers.withId(this)).inRoot(RootMatchers.isDialog())
|
||||||
|
.check(ViewAssertions.matches(withIcon(drawable, tintColor)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun withTitle(@StringRes title: Int, @ColorRes tintColor: Int?): Matcher<View> =
|
||||||
|
object : BoundedMatcher<View, BottomSheetFragmentItemView>(BottomSheetFragmentItemView::class.java) {
|
||||||
|
|
||||||
|
override fun describeTo(description: Description) {
|
||||||
|
description.appendText("BottomSheetFragmentItemView with text: $title")
|
||||||
|
tintColor?.let { description.appendText(" and tint color id: $tintColor") }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun matchesSafely(item: BottomSheetFragmentItemView): Boolean {
|
||||||
|
val itemTitleView = item.findViewById<TextView>(R.id.item_title)
|
||||||
|
val textMatches = withText(title).matches(itemTitleView)
|
||||||
|
val textColorMatches = tintColor?.let { withTextColor(tintColor).matches(itemTitleView) } ?: true
|
||||||
|
return textMatches && textColorMatches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun withIcon(@DrawableRes drawable: Int, @ColorRes tintColor: Int?): Matcher<View> =
|
||||||
|
object : BoundedMatcher<View, BottomSheetFragmentItemView>(BottomSheetFragmentItemView::class.java) {
|
||||||
|
|
||||||
|
override fun describeTo(description: Description) {
|
||||||
|
description.appendText("BottomSheetFragmentItemView with icon: $drawable")
|
||||||
|
tintColor?.let { description.appendText(" and tint color id: $tintColor") }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun matchesSafely(item: BottomSheetFragmentItemView): Boolean {
|
||||||
|
val itemIconView = item.findViewById<ImageView>(R.id.item_icon)
|
||||||
|
return withDrawable(drawable, tintColor).matches(itemIconView)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Christian Schabesberger
|
||||||
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.utils.matchers
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.test.espresso.matcher.BoundedMatcher
|
||||||
|
import org.hamcrest.Matcher
|
||||||
|
import org.hamcrest.TypeSafeMatcher
|
||||||
|
import org.hamcrest.Description
|
||||||
|
|
||||||
|
fun withChildViewCount(count: Int, childMatcher: Matcher<View>): Matcher<View> {
|
||||||
|
return object : BoundedMatcher<View, ViewGroup>(ViewGroup::class.java) {
|
||||||
|
override fun matchesSafely(viewGroup: ViewGroup): Boolean {
|
||||||
|
var matchCount = 0
|
||||||
|
for (i in 0 until viewGroup.childCount) {
|
||||||
|
if (childMatcher.matches(viewGroup.getChildAt(i))) {
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matchCount == count
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeTo(description: Description?) {
|
||||||
|
description?.appendText("ViewGroup with child-count=$count and")
|
||||||
|
childMatcher.describeTo(description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nthChildOf(parentMatcher: Matcher<View>, childPosition: Int): Matcher<View> {
|
||||||
|
return object : TypeSafeMatcher<View>() {
|
||||||
|
override fun matchesSafely(view: View): Boolean {
|
||||||
|
if (view.parent !is ViewGroup) {
|
||||||
|
return parentMatcher.matches(view.parent)
|
||||||
|
}
|
||||||
|
val group = view.parent as ViewGroup
|
||||||
|
return parentMatcher.matches(view.parent) && group.getChildAt(childPosition) == view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeTo(description: Description) {
|
||||||
|
description.appendText("with $childPosition child view of type parentMatcher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Abel García de Prada
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
* <p>
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package eu.qsfera.android.utils.matchers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import org.hamcrest.Description
|
||||||
|
import org.hamcrest.TypeSafeMatcher
|
||||||
|
|
||||||
|
fun withDrawable(
|
||||||
|
@DrawableRes id: Int,
|
||||||
|
@ColorRes tint: Int? = null,
|
||||||
|
tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_IN
|
||||||
|
) = object : TypeSafeMatcher<View>() {
|
||||||
|
override fun describeTo(description: Description) {
|
||||||
|
description.appendText("ImageView with drawable same as drawable with id $id")
|
||||||
|
tint?.let { description.appendText(", tint color id: $tint, mode: $tintMode") }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun matchesSafely(view: View): Boolean {
|
||||||
|
val context = view.context
|
||||||
|
val tintColor = tint?.toColor(context)
|
||||||
|
val expectedBitmap = context.getDrawable(id)?.tinted(tintColor, tintMode)?.toBitmap()
|
||||||
|
|
||||||
|
return view is ImageView && view.drawable.toBitmap().sameAs(expectedBitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.toColor(context: Context) = ContextCompat.getColor(context, this)
|
||||||
|
|
||||||
|
private fun Drawable.tinted(@ColorInt tintColor: Int? = null, tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_IN) =
|
||||||
|
apply {
|
||||||
|
setTintList(tintColor?.toColorStateList())
|
||||||
|
setTintMode(tintMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.toColorStateList() = ColorStateList.valueOf(this)
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Abel García de Prada
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
* <p>
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.utils.matchers
|
||||||
|
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.hasChildCount
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import org.hamcrest.CoreMatchers
|
||||||
|
|
||||||
|
fun Int.isDisplayed(displayed: Boolean) {
|
||||||
|
val displayMatcher = if (displayed) ViewMatchers.isDisplayed() else CoreMatchers.not(ViewMatchers.isDisplayed())
|
||||||
|
|
||||||
|
onView(withId(this))
|
||||||
|
.check(matches(displayMatcher))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.isEnabled(enabled: Boolean) {
|
||||||
|
val enableMatcher = if (enabled) ViewMatchers.isEnabled() else CoreMatchers.not(ViewMatchers.isEnabled())
|
||||||
|
|
||||||
|
onView(withId(this))
|
||||||
|
.check(matches(enableMatcher))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.isFocusable(focusable: Boolean) {
|
||||||
|
val focusableMatcher = if (focusable) ViewMatchers.isFocusable() else CoreMatchers.not(ViewMatchers.isFocusable())
|
||||||
|
|
||||||
|
onView(withId(this))
|
||||||
|
.check(matches(focusableMatcher))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.withText(text: String) {
|
||||||
|
onView(withId(this))
|
||||||
|
.check(matches(ViewMatchers.withText(text)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.withText(resourceId: Int) {
|
||||||
|
onView(withId(this))
|
||||||
|
.check(matches(ViewMatchers.withText(resourceId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.withChildCountAndId(count: Int, resourceId: Int) {
|
||||||
|
onView(withId(this))
|
||||||
|
.check(matches(withChildViewCount(count, withId(resourceId))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.assertVisibility(visibility: ViewMatchers.Visibility) {
|
||||||
|
onView(withId(this))
|
||||||
|
.check(matches(ViewMatchers.withEffectiveVisibility(visibility)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.assertChildCount(childs: Int) {
|
||||||
|
onView(withId(this))
|
||||||
|
.check(matches(hasChildCount(childs)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Juan Carlos Garrote Gascón
|
||||||
|
*
|
||||||
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
|
* <p>
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
* <p>
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.utils.matchers
|
||||||
|
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
|
||||||
|
fun Preference.verifyPreference(
|
||||||
|
keyPref: String,
|
||||||
|
titlePref: String,
|
||||||
|
summaryPref: String? = null,
|
||||||
|
visible: Boolean,
|
||||||
|
enabled: Boolean? = null
|
||||||
|
) {
|
||||||
|
if (visible) onView(withText(titlePref)).check(matches(isDisplayed()))
|
||||||
|
summaryPref?.let {
|
||||||
|
if (visible) onView(withText(it)).check(matches(isDisplayed()))
|
||||||
|
assertEquals(it, summary)
|
||||||
|
}
|
||||||
|
assertEquals(keyPref, key)
|
||||||
|
assertEquals(titlePref, title)
|
||||||
|
assertEquals(visible, isVisible)
|
||||||
|
enabled?.let {
|
||||||
|
assertEquals(enabled, isEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Abel García de Prada
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
* <p>
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package eu.qsfera.android.utils.matchers
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.test.espresso.matcher.BoundedMatcher
|
||||||
|
import org.hamcrest.Description
|
||||||
|
import org.hamcrest.Matcher
|
||||||
|
|
||||||
|
fun withTextColor(
|
||||||
|
@ColorRes textColor: Int
|
||||||
|
): Matcher<View> =
|
||||||
|
object : BoundedMatcher<View, TextView>(TextView::class.java) {
|
||||||
|
override fun describeTo(description: Description) {
|
||||||
|
description.appendText("TextView with text color: $textColor")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun matchesSafely(view: TextView): Boolean {
|
||||||
|
val expectedColor = ContextCompat.getColor(view.context, textColor)
|
||||||
|
val actualColor = view.currentTextColor
|
||||||
|
return actualColor == expectedColor
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (C) 2017 The Android Open Source Project
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<application>
|
||||||
|
<activity android:name="eu.qsfera.android.sharing.shares.ui.TestShareFileActivity" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.testing
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import eu.qsfera.android.ui.activity.BaseActivity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for testing fragments inside a fake activity.
|
||||||
|
*/
|
||||||
|
open class SingleFragmentActivity : BaseActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val content = FrameLayout(this).apply {
|
||||||
|
layoutParams = FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
id = R.id.container
|
||||||
|
}
|
||||||
|
setContentView(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Christian Schabesberger
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.StrictMode
|
||||||
|
import com.facebook.stetho.Stetho
|
||||||
|
|
||||||
|
object DebugInjector {
|
||||||
|
open fun injectDebugTools(context: Context) {
|
||||||
|
Stetho.initializeWithDefaults(context)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
StrictMode.setVmPolicy(
|
||||||
|
StrictMode.VmPolicy.Builder()
|
||||||
|
.penaltyLog()
|
||||||
|
.detectNonSdkApiUsage()
|
||||||
|
.detectUnsafeIntentLaunch()
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package eu.qsfera.android
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||||
|
import androidx.test.rule.GrantPermissionRule
|
||||||
|
import com.kaspersky.kaspresso.kaspresso.Kaspresso
|
||||||
|
import com.kaspersky.kaspresso.params.FlakySafetyParams
|
||||||
|
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
|
||||||
|
import eu.qsfera.android.ui.activity.SplashActivity
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import screens.LoginScreen
|
||||||
|
import screens.MainScreen
|
||||||
|
import screens.ManageAccountsDialog
|
||||||
|
import screens.StartScreen
|
||||||
|
import screens.TrustCertificate
|
||||||
|
|
||||||
|
class LoginScreenTest : TestCase(
|
||||||
|
kaspressoBuilder = Kaspresso.Builder.advanced {
|
||||||
|
flakySafetyParams = FlakySafetyParams.custom(
|
||||||
|
timeoutMs = 20_000L,
|
||||||
|
intervalMs = 100L
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
@get:Rule
|
||||||
|
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
|
||||||
|
android.Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val activityRule = ActivityScenarioRule(SplashActivity::class.java)
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loginApp() {
|
||||||
|
before {
|
||||||
|
adbServer.performCmd("adb", listOf("reverse", "tcp:9200", "tcp:9200"))
|
||||||
|
}.after {
|
||||||
|
adbServer.performCmd("adb", listOf("shell", "am", "force-stop", "com.android.chrome"))
|
||||||
|
adbServer.performCmd("adb", listOf("reverse", "--remove", "tcp:9200"))
|
||||||
|
}.run {
|
||||||
|
step("set qsfera url") {
|
||||||
|
StartScreen {
|
||||||
|
hostUrlInput {
|
||||||
|
isVisible()
|
||||||
|
typeText("https://localhost:9200")
|
||||||
|
}
|
||||||
|
checkServerButton {
|
||||||
|
isVisible()
|
||||||
|
isClickable()
|
||||||
|
click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("trust certificate") {
|
||||||
|
TrustCertificate {
|
||||||
|
yesBtn {
|
||||||
|
isVisible()
|
||||||
|
isClickable()
|
||||||
|
click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("login") {
|
||||||
|
LoginScreen {
|
||||||
|
username.isDisplayed()
|
||||||
|
password.isDisplayed()
|
||||||
|
loginButton.isDisplayed()
|
||||||
|
username.typeText("alan")
|
||||||
|
password.typeText("demo")
|
||||||
|
loginButton.click()
|
||||||
|
keepAccessForeverBtn {
|
||||||
|
isDisplayed()
|
||||||
|
isClickable()
|
||||||
|
click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("check personal space") {
|
||||||
|
MainScreen {
|
||||||
|
avatarButton.isVisible()
|
||||||
|
avatarButton.isClickable()
|
||||||
|
avatarButton.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("remove account") {
|
||||||
|
ManageAccountsDialog {
|
||||||
|
removeBtn {
|
||||||
|
isVisible()
|
||||||
|
click()
|
||||||
|
}
|
||||||
|
message.isVisible()
|
||||||
|
confirmBtn.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package screens
|
||||||
|
|
||||||
|
import com.kaspersky.components.kautomator.component.edit.UiEditText
|
||||||
|
import com.kaspersky.components.kautomator.component.text.UiButton
|
||||||
|
import com.kaspersky.components.kautomator.screen.UiScreen
|
||||||
|
|
||||||
|
object LoginScreen : UiScreen<LoginScreen>() {
|
||||||
|
override val packageName: String = "com.android.chrome"
|
||||||
|
|
||||||
|
// can't find it using withId("com.android.chrome", "username") so using withResourceName()
|
||||||
|
val username = UiEditText { withResourceName("oc-login-username") }
|
||||||
|
val password = UiEditText { withResourceName("oc-login-password") }
|
||||||
|
val loginButton = UiButton { withText("Log in") }
|
||||||
|
|
||||||
|
val keepAccessForeverBtn = UiButton { withText("Allow") }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package screens
|
||||||
|
|
||||||
|
import com.kaspersky.kaspresso.screens.KScreen
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import io.github.kakaocup.kakao.text.KButton
|
||||||
|
|
||||||
|
object MainScreen : KScreen<MainScreen>() {
|
||||||
|
override val layoutId: Int? = R.layout.activity_main
|
||||||
|
override val viewClass: Class<*>? = null
|
||||||
|
|
||||||
|
val avatarButton = KButton { withId(R.id.root_toolbar_avatar) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package screens
|
||||||
|
|
||||||
|
import com.kaspersky.kaspresso.screens.KScreen
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import io.github.kakaocup.kakao.text.KButton
|
||||||
|
import io.github.kakaocup.kakao.text.KTextView
|
||||||
|
|
||||||
|
object ManageAccountsDialog : KScreen<ManageAccountsDialog>() {
|
||||||
|
override val layoutId: Int = R.layout.manage_accounts_dialog
|
||||||
|
override val viewClass: Class<*>? = null
|
||||||
|
|
||||||
|
val removeBtn = KButton { withId(R.id.removeButton) }
|
||||||
|
val message = KTextView {
|
||||||
|
containsText("Do you really want to remove the account")
|
||||||
|
}
|
||||||
|
val confirmBtn = KButton { withText(R.string.common_yes) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package screens
|
||||||
|
|
||||||
|
import com.kaspersky.kaspresso.screens.KScreen
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import io.github.kakaocup.kakao.edit.KEditText
|
||||||
|
import io.github.kakaocup.kakao.text.KButton
|
||||||
|
|
||||||
|
object StartScreen : KScreen<StartScreen>() {
|
||||||
|
override val layoutId: Int? = R.layout.account_setup
|
||||||
|
override val viewClass: Class<*>? = null
|
||||||
|
|
||||||
|
val hostUrlInput = KEditText { withId(R.id.hostUrlInput) }
|
||||||
|
val checkServerButton = KButton { withId(R.id.embeddedCheckServerButton) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package screens
|
||||||
|
|
||||||
|
import com.kaspersky.kaspresso.screens.KScreen
|
||||||
|
import eu.qsfera.android.R
|
||||||
|
import io.github.kakaocup.kakao.text.KButton
|
||||||
|
|
||||||
|
object TrustCertificate: KScreen<TrustCertificate>() {
|
||||||
|
override val layoutId: Int? = R.layout.ssl_untrusted_cert_layout
|
||||||
|
override val viewClass: Class<*>? = null
|
||||||
|
|
||||||
|
val yesBtn = KButton { withId(R.id.ok) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
qsfera Android client application
|
||||||
|
|
||||||
|
Copyright (C) 2012 Bartek Przybylski
|
||||||
|
Copyright (C) 2023 ownCloud GmbH.
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License version 2,
|
||||||
|
as published by the Free Software Foundation.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!--
|
||||||
|
WRITE_EXTERNAL_STORAGE may be enabled or disabled by the user after installation in
|
||||||
|
API >= 23; the app needs to handle this
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<!--
|
||||||
|
Notifications are off by default since API 33;
|
||||||
|
See note in https://developer.android.com/develop/ui/views/notifications/notification-permission
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<!--
|
||||||
|
Next permissions are always approved in installation time,
|
||||||
|
the apps needs to do nothing special in runtime
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||||
|
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission android:name="android.permission.REORDER_TASKS" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".MainApp"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:icon="@mipmap/icon"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:localeConfig="@xml/locales_config"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:preserveLegacyExternalStorage="true"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:supportsPictureInPicture="false"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/Theme.qsfera.Toolbar">
|
||||||
|
<activity
|
||||||
|
android:name=".ui.preview.PreviewVideoActivity"
|
||||||
|
android:label="@string/video_preview_label"
|
||||||
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||||
|
android:theme="@style/Theme.qsfera.Video"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.content.APP_RESTRICTIONS"
|
||||||
|
android:resource="@xml/managed_configurations" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".presentation.releasenotes.ReleaseNotesActivity"
|
||||||
|
android:label="@string/release_notes_label"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".presentation.settings.privacypolicy.PrivacyPolicyActivity"
|
||||||
|
android:label="@string/actionbar_privacy_policy" />
|
||||||
|
<activity android:name=".presentation.settings.SettingsActivity" />
|
||||||
|
<activity android:name=".presentation.migration.StorageMigrationActivity"
|
||||||
|
android:theme="@style/Theme.qsfera.Toolbar.Fill" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.SplashActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/Theme.qsfera.Splash">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.FileDisplayActivity"
|
||||||
|
android:configChanges="orientation|screenSize"
|
||||||
|
android:theme="@style/Theme.qsfera.Toolbar.Drawer"
|
||||||
|
android:windowSoftInputMode="adjustPan" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.ReceiveExternalFilesActivity"
|
||||||
|
android:configChanges="orientation|screenSize"
|
||||||
|
android:label="@string/receive_external_files_label"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:taskAffinity="">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.preview.PreviewImageActivity"
|
||||||
|
android:label="@string/image_preview_label"
|
||||||
|
android:theme="@style/Theme.qsfera.Overlay" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".presentation.authentication.AccountAuthenticatorService"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter android:priority="100">
|
||||||
|
<action android:name="android.accounts.AccountAuthenticator" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.accounts.AccountAuthenticator"
|
||||||
|
android:resource="@xml/authenticator" />
|
||||||
|
</service>
|
||||||
|
<service
|
||||||
|
android:name=".syncadapter.FileSyncService"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.content.SyncAdapter" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.content.SyncAdapter"
|
||||||
|
android:resource="@xml/syncadapter_files" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name=".providers.FileContentProvider"
|
||||||
|
android:authorities="@string/authority"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/sync_string_files"
|
||||||
|
android:syncable="true" />
|
||||||
|
<provider
|
||||||
|
android:name=".presentation.sharing.sharees.UsersAndGroupsSearchProvider"
|
||||||
|
android:authorities="@string/search_suggest_authority"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/search_users_and_groups_hint" />
|
||||||
|
<provider
|
||||||
|
android:name=".presentation.documentsprovider.DocumentsStorageProvider"
|
||||||
|
android:authorities="@string/document_provider_authority"
|
||||||
|
android:exported="true"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
|
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||||
|
</intent-filter>
|
||||||
|
</provider>
|
||||||
|
<!-- new provider used to generate URIs without file:// scheme (forbidden from Android 7) -->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="@string/file_provider_authority"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
|
tools:replace="android:authorities">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/exposed_filepaths"
|
||||||
|
tools:replace="android:resource" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<service android:name=".services.OperationsService" />
|
||||||
|
<service
|
||||||
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:exported="false"
|
||||||
|
tools:replace="android:foregroundServiceType" />
|
||||||
|
<service
|
||||||
|
android:name=".media.MediaService"
|
||||||
|
android:foregroundServiceType="mediaPlayback"
|
||||||
|
android:exported="false">
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".presentation.security.passcode.PassCodeActivity"
|
||||||
|
android:label="@string/passcode_label"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
<activity
|
||||||
|
android:name=".presentation.conflicts.ConflictsResolveActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".presentation.logging.LogsListActivity"
|
||||||
|
android:label="@string/prefs_log_open_logs_list_view"
|
||||||
|
android:configChanges="orientation|screenSize" />
|
||||||
|
<activity android:name=".ui.errorhandling.ErrorShowActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.UploadListActivity"
|
||||||
|
android:label="@string/bottom_nav_uploads"
|
||||||
|
android:theme="@style/Theme.qsfera.Toolbar.Drawer" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.WhatsNewActivity"
|
||||||
|
android:label="@string/whats_new_label"
|
||||||
|
android:configChanges="keyboardHidden|orientation|screenSize" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.CopyToClipboardActivity"
|
||||||
|
android:icon="@drawable/copy_link"
|
||||||
|
android:label="@string/copy_link" />
|
||||||
|
<activity android:name=".ui.activity.FolderPickerActivity"
|
||||||
|
android:label="@string/folder_picker_label"/>
|
||||||
|
<activity
|
||||||
|
android:name=".presentation.sharing.ShareActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/share_dialog_title"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:theme="@style/Theme.qsfera.Toolbar"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.searchable"
|
||||||
|
android:resource="@xml/users_and_groups_searchable" />
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".presentation.security.pattern.PatternActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:label="@string/pattern_label" />
|
||||||
|
<activity android:name=".presentation.security.biometric.BiometricActivity" />
|
||||||
|
<!-- Own taskAffinity + singleTask so LoginActivity always runs in its own task.
|
||||||
|
During re-auth, startActivityForResult overrides singleTask, so the first
|
||||||
|
instance still lands in the main task — but the OAuth redirect instance gets
|
||||||
|
its own task and finishes the orphaned one via a static reference.
|
||||||
|
autoRemoveFromRecents cleans the login task from recents once it finishes. -->
|
||||||
|
<activity
|
||||||
|
android:name=".presentation.authentication.LoginActivity"
|
||||||
|
android:autoRemoveFromRecents="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/login_label"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:taskAffinity="eu.qsfera.android.login"
|
||||||
|
android:theme="@style/Theme.qsfera.Toolbar">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="@string/oauth2_redirect_uri_host"
|
||||||
|
android:scheme="@string/oauth2_redirect_uri_scheme" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="@string/deep_link_uri_schemes" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
* <p>
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
* <p>
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
* <p>
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package eu.qsfera.android;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
|
import eu.qsfera.android.ui.dialog.RateMeDialog;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
public class AppRater {
|
||||||
|
private static final String DIALOG_RATE_ME_TAG = "DIALOG_RATE_ME";
|
||||||
|
|
||||||
|
private final static int DAYS_UNTIL_PROMPT = 2;
|
||||||
|
private final static int LAUNCHES_UNTIL_PROMPT = 2;
|
||||||
|
private final static int DAYS_UNTIL_NEUTRAL_CLICK = 1;
|
||||||
|
|
||||||
|
public static final String APP_RATER_PREF_TITLE = "app_rater";
|
||||||
|
public static final String APP_RATER_PREF_DONT_SHOW = "don't_show_again";
|
||||||
|
private static final String APP_RATER_PREF_LAUNCH_COUNT = "launch_count";
|
||||||
|
private static final String APP_RATER_PREF_DATE_FIRST_LAUNCH = "date_first_launch";
|
||||||
|
public static final String APP_RATER_PREF_DATE_NEUTRAL = "date_neutral";
|
||||||
|
|
||||||
|
public static void appLaunched(Context mContext, String packageName) {
|
||||||
|
SharedPreferences prefs = mContext.getSharedPreferences(APP_RATER_PREF_TITLE, 0);
|
||||||
|
if (prefs.getBoolean(APP_RATER_PREF_DONT_SHOW, false)) {
|
||||||
|
Timber.d("Do not show the rate dialog again as the user decided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPreferences.Editor editor = prefs.edit();
|
||||||
|
|
||||||
|
/// Increment launch counter
|
||||||
|
long launchCount = prefs.getLong(APP_RATER_PREF_LAUNCH_COUNT, 0) + 1;
|
||||||
|
Timber.d("The app has been launched " + launchCount + " times");
|
||||||
|
editor.putLong(APP_RATER_PREF_LAUNCH_COUNT, launchCount);
|
||||||
|
|
||||||
|
/// Get date of first launch
|
||||||
|
long dateFirstLaunch = prefs.getLong(APP_RATER_PREF_DATE_FIRST_LAUNCH, 0);
|
||||||
|
if (dateFirstLaunch == 0) {
|
||||||
|
dateFirstLaunch = System.currentTimeMillis();
|
||||||
|
Timber.d("The app has been launched in " + dateFirstLaunch + " for the first time");
|
||||||
|
editor.putLong(APP_RATER_PREF_DATE_FIRST_LAUNCH, dateFirstLaunch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get date of neutral click
|
||||||
|
long dateNeutralClick = prefs.getLong(APP_RATER_PREF_DATE_NEUTRAL, 0);
|
||||||
|
|
||||||
|
/// Wait at least n days before opening
|
||||||
|
if (launchCount >= LAUNCHES_UNTIL_PROMPT) {
|
||||||
|
Timber.d("The number of launches already exceed " + LAUNCHES_UNTIL_PROMPT +
|
||||||
|
", the default number of launches, so let's check some dates");
|
||||||
|
Timber.d("Current moment is %s", System.currentTimeMillis());
|
||||||
|
Timber.d("The date of the first launch + days until prompt is " + dateFirstLaunch +
|
||||||
|
daysToMilliseconds(DAYS_UNTIL_PROMPT));
|
||||||
|
Timber.d("The date of the neutral click + days until neutral click is " + dateNeutralClick +
|
||||||
|
daysToMilliseconds(DAYS_UNTIL_NEUTRAL_CLICK));
|
||||||
|
if (System.currentTimeMillis() >= Math.max(dateFirstLaunch
|
||||||
|
+ daysToMilliseconds(DAYS_UNTIL_PROMPT), dateNeutralClick
|
||||||
|
+ daysToMilliseconds(DAYS_UNTIL_NEUTRAL_CLICK))) {
|
||||||
|
Timber.d("The current moment is later than any of the days set, so let's show the rate dialog");
|
||||||
|
showRateDialog(mContext, packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int daysToMilliseconds(int days) {
|
||||||
|
return days * 24 * 60 * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void showRateDialog(Context mContext, String packageName) {
|
||||||
|
RateMeDialog rateMeDialog = RateMeDialog.newInstance(packageName, false);
|
||||||
|
FragmentManager fm = ((FragmentActivity) mContext).getSupportFragmentManager();
|
||||||
|
FragmentTransaction ft = fm.beginTransaction();
|
||||||
|
rateMeDialog.show(ft, DIALOG_RATE_ME_TAG);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author masensio
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author David González Verdugo
|
||||||
|
* @author Christian Schabesberger
|
||||||
|
* @author David Crespo Ríos
|
||||||
|
* @author Juan Carlos Garrote Gascón
|
||||||
|
* @author Aitor Ballesteros Pavón
|
||||||
|
*
|
||||||
|
* Copyright (C) 2024 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Application
|
||||||
|
import android.app.NotificationManager.IMPORTANCE_LOW
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.CheckBox
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
|
import eu.qsfera.android.data.providers.implementation.OCSharedPreferencesProvider
|
||||||
|
|
||||||
|
|
||||||
|
import eu.qsfera.android.db.PreferenceManager
|
||||||
|
import eu.qsfera.android.dependecyinjection.commonModule
|
||||||
|
import eu.qsfera.android.dependecyinjection.localDataSourceModule
|
||||||
|
import eu.qsfera.android.dependecyinjection.remoteDataSourceModule
|
||||||
|
import eu.qsfera.android.dependecyinjection.repositoryModule
|
||||||
|
import eu.qsfera.android.dependecyinjection.useCaseModule
|
||||||
|
import eu.qsfera.android.dependecyinjection.viewModelModule
|
||||||
|
import eu.qsfera.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase
|
||||||
|
import eu.qsfera.android.domain.spaces.model.OCSpace
|
||||||
|
import eu.qsfera.android.domain.spaces.usecases.GetPersonalSpaceForAccountUseCase
|
||||||
|
import eu.qsfera.android.domain.user.usecases.GetStoredQuotaUseCase
|
||||||
|
import eu.qsfera.android.extensions.createNotificationChannel
|
||||||
|
import eu.qsfera.android.lib.common.SingleSessionManager
|
||||||
|
import eu.qsfera.android.presentation.authentication.AccountUtils
|
||||||
|
import eu.qsfera.android.presentation.migration.StorageMigrationActivity
|
||||||
|
import eu.qsfera.android.presentation.releasenotes.ReleaseNotesActivity
|
||||||
|
import eu.qsfera.android.presentation.security.biometric.BiometricActivity
|
||||||
|
import eu.qsfera.android.presentation.security.biometric.BiometricManager
|
||||||
|
import eu.qsfera.android.presentation.security.passcode.PassCodeActivity
|
||||||
|
import eu.qsfera.android.presentation.security.passcode.PassCodeManager
|
||||||
|
import eu.qsfera.android.presentation.security.pattern.PatternActivity
|
||||||
|
import eu.qsfera.android.presentation.security.pattern.PatternManager
|
||||||
|
import eu.qsfera.android.presentation.settings.logging.SettingsLogsFragment.Companion.PREFERENCE_ENABLE_LOGGING
|
||||||
|
import eu.qsfera.android.providers.CoroutinesDispatcherProvider
|
||||||
|
import eu.qsfera.android.providers.LogsProvider
|
||||||
|
import eu.qsfera.android.providers.MdmProvider
|
||||||
|
import eu.qsfera.android.providers.WorkManagerProvider
|
||||||
|
import eu.qsfera.android.ui.activity.FileDisplayActivity
|
||||||
|
import eu.qsfera.android.ui.activity.FileDisplayActivity.Companion.PREFERENCE_CLEAR_DATA_ALREADY_TRIGGERED
|
||||||
|
import eu.qsfera.android.ui.activity.WhatsNewActivity
|
||||||
|
import eu.qsfera.android.utils.CONFIGURATION_ALLOW_SCREENSHOTS
|
||||||
|
import eu.qsfera.android.utils.DOWNLOAD_NOTIFICATION_CHANNEL_ID
|
||||||
|
import eu.qsfera.android.utils.DebugInjector
|
||||||
|
import eu.qsfera.android.utils.FILE_SYNC_CONFLICT_NOTIFICATION_CHANNEL_ID
|
||||||
|
import eu.qsfera.android.utils.FILE_SYNC_NOTIFICATION_CHANNEL_ID
|
||||||
|
import eu.qsfera.android.utils.MEDIA_SERVICE_NOTIFICATION_CHANNEL_ID
|
||||||
|
import eu.qsfera.android.utils.UPLOAD_NOTIFICATION_CHANNEL_ID
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main Application of the project
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Contains methods to build the "static" strings. These strings were before constants in different
|
||||||
|
* classes
|
||||||
|
*/
|
||||||
|
class MainApp : Application() {
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
appContext = applicationContext
|
||||||
|
|
||||||
|
// Ensure Logcat shows Timber logs in debug builds
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
try {
|
||||||
|
Timber.plant(Timber.DebugTree())
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
// ignore if already planted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startLogsIfEnabled()
|
||||||
|
|
||||||
|
DebugInjector.injectDebugTools(appContext)
|
||||||
|
|
||||||
|
createNotificationChannels()
|
||||||
|
|
||||||
|
SingleSessionManager.setUserAgent(userAgent)
|
||||||
|
|
||||||
|
initDependencyInjection()
|
||||||
|
|
||||||
|
val workManagerProvider: WorkManagerProvider by inject()
|
||||||
|
var startedActivities = 0
|
||||||
|
|
||||||
|
// register global protection with pass code, pattern lock and biometric lock
|
||||||
|
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
|
||||||
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||||
|
Timber.d("${activity.javaClass.simpleName} onCreate(Bundle) starting")
|
||||||
|
|
||||||
|
// To prevent taking screenshots in MDM
|
||||||
|
if (!areScreenshotsAllowed()) {
|
||||||
|
activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's any lock protection, don't show wizard at this point, show it when lock activities
|
||||||
|
// have finished
|
||||||
|
if (activity !is PassCodeActivity &&
|
||||||
|
activity !is PatternActivity &&
|
||||||
|
activity !is BiometricActivity
|
||||||
|
) {
|
||||||
|
StorageMigrationActivity.runIfNeeded(activity)
|
||||||
|
if (isFirstRun()) {
|
||||||
|
WhatsNewActivity.runIfNeeded(activity)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ReleaseNotesActivity.runIfNeeded(activity)
|
||||||
|
|
||||||
|
val pref = PreferenceManager.getDefaultSharedPreferences(appContext)
|
||||||
|
val clearDataAlreadyTriggered = pref.contains(PREFERENCE_CLEAR_DATA_ALREADY_TRIGGERED)
|
||||||
|
if (clearDataAlreadyTriggered || isNewVersionCode()) {
|
||||||
|
val dontShowAgainDialogPref = pref.getBoolean(PREFERENCE_KEY_DONT_SHOW_SERVER_ACCOUNT_WARNING_DIALOG, false)
|
||||||
|
if (!dontShowAgainDialogPref && shouldShowDialog(activity)) {
|
||||||
|
val checkboxDialog = activity.layoutInflater.inflate(R.layout.checkbox_dialog, null)
|
||||||
|
val checkbox = checkboxDialog.findViewById<CheckBox>(R.id.checkbox_dialog)
|
||||||
|
checkbox.setText(R.string.server_accounts_warning_checkbox_message)
|
||||||
|
val builder = AlertDialog.Builder(activity).apply {
|
||||||
|
setView(checkboxDialog)
|
||||||
|
setTitle(R.string.server_accounts_warning_title)
|
||||||
|
setMessage(R.string.server_accounts_warning_message)
|
||||||
|
setCancelable(false)
|
||||||
|
setPositiveButton(R.string.server_accounts_warning_button) { _, _ ->
|
||||||
|
if (checkbox.isChecked) {
|
||||||
|
pref.edit().putBoolean(PREFERENCE_KEY_DONT_SHOW_SERVER_ACCOUNT_WARNING_DIALOG, true).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val alertDialog = builder.create()
|
||||||
|
alertDialog.show()
|
||||||
|
}
|
||||||
|
} else { // "Clear data" button is pressed from the app settings in the device settings.
|
||||||
|
AccountUtils.deleteAccounts(appContext)
|
||||||
|
WhatsNewActivity.runIfNeeded(activity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PreferenceManager.migrateFingerprintToBiometricKey(applicationContext)
|
||||||
|
PreferenceManager.deleteOldSettingsPreferences(applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldShowDialog(activity: Activity) =
|
||||||
|
runBlocking(CoroutinesDispatcherProvider().io) {
|
||||||
|
if (activity !is FileDisplayActivity) return@runBlocking false
|
||||||
|
val account = AccountUtils.getCurrentQSferaAccount(appContext) ?: return@runBlocking false
|
||||||
|
|
||||||
|
val getStoredCapabilitiesUseCase: GetStoredCapabilitiesUseCase by inject()
|
||||||
|
val capabilities = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
|
||||||
|
getStoredCapabilitiesUseCase(
|
||||||
|
GetStoredCapabilitiesUseCase.Params(
|
||||||
|
accountName = account.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val spacesAllowed = capabilities != null && capabilities.isSpacesAllowed()
|
||||||
|
|
||||||
|
var personalSpace: OCSpace? = null
|
||||||
|
if (spacesAllowed) {
|
||||||
|
val getPersonalSpaceForAccountUseCase: GetPersonalSpaceForAccountUseCase by inject()
|
||||||
|
personalSpace = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
|
||||||
|
getPersonalSpaceForAccountUseCase(
|
||||||
|
GetPersonalSpaceForAccountUseCase.Params(
|
||||||
|
accountName = account.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val getStoredQuotaUseCase: GetStoredQuotaUseCase by inject()
|
||||||
|
val quota = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
|
||||||
|
getStoredQuotaUseCase(
|
||||||
|
GetStoredQuotaUseCase.Params(
|
||||||
|
accountName = account.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val isLightUser = quota.getDataOrNull()?.available == -4L
|
||||||
|
|
||||||
|
spacesAllowed && personalSpace == null && !isLightUser
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityStarted(activity: Activity) {
|
||||||
|
Timber.v("${activity.javaClass.simpleName} onStart() starting")
|
||||||
|
if (startedActivities == 0) {
|
||||||
|
// App entered foreground — ensure the periodic worker is registered
|
||||||
|
// (recovers if the chain was dropped) and trigger an immediate scan
|
||||||
|
// so the user doesn't have to wait up to 15 min.
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
workManagerProvider.enqueueAutomaticUploadsWorker()
|
||||||
|
workManagerProvider.enqueueMediaStoreAutomaticUploadsWorker()
|
||||||
|
workManagerProvider.enqueueImmediateAutomaticUploadsWorker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startedActivities++
|
||||||
|
PassCodeManager.onActivityStarted(activity)
|
||||||
|
PatternManager.onActivityStarted(activity)
|
||||||
|
BiometricManager.onActivityStarted(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResumed(activity: Activity) {
|
||||||
|
Timber.v("${activity.javaClass.simpleName} onResume() starting")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityPaused(activity: Activity) {
|
||||||
|
Timber.v("${activity.javaClass.simpleName} onPause() ending")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityStopped(activity: Activity) {
|
||||||
|
startedActivities--
|
||||||
|
Timber.v("${activity.javaClass.simpleName} onStop() ending")
|
||||||
|
PassCodeManager.onActivityStopped(activity)
|
||||||
|
PatternManager.onActivityStopped(activity)
|
||||||
|
BiometricManager.onActivityStopped(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
|
||||||
|
Timber.v("${activity.javaClass.simpleName} onSaveInstanceState(Bundle) starting")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityDestroyed(activity: Activity) {
|
||||||
|
Timber.v("${activity.javaClass.simpleName} onDestroy() ending")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startLogsIfEnabled() {
|
||||||
|
val preferenceProvider = OCSharedPreferencesProvider(applicationContext)
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
val alreadySet = preferenceProvider.containsPreference(PREFERENCE_ENABLE_LOGGING)
|
||||||
|
if (!alreadySet) {
|
||||||
|
preferenceProvider.putBoolean(PREFERENCE_ENABLE_LOGGING, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledLogging = preferenceProvider.getBoolean(PREFERENCE_ENABLE_LOGGING, false)
|
||||||
|
|
||||||
|
if (enabledLogging) {
|
||||||
|
val mdmProvider = MdmProvider(applicationContext)
|
||||||
|
LogsProvider(applicationContext, mdmProvider).startLogging()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Screenshots allowed in debug mode. Devs and tests <3
|
||||||
|
* Otherwise, depends on branding.
|
||||||
|
*/
|
||||||
|
private fun areScreenshotsAllowed(): Boolean {
|
||||||
|
if (BuildConfig.DEBUG) return true
|
||||||
|
|
||||||
|
val mdmProvider = MdmProvider(applicationContext)
|
||||||
|
return mdmProvider.getBrandingBoolean(CONFIGURATION_ALLOW_SCREENSHOTS, R.bool.allow_screenshots)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannels() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotificationChannel(
|
||||||
|
id = DOWNLOAD_NOTIFICATION_CHANNEL_ID,
|
||||||
|
name = getString(R.string.download_notification_channel_name),
|
||||||
|
description = getString(R.string.download_notification_channel_description),
|
||||||
|
importance = IMPORTANCE_LOW
|
||||||
|
)
|
||||||
|
|
||||||
|
createNotificationChannel(
|
||||||
|
id = UPLOAD_NOTIFICATION_CHANNEL_ID,
|
||||||
|
name = getString(R.string.upload_notification_channel_name),
|
||||||
|
description = getString(R.string.upload_notification_channel_description),
|
||||||
|
importance = IMPORTANCE_LOW
|
||||||
|
)
|
||||||
|
|
||||||
|
createNotificationChannel(
|
||||||
|
id = MEDIA_SERVICE_NOTIFICATION_CHANNEL_ID,
|
||||||
|
name = getString(R.string.media_service_notification_channel_name),
|
||||||
|
description = getString(R.string.media_service_notification_channel_description),
|
||||||
|
importance = IMPORTANCE_LOW
|
||||||
|
)
|
||||||
|
|
||||||
|
createNotificationChannel(
|
||||||
|
id = FILE_SYNC_CONFLICT_NOTIFICATION_CHANNEL_ID,
|
||||||
|
name = getString(R.string.file_sync_conflict_notification_channel_name),
|
||||||
|
description = getString(R.string.file_sync_conflict_notification_channel_description),
|
||||||
|
importance = IMPORTANCE_LOW
|
||||||
|
)
|
||||||
|
|
||||||
|
createNotificationChannel(
|
||||||
|
id = FILE_SYNC_NOTIFICATION_CHANNEL_ID,
|
||||||
|
name = getString(R.string.file_sync_notification_channel_name),
|
||||||
|
description = getString(R.string.file_sync_notification_channel_description),
|
||||||
|
importance = IMPORTANCE_LOW
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isFirstRun(): Boolean {
|
||||||
|
if (getLastSeenVersionCode() != 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return AccountUtils.getCurrentQSferaAccount(appContext) == null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MDM_FLAVOR = "mdm"
|
||||||
|
|
||||||
|
lateinit var appContext: Context
|
||||||
|
private set
|
||||||
|
var enabledLogging: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
const val PREFERENCE_KEY_LAST_SEEN_VERSION_CODE = "lastSeenVersionCode"
|
||||||
|
|
||||||
|
const val PREFERENCE_KEY_DONT_SHOW_SERVER_ACCOUNT_WARNING_DIALOG = "PREFERENCE_KEY_DONT_SHOW_SERVER_ACCOUNT_WARNING_DIALOG"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Next methods give access in code to some constants that need to be defined in string resources to be referred
|
||||||
|
* in AndroidManifest.xml file or other xml resource files; or that need to be easy to modify in build time.
|
||||||
|
*/
|
||||||
|
|
||||||
|
val accountType: String
|
||||||
|
get() = appContext.resources.getString(R.string.account_type)
|
||||||
|
|
||||||
|
val versionCode: Int
|
||||||
|
get() =
|
||||||
|
try {
|
||||||
|
val pInfo: PackageInfo = appContext.packageManager.getPackageInfo(appContext.packageName, 0)
|
||||||
|
val longVersionCode: Long = PackageInfoCompat.getLongVersionCode(pInfo)
|
||||||
|
longVersionCode.toInt()
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
Timber.w(e, "Version code not found, using 0 as fallback")
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
val authority: String
|
||||||
|
get() = appContext.resources.getString(R.string.authority)
|
||||||
|
|
||||||
|
val authTokenType: String
|
||||||
|
get() = appContext.resources.getString(R.string.authority)
|
||||||
|
|
||||||
|
val dataFolder: String
|
||||||
|
get() = appContext.resources.getString(R.string.data_folder)
|
||||||
|
|
||||||
|
// user agent
|
||||||
|
// Mozilla/5.0 (Android) qsfera-android/1.7.0
|
||||||
|
val userAgent: String
|
||||||
|
get() {
|
||||||
|
val appString = appContext.resources.getString(R.string.user_agent)
|
||||||
|
val packageName = appContext.packageName
|
||||||
|
var version: String? = ""
|
||||||
|
|
||||||
|
val pInfo: PackageInfo?
|
||||||
|
try {
|
||||||
|
pInfo = appContext.packageManager.getPackageInfo(packageName, 0)
|
||||||
|
if (pInfo != null) {
|
||||||
|
version = pInfo.versionName
|
||||||
|
}
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
Timber.e(e, "Trying to get packageName")
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.format(appString, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initDependencyInjection() {
|
||||||
|
stopKoin()
|
||||||
|
startKoin {
|
||||||
|
androidContext(appContext)
|
||||||
|
modules(
|
||||||
|
listOf(
|
||||||
|
commonModule,
|
||||||
|
viewModelModule,
|
||||||
|
useCaseModule,
|
||||||
|
repositoryModule,
|
||||||
|
localDataSourceModule,
|
||||||
|
remoteDataSourceModule
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLastSeenVersionCode(): Int {
|
||||||
|
val pref = PreferenceManager.getDefaultSharedPreferences(appContext)
|
||||||
|
return pref.getInt(PREFERENCE_KEY_LAST_SEEN_VERSION_CODE, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isNewVersionCode(): Boolean {
|
||||||
|
val lastSeenVersionCode = getLastSeenVersionCode()
|
||||||
|
if (lastSeenVersionCode == 0) { // The preferences have been deleted, so we can delete the accounts and navigate to login
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return lastSeenVersionCode != versionCode // The version has changed and the accounts must not be deleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Bartek Przybylski
|
||||||
|
* @author Christian Schabesberger
|
||||||
|
* @author David González Verdugo
|
||||||
|
* @author Abel García de Prada
|
||||||
|
* @author Juan Carlos Garrote Gascón
|
||||||
|
*
|
||||||
|
* Copyright (C) 2012 Bartek Przybylski
|
||||||
|
* Copyright (C) 2023 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http:></http:>//www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.datamodel
|
||||||
|
|
||||||
|
import android.accounts.Account
|
||||||
|
import eu.qsfera.android.domain.files.model.OCFile
|
||||||
|
import eu.qsfera.android.domain.files.model.OCFile.Companion.ROOT_PATH
|
||||||
|
import eu.qsfera.android.domain.files.usecases.GetFileByIdUseCase
|
||||||
|
import eu.qsfera.android.domain.files.usecases.GetFileByRemotePathUseCase
|
||||||
|
import eu.qsfera.android.domain.files.usecases.GetFolderContentUseCase
|
||||||
|
import eu.qsfera.android.domain.files.usecases.GetFolderImagesUseCase
|
||||||
|
import eu.qsfera.android.domain.files.usecases.GetPersonalRootFolderForAccountUseCase
|
||||||
|
import eu.qsfera.android.domain.files.usecases.GetSharesRootFolderForAccount
|
||||||
|
import eu.qsfera.android.providers.CoroutinesDispatcherProvider
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
class FileDataStorageManager(
|
||||||
|
val account: Account,
|
||||||
|
) : KoinComponent {
|
||||||
|
|
||||||
|
fun getFileByPath(remotePath: String, spaceId: String? = null): OCFile? =
|
||||||
|
if (remotePath == ROOT_PATH && spaceId == null) {
|
||||||
|
getRootPersonalFolder()
|
||||||
|
} else {
|
||||||
|
getFileByPathAndAccount(remotePath, account.name, spaceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFileByPathAndAccount(remotePath: String, accountName: String, spaceId: String? = null): OCFile? =
|
||||||
|
runBlocking(CoroutinesDispatcherProvider().io) {
|
||||||
|
val getFileByRemotePathUseCase: GetFileByRemotePathUseCase by inject()
|
||||||
|
|
||||||
|
val result = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
|
||||||
|
getFileByRemotePathUseCase(GetFileByRemotePathUseCase.Params(accountName, remotePath, spaceId))
|
||||||
|
}.getDataOrNull()
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRootPersonalFolder() = runBlocking(CoroutinesDispatcherProvider().io) {
|
||||||
|
val getPersonalRootFolderForAccountUseCase: GetPersonalRootFolderForAccountUseCase by inject()
|
||||||
|
|
||||||
|
val result = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
|
||||||
|
getPersonalRootFolderForAccountUseCase(GetPersonalRootFolderForAccountUseCase.Params(account.name))
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRootSharesFolder() = runBlocking(CoroutinesDispatcherProvider().io) {
|
||||||
|
val getSharesRootFolderForAccount: GetSharesRootFolderForAccount by inject()
|
||||||
|
|
||||||
|
val result = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
|
||||||
|
getSharesRootFolderForAccount(GetSharesRootFolderForAccount.Params(account.name))
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// To do: New_arch. Remove this and call usecase inside FilesViewModel
|
||||||
|
fun getFileById(id: Long): OCFile? = runBlocking(CoroutinesDispatcherProvider().io) {
|
||||||
|
val getFileByIdUseCase: GetFileByIdUseCase by inject()
|
||||||
|
|
||||||
|
val result = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
|
||||||
|
getFileByIdUseCase(GetFileByIdUseCase.Params(id))
|
||||||
|
}.getDataOrNull()
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fileExists(path: String): Boolean = getFileByPath(path) != null
|
||||||
|
|
||||||
|
fun getFolderContent(f: OCFile?): List<OCFile> =
|
||||||
|
if (f != null && f.isFolder && f.id != -1L) {
|
||||||
|
// To do: Remove !!
|
||||||
|
getFolderContent(f.id!!)
|
||||||
|
} else {
|
||||||
|
listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
// To do: New_arch. Remove this and call usecase inside FilesViewModel
|
||||||
|
fun getFolderImages(folder: OCFile?): List<OCFile> = runBlocking(CoroutinesDispatcherProvider().io) {
|
||||||
|
val getFolderImagesUseCase: GetFolderImagesUseCase by inject()
|
||||||
|
|
||||||
|
val result = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
|
||||||
|
// To do: Remove !!
|
||||||
|
getFolderImagesUseCase(GetFolderImagesUseCase.Params(folderId = folder!!.id!!))
|
||||||
|
}.getDataOrNull()
|
||||||
|
result ?: listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
// To do: New_arch. Remove this and call usecase inside FilesViewModel
|
||||||
|
private fun getFolderContent(parentId: Long): List<OCFile> = runBlocking(CoroutinesDispatcherProvider().io) {
|
||||||
|
val getFolderContentUseCase: GetFolderContentUseCase by inject()
|
||||||
|
|
||||||
|
val result = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
|
||||||
|
getFolderContentUseCase(GetFolderContentUseCase.Params(parentId))
|
||||||
|
}.getDataOrNull()
|
||||||
|
result ?: listOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
/*
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author David González Verdugo
|
||||||
|
* @author Shashvat Kedia
|
||||||
|
* @author Juan Carlos Garrote Gascón
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
* <p>
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
* <p>
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.db;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import eu.qsfera.android.presentation.security.biometric.BiometricActivity;
|
||||||
|
import eu.qsfera.android.utils.FileStorageUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to simplify reading of Preferences all around the app
|
||||||
|
*/
|
||||||
|
public abstract class PreferenceManager {
|
||||||
|
// Legacy preferences - done in version 2.18
|
||||||
|
public static final String PREF__LEGACY_CLICK_DEV_MENU = "clickDeveloperMenu";
|
||||||
|
public static final String PREF__LEGACY_CAMERA_PICTURE_UPLOADS_ENABLED = "camera_picture_uploads";
|
||||||
|
public static final String PREF__LEGACY_CAMERA_VIDEO_UPLOADS_ENABLED = "camera_video_uploads";
|
||||||
|
public static final String PREF__LEGACY_CAMERA_PICTURE_UPLOADS_WIFI_ONLY = "camera_picture_uploads_on_wifi";
|
||||||
|
public static final String PREF__LEGACY_CAMERA_VIDEO_UPLOADS_WIFI_ONLY = "camera_video_uploads_on_wifi";
|
||||||
|
public static final String PREF__LEGACY_CAMERA_PICTURE_UPLOADS_PATH = "camera_picture_uploads_path";
|
||||||
|
public static final String PREF__LEGACY_CAMERA_VIDEO_UPLOADS_PATH = "camera_video_uploads_path";
|
||||||
|
public static final String PREF__LEGACY_CAMERA_UPLOADS_BEHAVIOUR = "camera_uploads_behaviour";
|
||||||
|
public static final String PREF__LEGACY_CAMERA_UPLOADS_SOURCE = "camera_uploads_source_path";
|
||||||
|
public static final String PREF__LEGACY_CAMERA_UPLOADS_ACCOUNT_NAME = "camera_uploads_account_name";
|
||||||
|
public static final String PREF__CAMERA_PICTURE_UPLOADS_ENABLED = "enable_picture_uploads";
|
||||||
|
public static final String PREF__CAMERA_VIDEO_UPLOADS_ENABLED = "enable_video_uploads";
|
||||||
|
public static final String PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY = "picture_uploads_on_wifi";
|
||||||
|
public static final String PREF__CAMERA_PICTURE_UPLOADS_CHARGING_ONLY = "picture_uploads_on_charging";
|
||||||
|
public static final String PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY = "video_uploads_on_wifi";
|
||||||
|
public static final String PREF__CAMERA_VIDEO_UPLOADS_CHARGING_ONLY = "video_uploads_on_charging";
|
||||||
|
public static final String PREF__CAMERA_PICTURE_UPLOADS_PATH = "picture_uploads_path";
|
||||||
|
public static final String PREF__CAMERA_VIDEO_UPLOADS_PATH = "video_uploads_path";
|
||||||
|
public static final String PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR = "picture_uploads_behaviour";
|
||||||
|
public static final String PREF__CAMERA_PICTURE_UPLOADS_SOURCE = "picture_uploads_source_path";
|
||||||
|
public static final String PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR = "video_uploads_behaviour";
|
||||||
|
public static final String PREF__CAMERA_VIDEO_UPLOADS_SOURCE = "video_uploads_source_path";
|
||||||
|
public static final String PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME = "picture_uploads_account_name";
|
||||||
|
public static final String PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME = "video_uploads_account_name";
|
||||||
|
public static final String PREF__CAMERA_PICTURE_UPLOADS_LAST_SYNC = "picture_uploads_last_sync";
|
||||||
|
public static final String PREF__CAMERA_VIDEO_UPLOADS_LAST_SYNC = "video_uploads_last_sync";
|
||||||
|
public static final String PREF__CAMERA_UPLOADS_DEFAULT_PATH = "/CameraUpload";
|
||||||
|
public static final String PREF__LEGACY_FINGERPRINT = "set_fingerprint";
|
||||||
|
/**
|
||||||
|
* Constant to access value of last path selected by the user to upload a file shared from other app.
|
||||||
|
* Value handled by the app without direct access in the UI.
|
||||||
|
*/
|
||||||
|
private static final String AUTO_PREF__LAST_UPLOAD_PATH = "last_upload_path";
|
||||||
|
private static final String AUTO_PREF__SORT_ORDER_FILE_DISP = "sortOrderFileDisp";
|
||||||
|
private static final String AUTO_PREF__SORT_ASCENDING_FILE_DISP = "sortAscendingFileDisp";
|
||||||
|
private static final String AUTO_PREF__SORT_ORDER_UPLOAD = "sortOrderUpload";
|
||||||
|
private static final String AUTO_PREF__SORT_ASCENDING_UPLOAD = "sortAscendingUpload";
|
||||||
|
|
||||||
|
public static void migrateFingerprintToBiometricKey(Context context) {
|
||||||
|
SharedPreferences sharedPref = getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
|
// Check if legacy fingerprint key exists, delete it and migrate its value to the new key
|
||||||
|
if (sharedPref.contains(PREF__LEGACY_FINGERPRINT)) {
|
||||||
|
boolean currentFingerprintValue = sharedPref.getBoolean(PREF__LEGACY_FINGERPRINT, false);
|
||||||
|
SharedPreferences.Editor editor = sharedPref.edit();
|
||||||
|
editor.remove(PREF__LEGACY_FINGERPRINT);
|
||||||
|
editor.putBoolean(BiometricActivity.PREFERENCE_SET_BIOMETRIC, currentFingerprintValue);
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void deleteOldSettingsPreferences(Context context) {
|
||||||
|
SharedPreferences sharedPref = getDefaultSharedPreferences(context);
|
||||||
|
SharedPreferences.Editor editor = sharedPref.edit();
|
||||||
|
if (sharedPref.contains(PREF__LEGACY_CLICK_DEV_MENU)) {
|
||||||
|
editor.remove(PREF__LEGACY_CLICK_DEV_MENU);
|
||||||
|
}
|
||||||
|
if (sharedPref.contains(PREF__LEGACY_CAMERA_PICTURE_UPLOADS_ENABLED)) {
|
||||||
|
editor.remove(PREF__LEGACY_CAMERA_PICTURE_UPLOADS_ENABLED);
|
||||||
|
}
|
||||||
|
if (sharedPref.contains(PREF__LEGACY_CAMERA_VIDEO_UPLOADS_ENABLED)) {
|
||||||
|
editor.remove(PREF__LEGACY_CAMERA_VIDEO_UPLOADS_ENABLED);
|
||||||
|
}
|
||||||
|
if (sharedPref.contains(PREF__LEGACY_CAMERA_PICTURE_UPLOADS_WIFI_ONLY)) {
|
||||||
|
editor.remove(PREF__LEGACY_CAMERA_PICTURE_UPLOADS_WIFI_ONLY);
|
||||||
|
}
|
||||||
|
if (sharedPref.contains(PREF__LEGACY_CAMERA_VIDEO_UPLOADS_WIFI_ONLY)) {
|
||||||
|
editor.remove(PREF__LEGACY_CAMERA_VIDEO_UPLOADS_WIFI_ONLY);
|
||||||
|
}
|
||||||
|
if (sharedPref.contains(PREF__LEGACY_CAMERA_PICTURE_UPLOADS_PATH)) {
|
||||||
|
editor.remove(PREF__LEGACY_CAMERA_PICTURE_UPLOADS_PATH);
|
||||||
|
}
|
||||||
|
if (sharedPref.contains(PREF__LEGACY_CAMERA_VIDEO_UPLOADS_PATH)) {
|
||||||
|
editor.remove(PREF__LEGACY_CAMERA_VIDEO_UPLOADS_PATH);
|
||||||
|
}
|
||||||
|
if (sharedPref.contains(PREF__LEGACY_CAMERA_UPLOADS_BEHAVIOUR)) {
|
||||||
|
editor.remove(PREF__LEGACY_CAMERA_UPLOADS_BEHAVIOUR);
|
||||||
|
}
|
||||||
|
if (sharedPref.contains(PREF__LEGACY_CAMERA_UPLOADS_SOURCE)) {
|
||||||
|
editor.remove(PREF__LEGACY_CAMERA_UPLOADS_SOURCE);
|
||||||
|
}
|
||||||
|
if (sharedPref.contains(PREF__LEGACY_CAMERA_UPLOADS_ACCOUNT_NAME)) {
|
||||||
|
editor.remove(PREF__LEGACY_CAMERA_UPLOADS_ACCOUNT_NAME);
|
||||||
|
}
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the path where the user selected to do the last upload of a file shared from other app.
|
||||||
|
*
|
||||||
|
* @param context Caller {@link Context}, used to access to shared preferences manager.
|
||||||
|
* @return path Absolute path to a folder, as previously stored by {@link #setLastUploadPath(String, Context)},
|
||||||
|
* or empty String if never saved before.
|
||||||
|
*/
|
||||||
|
public static String getLastUploadPath(Context context) {
|
||||||
|
return getDefaultSharedPreferences(context).getString(AUTO_PREF__LAST_UPLOAD_PATH, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the path where the user selected to do the last upload of a file shared from other app.
|
||||||
|
*
|
||||||
|
* @param path Absolute path to a folder.
|
||||||
|
* @param context Caller {@link Context}, used to access to shared preferences manager.
|
||||||
|
*/
|
||||||
|
public static void setLastUploadPath(String path, Context context) {
|
||||||
|
saveStringPreference(AUTO_PREF__LAST_UPLOAD_PATH, path, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the sort order which the user has set last.
|
||||||
|
*
|
||||||
|
* @param context Caller {@link Context}, used to access to shared preferences manager.
|
||||||
|
* @return sort order the sort order, default is {@link FileStorageUtils#SORT_NAME} (sort by name)
|
||||||
|
*/
|
||||||
|
public static int getSortOrder(Context context, int flag) {
|
||||||
|
if (flag == FileStorageUtils.FILE_DISPLAY_SORT) {
|
||||||
|
return getDefaultSharedPreferences(context)
|
||||||
|
.getInt(AUTO_PREF__SORT_ORDER_FILE_DISP, FileStorageUtils.SORT_NAME);
|
||||||
|
} else {
|
||||||
|
return getDefaultSharedPreferences(context)
|
||||||
|
.getInt(AUTO_PREF__SORT_ORDER_UPLOAD, FileStorageUtils.SORT_DATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the sort order which the user has set last.
|
||||||
|
*
|
||||||
|
* @param order the sort order
|
||||||
|
* @param context Caller {@link Context}, used to access to shared preferences manager.
|
||||||
|
*/
|
||||||
|
public static void setSortOrder(int order, Context context, int flag) {
|
||||||
|
if (flag == FileStorageUtils.FILE_DISPLAY_SORT) {
|
||||||
|
saveIntPreference(AUTO_PREF__SORT_ORDER_FILE_DISP, order, context);
|
||||||
|
} else {
|
||||||
|
saveIntPreference(AUTO_PREF__SORT_ORDER_UPLOAD, order, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the ascending order flag which the user has set last.
|
||||||
|
*
|
||||||
|
* @param context Caller {@link Context}, used to access to shared preferences manager.
|
||||||
|
* @return ascending order the ascending order, default is true
|
||||||
|
*/
|
||||||
|
public static boolean getSortAscending(Context context, int flag) {
|
||||||
|
if (flag == FileStorageUtils.FILE_DISPLAY_SORT) {
|
||||||
|
return getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(AUTO_PREF__SORT_ASCENDING_FILE_DISP, true);
|
||||||
|
} else {
|
||||||
|
return getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(AUTO_PREF__SORT_ASCENDING_UPLOAD, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the ascending order flag which the user has set last.
|
||||||
|
*
|
||||||
|
* @param ascending flag if sorting is ascending or descending
|
||||||
|
* @param context Caller {@link Context}, used to access to shared preferences manager.
|
||||||
|
*/
|
||||||
|
public static void setSortAscending(boolean ascending, Context context, int flag) {
|
||||||
|
if (flag == FileStorageUtils.FILE_DISPLAY_SORT) {
|
||||||
|
saveBooleanPreference(AUTO_PREF__SORT_ASCENDING_FILE_DISP, ascending, context);
|
||||||
|
} else {
|
||||||
|
saveBooleanPreference(AUTO_PREF__SORT_ASCENDING_UPLOAD, ascending, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void saveBooleanPreference(String key, boolean value, Context context) {
|
||||||
|
SharedPreferences.Editor appPreferences = getDefaultSharedPreferences(context.getApplicationContext()).edit();
|
||||||
|
appPreferences.putBoolean(key, value);
|
||||||
|
appPreferences.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void saveStringPreference(String key, String value, Context context) {
|
||||||
|
SharedPreferences.Editor appPreferences = getDefaultSharedPreferences(context.getApplicationContext()).edit();
|
||||||
|
appPreferences.putString(key, value);
|
||||||
|
appPreferences.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void saveIntPreference(String key, int value, Context context) {
|
||||||
|
SharedPreferences.Editor appPreferences = getDefaultSharedPreferences(context.getApplicationContext()).edit();
|
||||||
|
appPreferences.putInt(key, value);
|
||||||
|
appPreferences.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SharedPreferences getDefaultSharedPreferences(Context context) {
|
||||||
|
return android.preference.PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author Bartek Przybylski
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author masensio
|
||||||
|
* @author David González Verdugo
|
||||||
|
* @author Abel García de Prada
|
||||||
|
* Copyright (C) 2011 Bartek Przybylski
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package eu.qsfera.android.db;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.provider.BaseColumns;
|
||||||
|
|
||||||
|
import eu.qsfera.android.MainApp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta-Class that holds various static field information
|
||||||
|
* This is only used in FileContentProvider for legacy DB
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public class ProviderMeta {
|
||||||
|
|
||||||
|
private ProviderMeta() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static public class ProviderTableMeta implements BaseColumns {
|
||||||
|
public static final String FILE_TABLE_NAME = "filelist";
|
||||||
|
public static final String OCSHARES_TABLE_NAME = "ocshares";
|
||||||
|
public static final String CAPABILITIES_TABLE_NAME = "capabilities";
|
||||||
|
public static final String UPLOADS_TABLE_NAME = "list_of_uploads";
|
||||||
|
public static final String USER_AVATARS__TABLE_NAME = "user_avatars";
|
||||||
|
public static final String CAMERA_UPLOADS_SYNC_TABLE_NAME = "camera_uploads_sync";
|
||||||
|
public static final String USER_QUOTAS_TABLE_NAME = "user_quotas";
|
||||||
|
|
||||||
|
public static final Uri CONTENT_URI = Uri.parse("content://"
|
||||||
|
+ MainApp.Companion.getAuthority() + "/");
|
||||||
|
public static final Uri CONTENT_URI_FILE = Uri.parse("content://"
|
||||||
|
+ MainApp.Companion.getAuthority() + "/file");
|
||||||
|
public static final Uri CONTENT_URI_DIR = Uri.parse("content://"
|
||||||
|
+ MainApp.Companion.getAuthority() + "/dir");
|
||||||
|
public static final Uri CONTENT_URI_SHARE = Uri.parse("content://"
|
||||||
|
+ MainApp.Companion.getAuthority() + "/shares");
|
||||||
|
public static final Uri CONTENT_URI_CAPABILITIES = Uri.parse("content://"
|
||||||
|
+ MainApp.Companion.getAuthority() + "/capabilities");
|
||||||
|
public static final Uri CONTENT_URI_UPLOADS = Uri.parse("content://"
|
||||||
|
+ MainApp.Companion.getAuthority() + "/uploads");
|
||||||
|
public static final Uri CONTENT_URI_CAMERA_UPLOADS_SYNC = Uri.parse("content://"
|
||||||
|
+ MainApp.Companion.getAuthority() + "/cameraUploadsSync");
|
||||||
|
public static final Uri CONTENT_URI_QUOTAS = Uri.parse("content://"
|
||||||
|
+ MainApp.Companion.getAuthority() + "/quotas");
|
||||||
|
|
||||||
|
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.qsfera.file";
|
||||||
|
public static final String CONTENT_TYPE_ITEM = "vnd.android.cursor.item/vnd.qsfera.file";
|
||||||
|
|
||||||
|
public static final String ID = "id";
|
||||||
|
|
||||||
|
// Columns of filelist table
|
||||||
|
public static final String FILE_PARENT = "parent";
|
||||||
|
public static final String FILE_NAME = "filename";
|
||||||
|
public static final String FILE_CREATION = "created";
|
||||||
|
public static final String FILE_MODIFIED = "modified";
|
||||||
|
public static final String FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA = "modified_at_last_sync_for_data";
|
||||||
|
public static final String FILE_CONTENT_LENGTH = "content_length";
|
||||||
|
public static final String FILE_CONTENT_TYPE = "content_type";
|
||||||
|
public static final String FILE_STORAGE_PATH = "media_path";
|
||||||
|
public static final String FILE_PATH = "path";
|
||||||
|
public static final String FILE_ACCOUNT_OWNER = "file_owner";
|
||||||
|
public static final String FILE_LAST_SYNC_DATE = "last_sync_date";// _for_properties, but let's keep it as it is
|
||||||
|
public static final String FILE_LAST_SYNC_DATE_FOR_DATA = "last_sync_date_for_data";
|
||||||
|
public static final String FILE_KEEP_IN_SYNC = "keep_in_sync";
|
||||||
|
public static final String FILE_ETAG = "etag";
|
||||||
|
public static final String FILE_TREE_ETAG = "tree_etag";
|
||||||
|
public static final String FILE_SHARED_VIA_LINK = "share_by_link";
|
||||||
|
public static final String FILE_SHARED_WITH_SHAREE = "shared_via_users";
|
||||||
|
public static final String FILE_PERMISSIONS = "permissions";
|
||||||
|
public static final String FILE_REMOTE_ID = "remote_id";
|
||||||
|
public static final String FILE_UPDATE_THUMBNAIL = "update_thumbnail";
|
||||||
|
public static final String FILE_IS_DOWNLOADING = "is_downloading";
|
||||||
|
public static final String FILE_ETAG_IN_CONFLICT = "etag_in_conflict";
|
||||||
|
public static final String FILE_PRIVATE_LINK = "private_link";
|
||||||
|
|
||||||
|
public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME
|
||||||
|
+ " collate nocase asc";
|
||||||
|
|
||||||
|
// @deprecated
|
||||||
|
public static final String FILE_PUBLIC_LINK = "public_link";
|
||||||
|
|
||||||
|
// Columns of ocshares table
|
||||||
|
public static final String OCSHARES_SHARE_TYPE = "share_type";
|
||||||
|
public static final String OCSHARES_SHARE_WITH = "share_with";
|
||||||
|
public static final String OCSHARES_PATH = "path";
|
||||||
|
public static final String OCSHARES_PERMISSIONS = "permissions";
|
||||||
|
public static final String OCSHARES_SHARED_DATE = "shared_date";
|
||||||
|
public static final String OCSHARES_EXPIRATION_DATE = "expiration_date";
|
||||||
|
public static final String OCSHARES_TOKEN = "token";
|
||||||
|
public static final String OCSHARES_SHARE_WITH_DISPLAY_NAME = "shared_with_display_name";
|
||||||
|
public static final String OCSHARES_SHARE_WITH_ADDITIONAL_INFO = "share_with_additional_info";
|
||||||
|
public static final String OCSHARES_IS_DIRECTORY = "is_directory";
|
||||||
|
public static final String OCSHARES_ID_REMOTE_SHARED = "id_remote_shared";
|
||||||
|
public static final String OCSHARES_ACCOUNT_OWNER = "owner_share";
|
||||||
|
public static final String OCSHARES_NAME = "name";
|
||||||
|
public static final String OCSHARES_URL = "url";
|
||||||
|
|
||||||
|
public static final String OCSHARES_DEFAULT_SORT_ORDER = OCSHARES_ID_REMOTE_SHARED
|
||||||
|
+ " collate nocase asc";
|
||||||
|
|
||||||
|
// Columns of capabilities table
|
||||||
|
public static final String CAPABILITIES_ACCOUNT_NAME = "account";
|
||||||
|
public static final String CAPABILITIES_VERSION_MAYOR = "version_mayor";
|
||||||
|
public static final String CAPABILITIES_VERSION_MINOR = "version_minor";
|
||||||
|
public static final String CAPABILITIES_VERSION_MICRO = "version_micro";
|
||||||
|
public static final String CAPABILITIES_VERSION_STRING = "version_string";
|
||||||
|
public static final String CAPABILITIES_VERSION_EDITION = "version_edition";
|
||||||
|
public static final String CAPABILITIES_CORE_POLLINTERVAL = "core_pollinterval";
|
||||||
|
public static final String CAPABILITIES_DAV_CHUNKING_VERSION = "dav_chunking_version";
|
||||||
|
public static final String CAPABILITIES_SHARING_API_ENABLED = "sharing_api_enabled";
|
||||||
|
public static final String CAPABILITIES_SHARING_PUBLIC_ENABLED = "sharing_public_enabled";
|
||||||
|
public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED = "sharing_public_password_enforced";
|
||||||
|
public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_ONLY =
|
||||||
|
"sharing_public_password_enforced_read_only";
|
||||||
|
public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_WRITE =
|
||||||
|
"sharing_public_password_enforced_read_write";
|
||||||
|
public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_UPLOAD_ONLY =
|
||||||
|
"sharing_public_password_enforced_public_only";
|
||||||
|
public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED =
|
||||||
|
"sharing_public_expire_date_enabled";
|
||||||
|
public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS =
|
||||||
|
"sharing_public_expire_date_days";
|
||||||
|
public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED =
|
||||||
|
"sharing_public_expire_date_enforced";
|
||||||
|
public static final String CAPABILITIES_SHARING_PUBLIC_UPLOAD = "sharing_public_upload";
|
||||||
|
public static final String CAPABILITIES_SHARING_PUBLIC_MULTIPLE = "sharing_public_multiple";
|
||||||
|
public static final String CAPABILITIES_SHARING_PUBLIC_SUPPORTS_UPLOAD_ONLY = "supports_upload_only";
|
||||||
|
public static final String CAPABILITIES_SHARING_RESHARING = "sharing_resharing";
|
||||||
|
public static final String CAPABILITIES_SHARING_FEDERATION_OUTGOING = "sharing_federation_outgoing";
|
||||||
|
public static final String CAPABILITIES_SHARING_FEDERATION_INCOMING = "sharing_federation_incoming";
|
||||||
|
public static final String CAPABILITIES_FILES_BIGFILECHUNKING = "files_bigfilechunking";
|
||||||
|
public static final String CAPABILITIES_FILES_UNDELETE = "files_undelete";
|
||||||
|
public static final String CAPABILITIES_FILES_VERSIONING = "files_versioning";
|
||||||
|
|
||||||
|
public static final String CAPABILITIES_DEFAULT_SORT_ORDER = CAPABILITIES_ACCOUNT_NAME
|
||||||
|
+ " collate nocase asc";
|
||||||
|
|
||||||
|
//Columns of Uploads table
|
||||||
|
public static final String UPLOADS_LOCAL_PATH = "local_path";
|
||||||
|
public static final String UPLOADS_REMOTE_PATH = "remote_path";
|
||||||
|
public static final String UPLOADS_ACCOUNT_NAME = "account_name";
|
||||||
|
public static final String UPLOADS_FILE_SIZE = "file_size";
|
||||||
|
public static final String UPLOADS_STATUS = "status";
|
||||||
|
public static final String UPLOADS_LOCAL_BEHAVIOUR = "local_behaviour";
|
||||||
|
public static final String UPLOADS_UPLOAD_TIME = "upload_time";
|
||||||
|
public static final String UPLOADS_FORCE_OVERWRITE = "force_overwrite";
|
||||||
|
public static final String UPLOADS_IS_CREATE_REMOTE_FOLDER = "is_create_remote_folder";
|
||||||
|
public static final String UPLOADS_UPLOAD_END_TIMESTAMP = "upload_end_timestamp";
|
||||||
|
public static final String UPLOADS_LAST_RESULT = "last_result";
|
||||||
|
public static final String UPLOADS_CREATED_BY = "created_by";
|
||||||
|
public static final String UPLOADS_TRANSFER_ID = "transfer_id";
|
||||||
|
|
||||||
|
public static final String UPLOADS_DEFAULT_SORT_ORDER =
|
||||||
|
ProviderTableMeta._ID + " collate nocase desc";
|
||||||
|
|
||||||
|
// Columns of user_avatars table
|
||||||
|
public static final String USER_AVATARS__ACCOUNT_NAME = "account_name";
|
||||||
|
public static final String USER_AVATARS__CACHE_KEY = "cache_key";
|
||||||
|
public static final String USER_AVATARS__ETAG = "etag";
|
||||||
|
public static final String USER_AVATARS__MIME_TYPE = "mime_type";
|
||||||
|
|
||||||
|
// Columns of camera upload synchronization table
|
||||||
|
public static final String PICTURES_LAST_SYNC_TIMESTAMP = "pictures_last_sync_date";
|
||||||
|
public static final String VIDEOS_LAST_SYNC_TIMESTAMP = "videos_last_sync_date";
|
||||||
|
public static final String CAMERA_UPLOADS_SYNC_DEFAULT_SORT_ORDER =
|
||||||
|
ProviderTableMeta._ID + " collate nocase asc";
|
||||||
|
|
||||||
|
// Columns of user_quotas table
|
||||||
|
public static final String USER_QUOTAS__ACCOUNT_NAME = "account_name";
|
||||||
|
public static final String USER_QUOTAS__FREE = "free";
|
||||||
|
public static final String USER_QUOTAS__RELATIVE = "relative";
|
||||||
|
public static final String USER_QUOTAS__TOTAL = "total";
|
||||||
|
public static final String USER_QUOTAS__USED = "used";
|
||||||
|
public static final String USER_QUOTAS_DEFAULT_SORT_ORDER =
|
||||||
|
ProviderTableMeta._ID + " collate nocase asc";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* qsfera Android client application
|
||||||
|
*
|
||||||
|
* @author David González Verdugo
|
||||||
|
* @author Abel García de Prada
|
||||||
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.qsfera.android.dependecyinjection
|
||||||
|
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
|
||||||
|
import eu.qsfera.android.providers.AccountProvider
|
||||||
|
import eu.qsfera.android.providers.ContextProvider
|
||||||
|
import eu.qsfera.android.providers.CoroutinesDispatcherProvider
|
||||||
|
import eu.qsfera.android.providers.LogsProvider
|
||||||
|
import eu.qsfera.android.providers.MdmProvider
|
||||||
|
import eu.qsfera.android.providers.WorkManagerProvider
|
||||||
|
import eu.qsfera.android.providers.implementation.OCContextProvider
|
||||||
|
import org.koin.android.ext.koin.androidApplication
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val commonModule = module {
|
||||||
|
|
||||||
|
|
||||||
|
single { CoroutinesDispatcherProvider() }
|
||||||
|
factory<ContextProvider> { OCContextProvider(androidContext()) }
|
||||||
|
single { LogsProvider(get(), get()) }
|
||||||
|
single { MdmProvider(androidContext()) }
|
||||||
|
single { WorkManagerProvider(androidContext()) }
|
||||||
|
single { AccountProvider(androidContext()) }
|
||||||
|
single { WorkManager.getInstance(androidApplication()) }
|
||||||
|
}
|
||||||