From 5f1984fe4ba7ffd4c4f3361cf165283f627c9c58 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sun, 29 Mar 2020 14:14:04 +0200 Subject: [PATCH] Kotlin rewrite --- app/build.gradle | 136 +- app/contacts.xml | 2 +- app/proguard-rules.pro | 22 +- app/src/main/AndroidManifest.xml | 247 +-- .../assistant_default_values} | 6 - .../assistant_linphone_default_values} | 0 .../{res/raw => assets}/linphonerc_default | 7 +- .../{res/raw => assets}/linphonerc_factory | 8 +- .../vending/billing/IInAppBillingService.aidl | 144 -- .../java/org/linphone/LinphoneApplication.kt | 55 + .../java/org/linphone/LinphoneContext.java | 342 ---- .../java/org/linphone/LinphoneManager.java | 635 ------- .../linphone/activities/AboutActivity.java | 209 --- .../linphone/activities/GenericActivity.kt | 83 + .../activities/LinphoneGenericActivity.java | 94 - .../activities/LinphoneLauncherActivity.java | 109 -- .../org/linphone/activities/MainActivity.java | 968 ---------- .../SnackBarActivity.kt} | 8 +- .../activities/ThemeableActivity.java | 58 - .../activities/assistant/AssistantActivity.kt | 47 + .../adapters/CountryPickerAdapter.kt | 95 + .../fragments/AbstractPhoneFragment.kt | 77 + .../fragments/AccountLoginFragment.kt | 95 + .../fragments/CountryPickerFragment.kt | 71 + .../EchoCancellerCalibrationFragment.kt | 84 + .../fragments/EmailAccountCreationFragment.kt | 70 + .../EmailAccountValidationFragment.kt | 72 + .../fragments/GenericAccountLoginFragment.kt | 75 + .../fragments/PhoneAccountCreationFragment.kt | 82 + .../fragments/PhoneAccountLinkingFragment.kt | 110 ++ .../PhoneAccountValidationFragment.kt | 104 ++ .../assistant/fragments/QrCodeFragment.kt | 107 ++ .../fragments/RemoteProvisioningFragment.kt | 104 ++ .../assistant/fragments/TopBarFragment.kt | 53 + .../assistant/fragments/WelcomeFragment.kt | 76 + .../viewmodels/AbstractPhoneViewModel.kt | 80 + .../viewmodels/AccountLoginViewModel.kt | 155 ++ .../EchoCancellerCalibrationViewModel.kt | 65 + .../EmailAccountCreationViewModel.kt | 166 ++ .../EmailAccountValidationViewModel.kt | 108 ++ .../viewmodels/GenericLoginViewModel.kt | 97 + .../PhoneAccountCreationViewModel.kt | 164 ++ .../PhoneAccountLinkingViewModel.kt | 138 ++ .../PhoneAccountValidationViewModel.kt | 153 ++ .../assistant/viewmodels/QrCodeViewModel.kt | 69 + .../viewmodels/RemoteProvisioningViewModel.kt | 67 + .../viewmodels/SharedAssistantViewModel.kt | 59 + .../linphone/activities/call/CallActivity.kt | 205 +++ .../activities/call/IncomingCallActivity.kt | 103 ++ .../activities/call/OutgoingCallActivity.kt | 118 ++ .../activities/call/VideoZoomHelper.kt | 139 ++ .../call/fragments/ControlsFragment.kt | 107 ++ .../call/fragments/StatisticsFragment.kt | 52 + .../call/fragments/StatusFragment.kt | 143 ++ .../viewmodels/CallStatisticsViewModel.kt | 116 ++ .../call/viewmodels/CallViewModel.kt | 99 ++ .../call/viewmodels/CallsViewModel.kt | 177 ++ .../viewmodels/ControlsFadingViewModel.kt | 99 ++ .../call/viewmodels/ControlsViewModel.kt | 317 ++++ .../call/viewmodels/IncomingCallViewModel.kt | 57 + .../call/viewmodels/SharedCallViewModel.kt} | 12 +- .../viewmodels/SingleStatisticViewModel.kt | 76 + .../viewmodels/StatisticsListViewModel.kt | 66 + .../call/viewmodels/StatusViewModel.kt | 153 ++ .../views/AnswerDeclineIncomingCallButtons.kt | 134 ++ .../call/views/ConferenceCallView.kt | 67 + .../activities/call/views/PausedCallView.kt | 67 + .../activities/launcher/LauncherActivity.kt | 59 + .../linphone/activities/main/MainActivity.kt | 215 +++ .../activities/main/about/AboutFragment.kt | 109 ++ .../activities/main/about/AboutViewModel.kt | 77 + .../main/chat/ChatScrollListener.kt | 75 + .../main/chat/GroupChatRoomMember.kt | 30 + .../chat/adapters/ChatMessagesListAdapter.kt | 330 ++++ .../ChatRoomCreationContactsAdapter.kt | 123 ++ .../chat/adapters/ChatRoomsListAdapter.kt | 100 ++ .../adapters/GroupInfoParticipantsAdapter.kt | 96 + .../main/chat/adapters/ImdnAdapter.kt | 123 ++ .../fragments/ChatRoomCreationFragment.kt | 173 ++ .../chat/fragments/DetailChatRoomFragment.kt | 565 ++++++ .../main/chat/fragments/DevicesFragment.kt | 70 + .../main/chat/fragments/EphemeralFragment.kt | 75 + .../main/chat/fragments/GroupInfoFragment.kt | 194 ++ .../main/chat/fragments/ImdnFragment.kt | 112 ++ .../chat/fragments/MasterChatRoomsFragment.kt | 250 +++ .../ChatMessageAttachmentViewModel.kt} | 24 +- .../viewmodels/ChatMessageContentViewModel.kt | 89 + .../viewmodels/ChatMessageSendingViewModel.kt | 139 ++ .../chat/viewmodels/ChatMessageViewModel.kt | 235 +++ .../viewmodels/ChatMessagesListViewModel.kt | 221 +++ .../ChatRoomCreationContactViewModel.kt | 84 + .../viewmodels/ChatRoomCreationViewModel.kt | 202 +++ .../main/chat/viewmodels/ChatRoomViewModel.kt | 292 +++ .../chat/viewmodels/ChatRoomsListViewModel.kt | 152 ++ .../viewmodels/DevicesListChildViewModel.kt | 50 + .../viewmodels/DevicesListGroupViewModel.kt | 74 + .../chat/viewmodels/DevicesListViewModel.kt | 77 + .../viewmodels/EphemeralDurationViewModel.kt} | 30 +- .../chat/viewmodels/EphemeralViewModel.kt | 86 + .../main/chat/viewmodels/EventViewModel.kt | 111 ++ .../GroupInfoParticipantViewModel.kt | 51 + .../chat/viewmodels/GroupInfoViewModel.kt | 201 +++ .../viewmodels/ImdnParticipantViewModel.kt | 30 + .../main/chat/viewmodels/ImdnViewModel.kt | 70 + .../MultiLineWrapContentWidthTextView.kt | 70 + .../main/chat/views/RichEditText.kt | 79 + .../contact/adapters/ContactsListAdapter.kt | 125 ++ .../fragments/ContactEditorFragment.kt | 169 ++ .../fragments/DetailContactFragment.kt | 133 ++ .../fragments/MasterContactsFragment.kt | 255 +++ .../viewmodels/ContactEditorViewModel.kt | 183 ++ .../ContactNumberOrAddressViewModel.kt | 54 + .../contact/viewmodels/ContactViewModel.kt | 155 ++ .../viewmodels/ContactsListViewModel.kt | 113 ++ .../NumberOrAddressEditorViewModel.kt} | 24 +- .../main/dialer/NumpadDigitListener.kt} | 11 +- .../main/dialer/fragments/DialerFragment.kt | 97 + .../main/dialer/viewmodels/DialerViewModel.kt | 113 ++ .../main/fragments/EmptyFragment.kt} | 30 +- .../main/fragments/ListTopBarFragment.kt | 69 + .../main/fragments/MasterFragment.kt | 90 + .../main/fragments/StatusFragment.kt | 78 + .../activities/main/fragments/TabsFragment.kt | 113 ++ .../history/adapters/CallLogsListAdapter.kt | 143 ++ .../fragments/DetailCallLogFragment.kt | 134 ++ .../fragments/MasterCallLogsFragment.kt | 192 ++ .../history/viewmodels/CallLogViewModel.kt | 156 ++ .../viewmodels/CallLogsListViewModel.kt | 102 ++ .../adapters/RecordingsListAdapter.kt | 152 ++ .../fragments/RecordingsFragment.kt | 100 ++ .../viewmodels/RecordingViewModel.kt | 108 ++ .../viewmodels/RecordingsViewModel.kt | 61 + .../main/settings/SettingListener.kt} | 14 +- .../main/settings/SettingListenerStub.kt} | 16 +- .../fragments/AccountSettingsFragment.kt | 94 + .../fragments/AdvancedSettingsFragment.kt | 84 + .../fragments/AudioSettingsFragment.kt | 123 ++ .../fragments/CallSettingsFragment.kt | 78 + .../fragments/ChatSettingsFragment.kt | 57 + .../fragments/ContactsSettingsFragment.kt | 69 + .../fragments/NetworkSettingsFragment.kt | 57 + .../settings/fragments/SettingsFragment.kt | 225 +++ .../fragments/TunnelSettingsFragment.kt | 57 + .../fragments/VideoSettingsFragment.kt | 103 ++ .../viewmodels/AccountSettingsViewModel.kt | 310 ++++ .../viewmodels/AdvancedSettingsViewModel.kt | 117 ++ .../viewmodels/AudioSettingsViewModel.kt | 186 ++ .../viewmodels/CallSettingsViewModel.kt | 152 ++ .../viewmodels/ChatSettingsViewModel.kt | 60 + .../viewmodels/ContactsSettingsViewModel.kt | 63 + .../viewmodels/GenericSettingsViewModel.kt | 29 + .../viewmodels/NetworkSettingsViewModel.kt | 85 + .../settings/viewmodels/SettingsViewModel.kt | 71 + .../viewmodels/TunnelSettingsViewModel.kt} | 8 +- .../viewmodels/VideoSettingsViewModel.kt | 159 ++ .../sidemenu/fragments/SideMenuFragment.kt | 116 ++ .../sidemenu/viewmodels/SideMenuViewModel.kt | 109 ++ .../main/viewmodels/DialogViewModel.kt | 85 + .../viewmodels/ErrorReportingViewModel.kt} | 13 +- .../main/viewmodels/ListTopBarViewModel.kt | 82 + .../main/viewmodels/SharedMainViewModel.kt | 60 + .../main/viewmodels/StatusViewModel.kt | 111 ++ .../main/viewmodels/TabsViewModel.kt | 77 + .../AccountConnectionAssistantActivity.java | 286 --- .../linphone/assistant/AssistantActivity.java | 351 ---- .../linphone/assistant/CountryAdapter.java | 114 -- .../org/linphone/assistant/CountryPicker.java | 103 -- ...CancellerCalibrationAssistantActivity.java | 123 -- ...EmailAccountCreationAssistantActivity.java | 272 --- ...ailAccountValidationAssistantActivity.java | 103 -- .../GenericConnectionAssistantActivity.java | 109 -- .../assistant/MenuAssistantActivity.java | 262 --- ...PhoneAccountCreationAssistantActivity.java | 303 ---- .../PhoneAccountLinkingAssistantActivity.java | 308 ---- ...oneAccountValidationAssistantActivity.java | 197 --- .../QrCodeConfigurationAssistantActivity.java | 124 -- .../RemoteConfigurationAssistantActivity.java | 203 --- .../linphone/call/AndroidAudioManager.java | 274 --- .../org/linphone/call/BandwidthManager.java | 56 - .../java/org/linphone/call/CallActivity.java | 1211 ------------- .../linphone/call/CallIncomingActivity.java | 310 ---- .../java/org/linphone/call/CallManager.java | 378 ---- .../linphone/call/CallOutgoingActivity.java | 334 ---- .../org/linphone/call/CallStatsAdapter.java | 168 -- .../call/CallStatsChildViewHolder.java | 419 ----- .../org/linphone/call/CallStatsFragment.java | 126 -- .../linphone/call/CallStatusBarFragment.java | 441 ----- .../org/linphone/call/VideoZoomHelper.java | 165 -- .../org/linphone/call/views/CallButton.java | 94 - .../call/views/CallIncomingAnswerButton.java | 125 -- .../call/views/CallIncomingDeclineButton.java | 119 -- .../call/views/LinphoneGL2JNIViewOverlay.java | 184 -- .../views/LinphoneLinearLayoutManager.java | 61 - .../views/LinphoneTextureViewOverlay.java | 188 -- .../java/org/linphone/chat/ChatActivity.java | 390 ---- .../linphone/chat/ChatMessageViewHolder.java | 479 ----- .../ChatMessageViewHolderClickListener.java | 24 - .../linphone/chat/ChatMessagesAdapter.java | 452 ----- .../linphone/chat/ChatMessagesFragment.java | 1561 ----------------- .../chat/ChatMessagesGenericAdapter.java | 42 - .../chat/ChatRoomCreationFragment.java | 654 ------- .../org/linphone/chat/ChatRoomViewHolder.java | 200 --- .../org/linphone/chat/ChatRoomsAdapter.java | 137 -- .../org/linphone/chat/ChatRoomsFragment.java | 310 ---- .../org/linphone/chat/ChatScrollListener.java | 78 - .../linphone/chat/DeviceGroupViewHolder.java | 40 - .../org/linphone/chat/DevicesAdapter.java | 206 --- .../org/linphone/chat/DevicesFragment.java | 160 -- .../org/linphone/chat/EphemeralFragment.java | 162 -- .../org/linphone/chat/GroupInfoAdapter.java | 181 -- .../org/linphone/chat/GroupInfoFragment.java | 503 ------ .../linphone/chat/GroupInfoViewHolder.java | 46 - .../java/org/linphone/chat/ImdnFragment.java | 355 ---- .../compatibility/Api21Compatibility.kt | 154 ++ .../compatibility/Api23Compatibility.kt | 107 ++ .../compatibility/Api25Compatibility.kt | 59 + .../compatibility/Api26Compatibility.kt | 115 ++ .../compatibility/Api28Compatibility.kt | 42 + .../compatibility/Api29Compatibility.kt | 149 ++ .../compatibility/ApiTwentyEightPlus.java | 177 -- .../compatibility/ApiTwentyFivePlus.java | 110 -- .../compatibility/ApiTwentyFourPlus.java | 246 --- .../compatibility/ApiTwentyNinePlus.java | 125 -- .../compatibility/ApiTwentyOnePlus.java | 251 --- .../compatibility/ApiTwentySixPlus.java | 348 ---- .../compatibility/ApiTwentyThreePlus.java | 119 -- .../linphone/compatibility/Compatibility.java | 338 ---- .../linphone/compatibility/Compatibility.kt | 127 ++ .../CompatibilityScaleGestureDetector.java | 60 - .../linphone/contact/AsyncContactsLoader.kt | 260 +++ .../linphone/contact/BigContactAvatarView.kt | 71 + .../main/java/org/linphone/contact/Contact.kt | 161 ++ .../org/linphone/contact/ContactAvatarView.kt | 78 + .../org/linphone/contact/ContactsManager.kt | 260 +++ .../contact/DummyAuthenticationService.kt | 96 + .../org/linphone/contact/DummySyncService.kt | 56 + .../contact/GenericContactViewModel.kt | 68 + .../org/linphone/contact/NativeContact.kt | 223 +++ .../linphone/contact/NativeContactEditor.kt | 410 +++++ .../org/linphone/contact/ShortcutsHelper.kt | 123 ++ .../org/linphone/contacts/AndroidContact.java | 725 -------- .../contacts/AsyncContactPresence.java | 44 - .../contacts/AsyncContactsLoader.java | 300 ---- .../org/linphone/contacts/ContactAddress.java | 130 -- .../contacts/ContactDetailsFragment.java | 433 ----- .../contacts/ContactEditorFragment.java | 720 -------- .../linphone/contacts/ContactViewHolder.java | 77 - .../linphone/contacts/ContactsActivity.java | 239 --- .../linphone/contacts/ContactsAdapter.java | 172 -- .../linphone/contacts/ContactsFragment.java | 370 ---- .../linphone/contacts/ContactsManager.java | 595 ------- .../contacts/ContactsUpdatedListener.java | 24 - .../linphone/contacts/LinphoneContact.java | 673 ------- .../contacts/LinphoneNumberOrAddress.java | 96 - .../contacts/SearchContactViewHolder.java | 66 - .../contacts/SearchContactsAdapter.java | 275 --- .../contacts/views/ContactAvatar.java | 203 --- .../contacts/views/ContactAvatarHolder.java | 49 - .../contacts/views/ContactSelectView.java | 61 - .../java/org/linphone/core/BootReceiver.kt | 48 + .../java/org/linphone/core/CoreContext.kt | 439 +++++ .../java/org/linphone/core/CorePreferences.kt | 274 +++ .../CorePushReceiver.kt} | 21 +- .../java/org/linphone/core/CoreService.kt | 61 + .../org/linphone/dialer/DialerActivity.java | 448 ----- .../linphone/dialer/views/AddressText.java | 133 -- .../java/org/linphone/dialer/views/Digit.java | 226 --- .../linphone/dialer/views/EraseButton.java | 78 - .../linphone/firebase/FirebaseMessaging.java | 73 - .../linphone/firebase/FirebasePushHelper.java | 75 - .../linphone/fragments/StatusBarFragment.java | 234 --- .../org/linphone/history/HistoryActivity.java | 120 -- .../org/linphone/history/HistoryAdapter.java | 181 -- .../history/HistoryDetailFragment.java | 321 ---- .../org/linphone/history/HistoryFragment.java | 284 --- .../linphone/history/HistoryLogAdapter.java | 118 -- .../linphone/history/HistoryViewHolder.java | 77 - .../menu/SideMenuAccountsListAdapter.java | 126 -- .../org/linphone/menu/SideMenuAdapter.java | 75 - .../org/linphone/menu/SideMenuFragment.java | 260 --- .../linphone/notifications/Notifiable.java | 126 -- .../notifications/NotifiableMessage.java | 69 - .../NotificationBroadcastReceiver.java | 187 -- .../NotificationBroadcastReceiver.kt | 96 + .../notifications/NotificationsManager.java | 761 -------- .../notifications/NotificationsManager.kt | 700 ++++++++ .../receivers/AccountEnableReceiver.java | 47 - .../org/linphone/receivers/BootReceiver.java | 69 - .../org/linphone/recording/Recording.java | 163 -- .../linphone/recording/RecordingListener.java | 26 - .../recording/RecordingViewHolder.java | 83 - .../recording/RecordingsActivity.java | 242 --- .../linphone/recording/RecordingsAdapter.java | 244 --- .../org/linphone/service/ActivityMonitor.java | 140 -- .../org/linphone/service/LinphoneService.java | 182 -- .../linphone/service/ServiceWaitThread.java | 51 - .../service/ServiceWaitThreadListener.java | 24 - .../settings/AccountSettingsFragment.java | 700 -------- .../settings/AdvancedSettingsFragment.java | 235 --- .../settings/AudioSettingsFragment.java | 291 --- .../settings/CallSettingsFragment.java | 338 ---- .../settings/ChatSettingsFragment.java | 226 --- .../settings/ContactSettingsFragment.java | 140 -- .../settings/LinphonePreferences.java | 1334 -------------- .../settings/MenuSettingsFragment.java | 236 --- .../settings/NetworkSettingsFragment.java | 242 --- .../linphone/settings/SettingsActivity.java | 182 -- .../linphone/settings/SettingsFragment.java | 24 - .../settings/TunnelSettingsFragment.java | 152 -- .../settings/VideoSettingsFragment.java | 350 ---- .../settings/widget/BasicSetting.java | 127 -- .../settings/widget/CheckBoxSetting.java | 99 -- .../linphone/settings/widget/LedSetting.java | 81 - .../linphone/settings/widget/ListSetting.java | 143 -- .../settings/widget/SettingListenerBase.java | 30 - .../settings/widget/SwitchSetting.java | 99 -- .../linphone/settings/widget/TextSetting.java | 118 -- .../java/org/linphone/sync/Authenticator.java | 72 - .../java/org/linphone/sync/SyncAdapter.java | 42 - .../java/org/linphone/sync/SyncService.java | 44 - .../main/java/org/linphone/utils/AppUtils.kt | 129 ++ .../org/linphone/utils/DataBindingUtils.kt | 419 +++++ .../java/org/linphone/utils/DeviceUtils.java | 249 --- .../java/org/linphone/utils/DialogUtils.kt | 56 + app/src/main/java/org/linphone/utils/Event.kt | 41 + .../java/org/linphone/utils/FileUtils.java | 283 --- .../main/java/org/linphone/utils/FileUtils.kt | 223 +++ .../java/org/linphone/utils/ImageUtils.java | 99 -- .../java/org/linphone/utils/ImageUtils.kt | 86 + .../linphone/utils/LifecycleListAdapter.kt | 43 + .../org/linphone/utils/LifecycleViewHolder.kt | 50 + .../utils/LinphoneShortcutManager.java | 115 -- .../org/linphone/utils/LinphoneUtils.java | 392 ----- .../java/org/linphone/utils/LinphoneUtils.kt | 107 ++ .../java/org/linphone/utils/MediaScanner.java | 81 - .../linphone/utils/MediaScannerListener.java | 26 - .../org/linphone/utils/PermissionHelper.kt | 67 + .../org/linphone/utils/PhoneNumberUtils.kt | 69 + .../linphone/utils/PushNotificationUtils.java | 65 - .../utils/RecyclerViewHeaderDecoration.kt | 85 + .../linphone/utils/RecyclerViewSwipeUtils.kt | 279 +++ .../org/linphone/utils/SelectableAdapter.java | 113 -- .../org/linphone/utils/SelectableHelper.java | 197 --- .../org/linphone/utils/SingletonHolder.kt | 62 + .../java/org/linphone/utils/TimestampUtils.kt | 75 + .../org/linphone/views/MarqueeTextView.java | 60 - .../org/linphone/views/MarqueeTextView.kt | 52 + .../MultiLineWrapContentWidthTextView.java | 77 - .../java/org/linphone/views/RichEditText.java | 90 - .../main/res/anim/slide_in_bottom_to_top.xml | 10 - .../main/res/anim/slide_in_left_to_right.xml | 10 - .../main/res/anim/slide_in_right_to_left.xml | 10 - .../main/res/anim/slide_in_top_to_bottom.xml | 10 - .../main/res/anim/slide_out_bottom_to_top.xml | 10 - .../main/res/anim/slide_out_left_to_right.xml | 10 - .../main/res/anim/slide_out_right_to_left.xml | 10 - .../main/res/anim/slide_out_top_to_bottom.xml | 10 - app/src/main/res/drawable-xhdpi/.directory | 4 - .../res/drawable-xhdpi/add_field_default.png | Bin 478 -> 0 bytes .../main/res/drawable-xhdpi/avatar_mask.png | Bin 15535 -> 0 bytes .../res/drawable-xhdpi/chat_file_default.png | Bin 960 -> 0 bytes ...oup_add.png => chat_group_add_default.png} | Bin .../chat_group_informations_default.png | Bin 3297 -> 0 bytes .../res/drawable-xhdpi/chat_send_default.png | Bin 1175 -> 0 bytes .../main/res/drawable-xhdpi/chat_unsecure.png | Bin 298 -> 0 bytes .../drawable-xhdpi/clean_field_default.png | Bin 2272 -> 1951 bytes .../res/drawable-xhdpi/conference_start.png | Bin 5240 -> 0 bytes .../drawable-xhdpi/delete_field_default.png | Bin 1887 -> 0 bytes .../res/drawable-xhdpi/delete_field_over.png | Bin 1268 -> 0 bytes .../res/drawable-xhdpi/edit_list_default.png | Bin 2867 -> 0 bytes .../ephemeral_messages_default.png | Bin 9243 -> 8497 bytes .../ephemeral_messages_small_default.png | Bin 1553 -> 0 bytes app/src/main/res/drawable-xhdpi/field_add.png | Bin 0 -> 981 bytes .../main/res/drawable-xhdpi/field_remove.png | Bin 0 -> 736 bytes .../drawable-xhdpi/linphone_app_name_logo.png | Bin 0 -> 8422 bytes .../main/res/drawable-xhdpi/linphone_logo.png | Bin 20882 -> 15831 bytes .../drawable-xhdpi/linphone_logo_orange.png | Bin 9311 -> 0 bytes .../linphone_notification_icon.png | Bin 15831 -> 0 bytes .../main/res/drawable-xhdpi/linphone_user.png | Bin 2478 -> 0 bytes .../res/drawable-xhdpi/list_details_over.png | Bin 2962 -> 0 bytes .../main/res/drawable-xhdpi/next_default.png | Bin 1554 -> 0 bytes .../main/res/drawable-xhdpi/quit_default.png | Bin 3416 -> 11802 bytes app/src/main/res/drawable-xhdpi/quit_over.png | Bin 4389 -> 0 bytes .../main/res/drawable-xhdpi/record_pause.png | Bin 5820 -> 5179 bytes .../main/res/drawable-xhdpi/record_play.png | Bin 7888 -> 6663 bytes .../resizable_chat_bubble_incoming.9.png | Bin 101 -> 0 bytes .../resizable_chat_bubble_outgoing.9.png | Bin 101 -> 0 bytes .../resizable_confirm_delete_button.9.png | Bin 298 -> 0 bytes ...field.9.png => resizable_text_field.9.png} | Bin .../resizable_textfield_error.9.png | Bin 516 -> 0 bytes .../main/res/drawable-xhdpi/splashscreen.png | Bin 23756 -> 0 bytes .../main/res/drawable-xhdpi/topbar_avatar.png | Bin 4921 -> 0 bytes .../topbar_service_notification.png | Bin 0 -> 7443 bytes .../background_round_primary_color.xml | 5 + app/src/main/res/drawable/call_back.xml | 10 +- ...{hangup.xml => call_hangup_background.xml} | 0 app/src/main/res/drawable/chat_group_add.xml | 8 + .../res/drawable/chat_room_group_infos.xml | 15 - .../drawable/chat_send_ephemeral_message.xml | 11 - app/src/main/res/drawable/delete_field.xml | 7 - app/src/main/res/drawable/dialer.xml | 6 - app/src/main/res/drawable/edit_list.xml | 15 - .../main/res/drawable/ephemeral_messages.xml | 7 + ...ground.xml => field_button_background.xml} | 4 +- ...ml => field_button_background_default.xml} | 2 +- ...r.xml => field_button_background_over.xml} | 2 +- .../drawable/history_detail_background.xml | 5 + .../history_detail_background_pressed.xml | 4 + app/src/main/res/drawable/launch_screen.xml | 12 +- .../res/drawable/linphone_logo_tinted.xml | 7 + app/src/main/res/drawable/list_detail.xml | 8 +- app/src/main/res/drawable/next.xml | 15 - app/src/main/res/drawable/quit.xml | 5 - .../res/drawable/recording_play_pause.xml | 11 + app/src/main/res/drawable/status_level.xml | 7 - app/src/main/res/layout-land/about.xml | 162 -- .../main/res/layout-land/about_fragment.xml | 218 +++ .../assistant_email_account_creation.xml | 211 --- .../assistant_generic_connection.xml | 233 --- .../main/res/layout-land/assistant_menu.xml | 139 -- app/src/main/res/layout-land/call.xml | 175 -- .../layout-land/call_controls_fragment.xml | 175 ++ .../res/layout-land/call_statistics_cell.xml | 102 ++ .../main/res/layout-land/call_stats_child.xml | 325 ---- .../layout-land/chat_room_master_fragment.xml | 107 ++ .../layout-land/contact_master_fragment.xml | 180 ++ app/src/main/res/layout-land/dialer.xml | 118 -- .../main/res/layout-land/dialer_fragment.xml | 150 ++ .../main/res/layout-land/history_detail.xml | 174 -- .../layout-land/history_detail_fragment.xml | 209 +++ .../layout-land/history_master_fragment.xml | 142 ++ app/src/main/res/layout-land/main.xml | 231 --- .../main/res/layout-land/tabs_fragment.xml | 161 ++ .../chat_room_master_fragment.xml | 140 ++ .../contact_master_fragment.xml | 212 +++ .../main/res/layout-sw533dp-land/dialer.xml | 154 -- .../history_master_fragment.xml | 175 ++ app/src/main/res/layout-sw533dp-land/main.xml | 246 --- .../layout-sw533dp-land/settings_fragment.xml | 178 ++ app/src/main/res/layout-sw533dp/dialer.xml | 154 -- app/src/main/res/layout-sw533dp/main.xml | 245 --- app/src/main/res/layout/about.xml | 141 -- app/src/main/res/layout/about_fragment.xml | 222 +++ .../layout/assistant_account_connection.xml | 251 --- .../assistant_account_login_fragment.xml | 270 +++ .../main/res/layout/assistant_activity.xml | 31 + .../res/layout/assistant_country_list.xml | 59 - ....xml => assistant_country_picker_cell.xml} | 0 .../assistant_country_picker_fragment.xml | 69 + .../assistant_echo_canceller_calibration.xml | 50 - ...nt_echo_canceller_calibration_fragment.xml | 57 + .../assistant_email_account_creation.xml | 211 --- ...istant_email_account_creation_fragment.xml | 201 +++ .../assistant_email_account_validation.xml | 77 - ...tant_email_account_validation_fragment.xml | 88 + ...sistant_generic_account_login_fragment.xml | 240 +++ .../layout/assistant_generic_connection.xml | 233 --- app/src/main/res/layout/assistant_menu.xml | 139 -- .../assistant_openh264_codec_download.xml | 92 - .../assistant_phone_account_creation.xml | 173 -- ...istant_phone_account_creation_fragment.xml | 197 +++ .../assistant_phone_account_linking.xml | 155 -- ...sistant_phone_account_linking_fragment.xml | 187 ++ .../assistant_phone_account_validation.xml | 110 -- ...tant_phone_account_validation_fragment.xml | 121 ++ .../res/layout/assistant_qr_code_fragment.xml | 49 + ...assistant_qr_code_remote_configuration.xml | 40 - .../layout/assistant_remote_configuration.xml | 99 -- ...assistant_remote_provisioning_fragment.xml | 113 ++ ...ces.xml => assistant_top_bar_fragment.xml} | 46 +- app/src/main/res/layout/assistant_topbar.xml | 40 - .../res/layout/assistant_welcome_fragment.xml | 134 ++ app/src/main/res/layout/call.xml | 168 -- .../main/res/layout/call_active_header.xml | 28 - app/src/main/res/layout/call_activity.xml | 83 + app/src/main/res/layout/call_conference.xml | 79 + .../main/res/layout/call_conference_cell.xml | 78 +- .../res/layout/call_conference_header.xml | 51 - .../layout/call_conference_paused_cell.xml | 36 - .../res/layout/call_controls_fragment.xml | 261 +++ app/src/main/res/layout/call_inactive_row.xml | 49 - app/src/main/res/layout/call_incoming.xml | 100 -- .../res/layout/call_incoming_activity.xml | 92 + .../layout/call_incoming_answer_button.xml | 47 - .../call_incoming_answer_decline_buttons.xml | 121 ++ .../layout/call_incoming_decline_button.xml | 47 - .../call_incoming_notification_heads_up.xml | 1 + app/src/main/res/layout/call_outgoing.xml | 115 -- .../res/layout/call_outgoing_activity.xml | 131 ++ app/src/main/res/layout/call_overlay.xml | 15 + app/src/main/res/layout/call_paused.xml | 62 + .../main/res/layout/call_paused_by_remote.xml | 24 - app/src/main/res/layout/call_paused_cell.xml | 16 + .../main/res/layout/call_primary_buttons.xml | 125 +- .../res/layout/call_secondary_buttons.xml | 343 ++-- .../res/layout/call_single_statistic_cell.xml | 34 + .../main/res/layout/call_statistics_cell.xml | 83 + .../layout/call_statistics_cell_header.xml | 65 + .../res/layout/call_statistics_fragment.xml | 34 + app/src/main/res/layout/call_stats.xml | 15 - app/src/main/res/layout/call_stats_child.xml | 312 ---- app/src/main/res/layout/call_stats_group.xml | 54 - app/src/main/res/layout/call_status_bar.xml | 52 - .../main/res/layout/call_status_fragment.xml | 72 + app/src/main/res/layout/chat.xml | 221 --- app/src/main/res/layout/chat_bubble.xml | 227 --- .../main/res/layout/chat_bubble_content.xml | 47 - app/src/main/res/layout/chat_device_cell.xml | 29 - .../res/layout/chat_device_cell_as_group.xml | 27 - app/src/main/res/layout/chat_device_group.xml | 64 - app/src/main/res/layout/chat_ephemeral.xml | 89 - .../main/res/layout/chat_ephemeral_item.xml | 32 - .../main/res/layout/chat_event_list_cell.xml | 66 + app/src/main/res/layout/chat_imdn.xml | 168 -- app/src/main/res/layout/chat_infos.xml | 139 -- app/src/main/res/layout/chat_infos_cell.xml | 112 -- .../layout/chat_message_attachment_cell.xml | 61 + .../res/layout/chat_message_content_cell.xml | 64 + .../res/layout/chat_message_list_cell.xml | 205 +++ .../chat_room_creation_contact_cell.xml | 101 ++ ...te.xml => chat_room_creation_fragment.xml} | 170 +- ...at_room_creation_selected_contact_cell.xml | 42 + .../res/layout/chat_room_detail_fragment.xml | 268 +++ .../layout/chat_room_devices_child_cell.xml | 47 + .../res/layout/chat_room_devices_fragment.xml | 71 + .../layout/chat_room_devices_group_cell.xml | 108 ++ .../chat_room_ephemeral_duration_cell.xml | 48 + .../layout/chat_room_ephemeral_fragment.xml | 112 ++ .../layout/chat_room_group_info_fragment.xml | 168 ++ .../chat_room_group_info_participant_cell.xml | 142 ++ .../res/layout/chat_room_imdn_fragment.xml | 68 + ...ml => chat_room_imdn_participant_cell.xml} | 48 +- .../main/res/layout/chat_room_list_cell.xml | 131 ++ .../res/layout/chat_room_master_fragment.xml | 106 ++ app/src/main/res/layout/chatlist.xml | 90 - app/src/main/res/layout/chatlist_cell.xml | 115 -- app/src/main/res/layout/contact_avatar.xml | 131 +- .../main/res/layout/contact_avatar_100.xml | 50 - .../main/res/layout/contact_avatar_200.xml | 50 - .../main/res/layout/contact_avatar_big.xml | 69 + .../res/layout/contact_avatar_call_paused.xml | 50 - app/src/main/res/layout/contact_cell.xml | 110 -- ...ntrol_cell.xml => contact_detail_cell.xml} | 56 +- ...ontact.xml => contact_detail_fragment.xml} | 97 +- app/src/main/res/layout/contact_edit_cell.xml | 31 - ...t_edit.xml => contact_editor_fragment.xml} | 220 +-- app/src/main/res/layout/contact_list_cell.xml | 100 ++ .../res/layout/contact_master_fragment.xml | 178 ++ .../contact_number_address_editor_cell.xml | 46 + app/src/main/res/layout/contact_selected.xml | 28 - app/src/main/res/layout/contacts_list.xml | 158 -- app/src/main/res/layout/dialer.xml | 122 -- app/src/main/res/layout/dialer_fragment.xml | 144 ++ app/src/main/res/layout/dialog.xml | 323 ++-- app/src/main/res/layout/edit_list.xml | 55 - app/src/main/res/layout/empty_fragment.xml | 15 +- app/src/main/res/layout/file_upload_cell.xml | 27 - .../main/res/layout/generic_list_header.xml | 29 + app/src/main/res/layout/history.xml | 112 -- app/src/main/res/layout/history_cell.xml | 101 -- app/src/main/res/layout/history_detail.xml | 174 -- .../main/res/layout/history_detail_cell.xml | 68 +- .../history_detail_fragment.xml} | 133 +- app/src/main/res/layout/history_list_cell.xml | 106 ++ .../res/layout/history_master_fragment.xml | 142 ++ app/src/main/res/layout/image_upload_cell.xml | 22 - app/src/main/res/layout/imdn_list_header.xml | 52 + app/src/main/res/layout/in_app.xml | 60 - app/src/main/res/layout/in_app_list.xml | 15 - .../main/res/layout/in_app_purchase_item.xml | 48 - app/src/main/res/layout/in_app_store.xml | 89 - ...aunch_screen.xml => launcher_activity.xml} | 26 +- .../res/layout/list_edit_top_bar_fragment.xml | 80 + app/src/main/res/layout/main.xml | 226 --- app/src/main/res/layout/main_activity.xml | 71 + app/src/main/res/layout/numpad.xml | 181 +- ...rding_cell.xml => recording_list_cell.xml} | 89 +- .../main/res/layout/recordings_fragment.xml | 90 + app/src/main/res/layout/recordings_list.xml | 83 - .../main/res/layout/search_contact_cell.xml | 89 - app/src/main/res/layout/settings.xml | 90 - app/src/main/res/layout/settings_account.xml | 177 -- .../main/res/layout/settings_account_cell.xml | 62 + .../res/layout/settings_account_fragment.xml | 249 +++ app/src/main/res/layout/settings_advanced.xml | 99 -- .../res/layout/settings_advanced_fragment.xml | 131 ++ app/src/main/res/layout/settings_audio.xml | 78 - .../res/layout/settings_audio_fragment.xml | 140 ++ app/src/main/res/layout/settings_call.xml | 92 - .../res/layout/settings_call_fragment.xml | 159 ++ app/src/main/res/layout/settings_chat.xml | 71 - .../res/layout/settings_chat_fragment.xml | 103 ++ app/src/main/res/layout/settings_contact.xml | 41 - .../res/layout/settings_contacts_fragment.xml | 101 ++ app/src/main/res/layout/settings_fragment.xml | 150 ++ app/src/main/res/layout/settings_network.xml | 87 - .../res/layout/settings_network_fragment.xml | 112 ++ app/src/main/res/layout/settings_tunnel.xml | 59 - .../res/layout/settings_tunnel_fragment.xml | 72 + app/src/main/res/layout/settings_video.xml | 98 -- .../res/layout/settings_video_fragment.xml | 169 ++ .../main/res/layout/settings_widget_basic.xml | 75 +- .../res/layout/settings_widget_checkbox.xml | 51 - .../main/res/layout/settings_widget_led.xml | 52 - .../main/res/layout/settings_widget_list.xml | 92 +- .../res/layout/settings_widget_switch.xml | 107 +- .../main/res/layout/settings_widget_text.xml | 103 +- app/src/main/res/layout/side_menu.xml | 65 - .../res/layout/side_menu_account_cell.xml | 62 +- .../main/res/layout/side_menu_fragment.xml | 254 +++ .../main/res/layout/side_menu_item_cell.xml | 22 - .../res/layout/side_menu_main_account.xml | 47 - app/src/main/res/layout/status_bar.xml | 60 - app/src/main/res/layout/status_fragment.xml | 83 + app/src/main/res/layout/tabs_fragment.xml | 157 ++ app/src/main/res/layout/wait_layout.xml | 49 +- app/src/main/res/menu/chat_bubble_menu.xml | 24 - .../res/menu/chat_bubble_menu_with_resend.xml | 28 - app/src/main/res/menu/chat_message_menu.xml | 28 + .../linphone_launcher_icon_foreground.png | Bin 1832 -> 6534 bytes .../linphone_launcher_icon_foreground.png | Bin 1213 -> 6048 bytes .../linphone_launcher_icon_foreground.png | Bin 2532 -> 7517 bytes .../linphone_launcher_icon_foreground.png | Bin 3874 -> 9499 bytes .../linphone_launcher_icon_foreground.png | Bin 5406 -> 11701 bytes .../res/navigation-sw533dp/chat_nav_graph.xml | 117 ++ .../navigation-sw533dp/contacts_nav_graph.xml | 52 + .../navigation-sw533dp/history_nav_graph.xml | 31 + .../res/navigation/assistant_nav_graph.xml | 158 ++ .../main/res/navigation/chat_nav_graph.xml | 87 + .../res/navigation/contacts_nav_graph.xml | 27 + .../main/res/navigation/history_nav_graph.xml | 13 + .../main/res/navigation/main_nav_graph.xml | 393 +++++ .../res/navigation/settings_nav_graph.xml | 144 ++ app/src/main/res/raw/lpconfig.xsd | 60 - app/src/main/res/values-ar/strings.xml | 483 ----- app/src/main/res/values-cs/strings.xml | 257 --- app/src/main/res/values-de/strings.xml | 556 ------ app/src/main/res/values-es/strings.xml | 325 ---- app/src/main/res/values-fi/strings.xml | 305 ---- app/src/main/res/values-fr/strings.xml | 625 ------- app/src/main/res/values-hu/strings.xml | 623 ------- app/src/main/res/values-it/strings.xml | 623 ------- app/src/main/res/values-iw/strings.xml | 108 -- app/src/main/res/values-ja/strings.xml | 434 ----- app/src/main/res/values-ka/strings.xml | 619 ------- app/src/main/res/values-night/styles.xml | 11 +- app/src/main/res/values-nl/strings.xml | 226 --- app/src/main/res/values-pl/strings.xml | 417 ----- app/src/main/res/values-pt-rBR/strings.xml | 487 ----- app/src/main/res/values-ru/strings.xml | 625 ------- app/src/main/res/values-sr/strings.xml | 250 --- app/src/main/res/values-sv/strings.xml | 620 ------- ...{non_localizable_strings.xml => bools.xml} | 0 app/src/main/res/values-tr/strings.xml | 623 ------- app/src/main/res/values-uk/strings.xml | 625 ------- app/src/main/res/values-zh-rCN/strings.xml | 628 ------- app/src/main/res/values-zh-rTW/strings.xml | 519 ------ app/src/main/res/values/attrs.xml | 16 +- app/src/main/res/values/bools.xml | 4 + .../main/res/values/{color.xml => colors.xml} | 4 + app/src/main/res/values/digit_style.xml | 17 - app/src/main/res/values/dimen.xml | 7 + .../res/values/non_localizable_custom.xml | 151 -- .../res/values/non_localizable_strings.xml | 65 - app/src/main/res/values/slidingtab_style.xml | 30 - app/src/main/res/values/strings.xml | 1069 ++++++----- app/src/main/res/values/styles.xml | 233 +-- app/src/main/res/xml/authenticator.xml | 5 +- app/src/main/res/xml/provider_paths.xml | 1 + .../xml/{syncadapter.xml => sync_adapter.xml} | 2 +- build.gradle | 12 +- check_unused_resources.py | 30 - gradle.properties | 27 +- gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 55616 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 18 +- gradlew.bat | 18 +- sample/.gitignore | 13 - sample/app/.gitignore | 1 - sample/app/build.gradle | 36 - sample/app/proguard-rules.pro | 21 - sample/app/src/main/AndroidManifest.xml | 49 - .../org/linphone/sample/CallActivity.java | 165 -- .../sample/ConfigureAccountActivity.java | 113 -- .../org/linphone/sample/LauncherActivity.java | 70 - .../org/linphone/sample/LinphoneService.java | 239 --- .../org/linphone/sample/MainActivity.java | 183 -- sample/app/src/main/res/drawable/banner.png | Bin 12440 -> 0 bytes .../src/main/res/drawable/led_connected.png | Bin 1046 -> 0 bytes .../main/res/drawable/led_disconnected.png | Bin 904 -> 0 bytes .../app/src/main/res/drawable/led_error.png | Bin 840 -> 0 bytes .../src/main/res/drawable/led_inprogress.png | Bin 952 -> 0 bytes sample/app/src/main/res/layout/call.xml | 27 - .../src/main/res/layout/configure_account.xml | 71 - sample/app/src/main/res/layout/launcher.xml | 21 - sample/app/src/main/res/layout/main.xml | 53 - .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 2132 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 2343 -> 0 bytes .../linphone_launcher_icon_foreground.png | Bin 1832 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1417 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 1467 -> 0 bytes .../linphone_launcher_icon_foreground.png | Bin 1213 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 2880 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 3178 -> 0 bytes .../linphone_launcher_icon_foreground.png | Bin 2532 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 4524 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 5416 -> 0 bytes .../linphone_launcher_icon_foreground.png | Bin 3874 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 6221 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 8083 -> 0 bytes .../linphone_launcher_icon_foreground.png | Bin 5406 -> 0 bytes .../app/src/main/res/raw/linphonerc_default | 20 - .../app/src/main/res/raw/linphonerc_factory | 34 - sample/app/src/main/res/values/color.xml | 5 - sample/app/src/main/res/values/colors.xml | 6 - sample/app/src/main/res/values/strings.xml | 3 - sample/app/src/main/res/values/styles.xml | 6 - sample/build.gradle | 27 - sample/gradle.properties | 20 - sample/gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 - sample/gradlew | 172 -- sample/gradlew.bat | 84 - sample/settings.gradle | 1 - settings.gradle | 3 +- 727 files changed, 35466 insertions(+), 59669 deletions(-) mode change 100755 => 100644 app/src/main/AndroidManifest.xml rename app/src/main/{res/raw/default_assistant_create.rc => assets/assistant_default_values} (91%) rename app/src/main/{res/raw/linphone_assistant_create.rc => assets/assistant_linphone_default_values} (100%) rename app/src/main/{res/raw => assets}/linphonerc_default (88%) rename app/src/main/{res/raw => assets}/linphonerc_factory (92%) delete mode 100644 app/src/main/java/com/android/vending/billing/IInAppBillingService.aidl create mode 100644 app/src/main/java/org/linphone/LinphoneApplication.kt delete mode 100644 app/src/main/java/org/linphone/LinphoneContext.java delete mode 100644 app/src/main/java/org/linphone/LinphoneManager.java delete mode 100644 app/src/main/java/org/linphone/activities/AboutActivity.java create mode 100644 app/src/main/java/org/linphone/activities/GenericActivity.kt delete mode 100644 app/src/main/java/org/linphone/activities/LinphoneGenericActivity.java delete mode 100644 app/src/main/java/org/linphone/activities/LinphoneLauncherActivity.java delete mode 100644 app/src/main/java/org/linphone/activities/MainActivity.java rename app/src/main/java/org/linphone/{call/views/CallIncomingButtonListener.java => activities/SnackBarActivity.kt} (82%) delete mode 100644 app/src/main/java/org/linphone/activities/ThemeableActivity.java create mode 100644 app/src/main/java/org/linphone/activities/assistant/AssistantActivity.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/adapters/CountryPickerAdapter.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/fragments/AbstractPhoneFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/fragments/CountryPickerFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/fragments/EchoCancellerCalibrationFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountCreationFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountValidationFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountLoginFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/fragments/QrCodeFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/fragments/RemoteProvisioningFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/fragments/TopBarFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/fragments/WelcomeFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPhoneViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/viewmodels/EchoCancellerCalibrationViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountCreationViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountCreationViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountLinkingViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountValidationViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/viewmodels/QrCodeViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/viewmodels/RemoteProvisioningViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/assistant/viewmodels/SharedAssistantViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/call/CallActivity.kt create mode 100644 app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt create mode 100644 app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt create mode 100644 app/src/main/java/org/linphone/activities/call/VideoZoomHelper.kt create mode 100644 app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/call/fragments/StatisticsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/call/fragments/StatusFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/call/viewmodels/CallStatisticsViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/call/viewmodels/ControlsFadingViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt rename app/src/main/java/org/linphone/{compatibility/CompatibilityScaleGestureListener.java => activities/call/viewmodels/SharedCallViewModel.kt} (69%) create mode 100644 app/src/main/java/org/linphone/activities/call/viewmodels/SingleStatisticViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/call/viewmodels/StatisticsListViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/call/viewmodels/StatusViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/call/views/AnswerDeclineIncomingCallButtons.kt create mode 100644 app/src/main/java/org/linphone/activities/call/views/ConferenceCallView.kt create mode 100644 app/src/main/java/org/linphone/activities/call/views/PausedCallView.kt create mode 100644 app/src/main/java/org/linphone/activities/launcher/LauncherActivity.kt create mode 100644 app/src/main/java/org/linphone/activities/main/MainActivity.kt create mode 100644 app/src/main/java/org/linphone/activities/main/about/AboutFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/about/AboutViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/ChatScrollListener.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/GroupChatRoomMember.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomCreationContactsAdapter.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomsListAdapter.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/adapters/GroupInfoParticipantsAdapter.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/adapters/ImdnAdapter.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/fragments/DevicesFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt rename app/src/main/java/org/linphone/{chat/DeviceChildViewHolder.java => activities/main/chat/viewmodels/ChatMessageAttachmentViewModel.kt} (60%) create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageContentViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessagesListViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationContactViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListChildViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListGroupViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListViewModel.kt rename app/src/main/java/org/linphone/{call/CallStatsViewHolder.java => activities/main/chat/viewmodels/EphemeralDurationViewModel.kt} (57%) create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/EphemeralViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/EventViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoParticipantViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/ImdnParticipantViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/viewmodels/ImdnViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt create mode 100644 app/src/main/java/org/linphone/activities/main/chat/views/RichEditText.kt create mode 100644 app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt create mode 100644 app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactEditorViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactNumberOrAddressViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt rename app/src/main/java/org/linphone/{sync/AuthenticationService.java => activities/main/contact/viewmodels/NumberOrAddressEditorViewModel.kt} (61%) rename app/src/main/java/org/linphone/{call/CallActivityInterface.java => activities/main/dialer/NumpadDigitListener.kt} (77%) create mode 100644 app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt rename app/src/main/java/org/linphone/{fragments/EmptyFragment.java => activities/main/fragments/EmptyFragment.kt} (63%) create mode 100644 app/src/main/java/org/linphone/activities/main/fragments/ListTopBarFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/fragments/MasterFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/fragments/StatusFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/fragments/TabsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/history/adapters/CallLogsListAdapter.kt create mode 100644 app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogsListViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/recordings/adapters/RecordingsListAdapter.kt create mode 100644 app/src/main/java/org/linphone/activities/main/recordings/fragments/RecordingsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/recordings/viewmodels/RecordingViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/recordings/viewmodels/RecordingsViewModel.kt rename app/src/main/java/org/linphone/{settings/widget/SettingListener.java => activities/main/settings/SettingListener.kt} (71%) rename app/src/main/java/org/linphone/{call/views/LinphoneOverlay.java => activities/main/settings/SettingListenerStub.kt} (64%) create mode 100644 app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/fragments/AdvancedSettingsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/fragments/AudioSettingsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/fragments/CallSettingsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/fragments/ChatSettingsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/fragments/NetworkSettingsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/fragments/TunnelSettingsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/fragments/VideoSettingsFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/viewmodels/AdvancedSettingsViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/viewmodels/GenericSettingsViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/settings/viewmodels/SettingsViewModel.kt rename app/src/main/java/org/linphone/{dialer/views/AddressAware.java => activities/main/settings/viewmodels/TunnelSettingsViewModel.kt} (79%) create mode 100644 app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt create mode 100644 app/src/main/java/org/linphone/activities/main/sidemenu/viewmodels/SideMenuViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/viewmodels/DialogViewModel.kt rename app/src/main/java/org/linphone/{dialer/views/AddressType.java => activities/main/viewmodels/ErrorReportingViewModel.kt} (64%) create mode 100644 app/src/main/java/org/linphone/activities/main/viewmodels/ListTopBarViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/viewmodels/SharedMainViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/viewmodels/StatusViewModel.kt create mode 100644 app/src/main/java/org/linphone/activities/main/viewmodels/TabsViewModel.kt delete mode 100644 app/src/main/java/org/linphone/assistant/AccountConnectionAssistantActivity.java delete mode 100644 app/src/main/java/org/linphone/assistant/AssistantActivity.java delete mode 100644 app/src/main/java/org/linphone/assistant/CountryAdapter.java delete mode 100644 app/src/main/java/org/linphone/assistant/CountryPicker.java delete mode 100644 app/src/main/java/org/linphone/assistant/EchoCancellerCalibrationAssistantActivity.java delete mode 100644 app/src/main/java/org/linphone/assistant/EmailAccountCreationAssistantActivity.java delete mode 100644 app/src/main/java/org/linphone/assistant/EmailAccountValidationAssistantActivity.java delete mode 100644 app/src/main/java/org/linphone/assistant/GenericConnectionAssistantActivity.java delete mode 100644 app/src/main/java/org/linphone/assistant/MenuAssistantActivity.java delete mode 100644 app/src/main/java/org/linphone/assistant/PhoneAccountCreationAssistantActivity.java delete mode 100644 app/src/main/java/org/linphone/assistant/PhoneAccountLinkingAssistantActivity.java delete mode 100644 app/src/main/java/org/linphone/assistant/PhoneAccountValidationAssistantActivity.java delete mode 100644 app/src/main/java/org/linphone/assistant/QrCodeConfigurationAssistantActivity.java delete mode 100644 app/src/main/java/org/linphone/assistant/RemoteConfigurationAssistantActivity.java delete mode 100644 app/src/main/java/org/linphone/call/AndroidAudioManager.java delete mode 100644 app/src/main/java/org/linphone/call/BandwidthManager.java delete mode 100644 app/src/main/java/org/linphone/call/CallActivity.java delete mode 100644 app/src/main/java/org/linphone/call/CallIncomingActivity.java delete mode 100644 app/src/main/java/org/linphone/call/CallManager.java delete mode 100644 app/src/main/java/org/linphone/call/CallOutgoingActivity.java delete mode 100644 app/src/main/java/org/linphone/call/CallStatsAdapter.java delete mode 100644 app/src/main/java/org/linphone/call/CallStatsChildViewHolder.java delete mode 100644 app/src/main/java/org/linphone/call/CallStatsFragment.java delete mode 100644 app/src/main/java/org/linphone/call/CallStatusBarFragment.java delete mode 100644 app/src/main/java/org/linphone/call/VideoZoomHelper.java delete mode 100644 app/src/main/java/org/linphone/call/views/CallButton.java delete mode 100644 app/src/main/java/org/linphone/call/views/CallIncomingAnswerButton.java delete mode 100644 app/src/main/java/org/linphone/call/views/CallIncomingDeclineButton.java delete mode 100644 app/src/main/java/org/linphone/call/views/LinphoneGL2JNIViewOverlay.java delete mode 100644 app/src/main/java/org/linphone/call/views/LinphoneLinearLayoutManager.java delete mode 100644 app/src/main/java/org/linphone/call/views/LinphoneTextureViewOverlay.java delete mode 100644 app/src/main/java/org/linphone/chat/ChatActivity.java delete mode 100644 app/src/main/java/org/linphone/chat/ChatMessageViewHolder.java delete mode 100644 app/src/main/java/org/linphone/chat/ChatMessageViewHolderClickListener.java delete mode 100644 app/src/main/java/org/linphone/chat/ChatMessagesAdapter.java delete mode 100644 app/src/main/java/org/linphone/chat/ChatMessagesFragment.java delete mode 100644 app/src/main/java/org/linphone/chat/ChatMessagesGenericAdapter.java delete mode 100644 app/src/main/java/org/linphone/chat/ChatRoomCreationFragment.java delete mode 100644 app/src/main/java/org/linphone/chat/ChatRoomViewHolder.java delete mode 100644 app/src/main/java/org/linphone/chat/ChatRoomsAdapter.java delete mode 100644 app/src/main/java/org/linphone/chat/ChatRoomsFragment.java delete mode 100644 app/src/main/java/org/linphone/chat/ChatScrollListener.java delete mode 100644 app/src/main/java/org/linphone/chat/DeviceGroupViewHolder.java delete mode 100644 app/src/main/java/org/linphone/chat/DevicesAdapter.java delete mode 100644 app/src/main/java/org/linphone/chat/DevicesFragment.java delete mode 100644 app/src/main/java/org/linphone/chat/EphemeralFragment.java delete mode 100644 app/src/main/java/org/linphone/chat/GroupInfoAdapter.java delete mode 100644 app/src/main/java/org/linphone/chat/GroupInfoFragment.java delete mode 100644 app/src/main/java/org/linphone/chat/GroupInfoViewHolder.java delete mode 100644 app/src/main/java/org/linphone/chat/ImdnFragment.java create mode 100644 app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt create mode 100644 app/src/main/java/org/linphone/compatibility/Api23Compatibility.kt create mode 100644 app/src/main/java/org/linphone/compatibility/Api25Compatibility.kt create mode 100644 app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt create mode 100644 app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt create mode 100644 app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt delete mode 100644 app/src/main/java/org/linphone/compatibility/ApiTwentyEightPlus.java delete mode 100644 app/src/main/java/org/linphone/compatibility/ApiTwentyFivePlus.java delete mode 100644 app/src/main/java/org/linphone/compatibility/ApiTwentyFourPlus.java delete mode 100644 app/src/main/java/org/linphone/compatibility/ApiTwentyNinePlus.java delete mode 100644 app/src/main/java/org/linphone/compatibility/ApiTwentyOnePlus.java delete mode 100644 app/src/main/java/org/linphone/compatibility/ApiTwentySixPlus.java delete mode 100644 app/src/main/java/org/linphone/compatibility/ApiTwentyThreePlus.java delete mode 100644 app/src/main/java/org/linphone/compatibility/Compatibility.java create mode 100644 app/src/main/java/org/linphone/compatibility/Compatibility.kt delete mode 100644 app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureDetector.java create mode 100644 app/src/main/java/org/linphone/contact/AsyncContactsLoader.kt create mode 100644 app/src/main/java/org/linphone/contact/BigContactAvatarView.kt create mode 100644 app/src/main/java/org/linphone/contact/Contact.kt create mode 100644 app/src/main/java/org/linphone/contact/ContactAvatarView.kt create mode 100644 app/src/main/java/org/linphone/contact/ContactsManager.kt create mode 100644 app/src/main/java/org/linphone/contact/DummyAuthenticationService.kt create mode 100644 app/src/main/java/org/linphone/contact/DummySyncService.kt create mode 100644 app/src/main/java/org/linphone/contact/GenericContactViewModel.kt create mode 100644 app/src/main/java/org/linphone/contact/NativeContact.kt create mode 100644 app/src/main/java/org/linphone/contact/NativeContactEditor.kt create mode 100644 app/src/main/java/org/linphone/contact/ShortcutsHelper.kt delete mode 100644 app/src/main/java/org/linphone/contacts/AndroidContact.java delete mode 100644 app/src/main/java/org/linphone/contacts/AsyncContactPresence.java delete mode 100644 app/src/main/java/org/linphone/contacts/AsyncContactsLoader.java delete mode 100644 app/src/main/java/org/linphone/contacts/ContactAddress.java delete mode 100644 app/src/main/java/org/linphone/contacts/ContactDetailsFragment.java delete mode 100644 app/src/main/java/org/linphone/contacts/ContactEditorFragment.java delete mode 100644 app/src/main/java/org/linphone/contacts/ContactViewHolder.java delete mode 100644 app/src/main/java/org/linphone/contacts/ContactsActivity.java delete mode 100644 app/src/main/java/org/linphone/contacts/ContactsAdapter.java delete mode 100644 app/src/main/java/org/linphone/contacts/ContactsFragment.java delete mode 100644 app/src/main/java/org/linphone/contacts/ContactsManager.java delete mode 100644 app/src/main/java/org/linphone/contacts/ContactsUpdatedListener.java delete mode 100644 app/src/main/java/org/linphone/contacts/LinphoneContact.java delete mode 100644 app/src/main/java/org/linphone/contacts/LinphoneNumberOrAddress.java delete mode 100644 app/src/main/java/org/linphone/contacts/SearchContactViewHolder.java delete mode 100644 app/src/main/java/org/linphone/contacts/SearchContactsAdapter.java delete mode 100644 app/src/main/java/org/linphone/contacts/views/ContactAvatar.java delete mode 100644 app/src/main/java/org/linphone/contacts/views/ContactAvatarHolder.java delete mode 100644 app/src/main/java/org/linphone/contacts/views/ContactSelectView.java create mode 100644 app/src/main/java/org/linphone/core/BootReceiver.kt create mode 100644 app/src/main/java/org/linphone/core/CoreContext.kt create mode 100644 app/src/main/java/org/linphone/core/CorePreferences.kt rename app/src/main/java/org/linphone/{menu/SideMenuItem.java => core/CorePushReceiver.kt} (65%) create mode 100644 app/src/main/java/org/linphone/core/CoreService.kt delete mode 100644 app/src/main/java/org/linphone/dialer/DialerActivity.java delete mode 100644 app/src/main/java/org/linphone/dialer/views/AddressText.java delete mode 100644 app/src/main/java/org/linphone/dialer/views/Digit.java delete mode 100644 app/src/main/java/org/linphone/dialer/views/EraseButton.java delete mode 100644 app/src/main/java/org/linphone/firebase/FirebaseMessaging.java delete mode 100644 app/src/main/java/org/linphone/firebase/FirebasePushHelper.java delete mode 100644 app/src/main/java/org/linphone/fragments/StatusBarFragment.java delete mode 100644 app/src/main/java/org/linphone/history/HistoryActivity.java delete mode 100644 app/src/main/java/org/linphone/history/HistoryAdapter.java delete mode 100644 app/src/main/java/org/linphone/history/HistoryDetailFragment.java delete mode 100644 app/src/main/java/org/linphone/history/HistoryFragment.java delete mode 100644 app/src/main/java/org/linphone/history/HistoryLogAdapter.java delete mode 100644 app/src/main/java/org/linphone/history/HistoryViewHolder.java delete mode 100644 app/src/main/java/org/linphone/menu/SideMenuAccountsListAdapter.java delete mode 100644 app/src/main/java/org/linphone/menu/SideMenuAdapter.java delete mode 100644 app/src/main/java/org/linphone/menu/SideMenuFragment.java delete mode 100644 app/src/main/java/org/linphone/notifications/Notifiable.java delete mode 100644 app/src/main/java/org/linphone/notifications/NotifiableMessage.java delete mode 100644 app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.java create mode 100644 app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt delete mode 100644 app/src/main/java/org/linphone/notifications/NotificationsManager.java create mode 100644 app/src/main/java/org/linphone/notifications/NotificationsManager.kt delete mode 100644 app/src/main/java/org/linphone/receivers/AccountEnableReceiver.java delete mode 100644 app/src/main/java/org/linphone/receivers/BootReceiver.java delete mode 100644 app/src/main/java/org/linphone/recording/Recording.java delete mode 100644 app/src/main/java/org/linphone/recording/RecordingListener.java delete mode 100644 app/src/main/java/org/linphone/recording/RecordingViewHolder.java delete mode 100644 app/src/main/java/org/linphone/recording/RecordingsActivity.java delete mode 100644 app/src/main/java/org/linphone/recording/RecordingsAdapter.java delete mode 100644 app/src/main/java/org/linphone/service/ActivityMonitor.java delete mode 100644 app/src/main/java/org/linphone/service/LinphoneService.java delete mode 100644 app/src/main/java/org/linphone/service/ServiceWaitThread.java delete mode 100644 app/src/main/java/org/linphone/service/ServiceWaitThreadListener.java delete mode 100644 app/src/main/java/org/linphone/settings/AccountSettingsFragment.java delete mode 100644 app/src/main/java/org/linphone/settings/AdvancedSettingsFragment.java delete mode 100644 app/src/main/java/org/linphone/settings/AudioSettingsFragment.java delete mode 100644 app/src/main/java/org/linphone/settings/CallSettingsFragment.java delete mode 100644 app/src/main/java/org/linphone/settings/ChatSettingsFragment.java delete mode 100644 app/src/main/java/org/linphone/settings/ContactSettingsFragment.java delete mode 100644 app/src/main/java/org/linphone/settings/LinphonePreferences.java delete mode 100644 app/src/main/java/org/linphone/settings/MenuSettingsFragment.java delete mode 100644 app/src/main/java/org/linphone/settings/NetworkSettingsFragment.java delete mode 100644 app/src/main/java/org/linphone/settings/SettingsActivity.java delete mode 100644 app/src/main/java/org/linphone/settings/SettingsFragment.java delete mode 100644 app/src/main/java/org/linphone/settings/TunnelSettingsFragment.java delete mode 100644 app/src/main/java/org/linphone/settings/VideoSettingsFragment.java delete mode 100644 app/src/main/java/org/linphone/settings/widget/BasicSetting.java delete mode 100644 app/src/main/java/org/linphone/settings/widget/CheckBoxSetting.java delete mode 100644 app/src/main/java/org/linphone/settings/widget/LedSetting.java delete mode 100644 app/src/main/java/org/linphone/settings/widget/ListSetting.java delete mode 100644 app/src/main/java/org/linphone/settings/widget/SettingListenerBase.java delete mode 100644 app/src/main/java/org/linphone/settings/widget/SwitchSetting.java delete mode 100644 app/src/main/java/org/linphone/settings/widget/TextSetting.java delete mode 100644 app/src/main/java/org/linphone/sync/Authenticator.java delete mode 100755 app/src/main/java/org/linphone/sync/SyncAdapter.java delete mode 100755 app/src/main/java/org/linphone/sync/SyncService.java create mode 100644 app/src/main/java/org/linphone/utils/AppUtils.kt create mode 100644 app/src/main/java/org/linphone/utils/DataBindingUtils.kt delete mode 100644 app/src/main/java/org/linphone/utils/DeviceUtils.java create mode 100644 app/src/main/java/org/linphone/utils/DialogUtils.kt create mode 100644 app/src/main/java/org/linphone/utils/Event.kt delete mode 100644 app/src/main/java/org/linphone/utils/FileUtils.java create mode 100644 app/src/main/java/org/linphone/utils/FileUtils.kt delete mode 100644 app/src/main/java/org/linphone/utils/ImageUtils.java create mode 100644 app/src/main/java/org/linphone/utils/ImageUtils.kt create mode 100644 app/src/main/java/org/linphone/utils/LifecycleListAdapter.kt create mode 100644 app/src/main/java/org/linphone/utils/LifecycleViewHolder.kt delete mode 100644 app/src/main/java/org/linphone/utils/LinphoneShortcutManager.java delete mode 100644 app/src/main/java/org/linphone/utils/LinphoneUtils.java create mode 100644 app/src/main/java/org/linphone/utils/LinphoneUtils.kt delete mode 100644 app/src/main/java/org/linphone/utils/MediaScanner.java delete mode 100644 app/src/main/java/org/linphone/utils/MediaScannerListener.java create mode 100644 app/src/main/java/org/linphone/utils/PermissionHelper.kt create mode 100644 app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt delete mode 100644 app/src/main/java/org/linphone/utils/PushNotificationUtils.java create mode 100644 app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt create mode 100644 app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt delete mode 100644 app/src/main/java/org/linphone/utils/SelectableAdapter.java delete mode 100644 app/src/main/java/org/linphone/utils/SelectableHelper.java create mode 100644 app/src/main/java/org/linphone/utils/SingletonHolder.kt create mode 100644 app/src/main/java/org/linphone/utils/TimestampUtils.kt delete mode 100644 app/src/main/java/org/linphone/views/MarqueeTextView.java create mode 100644 app/src/main/java/org/linphone/views/MarqueeTextView.kt delete mode 100644 app/src/main/java/org/linphone/views/MultiLineWrapContentWidthTextView.java delete mode 100644 app/src/main/java/org/linphone/views/RichEditText.java delete mode 100644 app/src/main/res/anim/slide_in_bottom_to_top.xml delete mode 100644 app/src/main/res/anim/slide_in_left_to_right.xml delete mode 100644 app/src/main/res/anim/slide_in_right_to_left.xml delete mode 100644 app/src/main/res/anim/slide_in_top_to_bottom.xml delete mode 100644 app/src/main/res/anim/slide_out_bottom_to_top.xml delete mode 100644 app/src/main/res/anim/slide_out_left_to_right.xml delete mode 100644 app/src/main/res/anim/slide_out_right_to_left.xml delete mode 100644 app/src/main/res/anim/slide_out_top_to_bottom.xml delete mode 100644 app/src/main/res/drawable-xhdpi/.directory delete mode 100644 app/src/main/res/drawable-xhdpi/add_field_default.png delete mode 100644 app/src/main/res/drawable-xhdpi/avatar_mask.png delete mode 100644 app/src/main/res/drawable-xhdpi/chat_file_default.png rename app/src/main/res/drawable-xhdpi/{chat_group_add.png => chat_group_add_default.png} (100%) delete mode 100644 app/src/main/res/drawable-xhdpi/chat_group_informations_default.png delete mode 100644 app/src/main/res/drawable-xhdpi/chat_send_default.png delete mode 100644 app/src/main/res/drawable-xhdpi/chat_unsecure.png delete mode 100644 app/src/main/res/drawable-xhdpi/conference_start.png delete mode 100644 app/src/main/res/drawable-xhdpi/delete_field_default.png delete mode 100644 app/src/main/res/drawable-xhdpi/delete_field_over.png delete mode 100644 app/src/main/res/drawable-xhdpi/edit_list_default.png delete mode 100644 app/src/main/res/drawable-xhdpi/ephemeral_messages_small_default.png create mode 100644 app/src/main/res/drawable-xhdpi/field_add.png create mode 100644 app/src/main/res/drawable-xhdpi/field_remove.png create mode 100644 app/src/main/res/drawable-xhdpi/linphone_app_name_logo.png delete mode 100644 app/src/main/res/drawable-xhdpi/linphone_logo_orange.png delete mode 100644 app/src/main/res/drawable-xhdpi/linphone_notification_icon.png delete mode 100644 app/src/main/res/drawable-xhdpi/linphone_user.png delete mode 100644 app/src/main/res/drawable-xhdpi/list_details_over.png delete mode 100644 app/src/main/res/drawable-xhdpi/next_default.png delete mode 100644 app/src/main/res/drawable-xhdpi/quit_over.png delete mode 100644 app/src/main/res/drawable-xhdpi/resizable_chat_bubble_incoming.9.png delete mode 100644 app/src/main/res/drawable-xhdpi/resizable_chat_bubble_outgoing.9.png delete mode 100644 app/src/main/res/drawable-xhdpi/resizable_confirm_delete_button.9.png rename app/src/main/res/drawable-xhdpi/{resizable_textfield.9.png => resizable_text_field.9.png} (100%) delete mode 100644 app/src/main/res/drawable-xhdpi/resizable_textfield_error.9.png delete mode 100644 app/src/main/res/drawable-xhdpi/splashscreen.png delete mode 100644 app/src/main/res/drawable-xhdpi/topbar_avatar.png create mode 100644 app/src/main/res/drawable-xhdpi/topbar_service_notification.png create mode 100644 app/src/main/res/drawable/background_round_primary_color.xml rename app/src/main/res/drawable/{hangup.xml => call_hangup_background.xml} (100%) create mode 100644 app/src/main/res/drawable/chat_group_add.xml delete mode 100644 app/src/main/res/drawable/chat_room_group_infos.xml delete mode 100644 app/src/main/res/drawable/chat_send_ephemeral_message.xml delete mode 100644 app/src/main/res/drawable/delete_field.xml delete mode 100644 app/src/main/res/drawable/dialer.xml delete mode 100644 app/src/main/res/drawable/edit_list.xml create mode 100644 app/src/main/res/drawable/ephemeral_messages.xml rename app/src/main/res/drawable/{round_button_background.xml => field_button_background.xml} (56%) rename app/src/main/res/drawable/{round_button_background_default.xml => field_button_background_default.xml} (79%) rename app/src/main/res/drawable/{round_button_background_over.xml => field_button_background_over.xml} (79%) create mode 100644 app/src/main/res/drawable/history_detail_background.xml create mode 100644 app/src/main/res/drawable/history_detail_background_pressed.xml create mode 100644 app/src/main/res/drawable/linphone_logo_tinted.xml delete mode 100644 app/src/main/res/drawable/next.xml delete mode 100644 app/src/main/res/drawable/quit.xml create mode 100644 app/src/main/res/drawable/recording_play_pause.xml delete mode 100644 app/src/main/res/drawable/status_level.xml delete mode 100644 app/src/main/res/layout-land/about.xml create mode 100644 app/src/main/res/layout-land/about_fragment.xml delete mode 100644 app/src/main/res/layout-land/assistant_email_account_creation.xml delete mode 100644 app/src/main/res/layout-land/assistant_generic_connection.xml delete mode 100644 app/src/main/res/layout-land/assistant_menu.xml delete mode 100644 app/src/main/res/layout-land/call.xml create mode 100644 app/src/main/res/layout-land/call_controls_fragment.xml create mode 100644 app/src/main/res/layout-land/call_statistics_cell.xml delete mode 100644 app/src/main/res/layout-land/call_stats_child.xml create mode 100644 app/src/main/res/layout-land/chat_room_master_fragment.xml create mode 100644 app/src/main/res/layout-land/contact_master_fragment.xml delete mode 100644 app/src/main/res/layout-land/dialer.xml create mode 100644 app/src/main/res/layout-land/dialer_fragment.xml delete mode 100644 app/src/main/res/layout-land/history_detail.xml create mode 100644 app/src/main/res/layout-land/history_detail_fragment.xml create mode 100644 app/src/main/res/layout-land/history_master_fragment.xml delete mode 100644 app/src/main/res/layout-land/main.xml create mode 100644 app/src/main/res/layout-land/tabs_fragment.xml create mode 100644 app/src/main/res/layout-sw533dp-land/chat_room_master_fragment.xml create mode 100644 app/src/main/res/layout-sw533dp-land/contact_master_fragment.xml delete mode 100644 app/src/main/res/layout-sw533dp-land/dialer.xml create mode 100644 app/src/main/res/layout-sw533dp-land/history_master_fragment.xml delete mode 100644 app/src/main/res/layout-sw533dp-land/main.xml create mode 100644 app/src/main/res/layout-sw533dp-land/settings_fragment.xml delete mode 100644 app/src/main/res/layout-sw533dp/dialer.xml delete mode 100644 app/src/main/res/layout-sw533dp/main.xml delete mode 100644 app/src/main/res/layout/about.xml create mode 100644 app/src/main/res/layout/about_fragment.xml delete mode 100644 app/src/main/res/layout/assistant_account_connection.xml create mode 100644 app/src/main/res/layout/assistant_account_login_fragment.xml create mode 100644 app/src/main/res/layout/assistant_activity.xml delete mode 100644 app/src/main/res/layout/assistant_country_list.xml rename app/src/main/res/layout/{assistant_country_cell.xml => assistant_country_picker_cell.xml} (100%) create mode 100644 app/src/main/res/layout/assistant_country_picker_fragment.xml delete mode 100644 app/src/main/res/layout/assistant_echo_canceller_calibration.xml create mode 100644 app/src/main/res/layout/assistant_echo_canceller_calibration_fragment.xml delete mode 100644 app/src/main/res/layout/assistant_email_account_creation.xml create mode 100644 app/src/main/res/layout/assistant_email_account_creation_fragment.xml delete mode 100644 app/src/main/res/layout/assistant_email_account_validation.xml create mode 100644 app/src/main/res/layout/assistant_email_account_validation_fragment.xml create mode 100644 app/src/main/res/layout/assistant_generic_account_login_fragment.xml delete mode 100644 app/src/main/res/layout/assistant_generic_connection.xml delete mode 100644 app/src/main/res/layout/assistant_menu.xml delete mode 100644 app/src/main/res/layout/assistant_openh264_codec_download.xml delete mode 100644 app/src/main/res/layout/assistant_phone_account_creation.xml create mode 100644 app/src/main/res/layout/assistant_phone_account_creation_fragment.xml delete mode 100644 app/src/main/res/layout/assistant_phone_account_linking.xml create mode 100644 app/src/main/res/layout/assistant_phone_account_linking_fragment.xml delete mode 100644 app/src/main/res/layout/assistant_phone_account_validation.xml create mode 100644 app/src/main/res/layout/assistant_phone_account_validation_fragment.xml create mode 100644 app/src/main/res/layout/assistant_qr_code_fragment.xml delete mode 100644 app/src/main/res/layout/assistant_qr_code_remote_configuration.xml delete mode 100644 app/src/main/res/layout/assistant_remote_configuration.xml create mode 100644 app/src/main/res/layout/assistant_remote_provisioning_fragment.xml rename app/src/main/res/layout/{chat_devices.xml => assistant_top_bar_fragment.xml} (52%) delete mode 100644 app/src/main/res/layout/assistant_topbar.xml create mode 100644 app/src/main/res/layout/assistant_welcome_fragment.xml delete mode 100644 app/src/main/res/layout/call.xml delete mode 100644 app/src/main/res/layout/call_active_header.xml create mode 100644 app/src/main/res/layout/call_activity.xml create mode 100644 app/src/main/res/layout/call_conference.xml delete mode 100644 app/src/main/res/layout/call_conference_header.xml delete mode 100644 app/src/main/res/layout/call_conference_paused_cell.xml create mode 100644 app/src/main/res/layout/call_controls_fragment.xml delete mode 100644 app/src/main/res/layout/call_inactive_row.xml delete mode 100644 app/src/main/res/layout/call_incoming.xml create mode 100644 app/src/main/res/layout/call_incoming_activity.xml delete mode 100644 app/src/main/res/layout/call_incoming_answer_button.xml create mode 100644 app/src/main/res/layout/call_incoming_answer_decline_buttons.xml delete mode 100644 app/src/main/res/layout/call_incoming_decline_button.xml delete mode 100644 app/src/main/res/layout/call_outgoing.xml create mode 100644 app/src/main/res/layout/call_outgoing_activity.xml create mode 100644 app/src/main/res/layout/call_overlay.xml create mode 100644 app/src/main/res/layout/call_paused.xml delete mode 100644 app/src/main/res/layout/call_paused_by_remote.xml create mode 100644 app/src/main/res/layout/call_paused_cell.xml create mode 100644 app/src/main/res/layout/call_single_statistic_cell.xml create mode 100644 app/src/main/res/layout/call_statistics_cell.xml create mode 100644 app/src/main/res/layout/call_statistics_cell_header.xml create mode 100644 app/src/main/res/layout/call_statistics_fragment.xml delete mode 100644 app/src/main/res/layout/call_stats.xml delete mode 100644 app/src/main/res/layout/call_stats_child.xml delete mode 100644 app/src/main/res/layout/call_stats_group.xml delete mode 100644 app/src/main/res/layout/call_status_bar.xml create mode 100644 app/src/main/res/layout/call_status_fragment.xml delete mode 100644 app/src/main/res/layout/chat.xml delete mode 100644 app/src/main/res/layout/chat_bubble.xml delete mode 100644 app/src/main/res/layout/chat_bubble_content.xml delete mode 100644 app/src/main/res/layout/chat_device_cell.xml delete mode 100644 app/src/main/res/layout/chat_device_cell_as_group.xml delete mode 100644 app/src/main/res/layout/chat_device_group.xml delete mode 100644 app/src/main/res/layout/chat_ephemeral.xml delete mode 100644 app/src/main/res/layout/chat_ephemeral_item.xml create mode 100644 app/src/main/res/layout/chat_event_list_cell.xml delete mode 100644 app/src/main/res/layout/chat_imdn.xml delete mode 100644 app/src/main/res/layout/chat_infos.xml delete mode 100644 app/src/main/res/layout/chat_infos_cell.xml create mode 100644 app/src/main/res/layout/chat_message_attachment_cell.xml create mode 100644 app/src/main/res/layout/chat_message_content_cell.xml create mode 100644 app/src/main/res/layout/chat_message_list_cell.xml create mode 100644 app/src/main/res/layout/chat_room_creation_contact_cell.xml rename app/src/main/res/layout/{chat_create.xml => chat_room_creation_fragment.xml} (52%) create mode 100644 app/src/main/res/layout/chat_room_creation_selected_contact_cell.xml create mode 100644 app/src/main/res/layout/chat_room_detail_fragment.xml create mode 100644 app/src/main/res/layout/chat_room_devices_child_cell.xml create mode 100644 app/src/main/res/layout/chat_room_devices_fragment.xml create mode 100644 app/src/main/res/layout/chat_room_devices_group_cell.xml create mode 100644 app/src/main/res/layout/chat_room_ephemeral_duration_cell.xml create mode 100644 app/src/main/res/layout/chat_room_ephemeral_fragment.xml create mode 100644 app/src/main/res/layout/chat_room_group_info_fragment.xml create mode 100644 app/src/main/res/layout/chat_room_group_info_participant_cell.xml create mode 100644 app/src/main/res/layout/chat_room_imdn_fragment.xml rename app/src/main/res/layout/{chat_imdn_cell.xml => chat_room_imdn_participant_cell.xml} (63%) create mode 100644 app/src/main/res/layout/chat_room_list_cell.xml create mode 100644 app/src/main/res/layout/chat_room_master_fragment.xml delete mode 100644 app/src/main/res/layout/chatlist.xml delete mode 100644 app/src/main/res/layout/chatlist_cell.xml delete mode 100644 app/src/main/res/layout/contact_avatar_100.xml delete mode 100644 app/src/main/res/layout/contact_avatar_200.xml create mode 100644 app/src/main/res/layout/contact_avatar_big.xml delete mode 100644 app/src/main/res/layout/contact_avatar_call_paused.xml delete mode 100644 app/src/main/res/layout/contact_cell.xml rename app/src/main/res/layout/{contact_control_cell.xml => contact_detail_cell.xml} (70%) rename app/src/main/res/layout/{contact.xml => contact_detail_fragment.xml} (56%) delete mode 100644 app/src/main/res/layout/contact_edit_cell.xml rename app/src/main/res/layout/{contact_edit.xml => contact_editor_fragment.xml} (52%) create mode 100644 app/src/main/res/layout/contact_list_cell.xml create mode 100644 app/src/main/res/layout/contact_master_fragment.xml create mode 100644 app/src/main/res/layout/contact_number_address_editor_cell.xml delete mode 100644 app/src/main/res/layout/contact_selected.xml delete mode 100644 app/src/main/res/layout/contacts_list.xml delete mode 100644 app/src/main/res/layout/dialer.xml create mode 100644 app/src/main/res/layout/dialer_fragment.xml delete mode 100644 app/src/main/res/layout/edit_list.xml delete mode 100644 app/src/main/res/layout/file_upload_cell.xml create mode 100644 app/src/main/res/layout/generic_list_header.xml delete mode 100644 app/src/main/res/layout/history.xml delete mode 100644 app/src/main/res/layout/history_cell.xml delete mode 100644 app/src/main/res/layout/history_detail.xml rename app/src/main/res/{layout-sw533dp/history_detail.xml => layout/history_detail_fragment.xml} (58%) create mode 100644 app/src/main/res/layout/history_list_cell.xml create mode 100644 app/src/main/res/layout/history_master_fragment.xml delete mode 100644 app/src/main/res/layout/image_upload_cell.xml create mode 100644 app/src/main/res/layout/imdn_list_header.xml delete mode 100644 app/src/main/res/layout/in_app.xml delete mode 100644 app/src/main/res/layout/in_app_list.xml delete mode 100644 app/src/main/res/layout/in_app_purchase_item.xml delete mode 100644 app/src/main/res/layout/in_app_store.xml rename app/src/main/res/layout/{launch_screen.xml => launcher_activity.xml} (61%) create mode 100644 app/src/main/res/layout/list_edit_top_bar_fragment.xml delete mode 100644 app/src/main/res/layout/main.xml create mode 100644 app/src/main/res/layout/main_activity.xml rename app/src/main/res/layout/{recording_cell.xml => recording_list_cell.xml} (61%) create mode 100644 app/src/main/res/layout/recordings_fragment.xml delete mode 100644 app/src/main/res/layout/recordings_list.xml delete mode 100644 app/src/main/res/layout/search_contact_cell.xml delete mode 100644 app/src/main/res/layout/settings.xml delete mode 100644 app/src/main/res/layout/settings_account.xml create mode 100644 app/src/main/res/layout/settings_account_cell.xml create mode 100644 app/src/main/res/layout/settings_account_fragment.xml delete mode 100644 app/src/main/res/layout/settings_advanced.xml create mode 100644 app/src/main/res/layout/settings_advanced_fragment.xml delete mode 100644 app/src/main/res/layout/settings_audio.xml create mode 100644 app/src/main/res/layout/settings_audio_fragment.xml delete mode 100644 app/src/main/res/layout/settings_call.xml create mode 100644 app/src/main/res/layout/settings_call_fragment.xml delete mode 100644 app/src/main/res/layout/settings_chat.xml create mode 100644 app/src/main/res/layout/settings_chat_fragment.xml delete mode 100644 app/src/main/res/layout/settings_contact.xml create mode 100644 app/src/main/res/layout/settings_contacts_fragment.xml create mode 100644 app/src/main/res/layout/settings_fragment.xml delete mode 100644 app/src/main/res/layout/settings_network.xml create mode 100644 app/src/main/res/layout/settings_network_fragment.xml delete mode 100644 app/src/main/res/layout/settings_tunnel.xml create mode 100644 app/src/main/res/layout/settings_tunnel_fragment.xml delete mode 100644 app/src/main/res/layout/settings_video.xml create mode 100644 app/src/main/res/layout/settings_video_fragment.xml delete mode 100644 app/src/main/res/layout/settings_widget_checkbox.xml delete mode 100644 app/src/main/res/layout/settings_widget_led.xml delete mode 100644 app/src/main/res/layout/side_menu.xml create mode 100644 app/src/main/res/layout/side_menu_fragment.xml delete mode 100644 app/src/main/res/layout/side_menu_item_cell.xml delete mode 100644 app/src/main/res/layout/side_menu_main_account.xml delete mode 100644 app/src/main/res/layout/status_bar.xml create mode 100644 app/src/main/res/layout/status_fragment.xml create mode 100644 app/src/main/res/layout/tabs_fragment.xml delete mode 100644 app/src/main/res/menu/chat_bubble_menu.xml delete mode 100644 app/src/main/res/menu/chat_bubble_menu_with_resend.xml create mode 100644 app/src/main/res/menu/chat_message_menu.xml create mode 100644 app/src/main/res/navigation-sw533dp/chat_nav_graph.xml create mode 100644 app/src/main/res/navigation-sw533dp/contacts_nav_graph.xml create mode 100644 app/src/main/res/navigation-sw533dp/history_nav_graph.xml create mode 100644 app/src/main/res/navigation/assistant_nav_graph.xml create mode 100644 app/src/main/res/navigation/chat_nav_graph.xml create mode 100644 app/src/main/res/navigation/contacts_nav_graph.xml create mode 100644 app/src/main/res/navigation/history_nav_graph.xml create mode 100644 app/src/main/res/navigation/main_nav_graph.xml create mode 100644 app/src/main/res/navigation/settings_nav_graph.xml delete mode 100644 app/src/main/res/raw/lpconfig.xsd delete mode 100644 app/src/main/res/values-ar/strings.xml delete mode 100644 app/src/main/res/values-cs/strings.xml delete mode 100644 app/src/main/res/values-de/strings.xml delete mode 100644 app/src/main/res/values-es/strings.xml delete mode 100644 app/src/main/res/values-fi/strings.xml delete mode 100644 app/src/main/res/values-fr/strings.xml delete mode 100644 app/src/main/res/values-hu/strings.xml delete mode 100644 app/src/main/res/values-it/strings.xml delete mode 100644 app/src/main/res/values-iw/strings.xml delete mode 100644 app/src/main/res/values-ja/strings.xml delete mode 100644 app/src/main/res/values-ka/strings.xml delete mode 100644 app/src/main/res/values-nl/strings.xml delete mode 100644 app/src/main/res/values-pl/strings.xml delete mode 100644 app/src/main/res/values-pt-rBR/strings.xml delete mode 100644 app/src/main/res/values-ru/strings.xml delete mode 100644 app/src/main/res/values-sr/strings.xml delete mode 100644 app/src/main/res/values-sv/strings.xml rename app/src/main/res/values-sw533dp/{non_localizable_strings.xml => bools.xml} (100%) delete mode 100644 app/src/main/res/values-tr/strings.xml delete mode 100644 app/src/main/res/values-uk/strings.xml delete mode 100644 app/src/main/res/values-zh-rCN/strings.xml delete mode 100644 app/src/main/res/values-zh-rTW/strings.xml create mode 100644 app/src/main/res/values/bools.xml rename app/src/main/res/values/{color.xml => colors.xml} (80%) delete mode 100644 app/src/main/res/values/digit_style.xml create mode 100644 app/src/main/res/values/dimen.xml delete mode 100644 app/src/main/res/values/non_localizable_custom.xml delete mode 100644 app/src/main/res/values/non_localizable_strings.xml delete mode 100644 app/src/main/res/values/slidingtab_style.xml rename app/src/main/res/xml/{syncadapter.xml => sync_adapter.xml} (87%) delete mode 100755 check_unused_resources.py delete mode 100644 sample/.gitignore delete mode 100644 sample/app/.gitignore delete mode 100644 sample/app/build.gradle delete mode 100644 sample/app/proguard-rules.pro delete mode 100644 sample/app/src/main/AndroidManifest.xml delete mode 100644 sample/app/src/main/java/org/linphone/sample/CallActivity.java delete mode 100644 sample/app/src/main/java/org/linphone/sample/ConfigureAccountActivity.java delete mode 100644 sample/app/src/main/java/org/linphone/sample/LauncherActivity.java delete mode 100644 sample/app/src/main/java/org/linphone/sample/LinphoneService.java delete mode 100644 sample/app/src/main/java/org/linphone/sample/MainActivity.java delete mode 100644 sample/app/src/main/res/drawable/banner.png delete mode 100644 sample/app/src/main/res/drawable/led_connected.png delete mode 100644 sample/app/src/main/res/drawable/led_disconnected.png delete mode 100644 sample/app/src/main/res/drawable/led_error.png delete mode 100644 sample/app/src/main/res/drawable/led_inprogress.png delete mode 100644 sample/app/src/main/res/layout/call.xml delete mode 100644 sample/app/src/main/res/layout/configure_account.xml delete mode 100644 sample/app/src/main/res/layout/launcher.xml delete mode 100644 sample/app/src/main/res/layout/main.xml delete mode 100644 sample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 sample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 sample/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 sample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 sample/app/src/main/res/mipmap-hdpi/linphone_launcher_icon_foreground.png delete mode 100644 sample/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 sample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 sample/app/src/main/res/mipmap-mdpi/linphone_launcher_icon_foreground.png delete mode 100644 sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 sample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 sample/app/src/main/res/mipmap-xhdpi/linphone_launcher_icon_foreground.png delete mode 100644 sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 sample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 sample/app/src/main/res/mipmap-xxhdpi/linphone_launcher_icon_foreground.png delete mode 100644 sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 sample/app/src/main/res/mipmap-xxxhdpi/linphone_launcher_icon_foreground.png delete mode 100644 sample/app/src/main/res/raw/linphonerc_default delete mode 100644 sample/app/src/main/res/raw/linphonerc_factory delete mode 100644 sample/app/src/main/res/values/color.xml delete mode 100644 sample/app/src/main/res/values/colors.xml delete mode 100644 sample/app/src/main/res/values/strings.xml delete mode 100644 sample/app/src/main/res/values/styles.xml delete mode 100644 sample/build.gradle delete mode 100644 sample/gradle.properties delete mode 100644 sample/gradle/wrapper/gradle-wrapper.jar delete mode 100644 sample/gradle/wrapper/gradle-wrapper.properties delete mode 100755 sample/gradlew delete mode 100644 sample/gradlew.bat delete mode 100644 sample/settings.gradle diff --git a/app/build.gradle b/app/build.gradle index f3f61580b..bbb8869ca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,13 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +apply plugin: 'kotlin-kapt' + +apply plugin: "org.jlleitschuh.gradle.ktlint" + static def getPackageName() { return "org.linphone" } @@ -10,7 +18,7 @@ static def firebaseEnabled() { } task getGitVersion() { - def gitVersion = "4.4.0" + def gitVersion = "5.0" def gitVersionStream = new ByteArrayOutputStream() def gitCommitsCount = new ByteArrayOutputStream() def gitCommitHash = new ByteArrayOutputStream() @@ -41,69 +49,18 @@ task getGitVersion() { project.version = gitVersion } -configurations { - customImpl.extendsFrom(implementation) -} - -task linphoneSdkSource() { - doLast { - configurations.customImpl.getIncoming().each { - it.getResolutionResult().allComponents.each { - if (it.id.getDisplayName().contains("linphone-sdk-android")) { - println 'Linphone SDK used is ' + it.moduleVersion.version + ' from ' + it.properties["repositoryName"] - } - } - } - } -} - -///// Exclude Files ///// - -def excludeFiles = [] -if (!firebaseEnabled()) { - excludeFiles.add('**/Firebase*') - println '[Push Notification] Firebase disabled' -} -// Remove or comment if you want to use those -excludeFiles.add('**/XmlRpc*') -excludeFiles.add('**/InAppPurchase*') - -def excludePackage = [] - -excludePackage.add('**/gdb.*') -excludePackage.add('**/libopenh264**') -excludePackage.add('**/**tester**') -excludePackage.add('**/LICENSE.txt') - -///////////////////////// - -repositories { - maven { - name "local linphone-sdk maven repository" - url file(LinphoneSdkBuildDir + '/maven_repository/') - } - maven { - name "linphone.org maven repository" - url "https://linphone.org/maven_repository" - } -} - project.tasks['preBuild'].dependsOn 'getGitVersion' project.tasks['preBuild'].dependsOn 'linphoneSdkSource' android { - lintOptions { - abortOnError false - } - compileSdkVersion 29 + buildToolsVersion "29.0.2" defaultConfig { minSdkVersion 23 targetSdkVersion 29 - versionCode 4400 + versionCode 4300 versionName "${project.version}" applicationId getPackageName() - multiDexEnabled true } applicationVariants.all { variant -> @@ -114,12 +71,10 @@ android { // https://developer.android.com/studio/releases/gradle-plugin#3-6-0-behavior for extractNativeLibs if (variant.buildType.name == "release") { variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address", - linphone_file_provider: getPackageName() + ".provider", - extractNativeLibs: "false"] + linphone_file_provider: getPackageName() + ".fileprovider"] } else { variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address", - linphone_file_provider: getPackageName() + ".debug.provider", - extractNativeLibs: "true"] + linphone_file_provider: getPackageName() + ".debug.fileprovider"] } } @@ -143,20 +98,21 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' resValue "string", "sync_account_type", getPackageName() + ".sync" - resValue "string", "file_provider", getPackageName() + ".provider" + resValue "string", "file_provider", getPackageName() + ".fileprovider" resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address" if (!firebaseEnabled()) { resValue "string", "gcm_defaultSenderId", "none" } } + debug { applicationIdSuffix ".debug" debuggable true jniDebuggable true resValue "string", "sync_account_type", getPackageName() + ".sync" - resValue "string", "file_provider", getPackageName() + ".debug.provider" + resValue "string", "file_provider", getPackageName() + ".debug.fileprovider" resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address" if (!firebaseEnabled()) { @@ -165,35 +121,44 @@ android { } } - sourceSets { - main { - java.excludes = excludeFiles + dataBinding { + enabled = true + } +} - packagingOptions { - excludes = excludePackage - } - } +repositories { + maven { + url file(LinphoneSdkBuildDir + '/maven_repository/') } - packagingOptions { - pickFirst 'META-INF/NOTICE' - pickFirst 'META-INF/LICENSE' - exclude 'META-INF/MANIFEST.MF' - } + /*maven { + url "https://linphone.org/maven_repository" + }*/ } dependencies { - compileOnly 'org.jetbrains:annotations:19.0.0' + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation "androidx.media:media:1.1.0" + implementation 'androidx.fragment:fragment-ktx:1.2.3' + implementation 'androidx.core:core-ktx:1.2.0' + implementation 'androidx.navigation:navigation-fragment-ktx:2.2.1' + implementation 'androidx.navigation:navigation-ui-ktx:2.2.1' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'com.google.android:flexbox:2.0.0' + implementation 'com.github.bumptech.glide:glide:4.11.0' + if (firebaseEnabled()) { implementation 'com.google.firebase:firebase-messaging:19.0.1' } - implementation 'androidx.media:media:1.2.0' - implementation 'androidx.recyclerview:recyclerview:1.0.0' - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'com.google.android:flexbox:1.1.0' - implementation 'com.github.bumptech.glide:glide:4.9.0' - implementation "org.linphone:linphone-sdk-android:4.5+" + + implementation 'org.linphone:linphone-sdk-android:4.4+' } + if (firebaseEnabled()) { apply plugin: 'com.google.gms.google-services' } @@ -210,12 +175,9 @@ task generateContactsXml(type: Copy) { } project.tasks['preBuild'].dependsOn 'generateContactsXml' -apply plugin: "com.diffplug.gradle.spotless" -spotless { - java { - target '**/*.java' - googleJavaFormat('1.6').aosp() - removeUnusedImports() - } +ktlint { + android = true + ignoreFailures = true } -project.tasks['preBuild'].dependsOn 'spotlessApply' + +project.tasks['preBuild'].dependsOn 'ktlintFormat' \ No newline at end of file diff --git a/app/contacts.xml b/app/contacts.xml index 766be1f0b..07ea82c10 100644 --- a/app/contacts.xml +++ b/app/contacts.xml @@ -4,7 +4,7 @@ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index efab2ccf6..f1b424510 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1 +1,21 @@ --dontwarn org.apache.** +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml old mode 100755 new mode 100644 index 05f552aef..b726b0b48 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,222 +1,122 @@ + package="org.linphone"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - + android:theme="@style/AppTheme"> + android:name=".activities.launcher.LauncherActivity" + android:noHistory="true" + android:theme="@style/LauncherTheme"> - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - + + + + + - + - + + + android:turnScreenOn="true" /> - - - - - - - - - - - - - - - - - - + android:noHistory="true" /> + android:name=".core.CoreService" + android:foregroundServiceType="phoneCall" + android:stopWithTask="false" + android:label="@string/app_name" /> + + + + + + @@ -224,13 +124,13 @@ + android:resource="@xml/sync_adapter" /> - + @@ -240,21 +140,11 @@ android:resource="@xml/authenticator" /> - - - - - - - + - - - + @@ -263,10 +153,10 @@ android:enabled="true" android:exported="false" /> - + - + + @@ -283,4 +173,5 @@ - + + \ No newline at end of file diff --git a/app/src/main/res/raw/default_assistant_create.rc b/app/src/main/assets/assistant_default_values similarity index 91% rename from app/src/main/res/raw/default_assistant_create.rc rename to app/src/main/assets/assistant_default_values index dc66cd4da..7e5d7543b 100644 --- a/app/src/main/res/raw/default_assistant_create.rc +++ b/app/src/main/assets/assistant_default_values @@ -21,12 +21,6 @@ -
- -
-
- -
MD5 diff --git a/app/src/main/res/raw/linphone_assistant_create.rc b/app/src/main/assets/assistant_linphone_default_values similarity index 100% rename from app/src/main/res/raw/linphone_assistant_create.rc rename to app/src/main/assets/assistant_linphone_default_values diff --git a/app/src/main/res/raw/linphonerc_default b/app/src/main/assets/linphonerc_default similarity index 88% rename from app/src/main/res/raw/linphonerc_default rename to app/src/main/assets/linphonerc_default index 808feee28..1691780b7 100644 --- a/app/src/main/res/raw/linphonerc_default +++ b/app/src/main/assets/linphonerc_default @@ -1,3 +1,6 @@ + +## Start of default rc + [sip] contact="Linphone Android" use_info=0 @@ -34,4 +37,6 @@ history_max_size=100 [in-app-purchase] server_url=https://subscribe.linphone.org:444/inapp.php -purchasable_items_ids=test_account_subscription \ No newline at end of file +purchasable_items_ids=test_account_subscription + +## End of default rc diff --git a/app/src/main/res/raw/linphonerc_factory b/app/src/main/assets/linphonerc_factory similarity index 92% rename from app/src/main/res/raw/linphonerc_factory rename to app/src/main/assets/linphonerc_factory index a9a51cb5a..3855f2db0 100644 --- a/app/src/main/res/raw/linphonerc_factory +++ b/app/src/main/assets/linphonerc_factory @@ -1,7 +1,9 @@ -# +## Start of factory rc + #This file shall not contain path referencing package name, in order to be portable when app is renamed. #Paths to resources must be set from LinphoneManager, after creating LinphoneCore. + [net] mtu=1300 force_ice_disablement=0 @@ -36,4 +38,6 @@ prefer_basic_chat_room=1 xmlrpc_url=https://subscribe.linphone.org:444/wizard.php [lime] -lime_update_threshold=-1 \ No newline at end of file +lime_update_threshold=-1 + +## End of factory rc diff --git a/app/src/main/java/com/android/vending/billing/IInAppBillingService.aidl b/app/src/main/java/com/android/vending/billing/IInAppBillingService.aidl deleted file mode 100644 index 2a492f784..000000000 --- a/app/src/main/java/com/android/vending/billing/IInAppBillingService.aidl +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.vending.billing; - -import android.os.Bundle; - -/** - * InAppBillingService is the service that provides in-app billing version 3 and beyond. - * This service provides the following features: - * 1. Provides a new API to get details of in-app items published for the app including - * price, type, title and description. - * 2. The purchase flow is synchronous and purchase information is available immediately - * after it completes. - * 3. Purchase information of in-app purchases is maintained within the Google Play system - * till the purchase is consumed. - * 4. An API to consume a purchase of an inapp item. All purchases of one-time - * in-app items are consumable and thereafter can be purchased again. - * 5. An API to get current purchases of the user immediately. This will not contain any - * consumed purchases. - * - * All calls will give a response code with the following possible values - * RESULT_OK = 0 - success - * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog - * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested - * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase - * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API - * RESULT_ERROR = 6 - Fatal error during the API action - * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned - * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned - */ -interface IInAppBillingService { - /** - * Checks support for the requested billing API version, package and in-app type. - * Minimum API version supported by this interface is 3. - * @param apiVersion the billing version which the app is using - * @param packageName the package name of the calling app - * @param type type of the in-app item being purchased "inapp" for one-time purchases - * and "subs" for subscription. - * @return RESULT_OK(0) on success, corresponding result code on failures - */ - int isBillingSupported(int apiVersion, String packageName, String type); - - /** - * Provides details of a list of SKUs - * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle - * with a list JSON strings containing the productId, price, title and description. - * This API can be called with a maximum of 20 SKUs. - * @param apiVersion billing API version that the Third-party is using - * @param packageName the package name of the calling app - * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST" - * @return Bundle containing the following key-value pairs - * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on - * failure as listed above. - * "DETAILS_LIST" with a StringArrayList containing purchase information - * in JSON format similar to: - * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00", - * "title : "Example Title", "description" : "This is an example description" }' - */ - Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle); - - /** - * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU, - * the type, a unique purchase token and an optional developer payload. - * @param apiVersion billing API version that the app is using - * @param packageName package name of the calling app - * @param sku the SKU of the in-app item as published in the developer console - * @param type the type of the in-app item ("inapp" for one-time purchases - * and "subs" for subscription). - * @param developerPayload optional argument to be sent back with the purchase information - * @return Bundle containing the following key-value pairs - * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on - * failure as listed above. - * "BUY_INTENT" - PendingIntent to start the purchase flow - * - * The Pending intent should be launched with startIntentSenderForResult. When purchase flow - * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. - * If the purchase is successful, the result data will contain the following key-value pairs - * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on - * failure as listed above. - * "INAPP_PURCHASE_DATA" - String in JSON format similar to - * '{"orderId":"12999763169054705758.1371079406387615", - * "packageName":"com.example.app", - * "productId":"exampleSku", - * "purchaseTime":1345678900000, - * "purchaseToken" : "122333444455555", - * "developerPayload":"example developer payload" }' - * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that - * was signed with the private key of the developer - * TODO: change this to app-specific keys. - */ - Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, - String developerPayload); - - /** - * Returns the current SKUs owned by the user of the type and package name specified along with - * purchase information and a signature of the data to be validated. - * This will return all SKUs that have been purchased in V3 and managed items purchased using - * V1 and V2 that have not been consumed. - * @param apiVersion billing API version that the app is using - * @param packageName package name of the calling app - * @param type the type of the in-app items being requested - * ("inapp" for one-time purchases and "subs" for subscription). - * @param continuationToken to be set as null for the first call, if the number of owned - * skus are too many, a continuationToken is returned in the response bundle. - * This method can be called again with the continuation token to get the next set of - * owned skus. - * @return Bundle containing the following key-value pairs - * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on - * failure as listed above. - * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs - * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information - * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures - * of the purchase information - * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the - * next set of in-app purchases. Only set if the - * user has more owned skus than the current list. - */ - Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken); - - /** - * Consume the last purchase of the given SKU. This will result in this item being removed - * from all subsequent responses to getPurchases() and allow re-purchase of this item. - * @param apiVersion billing API version that the app is using - * @param packageName package name of the calling app - * @param purchaseToken token in the purchase information JSON that identifies the purchase - * to be consumed - * @return 0 if consumption succeeded. Appropriate error values for failures. - */ - int consumePurchase(int apiVersion, String packageName, String purchaseToken); -} diff --git a/app/src/main/java/org/linphone/LinphoneApplication.kt b/app/src/main/java/org/linphone/LinphoneApplication.kt new file mode 100644 index 000000000..907968f90 --- /dev/null +++ b/app/src/main/java/org/linphone/LinphoneApplication.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone + +import android.app.Application +import org.linphone.core.CoreContext +import org.linphone.core.CorePreferences +import org.linphone.core.Factory +import org.linphone.core.LogCollectionState +import org.linphone.core.tools.Log + +class LinphoneApplication : Application() { + companion object { + lateinit var corePreferences: CorePreferences + lateinit var coreContext: CoreContext + } + + override fun onCreate() { + super.onCreate() + val appName = getString(R.string.app_name) + android.util.Log.i("[$appName]", "Application is being created") + + Factory.instance().setLogCollectionPath(applicationContext.filesDir.absolutePath) + Factory.instance().enableLogCollection(LogCollectionState.Enabled) + + corePreferences = CorePreferences(applicationContext) + corePreferences.copyAssetsFromPackage() + + val config = Factory.instance().createConfigWithFactory(corePreferences.configPath, corePreferences.factoryConfigPath) + corePreferences.config = config + + Factory.instance().setDebugMode(corePreferences.debugLogs, appName) + + coreContext = CoreContext(applicationContext, config) + coreContext.start() + Log.i("[Application] Created") + } +} diff --git a/app/src/main/java/org/linphone/LinphoneContext.java b/app/src/main/java/org/linphone/LinphoneContext.java deleted file mode 100644 index 61ea0ed2e..000000000 --- a/app/src/main/java/org/linphone/LinphoneContext.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone; - -import static android.content.Intent.ACTION_MAIN; - -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.provider.ContactsContract; -import java.util.ArrayList; -import org.linphone.call.CallActivity; -import org.linphone.call.CallIncomingActivity; -import org.linphone.call.CallOutgoingActivity; -import org.linphone.compatibility.Compatibility; -import org.linphone.contacts.ContactsManager; -import org.linphone.core.Call; -import org.linphone.core.ConfiguringState; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.Factory; -import org.linphone.core.GlobalState; -import org.linphone.core.LogLevel; -import org.linphone.core.LoggingService; -import org.linphone.core.LoggingServiceListener; -import org.linphone.core.tools.Log; -import org.linphone.mediastream.Version; -import org.linphone.notifications.NotificationsManager; -import org.linphone.service.LinphoneService; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.DeviceUtils; -import org.linphone.utils.LinphoneUtils; -import org.linphone.utils.PushNotificationUtils; - -public class LinphoneContext { - private static LinphoneContext sInstance = null; - - private Context mContext; - - private final LoggingServiceListener mJavaLoggingService = - new LoggingServiceListener() { - @Override - public void onLogMessageWritten( - LoggingService logService, String domain, LogLevel lev, String message) { - switch (lev) { - case Debug: - android.util.Log.d(domain, message); - break; - case Message: - android.util.Log.i(domain, message); - break; - case Warning: - android.util.Log.w(domain, message); - break; - case Error: - android.util.Log.e(domain, message); - break; - case Fatal: - default: - android.util.Log.wtf(domain, message); - break; - } - } - }; - private CoreListenerStub mListener; - private NotificationsManager mNotificationManager; - private LinphoneManager mLinphoneManager; - private ContactsManager mContactsManager; - private final ArrayList mCoreStartedListeners; - - public static boolean isReady() { - return sInstance != null; - } - - public static LinphoneContext instance() { - if (sInstance == null) { - throw new RuntimeException("[Context] Linphone Context not available!"); - } - return sInstance; - } - - public LinphoneContext(Context context) { - mContext = context; - mCoreStartedListeners = new ArrayList<>(); - - LinphonePreferences.instance().setContext(context); - Factory.instance().setLogCollectionPath(context.getFilesDir().getAbsolutePath()); - boolean isDebugEnabled = LinphonePreferences.instance().isDebugEnabled(); - LinphoneUtils.configureLoggingService(isDebugEnabled, context.getString(R.string.app_name)); - - // Dump some debugging information to the logs - dumpDeviceInformation(); - dumpLinphoneInformation(); - - sInstance = this; - Log.i("[Context] Ready"); - - mListener = - new CoreListenerStub() { - @Override - public void onGlobalStateChanged(Core core, GlobalState state, String message) { - Log.i("[Context] Global state is [", state, "]"); - - if (state == GlobalState.On) { - for (CoreStartedListener listener : mCoreStartedListeners) { - listener.onCoreStarted(); - } - } - } - - @Override - public void onConfiguringStatus( - Core core, ConfiguringState status, String message) { - Log.i("[Context] Configuring state is [", status, "]"); - - if (status == ConfiguringState.Successful) { - LinphonePreferences.instance() - .setPushNotificationEnabled( - LinphonePreferences.instance() - .isPushNotificationEnabled()); - } - } - - @Override - public void onCallStateChanged( - Core core, Call call, Call.State state, String message) { - Log.i("[Context] Call state is [", state, "]"); - - if (mContext.getResources().getBoolean(R.bool.enable_call_notification)) { - mNotificationManager.displayCallNotification(call); - } - - if (state == Call.State.IncomingReceived - || state == Call.State.IncomingEarlyMedia) { - // Starting SDK 24 (Android 7.0) we rely on the fullscreen intent of the - // call incoming notification - if (Version.sdkStrictlyBelow(Version.API24_NOUGAT_70)) { - if (!mLinphoneManager.getCallGsmON()) onIncomingReceived(); - } - - // In case of push notification Service won't be started until here - if (!LinphoneService.isReady()) { - Log.i("[Context] Service not running, starting it"); - Intent intent = new Intent(ACTION_MAIN); - intent.setClass(mContext, LinphoneService.class); - mContext.startService(intent); - } - } else if (state == Call.State.OutgoingInit) { - onOutgoingStarted(); - } else if (state == Call.State.Connected) { - onCallStarted(); - } else if (state == Call.State.End - || state == Call.State.Released - || state == Call.State.Error) { - if (LinphoneService.isReady()) { - LinphoneService.instance().destroyOverlay(); - } - - if (state == Call.State.Released - && call.getCallLog().getStatus() == Call.Status.Missed) { - mNotificationManager.displayMissedCallNotification(call); - } - } - } - }; - - mLinphoneManager = new LinphoneManager(context); - mNotificationManager = new NotificationsManager(context); - - if (DeviceUtils.isAppUserRestricted(mContext)) { - // See https://firebase.google.com/docs/cloud-messaging/android/receive#restricted - Log.w( - "[Context] Device has been restricted by user (Android 9+), push notifications won't work !"); - } - - int bucket = DeviceUtils.getAppStandbyBucket(mContext); - if (bucket > 0) { - Log.w( - "[Context] Device is in bucket " - + Compatibility.getAppStandbyBucketNameFromValue(bucket)); - } - - if (!PushNotificationUtils.isAvailable(mContext)) { - Log.w("[Context] Push notifications won't work !"); - } - } - - public void start(boolean isPush) { - Log.i("[Context] Starting, push status is ", isPush); - mLinphoneManager.startLibLinphone(isPush, mListener); - - mNotificationManager.onCoreReady(); - - mContactsManager = new ContactsManager(mContext); - if (!Version.sdkAboveOrEqual(Version.API26_O_80) - || (mContactsManager.hasReadContactsAccess())) { - mContext.getContentResolver() - .registerContentObserver( - ContactsContract.Contacts.CONTENT_URI, true, mContactsManager); - } - if (mContactsManager.hasReadContactsAccess()) { - mContactsManager.enableContactsAccess(); - } - mContactsManager.initializeContactManager(); - } - - public void destroy() { - Log.i("[Context] Destroying"); - Core core = LinphoneManager.getCore(); - if (core != null) { - core.removeListener(mListener); - core = null; // To allow the gc calls below to free the Core - } - - // Make sure our notification is gone. - if (mNotificationManager != null) { - mNotificationManager.destroy(); - } - - if (mContactsManager != null) { - mContactsManager.destroy(); - } - - // Destroy the LinphoneManager second to last to ensure any getCore() call will work - if (mLinphoneManager != null) { - mLinphoneManager.destroy(); - } - - // Wait for every other object to be destroyed to make LinphoneService.instance() invalid - sInstance = null; - - if (LinphonePreferences.instance().useJavaLogger()) { - Factory.instance().getLoggingService().removeListener(mJavaLoggingService); - } - LinphonePreferences.instance().destroy(); - } - - public void updateContext(Context context) { - mContext = context; - } - - public Context getApplicationContext() { - return mContext; - } - - /* Managers accessors */ - - public LoggingServiceListener getJavaLoggingService() { - return mJavaLoggingService; - } - - public NotificationsManager getNotificationManager() { - return mNotificationManager; - } - - public LinphoneManager getLinphoneManager() { - return mLinphoneManager; - } - - public ContactsManager getContactsManager() { - return mContactsManager; - } - - public void addCoreStartedListener(CoreStartedListener listener) { - mCoreStartedListeners.add(listener); - } - - public void removeCoreStartedListener(CoreStartedListener listener) { - mCoreStartedListeners.remove(listener); - } - - /* Log device related information */ - - private void dumpDeviceInformation() { - Log.i("==== Phone information dump ===="); - Log.i("DISPLAY NAME=" + Compatibility.getDeviceName(mContext)); - Log.i("DEVICE=" + Build.DEVICE); - Log.i("MODEL=" + Build.MODEL); - Log.i("MANUFACTURER=" + Build.MANUFACTURER); - Log.i("ANDROID SDK=" + Build.VERSION.SDK_INT); - StringBuilder sb = new StringBuilder(); - sb.append("ABIs="); - for (String abi : Version.getCpuAbis()) { - sb.append(abi).append(", "); - } - Log.i(sb.substring(0, sb.length() - 2)); - } - - private void dumpLinphoneInformation() { - Log.i("==== Linphone information dump ===="); - Log.i("VERSION NAME=" + org.linphone.BuildConfig.VERSION_NAME); - Log.i("VERSION CODE=" + org.linphone.BuildConfig.VERSION_CODE); - Log.i("PACKAGE=" + org.linphone.BuildConfig.APPLICATION_ID); - Log.i("BUILD TYPE=" + org.linphone.BuildConfig.BUILD_TYPE); - Log.i("SDK VERSION=" + mContext.getString(R.string.linphone_sdk_version)); - Log.i("SDK BRANCH=" + mContext.getString(R.string.linphone_sdk_branch)); - } - - /* Call activities */ - - private void onIncomingReceived() { - Intent intent = new Intent(mContext, CallIncomingActivity.class); - // This flag is required to start an Activity from a Service context - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(intent); - } - - private void onOutgoingStarted() { - Intent intent = new Intent(mContext, CallOutgoingActivity.class); - // This flag is required to start an Activity from a Service context - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(intent); - } - - private void onCallStarted() { - Intent intent = new Intent(mContext, CallActivity.class); - // This flag is required to start an Activity from a Service context - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(intent); - } - - public interface CoreStartedListener { - void onCoreStarted(); - } -} diff --git a/app/src/main/java/org/linphone/LinphoneManager.java b/app/src/main/java/org/linphone/LinphoneManager.java deleted file mode 100644 index 06bf7a197..000000000 --- a/app/src/main/java/org/linphone/LinphoneManager.java +++ /dev/null @@ -1,635 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.PowerManager; -import android.os.PowerManager.WakeLock; -import android.telephony.PhoneStateListener; -import android.telephony.TelephonyManager; -import java.io.File; -import java.util.Timer; -import java.util.TimerTask; -import org.linphone.call.AndroidAudioManager; -import org.linphone.call.CallManager; -import org.linphone.contacts.ContactsManager; -import org.linphone.core.AccountCreator; -import org.linphone.core.Call; -import org.linphone.core.Call.State; -import org.linphone.core.Core; -import org.linphone.core.CoreListener; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.Factory; -import org.linphone.core.FriendList; -import org.linphone.core.PresenceActivity; -import org.linphone.core.PresenceBasicStatus; -import org.linphone.core.PresenceModel; -import org.linphone.core.ProxyConfig; -import org.linphone.core.Reason; -import org.linphone.core.Tunnel; -import org.linphone.core.TunnelConfig; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.LinphoneUtils; -import org.linphone.utils.MediaScanner; -import org.linphone.utils.PushNotificationUtils; - -/** Handles Linphone's Core lifecycle */ -public class LinphoneManager implements SensorEventListener { - private final String mBasePath; - private final String mRingSoundFile; - private final String mCallLogDatabaseFile; - private final String mFriendsDatabaseFile; - private final String mUserCertsPath; - - private final Context mContext; - private AndroidAudioManager mAudioManager; - private CallManager mCallManager; - private final PowerManager mPowerManager; - private final ConnectivityManager mConnectivityManager; - private TelephonyManager mTelephonyManager; - private PhoneStateListener mPhoneStateListener; - private WakeLock mProximityWakelock; - private final SensorManager mSensorManager; - private final Sensor mProximity; - private final MediaScanner mMediaScanner; - private Timer mTimer; - - private final LinphonePreferences mPrefs; - private Core mCore; - private CoreListenerStub mCoreListener; - private AccountCreator mAccountCreator; - - private boolean mExited; - private boolean mCallGsmON; - private boolean mProximitySensingEnabled; - private boolean mHasLastCallSasBeenRejected; - private Runnable mIterateRunnable; - - public LinphoneManager(Context c) { - mExited = false; - mContext = c; - mBasePath = c.getFilesDir().getAbsolutePath(); - mCallLogDatabaseFile = mBasePath + "/linphone-log-history.db"; - mFriendsDatabaseFile = mBasePath + "/linphone-friends.db"; - mRingSoundFile = mBasePath + "/share/sounds/linphone/rings/notes_of_the_optimistic.mkv"; - mUserCertsPath = mBasePath + "/user-certs"; - - mPrefs = LinphonePreferences.instance(); - mPowerManager = (PowerManager) c.getSystemService(Context.POWER_SERVICE); - mConnectivityManager = - (ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE); - mSensorManager = (SensorManager) c.getSystemService(Context.SENSOR_SERVICE); - mProximity = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); - mTelephonyManager = (TelephonyManager) c.getSystemService(Context.TELEPHONY_SERVICE); - mPhoneStateListener = - new PhoneStateListener() { - @Override - public void onCallStateChanged(int state, String phoneNumber) { - switch (state) { - case TelephonyManager.CALL_STATE_OFFHOOK: - Log.i("[Manager] Phone state is off hook"); - setCallGsmON(true); - break; - case TelephonyManager.CALL_STATE_RINGING: - Log.i("[Manager] Phone state is ringing"); - setCallGsmON(true); - break; - case TelephonyManager.CALL_STATE_IDLE: - Log.i("[Manager] Phone state is idle"); - setCallGsmON(false); - break; - } - } - }; - - Log.i("[Manager] Registering phone state listener"); - mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); - - mHasLastCallSasBeenRejected = false; - mCallManager = new CallManager(c); - - File f = new File(mUserCertsPath); - if (!f.exists()) { - if (!f.mkdir()) { - Log.e("[Manager] " + mUserCertsPath + " can't be created."); - } - } - - mMediaScanner = new MediaScanner(c); - - mCoreListener = - new CoreListenerStub() { - @SuppressLint("Wakelock") - @Override - public void onCallStateChanged( - final Core core, - final Call call, - final State state, - final String message) { - Log.i("[Manager] Call state is [", state, "]"); - if (state == State.IncomingReceived - && !call.equals(core.getCurrentCall())) { - if (call.getReplacedCall() != null) { - // attended transfer will be accepted automatically. - return; - } - } - - if ((state == State.IncomingReceived || state == State.IncomingEarlyMedia) - && getCallGsmON()) { - if (mCore != null) { - call.decline(Reason.Busy); - } - } else if (state == State.IncomingReceived - && (LinphonePreferences.instance().isAutoAnswerEnabled()) - && !getCallGsmON()) { - LinphoneUtils.dispatchOnUIThreadAfter( - new Runnable() { - @Override - public void run() { - if (mCore != null) { - if (mCore.getCallsNb() > 0) { - mCallManager.acceptCall(call); - } - } - } - }, - mPrefs.getAutoAnswerTime()); - } else if (state == State.End || state == State.Error) { - if (mCore.getCallsNb() == 0) { - // Disabling proximity sensor - enableProximitySensing(false); - } - } else if (state == State.UpdatedByRemote) { - // If the correspondent proposes video while audio call - boolean remoteVideo = call.getRemoteParams().videoEnabled(); - boolean localVideo = call.getCurrentParams().videoEnabled(); - boolean autoAcceptCameraPolicy = - LinphonePreferences.instance() - .shouldAutomaticallyAcceptVideoRequests(); - if (remoteVideo - && !localVideo - && !autoAcceptCameraPolicy - && mCore.getConference() == null) { - call.deferUpdate(); - } - } - } - - @Override - public void onFriendListCreated(Core core, FriendList list) { - if (LinphoneContext.isReady()) { - list.addListener(ContactsManager.getInstance()); - } - } - - @Override - public void onFriendListRemoved(Core core, FriendList list) { - list.removeListener(ContactsManager.getInstance()); - } - }; - } - - public static synchronized LinphoneManager getInstance() { - LinphoneManager manager = LinphoneContext.instance().getLinphoneManager(); - if (manager == null) { - throw new RuntimeException( - "[Manager] Linphone Manager should be created before accessed"); - } - if (manager.mExited) { - throw new RuntimeException( - "[Manager] Linphone Manager was already destroyed. " - + "Better use getCore and check returned value"); - } - return manager; - } - - public static synchronized AndroidAudioManager getAudioManager() { - return getInstance().mAudioManager; - } - - public static synchronized CallManager getCallManager() { - return getInstance().mCallManager; - } - - public static synchronized Core getCore() { - if (!LinphoneContext.isReady()) return null; - - if (getInstance().mExited) { - // Can occur if the UI thread play a posted event but in the meantime the - // LinphoneManager was destroyed - // Ex: stop call and quickly terminate application. - return null; - } - return getInstance().mCore; - } - - /* End of static */ - - public MediaScanner getMediaScanner() { - return mMediaScanner; - } - - public synchronized void destroy() { - destroyManager(); - // Wait for Manager to destroy everything before setting mExited to true - // Otherwise some objects might crash during their own destroy if they try to call - // LinphoneManager.getCore(), for example to unregister a listener - mExited = true; - } - - public void restartCore() { - Log.w("[Manager] Restarting Core"); - mCore.stop(); - mCore.start(); - } - - private void destroyCore() { - Log.w("[Manager] Destroying Core"); - mCore.stop(); - mCore.removeListener(mCoreListener); - } - - private synchronized void destroyManager() { - Log.w("[Manager] Destroying Manager"); - changeStatusToOffline(); - - if (mTelephonyManager != null) { - Log.i("[Manager] Unregistering phone state listener"); - mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); - } - - if (mCallManager != null) mCallManager.destroy(); - if (mMediaScanner != null) mMediaScanner.destroy(); - if (mAudioManager != null) mAudioManager.destroy(); - - if (mTimer != null) mTimer.cancel(); - - if (mCore != null) { - destroyCore(); - mCore = null; - } - } - - public synchronized void startLibLinphone(boolean isPush, CoreListener listener) { - try { - mCore = - Factory.instance() - .createCore( - mPrefs.getLinphoneDefaultConfig(), - mPrefs.getLinphoneFactoryConfig(), - mContext); - mCore.addListener(listener); - mCore.addListener(mCoreListener); - - if (isPush) { - Log.w( - "[Manager] We are here because of a received push notification, enter background mode before starting the Core"); - mCore.enterBackground(); - } - - mCore.start(); - - mIterateRunnable = - new Runnable() { - @Override - public void run() { - if (mCore != null) { - mCore.iterate(); - } - } - }; - TimerTask lTask = - new TimerTask() { - @Override - public void run() { - LinphoneUtils.dispatchOnUIThread(mIterateRunnable); - } - }; - /*use schedule instead of scheduleAtFixedRate to avoid iterate from being call in burst after cpu wake up*/ - mTimer = new Timer("Linphone scheduler"); - mTimer.schedule(lTask, 0, 20); - - configureCore(); - } catch (Exception e) { - Log.e(e, "[Manager] Cannot start linphone"); - } - } - - private synchronized void configureCore() { - Log.i("[Manager] Configuring Core"); - mAudioManager = new AndroidAudioManager(mContext); - - mCore.setZrtpSecretsFile(mBasePath + "/zrtp_secrets"); - - String deviceName = mPrefs.getDeviceName(mContext); - String appName = mContext.getResources().getString(R.string.user_agent); - String androidVersion = org.linphone.BuildConfig.VERSION_NAME; - String userAgent = appName + "/" + androidVersion + " (" + deviceName + ") LinphoneSDK"; - - mCore.setUserAgent( - userAgent, - getString(R.string.linphone_sdk_version) - + " (" - + getString(R.string.linphone_sdk_branch) - + ")"); - - // mCore.setChatDatabasePath(mChatDatabaseFile); - mCore.setCallLogsDatabasePath(mCallLogDatabaseFile); - mCore.setFriendsDatabasePath(mFriendsDatabaseFile); - mCore.setUserCertificatesPath(mUserCertsPath); - // mCore.setCallErrorTone(Reason.NotFound, mErrorToneFile); - enableDeviceRingtone(mPrefs.isDeviceRingtoneEnabled()); - - int availableCores = Runtime.getRuntime().availableProcessors(); - Log.w("[Manager] MediaStreamer : " + availableCores + " cores detected and configured"); - - mCore.migrateLogsFromRcToDb(); - - // Migrate existing linphone accounts to have conference factory uri and LIME X3Dh url set - String uri = getString(R.string.default_conference_factory_uri); - for (ProxyConfig lpc : mCore.getProxyConfigList()) { - if (lpc.getIdentityAddress().getDomain().equals(getString(R.string.default_domain))) { - if (lpc.getConferenceFactoryUri() == null) { - lpc.edit(); - Log.i( - "[Manager] Setting conference factory on proxy config " - + lpc.getIdentityAddress().asString() - + " to default value: " - + uri); - lpc.setConferenceFactoryUri(uri); - lpc.done(); - } - - if (mCore.limeX3DhAvailable()) { - String url = mCore.getLimeX3DhServerUrl(); - if (url == null || url.isEmpty()) { - url = getString(R.string.default_lime_x3dh_server_url); - Log.i("[Manager] Setting LIME X3Dh server url to default value: " + url); - mCore.setLimeX3DhServerUrl(url); - } - } - } - } - - if (mContext.getResources().getBoolean(R.bool.enable_push_id)) { - PushNotificationUtils.init(mContext); - } - - mProximityWakelock = - mPowerManager.newWakeLock( - PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, - mContext.getPackageName() + ";manager_proximity_sensor"); - - resetCameraFromPreferences(); - - mAccountCreator = mCore.createAccountCreator(LinphonePreferences.instance().getXmlrpcUrl()); - mCallGsmON = false; - - Log.i("[Manager] Core configured"); - } - - public void resetCameraFromPreferences() { - Core core = getCore(); - if (core == null) return; - - boolean useFrontCam = LinphonePreferences.instance().useFrontCam(); - String firstDevice = null; - for (String camera : core.getVideoDevicesList()) { - if (firstDevice == null) { - firstDevice = camera; - } - - if (useFrontCam) { - if (camera.contains("Front")) { - Log.i("[Manager] Found front facing camera: " + camera); - core.setVideoDevice(camera); - return; - } - } - } - - Log.i("[Manager] Using first camera available: " + firstDevice); - core.setVideoDevice(firstDevice); - } - - /* Account linking */ - - public AccountCreator getAccountCreator() { - if (mAccountCreator == null) { - Log.w("[Manager] Account creator shouldn't be null !"); - mAccountCreator = - mCore.createAccountCreator(LinphonePreferences.instance().getXmlrpcUrl()); - } - return mAccountCreator; - } - - /* Presence stuff */ - - private boolean isPresenceModelActivitySet() { - if (mCore != null) { - return mCore.getPresenceModel() != null - && mCore.getPresenceModel().getActivity() != null; - } - return false; - } - - public void changeStatusToOnline() { - if (mCore == null) return; - PresenceModel model = mCore.createPresenceModel(); - model.setBasicStatus(PresenceBasicStatus.Open); - mCore.setPresenceModel(model); - } - - public void changeStatusToOnThePhone() { - if (mCore == null) return; - - if (isPresenceModelActivitySet() - && mCore.getPresenceModel().getActivity().getType() - != PresenceActivity.Type.OnThePhone) { - mCore.getPresenceModel().getActivity().setType(PresenceActivity.Type.OnThePhone); - } else if (!isPresenceModelActivitySet()) { - PresenceModel model = - mCore.createPresenceModelWithActivity(PresenceActivity.Type.OnThePhone, null); - mCore.setPresenceModel(model); - } - } - - private void changeStatusToOffline() { - if (mCore != null) { - PresenceModel model = mCore.getPresenceModel(); - model.setBasicStatus(PresenceBasicStatus.Closed); - mCore.setPresenceModel(model); - } - } - - /* Tunnel stuff */ - - public void initTunnelFromConf() { - if (!mCore.tunnelAvailable()) return; - - NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); - Tunnel tunnel = mCore.getTunnel(); - tunnel.cleanServers(); - TunnelConfig config = mPrefs.getTunnelConfig(); - if (config.getHost() != null) { - tunnel.addServer(config); - manageTunnelServer(info); - } - } - - private boolean isTunnelNeeded(NetworkInfo info) { - if (info == null) { - Log.i("[Manager] No connectivity: tunnel should be disabled"); - return false; - } - - String pref = mPrefs.getTunnelMode(); - - if (getString(R.string.tunnel_mode_entry_value_always).equals(pref)) { - return true; - } - - if (info.getType() != ConnectivityManager.TYPE_WIFI - && getString(R.string.tunnel_mode_entry_value_3G_only).equals(pref)) { - Log.i("[Manager] Need tunnel: 'no wifi' connection"); - return true; - } - - return false; - } - - private void manageTunnelServer(NetworkInfo info) { - if (mCore == null) return; - if (!mCore.tunnelAvailable()) return; - Tunnel tunnel = mCore.getTunnel(); - - Log.i("[Manager] Managing tunnel"); - if (isTunnelNeeded(info)) { - Log.i("[Manager] Tunnel need to be activated"); - tunnel.setMode(Tunnel.Mode.Enable); - } else { - Log.i("[Manager] Tunnel should not be used"); - String pref = mPrefs.getTunnelMode(); - tunnel.setMode(Tunnel.Mode.Disable); - if (getString(R.string.tunnel_mode_entry_value_auto).equals(pref)) { - tunnel.setMode(Tunnel.Mode.Auto); - } - } - } - - /* Proximity sensor stuff */ - - public void enableProximitySensing(boolean enable) { - if (enable) { - if (!mProximitySensingEnabled) { - mSensorManager.registerListener( - this, mProximity, SensorManager.SENSOR_DELAY_NORMAL); - mProximitySensingEnabled = true; - } - } else { - if (mProximitySensingEnabled) { - mSensorManager.unregisterListener(this); - mProximitySensingEnabled = false; - // Don't forgeting to release wakelock if held - if (mProximityWakelock.isHeld()) { - mProximityWakelock.release(); - } - } - } - } - - private Boolean isProximitySensorNearby(final SensorEvent event) { - float threshold = 4.001f; // <= 4 cm is near - - final float distanceInCm = event.values[0]; - final float maxDistance = event.sensor.getMaximumRange(); - Log.d( - "[Manager] Proximity sensor report [" - + distanceInCm - + "] , for max range [" - + maxDistance - + "]"); - - if (maxDistance <= threshold) { - // Case binary 0/1 and short sensors - threshold = maxDistance; - } - return distanceInCm < threshold; - } - - @Override - public void onSensorChanged(SensorEvent event) { - if (event.timestamp == 0) return; - if (isProximitySensorNearby(event)) { - if (!mProximityWakelock.isHeld()) { - mProximityWakelock.acquire(); - } - } else { - if (mProximityWakelock.isHeld()) { - mProximityWakelock.release(); - } - } - } - - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) {} - - /* Other stuff */ - - public void enableDeviceRingtone(boolean use) { - if (use) { - mCore.setRing(null); - } else { - mCore.setRing(mRingSoundFile); - } - } - - public boolean getCallGsmON() { - return mCallGsmON; - } - - public void setCallGsmON(boolean on) { - mCallGsmON = on; - if (on && mCore != null) { - mCore.pauseAllCalls(); - } - } - - private String getString(int key) { - return mContext.getString(key); - } - - public boolean hasLastCallSasBeenRejected() { - return mHasLastCallSasBeenRejected; - } - - public void lastCallSasRejected(boolean rejected) { - mHasLastCallSasBeenRejected = rejected; - } -} diff --git a/app/src/main/java/org/linphone/activities/AboutActivity.java b/app/src/main/java/org/linphone/activities/AboutActivity.java deleted file mode 100644 index b4ff91423..000000000 --- a/app/src/main/java/org/linphone/activities/AboutActivity.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.activities; - -import android.app.ProgressDialog; -import android.content.Intent; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.TextView; -import androidx.core.content.ContextCompat; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.settings.LinphonePreferences; - -public class AboutActivity extends MainActivity { - private CoreListenerStub mListener; - private ProgressDialog mProgress; - private boolean mUploadInProgress; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mOnBackPressGoHome = false; - mAlwaysHideTabBar = true; - - // Uses the fragment container layout to inflate the about view instead of using a fragment - View aboutView = LayoutInflater.from(this).inflate(R.layout.about, null, false); - LinearLayout fragmentContainer = findViewById(R.id.fragmentContainer); - LinearLayout.LayoutParams params = - new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - fragmentContainer.addView(aboutView, params); - - if (isTablet()) { - findViewById(R.id.fragmentContainer2).setVisibility(View.GONE); - } - - TextView aboutVersion = findViewById(R.id.about_android_version); - TextView aboutLiblinphoneVersion = findViewById(R.id.about_liblinphone_sdk_version); - aboutLiblinphoneVersion.setText( - String.format( - getString(R.string.about_liblinphone_sdk_version), - getString(R.string.linphone_sdk_version) - + " (" - + getString(R.string.linphone_sdk_branch) - + ")")); - // We can't access a library's BuildConfig, so we have to set it as a resource - aboutVersion.setText( - String.format( - getString(R.string.about_version), - org.linphone.BuildConfig.VERSION_NAME - + " (" - + org.linphone.BuildConfig.BUILD_TYPE - + ")")); - - TextView privacyPolicy = findViewById(R.id.privacy_policy_link); - privacyPolicy.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent browserIntent = - new Intent( - Intent.ACTION_VIEW, - Uri.parse(getString(R.string.about_privacy_policy_link))); - startActivity(browserIntent); - } - }); - - TextView license = findViewById(R.id.about_text); - license.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent browserIntent = - new Intent( - Intent.ACTION_VIEW, - Uri.parse(getString(R.string.about_license_link))); - startActivity(browserIntent); - } - }); - - Button sendLogs = findViewById(R.id.send_log); - sendLogs.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.uploadLogCollection(); - } - } - }); - sendLogs.setVisibility( - LinphonePreferences.instance().isDebugEnabled() ? View.VISIBLE : View.GONE); - - Button resetLogs = findViewById(R.id.reset_log); - resetLogs.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.resetLogCollection(); - } - } - }); - resetLogs.setVisibility( - LinphonePreferences.instance().isDebugEnabled() ? View.VISIBLE : View.GONE); - - mListener = - new CoreListenerStub() { - @Override - public void onLogCollectionUploadProgressIndication( - Core core, int offset, int total) {} - - @Override - public void onLogCollectionUploadStateChanged( - Core core, Core.LogCollectionUploadState state, String info) { - if (state == Core.LogCollectionUploadState.InProgress) { - displayUploadLogsInProgress(); - } else if (state == Core.LogCollectionUploadState.Delivered - || state == Core.LogCollectionUploadState.NotDelivered) { - mUploadInProgress = false; - if (mProgress != null) mProgress.dismiss(); - } - } - }; - } - - @Override - public void onResume() { - super.onResume(); - - showTopBarWithTitle(getString(R.string.about)); - if (getResources().getBoolean(R.bool.hide_bottom_bar_on_second_level_views)) { - hideTabBar(); - } - - Core core = LinphoneManager.getCore(); - if (core != null) { - core.addListener(mListener); - } - } - - @Override - public void onPause() { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.removeListener(mListener); - } - - super.onPause(); - } - - @Override - protected void onDestroy() { - mListener = null; - mProgress = null; - - super.onDestroy(); - } - - private void displayUploadLogsInProgress() { - if (mUploadInProgress) { - return; - } - mUploadInProgress = true; - - mProgress = ProgressDialog.show(this, null, null); - Drawable d = new ColorDrawable(ContextCompat.getColor(this, R.color.light_grey_color)); - d.setAlpha(200); - mProgress - .getWindow() - .setLayout( - WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.MATCH_PARENT); - mProgress.getWindow().setBackgroundDrawable(d); - mProgress.setContentView(R.layout.wait_layout); - mProgress.show(); - } -} diff --git a/app/src/main/java/org/linphone/activities/GenericActivity.kt b/app/src/main/java/org/linphone/activities/GenericActivity.kt new file mode 100644 index 000000000..710dde4ad --- /dev/null +++ b/app/src/main/java/org/linphone/activities/GenericActivity.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities + +import android.annotation.SuppressLint +import android.content.pm.ActivityInfo +import android.content.res.Configuration +import android.os.Bundle +import android.view.Surface +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.core.tools.Log + +abstract class GenericActivity : AppCompatActivity() { + @SuppressLint("SourceLockedOrientationActivity") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (corePreferences.forcePortrait) { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + + val nightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + val darkModeEnabled = corePreferences.darkMode + when (nightMode) { + Configuration.UI_MODE_NIGHT_NO, Configuration.UI_MODE_NIGHT_UNDEFINED -> { + if (darkModeEnabled == 1) { + // Force dark mode + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + } + } + Configuration.UI_MODE_NIGHT_YES -> { + if (darkModeEnabled == 0) { + // Force light mode + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + } + } + } + } + + override fun onResume() { + super.onResume() + + var degrees = 270 + val orientation = windowManager.defaultDisplay.rotation + when (orientation) { + Surface.ROTATION_0 -> degrees = 0 + Surface.ROTATION_90 -> degrees = 270 + Surface.ROTATION_180 -> degrees = 180 + Surface.ROTATION_270 -> degrees = 90 + } + Log.i("[Generic Activity] Device orientation is $degrees (raw value is $orientation)") + val rotation = (360 - degrees) % 360 + coreContext.core.deviceRotation = rotation + + // Remove service notification if it has been started by device boot + coreContext.notificationsManager.stopForegroundNotificationIfPossible() + } + + fun isTablet(): Boolean { + return resources.getBoolean(R.bool.isTablet) + } +} diff --git a/app/src/main/java/org/linphone/activities/LinphoneGenericActivity.java b/app/src/main/java/org/linphone/activities/LinphoneGenericActivity.java deleted file mode 100644 index 100fe0c41..000000000 --- a/app/src/main/java/org/linphone/activities/LinphoneGenericActivity.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.activities; - -import android.content.Intent; -import android.os.Bundle; -import android.view.Surface; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.core.Core; -import org.linphone.core.tools.Log; -import org.linphone.service.LinphoneService; - -public abstract class LinphoneGenericActivity extends ThemeableActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - ensureServiceIsRunning(); - } - - @Override - protected void onResume() { - super.onResume(); - - ensureServiceIsRunning(); - - if (LinphoneContext.isReady()) { - int degrees = 270; - int orientation = getWindowManager().getDefaultDisplay().getRotation(); - switch (orientation) { - case Surface.ROTATION_0: - degrees = 0; - break; - case Surface.ROTATION_90: - degrees = 270; - break; - case Surface.ROTATION_180: - degrees = 180; - break; - case Surface.ROTATION_270: - degrees = 90; - break; - } - - Log.i( - "[Generic Activity] Device orientation is " - + degrees - + " (raw value is " - + orientation - + ")"); - - int rotation = (360 - degrees) % 360; - Core core = LinphoneManager.getCore(); - if (core != null) { - core.setDeviceRotation(rotation); - } - } - } - - private void ensureServiceIsRunning() { - if (!LinphoneService.isReady()) { - if (!LinphoneContext.isReady()) { - new LinphoneContext(getApplicationContext()); - LinphoneContext.instance().start(false); - Log.i("[Generic Activity] Context created & started"); - } - - Log.i("[Generic Activity] Starting Service"); - try { - startService(new Intent().setClass(this, LinphoneService.class)); - } catch (IllegalStateException ise) { - Log.e("[Generic Activity] Couldn't start service, exception: ", ise); - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/LinphoneLauncherActivity.java b/app/src/main/java/org/linphone/activities/LinphoneLauncherActivity.java deleted file mode 100644 index 3ab8d80ad..000000000 --- a/app/src/main/java/org/linphone/activities/LinphoneLauncherActivity.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.activities; - -import android.app.Activity; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.os.Bundle; -import android.util.Log; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.assistant.MenuAssistantActivity; -import org.linphone.chat.ChatActivity; -import org.linphone.contacts.ContactsActivity; -import org.linphone.dialer.DialerActivity; -import org.linphone.history.HistoryActivity; -import org.linphone.service.LinphoneService; -import org.linphone.service.ServiceWaitThread; -import org.linphone.service.ServiceWaitThreadListener; -import org.linphone.settings.LinphonePreferences; - -/** Creates LinphoneService and wait until Core is ready to start main Activity */ -public class LinphoneLauncherActivity extends Activity implements ServiceWaitThreadListener { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (getResources().getBoolean(R.bool.orientation_portrait_only)) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - } - - if (!getResources().getBoolean(R.bool.use_full_screen_image_splashscreen)) { - setContentView(R.layout.launch_screen); - } // Otherwise use drawable/launch_screen layer list up until first activity starts - } - - @Override - protected void onStart() { - super.onStart(); - - if (LinphoneService.isReady()) { - onServiceReady(); - } else { - try { - startService( - new Intent() - .setClass(LinphoneLauncherActivity.this, LinphoneService.class)); - new ServiceWaitThread(this).start(); - } catch (IllegalStateException ise) { - Log.e("Linphone", "Exception raised while starting service: " + ise); - } - } - } - - @Override - public void onServiceReady() { - final Class classToStart; - - boolean useFirstLoginActivity = - getResources().getBoolean(R.bool.display_account_assistant_at_first_start); - if (useFirstLoginActivity && LinphonePreferences.instance().isFirstLaunch()) { - classToStart = MenuAssistantActivity.class; - } else { - if (getIntent().getExtras() != null) { - String activity = getIntent().getExtras().getString("Activity", null); - if (ChatActivity.NAME.equals(activity)) { - classToStart = ChatActivity.class; - } else if (HistoryActivity.NAME.equals(activity)) { - classToStart = HistoryActivity.class; - } else if (ContactsActivity.NAME.equals(activity)) { - classToStart = ContactsActivity.class; - } else { - classToStart = DialerActivity.class; - } - } else { - classToStart = DialerActivity.class; - } - } - - Intent intent = new Intent(); - intent.setClass(LinphoneLauncherActivity.this, classToStart); - if (getIntent() != null && getIntent().getExtras() != null) { - intent.putExtras(getIntent().getExtras()); - } - intent.setAction(getIntent().getAction()); - intent.setType(getIntent().getType()); - intent.setData(getIntent().getData()); - startActivity(intent); - - LinphoneManager.getInstance().changeStatusToOnline(); - } -} diff --git a/app/src/main/java/org/linphone/activities/MainActivity.java b/app/src/main/java/org/linphone/activities/MainActivity.java deleted file mode 100644 index bd5091699..000000000 --- a/app/src/main/java/org/linphone/activities/MainActivity.java +++ /dev/null @@ -1,968 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.activities; - -import android.Manifest; -import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.FragmentTransaction; -import android.app.KeyguardManager; -import android.content.ActivityNotFoundException; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.view.KeyEvent; -import android.view.View; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.Toast; -import androidx.core.app.ActivityCompat; -import androidx.drawerlayout.widget.DrawerLayout; -import java.sql.Timestamp; -import java.util.ArrayList; -import java.util.Date; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.assistant.PhoneAccountLinkingAssistantActivity; -import org.linphone.call.CallActivity; -import org.linphone.call.CallIncomingActivity; -import org.linphone.call.CallOutgoingActivity; -import org.linphone.chat.ChatActivity; -import org.linphone.compatibility.Compatibility; -import org.linphone.contacts.ContactsActivity; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.core.AccountCreator; -import org.linphone.core.AccountCreatorListenerStub; -import org.linphone.core.Address; -import org.linphone.core.Call; -import org.linphone.core.ChatMessage; -import org.linphone.core.ChatRoom; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.ProxyConfig; -import org.linphone.core.RegistrationState; -import org.linphone.core.tools.Log; -import org.linphone.dialer.DialerActivity; -import org.linphone.fragments.EmptyFragment; -import org.linphone.fragments.StatusBarFragment; -import org.linphone.history.HistoryActivity; -import org.linphone.menu.SideMenuFragment; -import org.linphone.service.LinphoneService; -import org.linphone.settings.LinphonePreferences; -import org.linphone.settings.SettingsActivity; -import org.linphone.utils.DeviceUtils; -import org.linphone.utils.LinphoneUtils; - -public abstract class MainActivity extends LinphoneGenericActivity - implements StatusBarFragment.MenuClikedListener, SideMenuFragment.QuitClikedListener { - private static final int MAIN_PERMISSIONS = 1; - protected static final int FRAGMENT_SPECIFIC_PERMISSION = 2; - - private TextView mMissedCalls; - private TextView mMissedMessages; - protected View mContactsSelected; - protected View mHistorySelected; - protected View mDialerSelected; - protected View mChatSelected; - private LinearLayout mTopBar; - private TextView mTopBarTitle; - private LinearLayout mTabBar; - - private SideMenuFragment mSideMenuFragment; - private StatusBarFragment mStatusBarFragment; - - protected boolean mOnBackPressGoHome; - protected boolean mAlwaysHideTabBar; - protected String[] mPermissionsToHave; - - private CoreListenerStub mListener; - private AccountCreatorListenerStub mAccountCreatorListener; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.main); - - mOnBackPressGoHome = true; - mAlwaysHideTabBar = false; - - RelativeLayout history = findViewById(R.id.history); - history.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(MainActivity.this, HistoryActivity.class); - addFlagsToIntent(intent); - startActivity(intent); - } - }); - RelativeLayout contacts = findViewById(R.id.contacts); - contacts.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(MainActivity.this, ContactsActivity.class); - addFlagsToIntent(intent); - startActivity(intent); - } - }); - RelativeLayout dialer = findViewById(R.id.dialer); - dialer.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(MainActivity.this, DialerActivity.class); - addFlagsToIntent(intent); - startActivity(intent); - } - }); - RelativeLayout chat = findViewById(R.id.chat); - chat.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(MainActivity.this, ChatActivity.class); - addFlagsToIntent(intent); - startActivity(intent); - } - }); - - mMissedCalls = findViewById(R.id.missed_calls); - mMissedMessages = findViewById(R.id.missed_chats); - - mHistorySelected = findViewById(R.id.history_select); - mContactsSelected = findViewById(R.id.contacts_select); - mDialerSelected = findViewById(R.id.dialer_select); - mChatSelected = findViewById(R.id.chat_select); - - mTabBar = findViewById(R.id.footer); - mTopBar = findViewById(R.id.top_bar); - mTopBarTitle = findViewById(R.id.top_bar_title); - - ImageView back = findViewById(R.id.cancel); - back.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - goBack(); - } - }); - - mStatusBarFragment = - (StatusBarFragment) getFragmentManager().findFragmentById(R.id.status_fragment); - - DrawerLayout mSideMenu = findViewById(R.id.side_menu); - RelativeLayout mSideMenuContent = findViewById(R.id.side_menu_content); - mSideMenuFragment = - (SideMenuFragment) - getSupportFragmentManager().findFragmentById(R.id.side_menu_fragment); - mSideMenuFragment.setDrawer(mSideMenu, mSideMenuContent); - - if (getResources().getBoolean(R.bool.disable_chat)) { - chat.setVisibility(View.GONE); - } - - mListener = - new CoreListenerStub() { - @Override - public void onCallStateChanged( - Core core, Call call, Call.State state, String message) { - if (state == Call.State.End || state == Call.State.Released) { - displayMissedCalls(); - } - } - - @Override - public void onMessageReceived(Core core, ChatRoom room, ChatMessage message) { - displayMissedChats(); - } - - @Override - public void onChatRoomRead(Core core, ChatRoom room) { - displayMissedChats(); - } - - @Override - public void onMessageReceivedUnableDecrypt( - Core core, ChatRoom room, ChatMessage message) { - displayMissedChats(); - } - - @Override - public void onRegistrationStateChanged( - Core core, - ProxyConfig proxyConfig, - RegistrationState state, - String message) { - mSideMenuFragment.displayAccountsInSideMenu(); - - if (state == RegistrationState.Ok) { - // For push notifications to work on some devices, - // app must be in "protected mode" in battery settings... - // https://stackoverflow.com/questions/31638986/protected-apps-setting-on-huawei-phones-and-how-to-handle-it - DeviceUtils - .displayDialogIfDeviceHasPowerManagerThatCouldPreventPushNotifications( - MainActivity.this); - - if (getResources().getBoolean(R.bool.use_phone_number_validation)) { - if (proxyConfig - .getDomain() - .equals(getString(R.string.default_domain))) { - isAccountWithAlias(); - } - } - - if (!Compatibility.isDoNotDisturbSettingsAccessGranted( - MainActivity.this)) { - displayDNDSettingsDialog(); - } - } - } - - @Override - public void onLogCollectionUploadStateChanged( - Core core, Core.LogCollectionUploadState state, String info) { - Log.d( - "[Main Activity] Log upload state: " - + state.toString() - + ", info = " - + info); - if (state == Core.LogCollectionUploadState.Delivered) { - ClipboardManager clipboard = - (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("Logs url", info); - clipboard.setPrimaryClip(clip); - Toast.makeText( - MainActivity.this, - getString(R.string.logs_url_copied_to_clipboard), - Toast.LENGTH_SHORT) - .show(); - shareUploadedLogsUrl(info); - } - } - }; - - mAccountCreatorListener = - new AccountCreatorListenerStub() { - @Override - public void onIsAccountExist( - AccountCreator accountCreator, - AccountCreator.Status status, - String resp) { - if (status.equals(AccountCreator.Status.AccountExist)) { - accountCreator.isAccountLinked(); - } - } - - @Override - public void onLinkAccount( - AccountCreator accountCreator, - AccountCreator.Status status, - String resp) { - if (status.equals(AccountCreator.Status.AccountNotLinked)) { - askLinkWithPhoneNumber(); - } - } - - @Override - public void onIsAccountLinked( - AccountCreator accountCreator, - AccountCreator.Status status, - String resp) { - if (status.equals(AccountCreator.Status.AccountNotLinked)) { - askLinkWithPhoneNumber(); - } - } - }; - } - - @Override - protected void onStart() { - super.onStart(); - - requestRequiredPermissions(); - } - - @Override - protected void onResume() { - super.onResume(); - - LinphoneContext.instance() - .getNotificationManager() - .removeForegroundServiceNotificationIfPossible(); - - hideTopBar(); - if (!mAlwaysHideTabBar - && (getFragmentManager().getBackStackEntryCount() == 0 - || !getResources() - .getBoolean(R.bool.hide_bottom_bar_on_second_level_views))) { - showTabBar(); - } - - mHistorySelected.setVisibility(View.GONE); - mContactsSelected.setVisibility(View.GONE); - mDialerSelected.setVisibility(View.GONE); - mChatSelected.setVisibility(View.GONE); - - mStatusBarFragment.setMenuListener(this); - mSideMenuFragment.setQuitListener(this); - mSideMenuFragment.displayAccountsInSideMenu(); - - if (mSideMenuFragment.isOpened()) { - mSideMenuFragment.closeDrawer(); - } - - Core core = LinphoneManager.getCore(); - if (core != null) { - core.addListener(mListener); - displayMissedChats(); - displayMissedCalls(); - } - } - - @Override - protected void onPause() { - mStatusBarFragment.setMenuListener(null); - mSideMenuFragment.setQuitListener(null); - - Core core = LinphoneManager.getCore(); - if (core != null) { - core.removeListener(mListener); - } - - super.onPause(); - } - - @Override - protected void onDestroy() { - mMissedCalls = null; - mMissedMessages = null; - mContactsSelected = null; - mHistorySelected = null; - mDialerSelected = null; - mChatSelected = null; - mTopBar = null; - mTopBarTitle = null; - mTabBar = null; - - mSideMenuFragment = null; - mStatusBarFragment = null; - - mListener = null; - - super.onDestroy(); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - try { - super.onSaveInstanceState(outState); - } catch (IllegalStateException ise) { - // Do not log this exception - } - } - - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - try { - super.onRestoreInstanceState(savedInstanceState); - } catch (IllegalStateException ise) { - // Do not log this exception - } - } - - @Override - public void onMenuCliked() { - if (mSideMenuFragment.isOpened()) { - mSideMenuFragment.openOrCloseSideMenu(false, true); - } else { - mSideMenuFragment.openOrCloseSideMenu(true, true); - } - } - - @Override - public void onQuitClicked() { - quit(); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - if (mOnBackPressGoHome) { - if (getFragmentManager().getBackStackEntryCount() == 0) { - goHomeAndClearStack(); - return true; - } - } - goBack(); - return true; - } - return super.onKeyDown(keyCode, event); - } - - public boolean popBackStack() { - if (getFragmentManager().getBackStackEntryCount() > 0) { - getFragmentManager().popBackStackImmediate(); - if (!mAlwaysHideTabBar - && (getFragmentManager().getBackStackEntryCount() == 0 - && getResources() - .getBoolean(R.bool.hide_bottom_bar_on_second_level_views))) { - showTabBar(); - } - return true; - } - return false; - } - - public void goBack() { - finish(); - } - - protected boolean isTablet() { - return getResources().getBoolean(R.bool.isTablet); - } - - private void goHomeAndClearStack() { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - try { - startActivity(intent); - } catch (IllegalStateException ise) { - Log.e("[Main Activity] Can't start home activity: ", ise); - } - } - - private void quit() { - goHomeAndClearStack(); - if (LinphoneService.isReady() - && LinphonePreferences.instance().getServiceNotificationVisibility()) { - LinphoneService.instance().stopSelf(); - } - } - - // Tab, Top and Status bars - - public void hideStatusBar() { - findViewById(R.id.status_fragment).setVisibility(View.GONE); - } - - public void showStatusBar() { - findViewById(R.id.status_fragment).setVisibility(View.VISIBLE); - } - - public void hideTabBar() { - if (!isTablet()) { // do not hide if tablet, otherwise won't be able to navigate... - mTabBar.setVisibility(View.GONE); - } - } - - public void showTabBar() { - mTabBar.setVisibility(View.VISIBLE); - } - - protected void hideTopBar() { - mTopBar.setVisibility(View.GONE); - mTopBarTitle.setText(""); - } - - private void showTopBar() { - mTopBar.setVisibility(View.VISIBLE); - } - - protected void showTopBarWithTitle(String title) { - showTopBar(); - mTopBarTitle.setText(title); - } - - // Permissions - - public boolean checkPermission(String permission) { - int granted = getPackageManager().checkPermission(permission, getPackageName()); - Log.i( - "[Permission] " - + permission - + " permission is " - + (granted == PackageManager.PERMISSION_GRANTED ? "granted" : "denied")); - return granted == PackageManager.PERMISSION_GRANTED; - } - - public boolean checkPermissions(String[] permissions) { - boolean allGranted = true; - for (String permission : permissions) { - allGranted &= checkPermission(permission); - } - return allGranted; - } - - public void requestPermissionIfNotGranted(String permission) { - if (!checkPermission(permission)) { - Log.i("[Permission] Requesting " + permission + " permission"); - - String[] permissions = new String[] {permission}; - KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); - boolean locked = km.inKeyguardRestrictedInputMode(); - if (!locked) { - // This is to workaround an infinite loop of pause/start in Activity issue - // if incoming call ends while screen if off and locked - ActivityCompat.requestPermissions(this, permissions, FRAGMENT_SPECIFIC_PERMISSION); - } - } - } - - public void requestPermissionsIfNotGranted(String[] perms) { - requestPermissionsIfNotGranted(perms, FRAGMENT_SPECIFIC_PERMISSION); - } - - private void requestPermissionsIfNotGranted(String[] perms, int resultCode) { - ArrayList permissionsToAskFor = new ArrayList<>(); - if (perms != null) { // This is created (or not) by the child activity - for (String permissionToHave : perms) { - if (!checkPermission(permissionToHave)) { - permissionsToAskFor.add(permissionToHave); - } - } - } - - if (permissionsToAskFor.size() > 0) { - for (String permission : permissionsToAskFor) { - Log.i("[Permission] Requesting " + permission + " permission"); - } - String[] permissions = new String[permissionsToAskFor.size()]; - permissions = permissionsToAskFor.toArray(permissions); - - KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); - boolean locked = km.inKeyguardRestrictedInputMode(); - if (!locked) { - // This is to workaround an infinite loop of pause/start in Activity issue - // if incoming call ends while screen if off and locked - ActivityCompat.requestPermissions(this, permissions, resultCode); - } - } - } - - private void requestRequiredPermissions() { - requestPermissionsIfNotGranted(mPermissionsToHave, MAIN_PERMISSIONS); - } - - @Override - public void onRequestPermissionsResult( - int requestCode, String[] permissions, int[] grantResults) { - if (permissions.length <= 0) return; - - for (int i = 0; i < permissions.length; i++) { - Log.i( - "[Permission] " - + permissions[i] - + " is " - + (grantResults[i] == PackageManager.PERMISSION_GRANTED - ? "granted" - : "denied")); - if (permissions[i].equals(Manifest.permission.READ_CONTACTS) - || permissions[i].equals(Manifest.permission.WRITE_CONTACTS)) { - if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { - if (LinphoneContext.isReady()) { - ContactsManager.getInstance().enableContactsAccess(); - ContactsManager.getInstance().initializeContactManager(); - ContactsManager.getInstance().fetchContactsAsync(); - } - } - } else if (permissions[i].equals(Manifest.permission.READ_EXTERNAL_STORAGE)) { - boolean enableRingtone = grantResults[i] == PackageManager.PERMISSION_GRANTED; - LinphonePreferences.instance().enableDeviceRingtone(enableRingtone); - LinphoneManager.getInstance().enableDeviceRingtone(enableRingtone); - } else if (permissions[i].equals(Manifest.permission.CAMERA) - && grantResults[i] == PackageManager.PERMISSION_GRANTED) { - LinphoneUtils.reloadVideoDevices(); - } - } - } - - // Missed calls & chat indicators - - protected void displayMissedCalls() { - int count = 0; - Core core = LinphoneManager.getCore(); - if (core != null) { - count = core.getMissedCallsCount(); - } - - if (count > 0) { - mMissedCalls.setText(String.valueOf(count)); - mMissedCalls.setVisibility(View.VISIBLE); - } else { - mMissedCalls.clearAnimation(); - mMissedCalls.setVisibility(View.GONE); - } - } - - public void displayMissedChats() { - int count = 0; - Core core = LinphoneManager.getCore(); - if (core != null) { - count = core.getUnreadChatMessageCountFromActiveLocals(); - } - - if (count > 0) { - mMissedMessages.setText(String.valueOf(count)); - mMissedMessages.setVisibility(View.VISIBLE); - } else { - mMissedMessages.clearAnimation(); - mMissedMessages.setVisibility(View.GONE); - } - } - - // Navigation between actvities - - public void goBackToCall() { - boolean incoming = false; - boolean outgoing = false; - Call[] calls = LinphoneManager.getCore().getCalls(); - - for (Call call : calls) { - Call.State state = call.getState(); - switch (state) { - case IncomingEarlyMedia: - case IncomingReceived: - incoming = true; - break; - case OutgoingEarlyMedia: - case OutgoingInit: - case OutgoingProgress: - case OutgoingRinging: - outgoing = true; - break; - } - } - - if (incoming) { - startActivity(new Intent(this, CallIncomingActivity.class)); - } else if (outgoing) { - startActivity(new Intent(this, CallOutgoingActivity.class)); - } else { - startActivity(new Intent(this, CallActivity.class)); - } - } - - public void newOutgoingCall(String to) { - if (LinphoneManager.getCore().getCallsNb() > 0) { - Intent intent = new Intent(this, DialerActivity.class); - intent.addFlags( - Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - intent.putExtra("SipUri", to); - this.startActivity(intent); - } else { - LinphoneManager.getCallManager().newOutgoingCall(to, null); - } - } - - private void addFlagsToIntent(Intent intent) { - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - } - - protected void changeFragment(Fragment fragment, String name, boolean isChild) { - FragmentManager fragmentManager = getFragmentManager(); - FragmentTransaction transaction = fragmentManager.beginTransaction(); - - if (transaction.isAddToBackStackAllowed()) { - int count = fragmentManager.getBackStackEntryCount(); - if (count > 0) { - FragmentManager.BackStackEntry entry = - fragmentManager.getBackStackEntryAt(count - 1); - - if (entry != null && name.equals(entry.getName())) { - fragmentManager.popBackStack(); - if (!isChild) { - // We just removed it's duplicate from the back stack - // And we want at least one in it - transaction.addToBackStack(name); - } - } - } - - if (isChild) { - transaction.addToBackStack(name); - } - } - - if (getResources().getBoolean(R.bool.hide_bottom_bar_on_second_level_views)) { - if (isChild) { - if (!isTablet()) { - hideTabBar(); - } - } else { - showTabBar(); - } - } - - Compatibility.setFragmentTransactionReorderingAllowed(transaction, false); - if (isChild && isTablet()) { - transaction.replace(R.id.fragmentContainer2, fragment, name); - findViewById(R.id.fragmentContainer2).setVisibility(View.VISIBLE); - } else { - transaction.replace(R.id.fragmentContainer, fragment, name); - } - transaction.commitAllowingStateLoss(); - fragmentManager.executePendingTransactions(); - } - - public void showEmptyChildFragment() { - changeFragment(new EmptyFragment(), "Empty", true); - } - - public void showAccountSettings(int accountIndex) { - Intent intent = new Intent(this, SettingsActivity.class); - addFlagsToIntent(intent); - intent.putExtra("Account", accountIndex); - startActivity(intent); - } - - public void showContactDetails(LinphoneContact contact) { - Intent intent = new Intent(this, ContactsActivity.class); - addFlagsToIntent(intent); - intent.putExtra("Contact", contact); - startActivity(intent); - } - - public void showContactsListForCreationOrEdition(Address address) { - if (address == null) return; - - Intent intent = new Intent(this, ContactsActivity.class); - addFlagsToIntent(intent); - intent.putExtra("CreateOrEdit", true); - intent.putExtra("SipUri", address.asStringUriOnly()); - if (address.getDisplayName() != null) { - intent.putExtra("DisplayName", address.getDisplayName()); - } - startActivity(intent); - } - - public void showChatRoom(Address localAddress, Address peerAddress) { - Intent intent = new Intent(this, ChatActivity.class); - addFlagsToIntent(intent); - if (localAddress != null) { - intent.putExtra("LocalSipUri", localAddress.asStringUriOnly()); - } - if (peerAddress != null) { - intent.putExtra("RemoteSipUri", peerAddress.asStringUriOnly()); - } - startActivity(intent); - } - - // Dialogs - - public Dialog displayDialog(String text) { - return LinphoneUtils.getDialog(this, text); - } - - public void displayChatRoomError() { - final Dialog dialog = displayDialog(getString(R.string.chat_room_creation_failed)); - dialog.findViewById(R.id.dialog_delete_button).setVisibility(View.GONE); - Button cancel = dialog.findViewById(R.id.dialog_cancel_button); - cancel.setText(getString(R.string.ok)); - cancel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - dialog.dismiss(); - } - }); - - dialog.show(); - } - - private void displayDNDSettingsDialog() { - if (!LinphonePreferences.instance().isDNDSettingsPopupEnabled()) return; - Log.w("[Permission] Asking user to grant us permission to read DND settings"); - - final Dialog dialog = - displayDialog(getString(R.string.pref_grant_read_dnd_settings_permission_desc)); - dialog.findViewById(R.id.dialog_do_not_ask_again_layout).setVisibility(View.VISIBLE); - final CheckBox doNotAskAgain = dialog.findViewById(R.id.doNotAskAgain); - dialog.findViewById(R.id.doNotAskAgainLabel) - .setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - doNotAskAgain.setChecked(!doNotAskAgain.isChecked()); - } - }); - Button cancel = dialog.findViewById(R.id.dialog_cancel_button); - cancel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - if (doNotAskAgain.isChecked()) { - LinphonePreferences.instance().enableDNDSettingsPopup(false); - } - dialog.dismiss(); - } - }); - Button ok = dialog.findViewById(R.id.dialog_ok_button); - ok.setVisibility(View.VISIBLE); - ok.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - try { - startActivity( - new Intent( - "android.settings.NOTIFICATION_POLICY_ACCESS_SETTINGS")); - } catch (ActivityNotFoundException anfe) { - Log.e("[Main Activity] Activity not found exception: ", anfe); - } - dialog.dismiss(); - } - }); - Button delete = dialog.findViewById(R.id.dialog_delete_button); - delete.setVisibility(View.GONE); - dialog.show(); - } - - public void isAccountWithAlias() { - if (LinphoneManager.getCore().getDefaultProxyConfig() != null) { - long now = new Timestamp(new Date().getTime()).getTime(); - AccountCreator accountCreator = LinphoneManager.getInstance().getAccountCreator(); - accountCreator.setListener(mAccountCreatorListener); - if (LinphonePreferences.instance().getLinkPopupTime() == null - || Long.parseLong(LinphonePreferences.instance().getLinkPopupTime()) < now) { - accountCreator.reset(); - accountCreator.setUsername( - LinphonePreferences.instance() - .getAccountUsername( - LinphonePreferences.instance().getDefaultAccountIndex())); - accountCreator.isAccountExist(); - } - } else { - LinphonePreferences.instance().setLinkPopupTime(null); - } - } - - private void askLinkWithPhoneNumber() { - if (!LinphonePreferences.instance().isLinkPopupEnabled()) return; - - long now = new Timestamp(new Date().getTime()).getTime(); - if (LinphonePreferences.instance().getLinkPopupTime() != null - && Long.parseLong(LinphonePreferences.instance().getLinkPopupTime()) >= now) return; - - ProxyConfig proxyConfig = LinphoneManager.getCore().getDefaultProxyConfig(); - if (proxyConfig == null) return; - if (!proxyConfig.getDomain().equals(getString(R.string.default_domain))) return; - - final Dialog dialog = - LinphoneUtils.getDialog( - this, - String.format( - getString(R.string.link_account_popup), - proxyConfig.getIdentityAddress().asStringUriOnly())); - Button delete = dialog.findViewById(R.id.dialog_delete_button); - delete.setVisibility(View.GONE); - Button ok = dialog.findViewById(R.id.dialog_ok_button); - ok.setText(getString(R.string.link)); - ok.setVisibility(View.VISIBLE); - Button cancel = dialog.findViewById(R.id.dialog_cancel_button); - cancel.setText(getString(R.string.maybe_later)); - - dialog.findViewById(R.id.dialog_do_not_ask_again_layout).setVisibility(View.VISIBLE); - final CheckBox doNotAskAgain = dialog.findViewById(R.id.doNotAskAgain); - dialog.findViewById(R.id.doNotAskAgainLabel) - .setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - doNotAskAgain.setChecked(!doNotAskAgain.isChecked()); - } - }); - - ok.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - Intent assistant = new Intent(); - assistant.setClass( - MainActivity.this, PhoneAccountLinkingAssistantActivity.class); - startActivity(assistant); - updatePopupTimestamp(); - dialog.dismiss(); - } - }); - - cancel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - if (doNotAskAgain.isChecked()) { - LinphonePreferences.instance().enableLinkPopup(false); - } - updatePopupTimestamp(); - dialog.dismiss(); - } - }); - dialog.show(); - } - - private void updatePopupTimestamp() { - long future = - new Timestamp( - getResources() - .getInteger( - R.integer.phone_number_linking_popup_time_interval)) - .getTime(); - long now = new Timestamp(new Date().getTime()).getTime(); - long newDate = now + future; - - LinphonePreferences.instance().setLinkPopupTime(String.valueOf(newDate)); - } - - // Logs - - private void shareUploadedLogsUrl(String info) { - final String appName = getString(R.string.app_name); - - Intent i = new Intent(Intent.ACTION_SEND); - i.putExtra(Intent.EXTRA_EMAIL, new String[] {getString(R.string.about_bugreport_email)}); - i.putExtra(Intent.EXTRA_SUBJECT, appName + " Logs"); - i.putExtra(Intent.EXTRA_TEXT, info); - i.setType("application/zip"); - - try { - startActivity(Intent.createChooser(i, "Send mail...")); - } catch (android.content.ActivityNotFoundException ex) { - Log.e(ex); - } - } - - // Others - - public SideMenuFragment getSideMenuFragment() { - return mSideMenuFragment; - } -} diff --git a/app/src/main/java/org/linphone/call/views/CallIncomingButtonListener.java b/app/src/main/java/org/linphone/activities/SnackBarActivity.kt similarity index 82% rename from app/src/main/java/org/linphone/call/views/CallIncomingButtonListener.java rename to app/src/main/java/org/linphone/activities/SnackBarActivity.kt index 9127431ec..439999b58 100644 --- a/app/src/main/java/org/linphone/call/views/CallIncomingButtonListener.java +++ b/app/src/main/java/org/linphone/activities/SnackBarActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2019 Belledonne Communications SARL. + * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,8 +17,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.call.views; +package org.linphone.activities -public interface CallIncomingButtonListener { - void onAction(); +interface SnackBarActivity { + fun showSnackBar(resourceId: Int) } diff --git a/app/src/main/java/org/linphone/activities/ThemeableActivity.java b/app/src/main/java/org/linphone/activities/ThemeableActivity.java deleted file mode 100644 index ce5d10e6a..000000000 --- a/app/src/main/java/org/linphone/activities/ThemeableActivity.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.activities; - -import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.app.AppCompatDelegate; -import org.linphone.R; -import org.linphone.settings.LinphonePreferences; - -public abstract class ThemeableActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - if (getResources().getBoolean(R.bool.orientation_portrait_only)) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - } - - super.onCreate(savedInstanceState); - } - - @Override - protected void onResume() { - super.onResume(); - - int nightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - switch (nightMode) { - case Configuration.UI_MODE_NIGHT_NO: - case Configuration.UI_MODE_NIGHT_UNDEFINED: - if (LinphonePreferences.instance().isDarkModeEnabled()) { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); - } - case Configuration.UI_MODE_NIGHT_YES: - if (!LinphonePreferences.instance().isDarkModeEnabled()) { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/AssistantActivity.kt b/app/src/main/java/org/linphone/activities/assistant/AssistantActivity.kt new file mode 100644 index 000000000..b222e0b4c --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/AssistantActivity.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant + +import android.os.Bundle +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.snackbar.Snackbar +import org.linphone.R +import org.linphone.activities.GenericActivity +import org.linphone.activities.SnackBarActivity +import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel +import org.linphone.databinding.AssistantActivityBinding + +class AssistantActivity : GenericActivity(), SnackBarActivity { + private lateinit var binding: AssistantActivityBinding + private lateinit var sharedViewModel: SharedAssistantViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.assistant_activity) + + sharedViewModel = ViewModelProvider(this).get(SharedAssistantViewModel::class.java) + } + + override fun showSnackBar(resourceId: Int) { + Snackbar.make(binding.coordinator, resourceId, Snackbar.LENGTH_LONG).show() + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/adapters/CountryPickerAdapter.kt b/app/src/main/java/org/linphone/activities/assistant/adapters/CountryPickerAdapter.kt new file mode 100644 index 000000000..0b36c1637 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/adapters/CountryPickerAdapter.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.Filter +import android.widget.Filterable +import android.widget.TextView +import kotlin.collections.ArrayList +import org.linphone.R +import org.linphone.core.DialPlan +import org.linphone.core.Factory + +class CountryPickerAdapter : BaseAdapter(), Filterable { + private var countries: ArrayList + + init { + val dialPlans = Factory.instance().dialPlans + countries = arrayListOf() + countries.addAll(dialPlans) + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view: View = convertView ?: LayoutInflater.from(parent.context).inflate(R.layout.assistant_country_picker_cell, parent, false) + val dialPlan: DialPlan = countries[position] + + val name = view.findViewById(R.id.country_name) + name.text = dialPlan.country + + val dialCode = view.findViewById(R.id.country_prefix) + dialCode.text = String.format("(%s)", dialPlan.countryCallingCode) + + view.tag = dialPlan + return view + } + + override fun getItem(position: Int): DialPlan { + return countries[position] + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getCount(): Int { + return countries.size + } + + override fun getFilter(): Filter { + return object : Filter() { + override fun performFiltering(constraint: CharSequence): FilterResults { + val filteredCountries = arrayListOf() + for (dialPlan in Factory.instance().dialPlans) { + if (dialPlan.country.contains(constraint, ignoreCase = true) || + dialPlan.countryCallingCode.contains(constraint) + ) { + filteredCountries.add(dialPlan) + } + } + val filterResults = FilterResults() + filterResults.values = filteredCountries + return filterResults + } + + @Suppress("UNCHECKED_CAST") + override fun publishResults( + constraint: CharSequence, + results: FilterResults + ) { + countries = results.values as ArrayList + notifyDataSetChanged() + } + } + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/AbstractPhoneFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/AbstractPhoneFragment.kt new file mode 100644 index 000000000..6e5416032 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/AbstractPhoneFragment.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +package org.linphone.activities.assistant.fragments + +import android.Manifest +import android.app.AlertDialog +import android.content.pm.PackageManager +import androidx.fragment.app.Fragment +import org.linphone.R +import org.linphone.activities.assistant.viewmodels.AbstractPhoneViewModel +import org.linphone.core.tools.Log +import org.linphone.utils.PermissionHelper +import org.linphone.utils.PhoneNumberUtils + +abstract class AbstractPhoneFragment : Fragment() { + abstract val viewModel: AbstractPhoneViewModel + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + if (requestCode == 0) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.i("[Assistant] READ_PHONE_NUMBERS permission granted") + updateFromDeviceInfo() + } else { + Log.w("[Assistant] READ_PHONE_NUMBERS permission denied") + } + } + } + + protected fun checkPermission() { + if (!PermissionHelper.get().hasReadPhoneState()) { + Log.i("[Assistant] Asking for READ_PHONE_STATE permission") + requestPermissions(arrayOf(Manifest.permission.READ_PHONE_STATE), 0) + } else { + updateFromDeviceInfo() + } + } + + private fun updateFromDeviceInfo() { + val phoneNumber = PhoneNumberUtils.getDevicePhoneNumber(requireContext()) + val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(requireContext()) + viewModel.updateFromPhoneNumberAndOrDialPlan(phoneNumber, dialPlan) + } + + protected fun showPhoneNumberInfoDialog() { + AlertDialog.Builder(context) + .setTitle(getString(R.string.assistant_phone_number_info_title)) + .setMessage( + getString(R.string.assistant_phone_number_link_info_content) + "\n" + + getString( + R.string.assistant_phone_number_link_info_content_already_account + ) + ) + .show() + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt new file mode 100644 index 000000000..30c5977e0 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.assistant.viewmodels.AccountLoginViewModel +import org.linphone.activities.assistant.viewmodels.AccountLoginViewModelFactory +import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel +import org.linphone.databinding.AssistantAccountLoginFragmentBinding + +class AccountLoginFragment : AbstractPhoneFragment() { + private lateinit var binding: AssistantAccountLoginFragmentBinding + override lateinit var viewModel: AccountLoginViewModel + private lateinit var sharedViewModel: SharedAssistantViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantAccountLoginFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedAssistantViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + viewModel = ViewModelProvider(this, AccountLoginViewModelFactory(sharedViewModel.getAccountCreator())).get(AccountLoginViewModel::class.java) + binding.viewModel = viewModel + + binding.setInfoClickListener { + showPhoneNumberInfoDialog() + } + + binding.setSelectCountryClickListener { + CountryPickerFragment(viewModel).show(childFragmentManager, "CountryPicker") + } + + viewModel.goToSmsValidationEvent.observe(viewLifecycleOwner, Observer { + it.consume { + if (findNavController().currentDestination?.id == R.id.accountLoginFragment) { + val args = Bundle() + args.putBoolean("IsLogin", true) + args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber) + findNavController().navigate(R.id.action_accountLoginFragment_to_phoneAccountValidationFragment, args) + } + } + }) + + viewModel.leaveAssistantEvent.observe(viewLifecycleOwner, Observer { + it.consume { + if (coreContext.core.isEchoCancellerCalibrationRequired) { + if (findNavController().currentDestination?.id == R.id.accountLoginFragment) { + findNavController().navigate(R.id.action_accountLoginFragment_to_echoCancellerCalibrationFragment) + } + } else { + requireActivity().finish() + } + } + }) + + checkPermission() + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/CountryPickerFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/CountryPickerFragment.kt new file mode 100644 index 000000000..488a9cba5 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/CountryPickerFragment.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.fragments + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import org.linphone.activities.assistant.adapters.CountryPickerAdapter +import org.linphone.core.DialPlan +import org.linphone.databinding.AssistantCountryPickerFragmentBinding + +class CountryPickerFragment(private val listener: CountryPickedListener) : DialogFragment() { + private lateinit var binding: AssistantCountryPickerFragmentBinding + private lateinit var adapter: CountryPickerAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantCountryPickerFragmentBinding.inflate(inflater, container, false) + + adapter = CountryPickerAdapter() + binding.countryList.adapter = adapter + + binding.countryList.setOnItemClickListener { _, _, position, _ -> + if (position > 0 && position < adapter.count) { + val dialPlan = adapter.getItem(position) + listener.onCountryClicked(dialPlan) + } + dismiss() + } + + binding.searchCountry.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + adapter.filter.filter(s) + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { } + }) + + return binding.root + } + + interface CountryPickedListener { + fun onCountryClicked(dialPlan: DialPlan) + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/EchoCancellerCalibrationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/EchoCancellerCalibrationFragment.kt new file mode 100644 index 000000000..d1883fe34 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/EchoCancellerCalibrationFragment.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.fragments + +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import org.linphone.activities.assistant.viewmodels.EchoCancellerCalibrationViewModel +import org.linphone.core.tools.Log +import org.linphone.databinding.AssistantEchoCancellerCalibrationFragmentBinding +import org.linphone.utils.PermissionHelper + +class EchoCancellerCalibrationFragment : Fragment() { + private lateinit var binding: AssistantEchoCancellerCalibrationFragmentBinding + private lateinit var viewModel: EchoCancellerCalibrationViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantEchoCancellerCalibrationFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(EchoCancellerCalibrationViewModel::class.java) + binding.viewModel = viewModel + + viewModel.echoCalibrationTerminated.observe(viewLifecycleOwner, Observer { + it.consume { + requireActivity().finish() + } + }) + + if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) { + Log.i("[Echo Canceller Calibration] Asking for RECORD_AUDIO permission") + requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 0) + } else { + viewModel.startEchoCancellerCalibration() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + val granted = grantResults[0] == PackageManager.PERMISSION_GRANTED + if (granted) { + Log.i("[Echo Canceller Calibration] RECORD_AUDIO permission granted") + viewModel.startEchoCancellerCalibration() + } else { + Log.w("[Echo Canceller Calibration] RECORD_AUDIO permission denied") + requireActivity().finish() + } + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountCreationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountCreationFragment.kt new file mode 100644 index 000000000..ad1d394b4 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountCreationFragment.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.assistant.viewmodels.EmailAccountCreationViewModel +import org.linphone.activities.assistant.viewmodels.EmailAccountCreationViewModelFactory +import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel +import org.linphone.databinding.AssistantEmailAccountCreationFragmentBinding + +class EmailAccountCreationFragment : Fragment() { + private lateinit var binding: AssistantEmailAccountCreationFragmentBinding + private lateinit var sharedViewModel: SharedAssistantViewModel + private lateinit var viewModel: EmailAccountCreationViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantEmailAccountCreationFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedAssistantViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + viewModel = ViewModelProvider(this, EmailAccountCreationViewModelFactory(sharedViewModel.getAccountCreator())).get(EmailAccountCreationViewModel::class.java) + binding.viewModel = viewModel + + viewModel.goToEmailValidationEvent.observe(viewLifecycleOwner, Observer { + it.consume { + if (findNavController().currentDestination?.id == R.id.emailAccountCreationFragment) { + findNavController().navigate(R.id.action_emailAccountCreationFragment_to_emailAccountValidationFragment) + } + } + }) + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountValidationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountValidationFragment.kt new file mode 100644 index 000000000..3547ed61d --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountValidationFragment.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.assistant.viewmodels.* +import org.linphone.databinding.AssistantEmailAccountValidationFragmentBinding + +class EmailAccountValidationFragment : Fragment() { + private lateinit var binding: AssistantEmailAccountValidationFragmentBinding + private lateinit var sharedViewModel: SharedAssistantViewModel + private lateinit var viewModel: EmailAccountValidationViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantEmailAccountValidationFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedAssistantViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + viewModel = ViewModelProvider(this, EmailAccountValidationViewModelFactory(sharedViewModel.getAccountCreator())).get(EmailAccountValidationViewModel::class.java) + binding.viewModel = viewModel + + viewModel.leaveAssistantEvent.observe(viewLifecycleOwner, Observer { + it.consume { + if (findNavController().currentDestination?.id == R.id.emailAccountValidationFragment) { + val args = Bundle() + args.putBoolean("AllowSkip", true) + args.putString("Username", viewModel.accountCreator.username) + args.putString("Password", viewModel.accountCreator.password) + findNavController().navigate(R.id.action_emailAccountValidationFragment_to_phoneAccountLinkingFragment, args) + } + } + }) + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountLoginFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountLoginFragment.kt new file mode 100644 index 000000000..a05a290ac --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountLoginFragment.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.assistant.viewmodels.GenericLoginViewModel +import org.linphone.activities.assistant.viewmodels.GenericLoginViewModelFactory +import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel +import org.linphone.databinding.AssistantGenericAccountLoginFragmentBinding + +class GenericAccountLoginFragment : Fragment() { + private lateinit var binding: AssistantGenericAccountLoginFragmentBinding + private lateinit var sharedViewModel: SharedAssistantViewModel + private lateinit var viewModel: GenericLoginViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantGenericAccountLoginFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedAssistantViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + viewModel = ViewModelProvider(this, GenericLoginViewModelFactory(sharedViewModel.getAccountCreator(true))).get(GenericLoginViewModel::class.java) + binding.viewModel = viewModel + + viewModel.leaveAssistantEvent.observe(viewLifecycleOwner, Observer { + it.consume { + if (coreContext.core.isEchoCancellerCalibrationRequired) { + if (findNavController().currentDestination?.id == R.id.genericAccountLoginFragment) { + findNavController().navigate(R.id.action_genericAccountLoginFragment_to_echoCancellerCalibrationFragment) + } + } else { + requireActivity().finish() + } + } + }) + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt new file mode 100644 index 000000000..6b5c194d5 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.assistant.viewmodels.PhoneAccountCreationViewModel +import org.linphone.activities.assistant.viewmodels.PhoneAccountCreationViewModelFactory +import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel +import org.linphone.databinding.AssistantPhoneAccountCreationFragmentBinding + +class PhoneAccountCreationFragment : AbstractPhoneFragment() { + private lateinit var binding: AssistantPhoneAccountCreationFragmentBinding + private lateinit var sharedViewModel: SharedAssistantViewModel + override lateinit var viewModel: PhoneAccountCreationViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantPhoneAccountCreationFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedAssistantViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + viewModel = ViewModelProvider(this, PhoneAccountCreationViewModelFactory(sharedViewModel.getAccountCreator())).get(PhoneAccountCreationViewModel::class.java) + binding.viewModel = viewModel + + binding.setInfoClickListener { + showPhoneNumberInfoDialog() + } + + binding.setSelectCountryClickListener { + CountryPickerFragment(viewModel).show(childFragmentManager, "CountryPicker") + } + + viewModel.goToSmsValidationEvent.observe(viewLifecycleOwner, Observer { + it.consume { + if (findNavController().currentDestination?.id == R.id.phoneAccountCreationFragment) { + val args = Bundle() + args.putBoolean("IsCreation", true) + args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber) + findNavController().navigate(R.id.action_phoneAccountCreationFragment_to_phoneAccountValidationFragment, args) + } + } + }) + + checkPermission() + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt new file mode 100644 index 000000000..151e1fa72 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.LinphoneApplication +import org.linphone.R +import org.linphone.activities.assistant.viewmodels.* +import org.linphone.core.tools.Log +import org.linphone.databinding.AssistantPhoneAccountLinkingFragmentBinding + +class PhoneAccountLinkingFragment : AbstractPhoneFragment() { + private lateinit var binding: AssistantPhoneAccountLinkingFragmentBinding + private lateinit var sharedViewModel: SharedAssistantViewModel + override lateinit var viewModel: PhoneAccountLinkingViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantPhoneAccountLinkingFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedAssistantViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + val accountCreator = sharedViewModel.getAccountCreator() + viewModel = ViewModelProvider(this, PhoneAccountLinkingViewModelFactory(accountCreator)).get(PhoneAccountLinkingViewModel::class.java) + binding.viewModel = viewModel + + val username = arguments?.getString("Username") + Log.i("[Phone Account Linking] username to link is $username") + viewModel.username.value = username + + val password = arguments?.getString("Password") + accountCreator.password = password + + val ha1 = arguments?.getString("HA1") + accountCreator.ha1 = ha1 + + val allowSkip = arguments?.getBoolean("AllowSkip", false) + viewModel.allowSkip.value = allowSkip + + binding.setInfoClickListener { + showPhoneNumberInfoDialog() + } + + binding.setSelectCountryClickListener { + CountryPickerFragment(viewModel).show(childFragmentManager, "CountryPicker") + } + + viewModel.goToSmsValidationEvent.observe(viewLifecycleOwner, Observer { + it.consume { + if (findNavController().currentDestination?.id == R.id.phoneAccountLinkingFragment) { + val args = Bundle() + args.putBoolean("IsLinking", true) + args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber) + findNavController().navigate(R.id.action_phoneAccountLinkingFragment_to_phoneAccountValidationFragment, args) + } + } + }) + + viewModel.leaveAssistantEvent.observe(viewLifecycleOwner, Observer { + it.consume { + if (findNavController().currentDestination?.id == R.id.phoneAccountLinkingFragment) { + if (LinphoneApplication.coreContext.core.isEchoCancellerCalibrationRequired) { + if (findNavController().currentDestination?.id == R.id.phoneAccountValidationFragment) { + findNavController().navigate(R.id.action_phoneAccountLinkingFragment_to_echoCancellerCalibrationFragment) + } + } else { + requireActivity().finish() + } + } + } + }) + + checkPermission() + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt new file mode 100644 index 000000000..930c2488e --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.fragments + +import android.content.ClipboardManager +import android.content.Context.CLIPBOARD_SERVICE +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewModel +import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewModelFactory +import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel +import org.linphone.databinding.AssistantPhoneAccountValidationFragmentBinding + +class PhoneAccountValidationFragment : Fragment() { + private lateinit var binding: AssistantPhoneAccountValidationFragmentBinding + private lateinit var sharedViewModel: SharedAssistantViewModel + private lateinit var viewModel: PhoneAccountValidationViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantPhoneAccountValidationFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedAssistantViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + viewModel = ViewModelProvider(this, PhoneAccountValidationViewModelFactory(sharedViewModel.getAccountCreator())).get(PhoneAccountValidationViewModel::class.java) + binding.viewModel = viewModel + + viewModel.phoneNumber.value = arguments?.getString("PhoneNumber") + viewModel.isLogin.value = arguments?.getBoolean("IsLogin", false) + viewModel.isCreation.value = arguments?.getBoolean("IsCreation", false) + viewModel.isLinking.value = arguments?.getBoolean("IsLinking", false) + + viewModel.leaveAssistantEvent.observe(viewLifecycleOwner, Observer { + it.consume { + when { + viewModel.isLogin.value == true || viewModel.isCreation.value == true -> { + if (coreContext.core.isEchoCancellerCalibrationRequired) { + if (findNavController().currentDestination?.id == R.id.phoneAccountValidationFragment) { + findNavController().navigate(R.id.action_phoneAccountValidationFragment_to_echoCancellerCalibrationFragment) + } + } else { + requireActivity().finish() + } + } + viewModel.isLinking.value == true -> { + if (findNavController().currentDestination?.id == R.id.phoneAccountValidationFragment) { + val args = Bundle() + args.putString("Identity", "sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}") + findNavController().navigate(R.id.action_phoneAccountValidationFragment_to_accountSettingsFragment, args) + } + } + } + } + }) + + val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + clipboard.addPrimaryClipChangedListener { + val data = clipboard.primaryClip + if (data != null && data.itemCount > 0) { + val clip = data.getItemAt(0).text.toString() + if (clip.length == 4) { + viewModel.code.value = clip + } + } + } + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/QrCodeFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/QrCodeFragment.kt new file mode 100644 index 000000000..efb785649 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/QrCodeFragment.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.fragments + +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.activities.assistant.viewmodels.QrCodeViewModel +import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel +import org.linphone.core.tools.Log +import org.linphone.databinding.AssistantQrCodeFragmentBinding +import org.linphone.utils.PermissionHelper + +class QrCodeFragment : Fragment() { + private lateinit var binding: AssistantQrCodeFragmentBinding + private lateinit var sharedViewModel: SharedAssistantViewModel + private lateinit var viewModel: QrCodeViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantQrCodeFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedAssistantViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + viewModel = ViewModelProvider(this).get(QrCodeViewModel::class.java) + binding.viewModel = viewModel + + viewModel.qrCodeFoundEvent.observe(viewLifecycleOwner, Observer { + it.consume { url -> + sharedViewModel.remoteProvisioningUrl.value = url + findNavController().navigateUp() + } + }) + viewModel.setBackCamera() + + if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) { + Log.i("[QR Code] Asking for CAMERA permission") + requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 0) + } + } + + override fun onResume() { + super.onResume() + + coreContext.core.nativePreviewWindowId = binding.qrCodeCaptureTexture + coreContext.core.enableQrcodeVideoPreview(true) + coreContext.core.enableVideoPreview(true) + } + + override fun onPause() { + coreContext.core.nativePreviewWindowId = null + coreContext.core.enableQrcodeVideoPreview(false) + coreContext.core.enableVideoPreview(false) + + super.onPause() + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + val granted = grantResults[0] == PackageManager.PERMISSION_GRANTED + if (granted) { + Log.i("[QR Code] CAMERA permission granted") + } else { + Log.w("[QR Code] CAMERA permission denied") + findNavController().navigateUp() + } + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/RemoteProvisioningFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/RemoteProvisioningFragment.kt new file mode 100644 index 000000000..2125479da --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/RemoteProvisioningFragment.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.fragments + +import android.os.Bundle +import android.util.Patterns +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.assistant.AssistantActivity +import org.linphone.activities.assistant.viewmodels.RemoteProvisioningViewModel +import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel +import org.linphone.databinding.AssistantRemoteProvisioningFragmentBinding + +class RemoteProvisioningFragment : Fragment() { + private lateinit var binding: AssistantRemoteProvisioningFragmentBinding + private lateinit var sharedViewModel: SharedAssistantViewModel + private lateinit var viewModel: RemoteProvisioningViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantRemoteProvisioningFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedAssistantViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + viewModel = ViewModelProvider(this).get(RemoteProvisioningViewModel::class.java) + binding.viewModel = viewModel + + binding.setApplyClickListener { + val url = viewModel.urlToFetch.value.orEmpty() + if (Patterns.WEB_URL.matcher(url).matches()) { + viewModel.fetchAndApply(url) + } else { + val activity = requireActivity() as AssistantActivity + activity.showSnackBar(R.string.assistant_remote_provisioning_wrong_format) + } + } + + binding.setQrCodeClickListener { + if (findNavController().currentDestination?.id == R.id.remoteProvisioningFragment) { + findNavController().navigate(R.id.action_remoteProvisioningFragment_to_qrCodeFragment) + } + } + + viewModel.fetchSuccessfulEvent.observe(viewLifecycleOwner, Observer { + it.consume { success -> + if (success) { + if (coreContext.core.isEchoCancellerCalibrationRequired) { + if (findNavController().currentDestination?.id == R.id.remoteProvisioningFragment) { + findNavController().navigate(R.id.action_remoteProvisioningFragment_to_echoCancellerCalibrationFragment) + } + } else { + requireActivity().finish() + } + } else { + val activity = requireActivity() as AssistantActivity + activity.showSnackBar(R.string.assistant_remote_provisioning_failure) + } + } + }) + + viewModel.urlToFetch.value = sharedViewModel.remoteProvisioningUrl.value ?: coreContext.core.provisioningUri + } + + override fun onDestroy() { + super.onDestroy() + sharedViewModel.remoteProvisioningUrl.value = null + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/TopBarFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/TopBarFragment.kt new file mode 100644 index 000000000..85dd13c3a --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/TopBarFragment.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import org.linphone.databinding.AssistantTopBarFragmentBinding + +class TopBarFragment : Fragment() { + private lateinit var binding: AssistantTopBarFragmentBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantTopBarFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + binding.setBackClickListener { + if (!findNavController().popBackStack()) { + activity?.finish() + } + } + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/WelcomeFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/WelcomeFragment.kt new file mode 100644 index 000000000..3802e7350 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/WelcomeFragment.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.databinding.AssistantWelcomeFragmentBinding + +class WelcomeFragment : Fragment() { + private lateinit var binding: AssistantWelcomeFragmentBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantWelcomeFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + binding.setCreateAccountClickListener { + if (findNavController().currentDestination?.id == R.id.welcomeFragment) { + if (resources.getBoolean(R.bool.isTablet)) { + findNavController().navigate(R.id.action_welcomeFragment_to_emailAccountCreationFragment) + } else { + findNavController().navigate(R.id.action_welcomeFragment_to_phoneAccountCreationFragment) + } + } + } + + binding.setAccountLoginClickListener { + if (findNavController().currentDestination?.id == R.id.welcomeFragment) { + findNavController().navigate(R.id.action_welcomeFragment_to_accountLoginFragment) + } + } + + binding.setGenericAccountLoginClickListener { + if (findNavController().currentDestination?.id == R.id.welcomeFragment) { + findNavController().navigate(R.id.action_welcomeFragment_to_genericAccountLoginFragment) + } + } + + binding.setRemoteProvisioningClickListener { + if (findNavController().currentDestination?.id == R.id.welcomeFragment) { + findNavController().navigate(R.id.action_welcomeFragment_to_remoteProvisioningFragment) + } + } + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPhoneViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPhoneViewModel.kt new file mode 100644 index 000000000..c9a3b48de --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPhoneViewModel.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +package org.linphone.activities.assistant.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModel +import org.linphone.activities.assistant.fragments.CountryPickerFragment +import org.linphone.core.AccountCreator +import org.linphone.core.DialPlan +import org.linphone.core.tools.Log +import org.linphone.utils.PhoneNumberUtils + +abstract class AbstractPhoneViewModel(val accountCreator: AccountCreator) : ViewModel(), + CountryPickerFragment.CountryPickedListener { + + val prefix = MutableLiveData() + + val phoneNumber = MutableLiveData() + val phoneNumberError = MutableLiveData() + + val countryName: LiveData = Transformations.switchMap(prefix) { + getCountryNameFromPrefix(it) + } + + init { + prefix.value = "+" + } + + override fun onCountryClicked(dialPlan: DialPlan) { + prefix.value = "+${dialPlan.countryCallingCode}" + } + + fun isPhoneNumberOk(): Boolean { + return countryName.value.orEmpty().isNotEmpty() && phoneNumber.value.orEmpty().isNotEmpty() && phoneNumberError.value.orEmpty().isEmpty() + } + + fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) { + if (dialPlan != null) { + Log.i("[Assistant] Found prefix from dial plan: ${dialPlan.countryCallingCode}") + prefix.value = "+${dialPlan.countryCallingCode}" + } + if (number != null) { + Log.i("[Assistant] Found phone number: $number") + phoneNumber.value = number + } + } + + private fun getCountryNameFromPrefix(prefix: String?): MutableLiveData { + val country = MutableLiveData() + country.value = "" + + if (prefix != null && prefix.isNotEmpty()) { + val countryCode = if (prefix.first() == '+') prefix.substring(1) else prefix + val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix(countryCode) + Log.i("[Assistant] Found dial plan $dialPlan from country code: $countryCode") + country.value = dialPlan?.country + } + return country + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt new file mode 100644 index 000000000..4418e335e --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.viewmodels + +import androidx.lifecycle.* +import org.linphone.core.AccountCreator +import org.linphone.core.AccountCreatorListenerStub +import org.linphone.core.ProxyConfig +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class AccountLoginViewModelFactory(private val accountCreator: AccountCreator) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return AccountLoginViewModel(accountCreator) as T + } +} + +class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) { + val loginWithUsernamePassword = MutableLiveData() + + val username = MutableLiveData() + + val password = MutableLiveData() + + val loginEnabled: MediatorLiveData = MediatorLiveData() + + val waitForServerAnswer = MutableLiveData() + + val leaveAssistantEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val goToSmsValidationEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private val listener = object : AccountCreatorListenerStub() { + override fun onRecoverAccount( + creator: AccountCreator, + status: AccountCreator.Status, + response: String? + ) { + Log.i("[Assistant] [Account Login] Recover account status is $status") + waitForServerAnswer.value = false + + if (status == AccountCreator.Status.RequestOk) { + goToSmsValidationEvent.value = Event(true) + } else { + // TODO: show error + } + } + } + + init { + accountCreator.addListener(listener) + + loginWithUsernamePassword.value = false + + loginEnabled.value = false + loginEnabled.addSource(prefix) { + loginEnabled.value = isLoginButtonEnabled() + } + loginEnabled.addSource(phoneNumber) { + loginEnabled.value = isLoginButtonEnabled() + } + loginEnabled.addSource(username) { + loginEnabled.value = isLoginButtonEnabled() + } + loginEnabled.addSource(password) { + loginEnabled.value = isLoginButtonEnabled() + } + loginEnabled.addSource(loginWithUsernamePassword) { + loginEnabled.value = isLoginButtonEnabled() + } + loginEnabled.addSource(phoneNumberError) { + loginEnabled.value = isLoginButtonEnabled() + } + } + + override fun onCleared() { + accountCreator.removeListener(listener) + super.onCleared() + } + + fun login() { + if (loginWithUsernamePassword.value == true) { + accountCreator.username = username.value + accountCreator.password = password.value + Log.i("[Assistant] [Account Login] Username is ${accountCreator.username}") + + waitForServerAnswer.value = true + if (createProxyConfig()) { + leaveAssistantEvent.value = Event(true) + } else { + waitForServerAnswer.value = false + // TODO: show error + } + } else { + accountCreator.setPhoneNumber(phoneNumber.value, prefix.value) + accountCreator.username = accountCreator.phoneNumber + Log.i("[Assistant] [Account Login] Phone number is ${accountCreator.phoneNumber}") + + waitForServerAnswer.value = true + val status = accountCreator.recoverAccount() + Log.i("[Assistant] [Account Login] Recover account returned $status") + if (status != AccountCreator.Status.RequestOk) { + waitForServerAnswer.value = false + // TODO: show error + } + } + } + + private fun isLoginButtonEnabled(): Boolean { + return if (loginWithUsernamePassword.value == true) { + username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty() + } else { + isPhoneNumberOk() + } + } + + private fun createProxyConfig(): Boolean { + val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig() + + if (proxyConfig == null) { + Log.e("[Assistant] [Account Login] Account creator couldn't create proxy config") + // TODO: show error + return false + } + + proxyConfig.isPushNotificationAllowed = true + + Log.i("[Assistant] [Account Login] Proxy config created") + return true + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EchoCancellerCalibrationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EchoCancellerCalibrationViewModel.kt new file mode 100644 index 000000000..fe2c9ddce --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EchoCancellerCalibrationViewModel.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +package org.linphone.activities.assistant.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub +import org.linphone.core.EcCalibratorStatus +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class EchoCancellerCalibrationViewModel : ViewModel() { + val echoCalibrationTerminated = MutableLiveData>() + + private val listener = object : CoreListenerStub() { + override fun onEcCalibrationResult(core: Core, status: EcCalibratorStatus, delayMs: Int) { + if (status == EcCalibratorStatus.InProgress) return + echoCancellerCalibrationFinished(status, delayMs) + } + } + + init { + coreContext.core.addListener(listener) + } + + fun startEchoCancellerCalibration() { + coreContext.core.startEchoCancellerCalibration() + } + + fun echoCancellerCalibrationFinished(status: EcCalibratorStatus, delay: Int) { + coreContext.core.removeListener(listener) + when (status) { + EcCalibratorStatus.DoneNoEcho -> { + Log.i("[Echo Canceller Calibration] Done, no echo") + } + EcCalibratorStatus.Done -> { + Log.i("[Echo Canceller Calibration] Done, delay is ${delay}ms") + } + EcCalibratorStatus.Failed -> { + Log.w("[Echo Canceller Calibration] Failed") + } + } + echoCalibrationTerminated.value = Event(true) + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountCreationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountCreationViewModel.kt new file mode 100644 index 000000000..b600419da --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountCreationViewModel.kt @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.viewmodels + +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.linphone.R +import org.linphone.core.AccountCreator +import org.linphone.core.AccountCreatorListenerStub +import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils +import org.linphone.utils.Event + +class EmailAccountCreationViewModelFactory(private val accountCreator: AccountCreator) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return EmailAccountCreationViewModel(accountCreator) as T + } +} + +class EmailAccountCreationViewModel(val accountCreator: AccountCreator) : ViewModel() { + val username = MutableLiveData() + val usernameError = MutableLiveData() + + val email = MutableLiveData() + val emailError = MutableLiveData() + + val password = MutableLiveData() + val passwordError = MutableLiveData() + + val passwordConfirmation = MutableLiveData() + val passwordConfirmationError = MutableLiveData() + + val createEnabled: MediatorLiveData = MediatorLiveData() + + val waitForServerAnswer = MutableLiveData() + + val goToEmailValidationEvent = MutableLiveData>() + + private val listener = object : AccountCreatorListenerStub() { + override fun onIsAccountExist( + creator: AccountCreator, + status: AccountCreator.Status, + response: String? + ) { + Log.i("[Assistant] [Account Creation] onIsAccountExist status is $status") + when (status) { + AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> { + waitForServerAnswer.value = false + usernameError.value = AppUtils.getString(R.string.assistant_error_username_already_exists) + } + AccountCreator.Status.AccountNotExist -> { + val createAccountStatus = creator.createAccount() + if (createAccountStatus != AccountCreator.Status.RequestOk) { + waitForServerAnswer.value = false + // TODO: show error + } + } + else -> { + waitForServerAnswer.value = false + // TODO: show error + } + } + } + + override fun onCreateAccount( + creator: AccountCreator, + status: AccountCreator.Status, + response: String? + ) { + Log.i("[Account Creation] onCreateAccount status is $status") + waitForServerAnswer.value = false + + when (status) { + AccountCreator.Status.AccountCreated -> { + goToEmailValidationEvent.value = Event(true) + } + else -> { + // TODO: show error + } + } + } + } + + init { + accountCreator.addListener(listener) + + createEnabled.value = false + createEnabled.addSource(username) { + createEnabled.value = isCreateButtonEnabled() + } + createEnabled.addSource(usernameError) { + createEnabled.value = isCreateButtonEnabled() + } + createEnabled.addSource(email) { + createEnabled.value = isCreateButtonEnabled() + } + createEnabled.addSource(emailError) { + createEnabled.value = isCreateButtonEnabled() + } + createEnabled.addSource(password) { + createEnabled.value = isCreateButtonEnabled() + } + createEnabled.addSource(passwordError) { + createEnabled.value = isCreateButtonEnabled() + } + createEnabled.addSource(passwordConfirmation) { + createEnabled.value = isCreateButtonEnabled() + } + createEnabled.addSource(passwordConfirmationError) { + createEnabled.value = isCreateButtonEnabled() + } + } + + override fun onCleared() { + accountCreator.removeListener(listener) + super.onCleared() + } + + fun create() { + accountCreator.username = username.value + accountCreator.password = password.value + accountCreator.email = email.value + + waitForServerAnswer.value = true + val status = accountCreator.isAccountExist + Log.i("[Assistant] [Account Creation] Account exists returned $status") + if (status != AccountCreator.Status.RequestOk) { + waitForServerAnswer.value = false + // TODO: show error + } + } + + private fun isCreateButtonEnabled(): Boolean { + return username.value.orEmpty().isNotEmpty() && + email.value.orEmpty().isNotEmpty() && + password.value.orEmpty().isNotEmpty() && + passwordConfirmation.value.orEmpty().isNotEmpty() && + password.value == passwordConfirmation.value && + usernameError.value.orEmpty().isEmpty() && + emailError.value.orEmpty().isEmpty() && + passwordError.value.orEmpty().isEmpty() && + passwordConfirmationError.value.orEmpty().isEmpty() + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt new file mode 100644 index 000000000..e68b576a4 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.linphone.core.AccountCreator +import org.linphone.core.AccountCreatorListenerStub +import org.linphone.core.ProxyConfig +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class EmailAccountValidationViewModelFactory(private val accountCreator: AccountCreator) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return EmailAccountValidationViewModel(accountCreator) as T + } +} + +class EmailAccountValidationViewModel(val accountCreator: AccountCreator) : ViewModel() { + val email = MutableLiveData() + + val waitForServerAnswer = MutableLiveData() + + val leaveAssistantEvent = MutableLiveData>() + + private val listener = object : AccountCreatorListenerStub() { + override fun onIsAccountActivated( + creator: AccountCreator, + status: AccountCreator.Status, + response: String? + ) { + Log.i("[Account Validation] onIsAccountActivated status is $status") + waitForServerAnswer.value = false + + when (status) { + AccountCreator.Status.AccountActivated -> { + if (createProxyConfig()) { + leaveAssistantEvent.value = Event(true) + } else { + // TODO: show error + } + } + AccountCreator.Status.AccountNotActivated -> { + // TODO: show error + } + else -> { + // TODO: show error + } + } + } + } + + init { + accountCreator.addListener(listener) + email.value = accountCreator.email + } + + override fun onCleared() { + accountCreator.removeListener(listener) + super.onCleared() + } + + fun finish() { + waitForServerAnswer.value = true + val status = accountCreator.isAccountActivated + Log.i("[Assistant] [Account Validation] Account exists returned $status") + if (status != AccountCreator.Status.RequestOk) { + waitForServerAnswer.value = false + // TODO: show error + } + } + + private fun createProxyConfig(): Boolean { + val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig() + + if (proxyConfig == null) { + Log.e("[Assistant] [Account Validation] Account creator couldn't create proxy config") + // TODO: show error + return false + } + + proxyConfig.isPushNotificationAllowed = true + + Log.i("[Assistant] [Account Validation] Proxy config created") + return true + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt new file mode 100644 index 000000000..50f496704 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.viewmodels + +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.linphone.core.AccountCreator +import org.linphone.core.ProxyConfig +import org.linphone.core.TransportType +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class GenericLoginViewModelFactory(private val accountCreator: AccountCreator) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return GenericLoginViewModel(accountCreator) as T + } +} + +class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewModel() { + val username = MutableLiveData() + + val password = MutableLiveData() + + val domain = MutableLiveData() + + val displayName = MutableLiveData() + + val transport = MutableLiveData() + + val loginEnabled: MediatorLiveData = MediatorLiveData() + + val leaveAssistantEvent = MutableLiveData>() + + init { + transport.value = TransportType.Tls + + loginEnabled.value = false + loginEnabled.addSource(username) { + loginEnabled.value = isLoginButtonEnabled() + } + loginEnabled.addSource(password) { + loginEnabled.value = isLoginButtonEnabled() + } + loginEnabled.addSource(domain) { + loginEnabled.value = isLoginButtonEnabled() + } + } + + fun setTransport(transportType: TransportType) { + transport.value = transportType + } + + fun createProxyConfig() { + accountCreator.username = username.value + accountCreator.password = password.value + accountCreator.domain = domain.value + accountCreator.displayName = displayName.value + accountCreator.transport = transport.value + + val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig() + + if (proxyConfig == null) { + Log.e("[Assistant] [Generic Login] Account creator couldn't create proxy config") + // TODO: show error + return + } + + Log.i("[Assistant] [Generic Login] Proxy config created") + leaveAssistantEvent.value = Event(true) + } + + private fun isLoginButtonEnabled(): Boolean { + return username.value.orEmpty().isNotEmpty() && domain.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty() + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountCreationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountCreationViewModel.kt new file mode 100644 index 000000000..7e6ebceba --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountCreationViewModel.kt @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +package org.linphone.activities.assistant.viewmodels + +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.core.AccountCreator +import org.linphone.core.AccountCreatorListenerStub +import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils +import org.linphone.utils.Event + +class PhoneAccountCreationViewModelFactory(private val accountCreator: AccountCreator) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return PhoneAccountCreationViewModel(accountCreator) as T + } +} + +class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) { + val username = MutableLiveData() + val useUsername = MutableLiveData() + val usernameError = MutableLiveData() + + val createEnabled: MediatorLiveData = MediatorLiveData() + + val waitForServerAnswer = MutableLiveData() + + val goToSmsValidationEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private val listener = object : AccountCreatorListenerStub() { + override fun onIsAccountExist( + creator: AccountCreator, + status: AccountCreator.Status, + response: String? + ) { + Log.i("[Phone Account Creation] onIsAccountExist status is $status") + when (status) { + AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> { + waitForServerAnswer.value = false + if (useUsername.value == true) { + usernameError.value = AppUtils.getString(R.string.assistant_error_username_already_exists) + } else { + phoneNumberError.value = AppUtils.getString(R.string.assistant_error_phone_number_already_exists) + } + } + AccountCreator.Status.AccountNotExist -> { + val createAccountStatus = creator.createAccount() + Log.i("[Phone Account Creation] createAccount returned $createAccountStatus") + if (createAccountStatus != AccountCreator.Status.RequestOk) { + waitForServerAnswer.value = false + // TODO: show error + } + } + else -> { + waitForServerAnswer.value = false + // TODO: show error + } + } + } + + override fun onCreateAccount( + creator: AccountCreator, + status: AccountCreator.Status, + response: String? + ) { + Log.i("[Phone Account Creation] onCreateAccount status is $status") + waitForServerAnswer.value = false + when (status) { + AccountCreator.Status.AccountCreated -> { + goToSmsValidationEvent.value = Event(true) + } + AccountCreator.Status.AccountExistWithAlias -> { + phoneNumberError.value = AppUtils.getString(R.string.assistant_error_phone_number_already_exists) + } + else -> { + // TODO: show error + } + } + } + } + + init { + useUsername.value = false + accountCreator.addListener(listener) + + createEnabled.value = false + createEnabled.addSource(prefix) { + createEnabled.value = isCreateButtonEnabled() + } + createEnabled.addSource(phoneNumber) { + createEnabled.value = isCreateButtonEnabled() + } + createEnabled.addSource(useUsername) { + createEnabled.value = isCreateButtonEnabled() + } + createEnabled.addSource(username) { + createEnabled.value = isCreateButtonEnabled() + } + createEnabled.addSource(usernameError) { + createEnabled.value = isCreateButtonEnabled() + } + createEnabled.addSource(phoneNumberError) { + createEnabled.value = isCreateButtonEnabled() + } + } + + override fun onCleared() { + accountCreator.removeListener(listener) + super.onCleared() + } + + fun create() { + accountCreator.setPhoneNumber(phoneNumber.value, prefix.value) + if (useUsername.value == true) { + accountCreator.username = username.value + } else { + accountCreator.username = accountCreator.phoneNumber + } + + waitForServerAnswer.value = true + val status = accountCreator.isAccountExist + Log.i("[Phone Account Creation] isAccountExist returned $status") + if (status != AccountCreator.Status.RequestOk) { + waitForServerAnswer.value = false + // TODO: show error + } + } + + private fun isCreateButtonEnabled(): Boolean { + val usernameRegexp = corePreferences.config.getString("assistant", "username_regex", "^[a-z0-9+_.\\-]*\$") + return isPhoneNumberOk() && + (useUsername.value == false || + username.value.orEmpty().matches(Regex(usernameRegexp)) && + username.value.orEmpty().isNotEmpty() && + usernameError.value.orEmpty().isEmpty()) + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountLinkingViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountLinkingViewModel.kt new file mode 100644 index 000000000..788c61622 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountLinkingViewModel.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +package org.linphone.activities.assistant.viewmodels + +import androidx.lifecycle.* +import org.linphone.core.AccountCreator +import org.linphone.core.AccountCreatorListenerStub +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class PhoneAccountLinkingViewModelFactory(private val accountCreator: AccountCreator) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return PhoneAccountLinkingViewModel(accountCreator) as T + } +} + +class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) { + val username = MutableLiveData() + + val allowSkip = MutableLiveData() + + val linkEnabled: MediatorLiveData = MediatorLiveData() + + val waitForServerAnswer = MutableLiveData() + + val leaveAssistantEvent = MutableLiveData>() + + val goToSmsValidationEvent = MutableLiveData>() + + private val listener = object : AccountCreatorListenerStub() { + override fun onIsAliasUsed( + creator: AccountCreator, + status: AccountCreator.Status, + response: String? + ) { + Log.i("[Phone Account Linking] onIsAliasUsed status is $status") + + when (status) { + AccountCreator.Status.AliasNotExist -> { + if (creator.linkAccount() != AccountCreator.Status.RequestOk) { + Log.e("[Phone Account Linking] linkAccount status is $status") + waitForServerAnswer.value = false + // TODO: show error + } + } + AccountCreator.Status.AliasExist, AccountCreator.Status.AliasIsAccount -> { + waitForServerAnswer.value = false + // TODO: show error + } + else -> { + waitForServerAnswer.value = false + // TODO: show error + } + } + } + + override fun onLinkAccount( + creator: AccountCreator, + status: AccountCreator.Status, + response: String? + ) { + Log.i("[Phone Account Linking] onLinkAccount status is $status") + waitForServerAnswer.value = false + + when (status) { + AccountCreator.Status.RequestOk -> { + goToSmsValidationEvent.value = Event(true) + } + else -> { + // TODO: show error + } + } + } + } + + init { + accountCreator.addListener(listener) + + linkEnabled.value = false + linkEnabled.addSource(prefix) { + linkEnabled.value = isLinkButtonEnabled() + } + linkEnabled.addSource(phoneNumber) { + linkEnabled.value = isLinkButtonEnabled() + } + linkEnabled.addSource(phoneNumberError) { + linkEnabled.value = isLinkButtonEnabled() + } + } + + override fun onCleared() { + accountCreator.removeListener(listener) + super.onCleared() + } + + fun link() { + accountCreator.setPhoneNumber(phoneNumber.value, prefix.value) + accountCreator.username = username.value + Log.i("[Assistant] [Phone Account Linking] Phone number is ${accountCreator.phoneNumber}") + + waitForServerAnswer.value = true + val status: AccountCreator.Status = accountCreator.isAliasUsed + Log.i("[Phone Account Linking] isAliasUsed returned $status") + if (status != AccountCreator.Status.RequestOk) { + waitForServerAnswer.value = false + // TODO: show error + } + } + + fun skip() { + leaveAssistantEvent.value = Event(true) + } + + private fun isLinkButtonEnabled(): Boolean { + return isPhoneNumberOk() + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountValidationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountValidationViewModel.kt new file mode 100644 index 000000000..ec2e7e2f0 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountValidationViewModel.kt @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.linphone.core.AccountCreator +import org.linphone.core.AccountCreatorListenerStub +import org.linphone.core.ProxyConfig +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class PhoneAccountValidationViewModelFactory(private val accountCreator: AccountCreator) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return PhoneAccountValidationViewModel(accountCreator) as T + } +} + +class PhoneAccountValidationViewModel(val accountCreator: AccountCreator) : ViewModel() { + val phoneNumber = MutableLiveData() + + val code = MutableLiveData() + + val isLogin = MutableLiveData() + + val isCreation = MutableLiveData() + + val isLinking = MutableLiveData() + + val waitForServerAnswer = MutableLiveData() + + val leaveAssistantEvent = MutableLiveData>() + + val listener = object : AccountCreatorListenerStub() { + override fun onLoginLinphoneAccount( + creator: AccountCreator, + status: AccountCreator.Status, + response: String? + ) { + Log.i("[Assistant] [Phone Account Validation] onLoginLinphoneAccount status is $status") + waitForServerAnswer.value = false + + if (status == AccountCreator.Status.RequestOk) { + if (createProxyConfig()) { + leaveAssistantEvent.value = Event(true) + } else { + // TODO: show error + } + } else { + // TODO: show error + } + } + + override fun onActivateAlias( + creator: AccountCreator, + status: AccountCreator.Status, + response: String? + ) { + Log.i("[Assistant] [Phone Account Validation] onActivateAlias status is $status") + waitForServerAnswer.value = false + + when (status) { + AccountCreator.Status.AccountActivated -> { + leaveAssistantEvent.value = Event(true) + } + else -> { + // TODO: show error + } + } + } + + override fun onActivateAccount( + creator: AccountCreator, + status: AccountCreator.Status, + response: String? + ) { + Log.i("[Assistant] [Phone Account Validation] onActivateAccount status is $status") + waitForServerAnswer.value = false + + if (status == AccountCreator.Status.AccountActivated) { + if (createProxyConfig()) { + leaveAssistantEvent.value = Event(true) + } else { + // TODO: show error + } + } else { + // TODO: show error + } + } + } + + init { + accountCreator.addListener(listener) + } + + override fun onCleared() { + accountCreator.removeListener(listener) + super.onCleared() + } + + fun finish() { + accountCreator.activationCode = code.value.orEmpty() + Log.i("[Assistant] [Phone Account Validation] Phone number is ${accountCreator.phoneNumber} and activation code is ${accountCreator.activationCode}") + waitForServerAnswer.value = true + + val status = when { + isLogin.value == true -> accountCreator.loginLinphoneAccount() + isCreation.value == true -> accountCreator.activateAccount() + isLinking.value == true -> accountCreator.activateAlias() + else -> AccountCreator.Status.UnexpectedError + } + Log.i("[Assistant] [Phone Account Validation] Code validation result is $status") + if (status != AccountCreator.Status.RequestOk) { + waitForServerAnswer.value = false + // TODO: show error + } + } + + private fun createProxyConfig(): Boolean { + val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig() + + if (proxyConfig == null) { + Log.e("[Assistant] [Phone Account Validation] Account creator couldn't create proxy config") + // TODO: show error + return false + } + + proxyConfig.isPushNotificationAllowed = true + Log.i("[Assistant] [Phone Account Validation] Proxy config created") + return true + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/QrCodeViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/QrCodeViewModel.kt new file mode 100644 index 000000000..ec3743957 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/QrCodeViewModel.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class QrCodeViewModel : ViewModel() { + val qrCodeFoundEvent = MutableLiveData>() + + val showSwitchCamera = MutableLiveData() + + private val listener = object : CoreListenerStub() { + override fun onQrcodeFound(core: Core, result: String) { + Log.i("[QR Code] Found [$result]") + qrCodeFoundEvent.postValue(Event(result)) + } + } + + init { + coreContext.core.addListener(listener) + showSwitchCamera.value = coreContext.core.videoDevicesList.size > 1 + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + super.onCleared() + } + + fun setBackCamera() { + for (camera in coreContext.core.videoDevicesList) { + if (camera.contains("Back")) { + Log.i("[QR Code] Found back facing camera: $camera") + coreContext.core.videoDevice = camera + return + } + } + + val first = coreContext.core.videoDevicesList[0] + Log.i("[QR Code] Using first camera found: $first") + coreContext.core.videoDevice = first + } + + fun switchCamera() { + coreContext.switchCamera() + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/RemoteProvisioningViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/RemoteProvisioningViewModel.kt new file mode 100644 index 000000000..7da8e9b87 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/RemoteProvisioningViewModel.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.ConfiguringState +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class RemoteProvisioningViewModel : ViewModel() { + val urlToFetch = MutableLiveData() + val fetchInProgress = MutableLiveData() + val fetchSuccessfulEvent = MutableLiveData>() + + private val listener = object : CoreListenerStub() { + override fun onConfiguringStatus(core: Core, status: ConfiguringState, message: String?) { + fetchInProgress.value = false + when (status) { + ConfiguringState.Successful -> { + fetchSuccessfulEvent.value = Event(true) + } + ConfiguringState.Failed -> { + fetchSuccessfulEvent.value = Event(false) + } + } + } + } + + init { + fetchInProgress.value = false + coreContext.core.addListener(listener) + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + super.onCleared() + } + + fun fetchAndApply(url: String) { + coreContext.core.provisioningUri = url + Log.w("[Remote Provisioning] Url set to [$url], restarting Core") + fetchInProgress.value = true + coreContext.core.stop() + coreContext.core.start() + } +} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/SharedAssistantViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/SharedAssistantViewModel.kt new file mode 100644 index 000000000..d97bdf1b7 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/SharedAssistantViewModel.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.assistant.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import java.util.* +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.core.* +import org.linphone.core.tools.Log + +class SharedAssistantViewModel : ViewModel() { + val remoteProvisioningUrl = MutableLiveData() + + private var accountCreator: AccountCreator + private var useGenericSipAccount: Boolean = false + + init { + Log.i("[Assistant] Loading linphone default values") + coreContext.core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath) + accountCreator = coreContext.core.createAccountCreator(corePreferences.xmlRpcServerUrl) + accountCreator.language = Locale.getDefault().language + } + + fun getAccountCreator(genericAccountCreator: Boolean = false): AccountCreator { + if (genericAccountCreator != useGenericSipAccount) { + accountCreator.reset() + accountCreator.language = Locale.getDefault().language + + if (genericAccountCreator) { + Log.i("[Assistant] Loading default values") + coreContext.core.loadConfigFromXml(corePreferences.defaultValuesPath) + } else { + Log.i("[Assistant] Loading linphone default values") + coreContext.core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath) + } + useGenericSipAccount = genericAccountCreator + } + return accountCreator + } +} diff --git a/app/src/main/java/org/linphone/activities/call/CallActivity.kt b/app/src/main/java/org/linphone/activities/call/CallActivity.kt new file mode 100644 index 000000000..2ba58a1a5 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/CallActivity.kt @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call + +import android.content.Context +import android.content.res.Configuration +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.os.Bundle +import android.os.PowerManager +import android.view.Gravity +import android.view.MotionEvent +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.GenericActivity +import org.linphone.activities.call.viewmodels.ControlsFadingViewModel +import org.linphone.activities.call.viewmodels.SharedCallViewModel +import org.linphone.compatibility.Compatibility +import org.linphone.core.tools.Log +import org.linphone.databinding.CallActivityBinding + +class CallActivity : GenericActivity() { + private lateinit var binding: CallActivityBinding + private lateinit var viewModel: ControlsFadingViewModel + private lateinit var sharedViewModel: SharedCallViewModel + + private var previewX: Float = 0f + private var previewY: Float = 0f + private lateinit var videoZoomHelper: VideoZoomHelper + + private lateinit var sensorManager: SensorManager + private lateinit var proximitySensor: Sensor + private lateinit var proximityWakeLock: PowerManager.WakeLock + private val proximityListener: SensorEventListener = object : SensorEventListener { + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { } + + override fun onSensorChanged(event: SensorEvent) { + if (event.timestamp == 0L) return + if (isProximitySensorNearby(event)) { + if (!proximityWakeLock.isHeld) { + Log.i("[Call Activity] Acquiring proximity wake lock") + proximityWakeLock.acquire() + } + } else { + if (proximityWakeLock.isHeld) { + Log.i("[Call Activity] Releasing proximity wake lock") + proximityWakeLock.release() + } + } + } + } + private var proximitySensorEnabled = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager + proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY) + proximityWakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager) + .newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "$packageName;proximity_sensor") + + binding = DataBindingUtil.setContentView(this, R.layout.call_activity) + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(ControlsFadingViewModel::class.java) + binding.viewModel = viewModel + + sharedViewModel = ViewModelProvider(this).get(SharedCallViewModel::class.java) + + sharedViewModel.toggleDrawerEvent.observe(this, Observer { + it.consume { + if (binding.statsMenu.isDrawerOpen(Gravity.LEFT)) { + binding.statsMenu.closeDrawer(binding.sideMenuContent, true) + } else { + binding.statsMenu.openDrawer(binding.sideMenuContent, true) + } + } + }) + + coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface + coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface + + binding.setPreviewTouchListener { v, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + previewX = v.x - event.rawX + previewY = v.y - event.rawY + } + MotionEvent.ACTION_MOVE -> { + v.animate().x(event.rawX + previewX).y(event.rawY + previewY).setDuration(0).start() + } + else -> false + } + true + } + + videoZoomHelper = VideoZoomHelper(this, binding.remoteVideoSurface) + + viewModel.videoEnabledEvent.observe(this, Observer { + it.consume { videoEnabled -> + enableProximitySensor(!videoEnabled) + } + }) + } + + override fun onResume() { + super.onResume() + + if (coreContext.core.callsNb == 0) { + Log.w("[Call Activity] Resuming but no call found...") + finish() + } else { + coreContext.removeCallOverlay() + + val currentCall = coreContext.core.currentCall ?: coreContext.core.calls[0] + if (currentCall != null) { + val videoEnabled = currentCall.currentParams.videoEnabled() + enableProximitySensor(!videoEnabled) + } + } + } + + override fun onPause() { + enableProximitySensor(false) + + val core = coreContext.core + if (core.callsNb > 0) { + coreContext.createCallOverlay() + } + + super.onPause() + } + + override fun onUserLeaveHint() { + super.onUserLeaveHint() + + if (coreContext.core.currentCall?.currentParams?.videoEnabled() == true) { + Compatibility.enterPipMode(this) + } + } + + override fun onPictureInPictureModeChanged( + isInPictureInPictureMode: Boolean, + newConfig: Configuration + ) { + if (isInPictureInPictureMode) { + viewModel.areControlsHidden.value = true + } + } + + private fun enableProximitySensor(enable: Boolean) { + if (enable) { + if (!proximitySensorEnabled) { + Log.i("[Call Activity] Enabling proximity sensor listener") + sensorManager.registerListener(proximityListener, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL) + proximitySensorEnabled = true + } + } else { + if (proximitySensorEnabled) { + Log.i("[Call Activity] Disabling proximity sensor listener") + sensorManager.unregisterListener(proximityListener) + if (proximityWakeLock.isHeld) { + proximityWakeLock.release() + } + proximitySensorEnabled = false + } + } + } + + private fun isProximitySensorNearby(event: SensorEvent): Boolean { + var threshold = 4.001f // <= 4 cm is near + + val distanceInCm = event.values[0] + val maxDistance = event.sensor.maximumRange + Log.d("[Call Activity] Proximity sensor report [$distanceInCm] , for max range [$maxDistance]") + + if (maxDistance <= threshold) { + // Case binary 0/1 and short sensors + threshold = maxDistance + } + return distanceInCm < threshold + } +} diff --git a/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt b/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt new file mode 100644 index 000000000..a112409c4 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call + +import android.annotation.TargetApi +import android.app.KeyguardManager +import android.content.Context +import android.os.Bundle +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.GenericActivity +import org.linphone.activities.call.viewmodels.IncomingCallViewModel +import org.linphone.activities.call.viewmodels.IncomingCallViewModelFactory +import org.linphone.core.Call +import org.linphone.core.tools.Log +import org.linphone.databinding.CallIncomingActivityBinding +import org.linphone.mediastream.Version +import org.linphone.utils.PermissionHelper + +class IncomingCallActivity : GenericActivity() { + private lateinit var binding: CallIncomingActivityBinding + private lateinit var viewModel: IncomingCallViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.call_incoming_activity) + binding.lifecycleOwner = this + + var incomingCall: Call? = null + for (call in coreContext.core.calls) { + if (call.state == Call.State.IncomingReceived || + call.state == Call.State.IncomingEarlyMedia) { + incomingCall = call + } + } + + if (incomingCall == null) { + Log.e("[Incoming Call] Couldn't find call in state Incoming") + finish() + return + } + + viewModel = ViewModelProvider( + this, + IncomingCallViewModelFactory(incomingCall) + )[IncomingCallViewModel::class.java] + binding.viewModel = viewModel + + viewModel.callEndedEvent.observe(this, Observer { + it.consume { + finish() + } + }) + + val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + viewModel.screenLocked.value = keyguardManager.isKeyguardLocked + + binding.buttons.setViewModel(viewModel) + + if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { + checkPermissions() + } + } + + @TargetApi(Version.API23_MARSHMALLOW_60) + private fun checkPermissions() { + val permissionsRequiredList = arrayListOf() + if (!PermissionHelper.get().hasRecordAudioPermission()) { + Log.i("[Incoming Call] Asking for RECORD_AUDIO permission") + permissionsRequiredList.add(android.Manifest.permission.RECORD_AUDIO) + } + if (viewModel.call.currentParams.videoEnabled() && !PermissionHelper.get().hasCameraPermission()) { + Log.i("[Incoming Call] Asking for CAMERA permission") + permissionsRequiredList.add(android.Manifest.permission.CAMERA) + } + if (permissionsRequiredList.isNotEmpty()) { + val permissionsRequired = arrayOfNulls(permissionsRequiredList.size) + permissionsRequiredList.toArray(permissionsRequired) + requestPermissions(permissionsRequired, 0) + } + } +} diff --git a/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt b/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt new file mode 100644 index 000000000..3ef548c14 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call + +import android.annotation.TargetApi +import android.os.Bundle +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.GenericActivity +import org.linphone.activities.call.viewmodels.CallViewModel +import org.linphone.activities.call.viewmodels.CallViewModelFactory +import org.linphone.activities.call.viewmodels.ControlsViewModel +import org.linphone.core.Call +import org.linphone.core.tools.Log +import org.linphone.databinding.CallOutgoingActivityBinding +import org.linphone.mediastream.Version +import org.linphone.utils.PermissionHelper + +class OutgoingCallActivity : GenericActivity() { + private lateinit var binding: CallOutgoingActivityBinding + private lateinit var viewModel: CallViewModel + private lateinit var controlsViewModel: ControlsViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.call_outgoing_activity) + binding.lifecycleOwner = this + + var outgoingCall: Call? = null + for (call in coreContext.core.calls) { + if (call.state == Call.State.OutgoingInit || + call.state == Call.State.OutgoingProgress || + call.state == Call.State.OutgoingRinging) { + outgoingCall = call + } + } + + if (outgoingCall == null) { + Log.e("[Outgoing Call] Couldn't find call in state Outgoing") + finish() + return + } + + viewModel = ViewModelProvider( + this, + CallViewModelFactory(outgoingCall) + )[CallViewModel::class.java] + binding.viewModel = viewModel + + controlsViewModel = ViewModelProvider(this).get(ControlsViewModel::class.java) + binding.controlsViewModel = controlsViewModel + + binding.setTerminateCallClickListener { + viewModel.terminateCall() + } + + binding.setToggleMicrophoneClickListener { + if (PermissionHelper.get().hasRecordAudioPermission()) { + controlsViewModel.toggleMuteMicrophone() + } else { + checkPermissions() + } + } + + binding.setToggleSpeakerClickListener { + controlsViewModel.toggleSpeaker() + } + + viewModel.callEndedEvent.observe(this, Observer { + it.consume { + finish() + } + }) + + if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { + checkPermissions() + } + } + + @TargetApi(Version.API23_MARSHMALLOW_60) + private fun checkPermissions() { + val permissionsRequiredList = arrayListOf() + if (!PermissionHelper.get().hasRecordAudioPermission()) { + Log.i("[Outgoing Call] Asking for RECORD_AUDIO permission") + permissionsRequiredList.add(android.Manifest.permission.RECORD_AUDIO) + } + if (viewModel.call.currentParams.videoEnabled() && !PermissionHelper.get().hasCameraPermission()) { + Log.i("[Outgoing Call] Asking for CAMERA permission") + permissionsRequiredList.add(android.Manifest.permission.CAMERA) + } + if (permissionsRequiredList.isNotEmpty()) { + val permissionsRequired = arrayOfNulls(permissionsRequiredList.size) + permissionsRequiredList.toArray(permissionsRequired) + requestPermissions(permissionsRequired, 0) + } + } +} diff --git a/app/src/main/java/org/linphone/activities/call/VideoZoomHelper.kt b/app/src/main/java/org/linphone/activities/call/VideoZoomHelper.kt new file mode 100644 index 000000000..392d0d4fe --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/VideoZoomHelper.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call + +import android.content.Context +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.ScaleGestureDetector +import android.view.View +import kotlin.math.max +import kotlin.math.min +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Call + +class VideoZoomHelper(context: Context, private var videoDisplayView: View) : GestureDetector.SimpleOnGestureListener() { + private var scaleDetector: ScaleGestureDetector + + private var zoomFactor = 1f + private var zoomCenterX = 0f + private var zoomCenterY = 0f + + init { + val gestureDetector = GestureDetector(context, this) + + scaleDetector = ScaleGestureDetector(context, object : + ScaleGestureDetector.SimpleOnScaleGestureListener() { + override fun onScale(detector: ScaleGestureDetector): Boolean { + zoomFactor *= detector.scaleFactor + // Don't let the object get too small or too large. + // Zoom to make the video fill the screen vertically + val portraitZoomFactor = videoDisplayView.height.toFloat() / (3 * videoDisplayView.width / 4) + // Zoom to make the video fill the screen horizontally + val landscapeZoomFactor = videoDisplayView.width.toFloat() / (3 * videoDisplayView.height / 4) + zoomFactor = max(0.1f, min(zoomFactor, max(portraitZoomFactor, landscapeZoomFactor))) + + val currentCall: Call? = coreContext.core.currentCall + if (currentCall != null) { + currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY) + return true + } + + return false + } + }) + + videoDisplayView.setOnTouchListener { v, event -> + val currentZoomFactor = zoomFactor + scaleDetector.onTouchEvent(event) + + if (currentZoomFactor != zoomFactor) { + // We did scale, prevent touch event from going further + return@setOnTouchListener true + } + + // If true, gesture detected, prevent touch event from going further + // Otherwise it seems we didn't use event, + // allow it to be dispatched somewhere else + gestureDetector.onTouchEvent(event) + } + } + + override fun onScroll( + e1: MotionEvent, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + val currentCall: Call? = coreContext.core.currentCall + if (currentCall != null) { + if (zoomFactor > 1) { + // Video is zoomed, slide is used to change center of zoom + if (distanceX > 0 && zoomCenterX < 1) { + zoomCenterX += 0.01f + } else if (distanceX < 0 && zoomCenterX > 0) { + zoomCenterX -= 0.01f + } + + if (distanceY < 0 && zoomCenterY < 1) { + zoomCenterY += 0.01f + } else if (distanceY > 0 && zoomCenterY > 0) { + zoomCenterY -= 0.01f + } + + if (zoomCenterX > 1) zoomCenterX = 1f + if (zoomCenterX < 0) zoomCenterX = 0f + if (zoomCenterY > 1) zoomCenterY = 1f + if (zoomCenterY < 0) zoomCenterY = 0f + + currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY) + return true + } + } + + return false + } + + override fun onDoubleTap(e: MotionEvent?): Boolean { + val currentCall: Call? = coreContext.core.currentCall + if (currentCall != null) { + if (zoomFactor == 1f) { + // Zoom to make the video fill the screen vertically + val portraitZoomFactor = videoDisplayView.height.toFloat() / (3 * videoDisplayView.width / 4) + // Zoom to make the video fill the screen horizontally + val landscapeZoomFactor = videoDisplayView.width.toFloat() / (3 * videoDisplayView.height / 4) + zoomFactor = max(portraitZoomFactor, landscapeZoomFactor) + } else { + resetZoom() + } + + currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY) + return true + } + + return false + } + + private fun resetZoom() { + zoomFactor = 1f + zoomCenterY = 0.5f + zoomCenterX = zoomCenterY + } +} diff --git a/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt b/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt new file mode 100644 index 000000000..02c0193ea --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.fragments + +import android.content.Intent +import android.os.Bundle +import android.os.SystemClock +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import org.linphone.activities.call.viewmodels.CallsViewModel +import org.linphone.activities.call.viewmodels.ControlsViewModel +import org.linphone.activities.main.MainActivity +import org.linphone.databinding.CallControlsFragmentBinding + +class ControlsFragment : Fragment() { + private lateinit var binding: CallControlsFragmentBinding + private lateinit var callsViewModel: CallsViewModel + private lateinit var controlsViewModel: ControlsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = CallControlsFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + callsViewModel = ViewModelProvider(this).get(CallsViewModel::class.java) + binding.viewModel = callsViewModel + + controlsViewModel = ViewModelProvider(this).get(ControlsViewModel::class.java) + binding.controlsViewModel = controlsViewModel + + callsViewModel.currentCallViewModel.observe(viewLifecycleOwner, Observer { + if (it != null) { + binding.activeCallTimer.base = + SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds + binding.activeCallTimer.start() + } + }) + + callsViewModel.noMoreCallEvent.observe(viewLifecycleOwner, Observer { + it.consume { + activity?.finish() + } + }) + + controlsViewModel.chatClickedEvent.observe(viewLifecycleOwner, Observer { + it.consume { + val intent = Intent() + intent.setClass(requireContext(), MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + intent.putExtra("Chat", true) + startActivity(intent) + } + }) + + controlsViewModel.addCallClickedEvent.observe(viewLifecycleOwner, Observer { + it.consume { + val intent = Intent() + intent.setClass(requireContext(), MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + intent.putExtra("Dialer", true) + intent.putExtra("Transfer", false) + startActivity(intent) + } + }) + + controlsViewModel.transferCallClickedEvent.observe(viewLifecycleOwner, Observer { + it.consume { + val intent = Intent() + intent.setClass(requireContext(), MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + intent.putExtra("Dialer", true) + intent.putExtra("Transfer", true) + startActivity(intent) + } + }) + } +} diff --git a/app/src/main/java/org/linphone/activities/call/fragments/StatisticsFragment.kt b/app/src/main/java/org/linphone/activities/call/fragments/StatisticsFragment.kt new file mode 100644 index 000000000..7e9bc8d57 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/fragments/StatisticsFragment.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import org.linphone.activities.call.viewmodels.StatisticsListViewModel +import org.linphone.databinding.CallStatisticsFragmentBinding + +class StatisticsFragment : Fragment() { + private lateinit var binding: CallStatisticsFragmentBinding + private lateinit var viewModel: StatisticsListViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = CallStatisticsFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(StatisticsListViewModel::class.java) + binding.viewModel = viewModel + } +} diff --git a/app/src/main/java/org/linphone/activities/call/fragments/StatusFragment.kt b/app/src/main/java/org/linphone/activities/call/fragments/StatusFragment.kt new file mode 100644 index 000000000..9e819759d --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/fragments/StatusFragment.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.fragments + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import java.util.* +import org.linphone.R +import org.linphone.activities.call.viewmodels.SharedCallViewModel +import org.linphone.activities.call.viewmodels.StatusViewModel +import org.linphone.activities.main.viewmodels.DialogViewModel +import org.linphone.core.Call +import org.linphone.core.tools.Log +import org.linphone.databinding.CallStatusFragmentBinding +import org.linphone.utils.DialogUtils +import org.linphone.utils.Event + +class StatusFragment : Fragment() { + private lateinit var binding: CallStatusFragmentBinding + private lateinit var viewModel: StatusViewModel + private lateinit var sharedViewModel: SharedCallViewModel + private var zrtpDialog: Dialog? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = CallStatusFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(StatusViewModel::class.java) + binding.viewModel = viewModel + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedCallViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + binding.setStatsClickListener { + sharedViewModel.toggleDrawerEvent.value = Event(true) + } + + binding.setRefreshClickListener { + viewModel.refreshRegister() + } + + viewModel.showZrtpDialogEvent.observe(viewLifecycleOwner, Observer { + it.consume { call -> + if (call.state == Call.State.Connected || call.state == Call.State.StreamsRunning) { + showZrtpDialog(call) + } + } + }) + } + + override fun onDestroy() { + if (zrtpDialog != null) { + zrtpDialog?.dismiss() + } + super.onDestroy() + } + + private fun showZrtpDialog(call: Call) { + if (zrtpDialog != null && zrtpDialog?.isShowing == true) { + Log.e("[Status Fragment] ZRTP dialog already visible") + return + } + + val token = call.authenticationToken + if (token == null || token.length < 4) { + Log.e("[Status Fragment] ZRTP token is invalid: $token") + return + } + + val toRead: String + val toListen: String + when (call.dir) { + Call.Dir.Incoming -> { + toRead = token.substring(0, 2) + toListen = token.substring(2) + } + else -> { + toRead = token.substring(2) + toListen = token.substring(0, 2) + } + } + + val viewModel = DialogViewModel(getString(R.string.zrtp_dialog_message), getString(R.string.zrtp_dialog_title)) + viewModel.showZrtp = true + viewModel.zrtpReadSas = toRead.toUpperCase(Locale.getDefault()) + viewModel.zrtpListenSas = toListen.toUpperCase(Locale.getDefault()) + viewModel.showIcon = true + viewModel.iconResource = R.drawable.security_2_indicator + + val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel) + + viewModel.showDeleteButton({ + call.authenticationTokenVerified = false + this@StatusFragment.viewModel.updateEncryptionInfo(call) + dialog.dismiss() + zrtpDialog = null + }, getString(R.string.zrtp_dialog_deny_button_label)) + + viewModel.showOkButton({ + call.authenticationTokenVerified = true + this@StatusFragment.viewModel.updateEncryptionInfo(call) + dialog.dismiss() + zrtpDialog = null + }, getString(R.string.zrtp_dialog_ok_button_label)) + + zrtpDialog = dialog + dialog.show() + } +} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/CallStatisticsViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/CallStatisticsViewModel.kt new file mode 100644 index 000000000..ab38d079d --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/viewmodels/CallStatisticsViewModel.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.viewmodels + +import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.contact.GenericContactViewModel +import org.linphone.core.* + +class CallStatisticsViewModel(val call: Call) : GenericContactViewModel(call.remoteAddress) { + val audioStats = MutableLiveData>() + + val videoStats = MutableLiveData>() + + val isVideoEnabled = MutableLiveData() + + val isExpanded = MutableLiveData() + + private val listener = object : CoreListenerStub() { + override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) { + if (call == this@CallStatisticsViewModel.call) { + isVideoEnabled.value = call.currentParams.videoEnabled() + updateCallStats(stats) + } + } + } + + init { + coreContext.core.addListener(listener) + + audioStats.value = arrayListOf() + videoStats.value = arrayListOf() + + initCallStats() + + val videoEnabled = call.currentParams.videoEnabled() + isVideoEnabled.value = videoEnabled + + isExpanded.value = coreContext.core.currentCall == call + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + + super.onCleared() + } + + fun toggleExpanded() { + isExpanded.value = isExpanded.value != true + } + + private fun initCallStats() { + val audioList = arrayListOf() + audioList.add(StatItemViewModel(StatType.CAPTURE)) + audioList.add(StatItemViewModel(StatType.PLAYBACK)) + audioList.add(StatItemViewModel(StatType.PAYLOAD)) + audioList.add(StatItemViewModel(StatType.ENCODER)) + audioList.add(StatItemViewModel(StatType.DECODER)) + audioList.add(StatItemViewModel(StatType.DOWNLOAD_BW)) + audioList.add(StatItemViewModel(StatType.UPLOAD_BW)) + audioList.add(StatItemViewModel(StatType.ICE)) + audioList.add(StatItemViewModel(StatType.IP_FAM)) + audioList.add(StatItemViewModel(StatType.SENDER_LOSS)) + audioList.add(StatItemViewModel(StatType.RECEIVER_LOSS)) + audioList.add(StatItemViewModel(StatType.JITTER)) + audioStats.value = audioList + + val videoList = arrayListOf() + videoList.add(StatItemViewModel(StatType.CAPTURE)) + videoList.add(StatItemViewModel(StatType.PLAYBACK)) + videoList.add(StatItemViewModel(StatType.PAYLOAD)) + videoList.add(StatItemViewModel(StatType.ENCODER)) + videoList.add(StatItemViewModel(StatType.DECODER)) + videoList.add(StatItemViewModel(StatType.DOWNLOAD_BW)) + videoList.add(StatItemViewModel(StatType.UPLOAD_BW)) + videoList.add(StatItemViewModel(StatType.ESTIMATED_AVAILABLE_DOWNLOAD_BW)) + videoList.add(StatItemViewModel(StatType.ICE)) + videoList.add(StatItemViewModel(StatType.IP_FAM)) + videoList.add(StatItemViewModel(StatType.SENDER_LOSS)) + videoList.add(StatItemViewModel(StatType.RECEIVER_LOSS)) + videoList.add(StatItemViewModel(StatType.SENT_RESOLUTION)) + videoList.add(StatItemViewModel(StatType.RECEIVED_RESOLUTION)) + videoList.add(StatItemViewModel(StatType.SENT_FPS)) + videoList.add(StatItemViewModel(StatType.RECEIVED_FPS)) + videoStats.value = videoList + } + + private fun updateCallStats(stats: CallStats) { + if (stats.type == StreamType.Audio) { + for (stat in audioStats.value.orEmpty()) { + stat.update(call, stats) + } + } else if (stats.type == StreamType.Video) { + for (stat in videoStats.value.orEmpty()) { + stat.update(call, stats) + } + } + } +} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt new file mode 100644 index 000000000..3330a6144 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.contact.GenericContactViewModel +import org.linphone.core.Call +import org.linphone.core.CallListenerStub +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class CallViewModelFactory(private val call: Call) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return CallViewModel(call) as T + } +} + +open class CallViewModel(val call: Call) : GenericContactViewModel(call.remoteAddress) { + val address: String by lazy { + call.remoteAddress.clean() // To remove gruu if any + call.remoteAddress.asStringUriOnly() + } + + val isPaused = MutableLiveData() + + val callEndedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private val listener = object : CallListenerStub() { + override fun onStateChanged(call: Call, state: Call.State, message: String) { + if (call != this@CallViewModel.call) return + + isPaused.value = state == Call.State.Paused + + if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) { + callEndedEvent.value = Event(true) + + if (state == Call.State.Error) { + Log.e("[Call View Model] Error state reason is ${call.reason}") + } + } + } + } + + init { + call.addListener(listener) + + isPaused.value = call.state == Call.State.Paused + } + + override fun onCleared() { + call.removeListener(listener) + + super.onCleared() + } + + fun terminateCall() { + coreContext.terminateCall(call) + } + + fun pause() { + call.pause() + } + + fun resume() { + call.resume() + } + + fun removeFromConference() { + if (call.conference != null) { + call.conference.removeParticipant(call.remoteAddress) + if (call.core.conferenceSize <= 1) call.core.leaveConference() + } + } +} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt new file mode 100644 index 000000000..d20364988 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Call +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub +import org.linphone.utils.Event + +class CallsViewModel : ViewModel() { + val currentCallViewModel = MutableLiveData() + + val callPausedByRemote = MutableLiveData() + + val pausedCalls = MutableLiveData>() + + val conferenceCalls = MutableLiveData>() + + val isConferencePaused = MutableLiveData() + + val noMoreCallEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private val listener = object : CoreListenerStub() { + override fun onCallStateChanged( + core: Core, + call: Call, + state: Call.State, + message: String + ) { + callPausedByRemote.value = state == Call.State.PausedByRemote + isConferencePaused.value = !coreContext.core.isInConference + + if (core.currentCall == null) { + currentCallViewModel.value = null + } else if (currentCallViewModel.value == null) { + currentCallViewModel.value = CallViewModel(core.currentCall) + } + + if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) { + if (core.callsNb == 0) { + noMoreCallEvent.value = Event(true) + conferenceCalls.value = arrayListOf() + } else { + removeCallFromPausedListIfPresent(call) + removeCallFromConferenceIfPresent(call) + } + } else { + if (state == Call.State.Pausing) { + addCallToPausedList(call) + } else if (state == Call.State.Resuming) { + removeCallFromPausedListIfPresent(call) + } else { + if (call.conference != null) { + addCallToConferenceListIfNotAlreadyInIt(call) + } else { + removeCallFromConferenceIfPresent(call) + } + } + } + } + } + + init { + coreContext.core.addListener(listener) + + val currentCall = coreContext.core.currentCall + if (currentCall != null) { + currentCallViewModel.value = CallViewModel(currentCall) + } + callPausedByRemote.value = currentCall?.state == Call.State.PausedByRemote + isConferencePaused.value = !coreContext.core.isInConference + + val conferenceList = arrayListOf() + for (call in coreContext.core.calls) { + if (call.state == Call.State.Paused || call.state == Call.State.Pausing) { + addCallToPausedList(call) + } else { + if (call.conference != null && call.core.isInConference) { + conferenceList.add(CallViewModel(call)) + } + } + } + conferenceCalls.value = conferenceList + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + + super.onCleared() + } + + fun pauseConference() { + if (coreContext.core.isInConference) { + coreContext.core.leaveConference() + isConferencePaused.value = true + } + } + + fun resumeConference() { + if (!coreContext.core.isInConference) { + coreContext.core.enterConference() + isConferencePaused.value = false + } + } + + private fun addCallToPausedList(call: Call) { + val list = arrayListOf() + list.addAll(pausedCalls.value.orEmpty()) + + val viewModel = CallViewModel(call) + list.add(viewModel) + pausedCalls.value = list + } + + private fun removeCallFromPausedListIfPresent(call: Call) { + val list = arrayListOf() + list.addAll(pausedCalls.value.orEmpty()) + + for (pausedCallViewModel in list) { + if (pausedCallViewModel.call == call) { + list.remove(pausedCallViewModel) + break + } + } + + pausedCalls.value = list + } + + private fun addCallToConferenceListIfNotAlreadyInIt(call: Call) { + val list = arrayListOf() + list.addAll(conferenceCalls.value.orEmpty()) + + for (viewModel in list) { + if (viewModel.call == call) return + } + + val viewModel = CallViewModel(call) + list.add(viewModel) + conferenceCalls.value = list + } + + private fun removeCallFromConferenceIfPresent(call: Call) { + val list = arrayListOf() + list.addAll(conferenceCalls.value.orEmpty()) + + for (viewModel in list) { + if (viewModel.call == call) { + list.remove(viewModel) + break + } + } + + conferenceCalls.value = list + } +} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsFadingViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsFadingViewModel.kt new file mode 100644 index 000000000..2a64907ac --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsFadingViewModel.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import java.util.* +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Call +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class ControlsFadingViewModel : ViewModel() { + val areControlsHidden = MutableLiveData() + + val videoEnabledEvent = MutableLiveData>() + + private var timer: Timer? = null + + private val listener = object : CoreListenerStub() { + override fun onCallStateChanged( + core: Core, + call: Call, + state: Call.State, + message: String? + ) { + if (state == Call.State.StreamsRunning || state == Call.State.Updating || state == Call.State.UpdatedByRemote) { + Log.i("[Controls Fading] Call is in state $state, video is enabled? ${call.currentParams.videoEnabled()}") + if (call.currentParams.videoEnabled()) { + videoEnabledEvent.value = Event(true) + startTimer() + } else { + videoEnabledEvent.value = Event(false) + stopTimer() + } + } + } + } + + init { + coreContext.core.addListener(listener) + + areControlsHidden.value = false + + val currentCall = coreContext.core.currentCall + if (currentCall != null && currentCall.currentParams.videoEnabled()) { + videoEnabledEvent.value = Event(true) + startTimer() + } + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + stopTimer() + + super.onCleared() + } + + fun showMomentarily() { + stopTimer() + startTimer() + } + + private fun stopTimer() { + timer?.cancel() + + areControlsHidden.value = false + } + + private fun startTimer() { + timer?.cancel() + + timer = Timer("Hide UI controls scheduler") + timer?.schedule(object : TimerTask() { + override fun run() { + areControlsHidden.postValue(coreContext.core.currentCall?.currentParams?.videoEnabled() ?: false) + } + }, 3000) + } +} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt new file mode 100644 index 000000000..dc9c86dab --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import kotlin.math.max +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.activities.main.dialer.NumpadDigitListener +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.Event +import org.linphone.utils.PermissionHelper + +class ControlsViewModel : ViewModel() { + val isMicrophoneMuted = MutableLiveData() + + val isMuteMicrophoneEnabled = MutableLiveData() + + val isSpeakerSelected = MutableLiveData() + + val isBluetoothHeadsetSelected = MutableLiveData() + + val isVideoAvailable = MutableLiveData() + + val isVideoEnabled = MutableLiveData() + + val isVideoUpdateInProgress = MutableLiveData() + + val isPauseEnabled = MutableLiveData() + + val isRecording = MutableLiveData() + + val isConferencingAvailable = MutableLiveData() + + val unreadMessagesCount = MutableLiveData() + + val numpadVisibility = MutableLiveData() + + val optionsVisibility = MutableLiveData() + + val audioRoutesVisibility = MutableLiveData() + + val audioRoutesEnabled = MutableLiveData() + + val chatClickedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val addCallClickedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val transferCallClickedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val onKeyClick: NumpadDigitListener = object : NumpadDigitListener { + override fun handleClick(key: Char) { + coreContext.core.playDtmf(key, 1) + } + + override fun handleLongClick(key: Char): Boolean { + return true + } + } + + private val listener: CoreListenerStub = object : CoreListenerStub() { + override fun onMessageReceived(core: Core, chatRoom: ChatRoom, message: ChatMessage) { + updateUnreadChatCount() + } + + override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) { + updateUnreadChatCount() + } + + override fun onCallStateChanged( + core: Core, + call: Call, + state: Call.State, + message: String? + ) { + if (state == Call.State.StreamsRunning) isVideoUpdateInProgress.value = false + + updateUI() + } + + override fun onAudioDeviceChanged(core: Core, audioDevice: AudioDevice) { + updateAudioRelated() + } + + override fun onAudioDevicesListUpdated(core: Core) { + updateAudioRelated() + } + } + + init { + coreContext.core.addListener(listener) + val currentCall = coreContext.core.currentCall + + updateMuteMicState() + updateAudioRelated() + updateUnreadChatCount() + + numpadVisibility.value = false + optionsVisibility.value = false + audioRoutesVisibility.value = false + + isRecording.value = currentCall?.isRecording + isVideoUpdateInProgress.value = false + + updateUI() + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + super.onCleared() + } + + fun updateUnreadChatCount() { + unreadMessagesCount.value = coreContext.core.unreadChatMessageCountFromActiveLocals + } + + fun toggleMuteMicrophone() { + val micEnabled = coreContext.core.micEnabled() + coreContext.core.enableMic(!micEnabled) + updateMuteMicState() + } + + fun toggleSpeaker() { + val audioDevice = coreContext.core.outputAudioDevice + if (audioDevice?.type == AudioDevice.Type.Speaker) { + forceEarpieceAudioRoute() + } else { + forceSpeakerAudioRoute() + } + } + + fun switchCamera() { + coreContext.switchCamera() + } + + fun terminateCall() { + val core = coreContext.core + when { + core.currentCall != null -> core.currentCall.terminate() + core.isInConference -> core.terminateConference() + else -> core.terminateAllCalls() + } + } + + fun toggleVideo() { + val core = coreContext.core + val currentCall = core.currentCall + + if (currentCall != null) { + val state = currentCall.state + if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) + return + + isVideoUpdateInProgress.value = true + val params = core.createCallParams(currentCall) + params.enableVideo(!currentCall.currentParams.videoEnabled()) + currentCall.update(params) + } + } + + fun toggleOptionsMenu() { + optionsVisibility.value = optionsVisibility.value != true + } + + fun toggleNumpadVisibility() { + numpadVisibility.value = numpadVisibility.value != true + } + + fun toggleRoutesMenu() { + audioRoutesVisibility.value = audioRoutesVisibility.value != true + } + + fun toggleRecording(closeMenu: Boolean) { + val currentCall = coreContext.core.currentCall + if (currentCall != null) { + if (currentCall.isRecording) { + currentCall.stopRecording() + } else { + currentCall.startRecording() + } + } + isRecording.value = currentCall?.isRecording + if (closeMenu) toggleOptionsMenu() + } + + fun onChatClicked() { + chatClickedEvent.value = Event(true) + } + + fun onAddCallClicked() { + addCallClickedEvent.value = Event(true) + toggleOptionsMenu() + } + + fun onTransferCallClicked() { + transferCallClickedEvent.value = Event(true) + toggleOptionsMenu() + } + + fun startConference() { + coreContext.core.addAllToConference() + toggleOptionsMenu() + } + + fun forceEarpieceAudioRoute() { + for (audioDevice in coreContext.core.audioDevices) { + if (audioDevice.type == AudioDevice.Type.Earpiece) { + Log.i("[Call] Found earpiece audio device [${audioDevice.deviceName}], routing audio to it") + coreContext.core.outputAudioDevice = audioDevice + return + } + } + Log.e("[Call] Couldn't find earpiece audio device") + } + + fun forceSpeakerAudioRoute() { + for (audioDevice in coreContext.core.audioDevices) { + if (audioDevice.type == AudioDevice.Type.Speaker) { + Log.i("[Call] Found speaker audio device [${audioDevice.deviceName}], routing audio to it") + coreContext.core.outputAudioDevice = audioDevice + return + } + } + Log.e("[Call] Couldn't find speaker audio device") + } + + fun forceBluetoothAudioRoute() { + for (audioDevice in coreContext.core.audioDevices) { + if (audioDevice.type == AudioDevice.Type.Bluetooth) { + Log.i("[Call] Found bluetooth audio device [${audioDevice.deviceName}], routing audio to it") + coreContext.core.outputAudioDevice = audioDevice + return + } + } + Log.e("[Call] Couldn't find bluetooth audio device") + } + + private fun updateAudioRelated() { + updateSpeakerState() + updateBluetoothHeadsetState() + updateAudioRoutesState() + } + + private fun updateUI() { + val currentCall = coreContext.core.currentCall + updateVideoAvailable() + updateVideoEnabled() + isPauseEnabled.value = currentCall != null && !currentCall.mediaInProgress() + isMuteMicrophoneEnabled.value = currentCall != null || coreContext.core.isInConference + updateConferenceState() + } + + private fun updateMuteMicState() { + isMicrophoneMuted.value = !PermissionHelper.get().hasRecordAudioPermission() || !coreContext.core.micEnabled() + } + + private fun updateSpeakerState() { + val audioDevice = coreContext.core.outputAudioDevice + isSpeakerSelected.value = audioDevice?.type == AudioDevice.Type.Speaker + } + + private fun updateAudioRoutesState() { + var bluetoothDeviceAvailable = false + for (audioDevice in coreContext.core.audioDevices) { + if (audioDevice.type == AudioDevice.Type.Bluetooth) { + bluetoothDeviceAvailable = true + break + } + } + audioRoutesEnabled.value = bluetoothDeviceAvailable + } + + private fun updateBluetoothHeadsetState() { + val audioDevice = coreContext.core.outputAudioDevice + isBluetoothHeadsetSelected.value = audioDevice?.type == AudioDevice.Type.Bluetooth + } + + private fun updateVideoAvailable() { + val core = coreContext.core + isVideoAvailable.value = (core.videoCaptureEnabled() || core.videoPreviewEnabled()) && + core.currentCall != null && !core.currentCall.mediaInProgress() + } + + private fun updateVideoEnabled() { + val core = coreContext.core + isVideoEnabled.value = core.currentCall?.currentParams?.videoEnabled() + } + + private fun updateConferenceState() { + val core = coreContext.core + isConferencingAvailable.value = core.callsNb > max(1, core.conferenceSize) && !core.soundResourcesLocked() + } +} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt new file mode 100644 index 000000000..1aff1a300 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.* + +class IncomingCallViewModelFactory(private val call: Call) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return IncomingCallViewModel(call) as T + } +} + +class IncomingCallViewModel(call: Call) : CallViewModel(call) { + val screenLocked = MutableLiveData() + + val earlyMediaVideoEnabled = MutableLiveData() + + val inviteWithVideo = MutableLiveData() + + init { + screenLocked.value = false + inviteWithVideo.value = call.currentParams.videoEnabled() + earlyMediaVideoEnabled.value = call.state == Call.State.IncomingEarlyMedia && call.currentParams?.videoEnabled() ?: false + } + + fun answer(doAction: Boolean) { + if (doAction) coreContext.answerCall(call) + } + + fun decline(doAction: Boolean) { + if (doAction) coreContext.declineCall(call) + } +} diff --git a/app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureListener.java b/app/src/main/java/org/linphone/activities/call/viewmodels/SharedCallViewModel.kt similarity index 69% rename from app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureListener.java rename to app/src/main/java/org/linphone/activities/call/viewmodels/SharedCallViewModel.kt index 53e035ef6..791c4bdda 100644 --- a/app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureListener.java +++ b/app/src/main/java/org/linphone/activities/call/viewmodels/SharedCallViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2019 Belledonne Communications SARL. + * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,8 +17,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.compatibility; +package org.linphone.activities.call.viewmodels -public interface CompatibilityScaleGestureListener { - boolean onScale(CompatibilityScaleGestureDetector detector); +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.utils.Event + +class SharedCallViewModel : ViewModel() { + val toggleDrawerEvent = MutableLiveData>() } diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/SingleStatisticViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/SingleStatisticViewModel.kt new file mode 100644 index 000000000..674343a10 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/viewmodels/SingleStatisticViewModel.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import java.text.DecimalFormat +import org.linphone.R +import org.linphone.core.AddressFamily +import org.linphone.core.Call +import org.linphone.core.CallStats +import org.linphone.core.StreamType + +enum class StatType(val nameResource: Int) { + CAPTURE(R.string.call_stats_capture_filter), + PLAYBACK(R.string.call_stats_player_filter), + PAYLOAD(R.string.call_stats_codec), + ENCODER(R.string.call_stats_encoder_name), + DECODER(R.string.call_stats_decoder_name), + DOWNLOAD_BW(R.string.call_stats_download), + UPLOAD_BW(R.string.call_stats_upload), + ICE(R.string.call_stats_ice), + IP_FAM(R.string.call_stats_ip), + SENDER_LOSS(R.string.call_stats_sender_loss_rate), + RECEIVER_LOSS(R.string.call_stats_receiver_loss_rate), + JITTER(R.string.call_stats_jitter_buffer), + SENT_RESOLUTION(R.string.call_stats_video_resolution_sent), + RECEIVED_RESOLUTION(R.string.call_stats_video_resolution_received), + SENT_FPS(R.string.call_stats_video_fps_sent), + RECEIVED_FPS(R.string.call_stats_video_fps_received), + ESTIMATED_AVAILABLE_DOWNLOAD_BW(R.string.call_stats_estimated_download) +} + +class StatItemViewModel(val type: StatType) : ViewModel() { + val value = MutableLiveData() + + fun update(call: Call, stats: CallStats) { + val payloadType = if (stats.type == StreamType.Audio) call.currentParams.usedAudioPayloadType else call.currentParams.usedVideoPayloadType + value.value = when (type) { + StatType.CAPTURE -> if (stats.type == StreamType.Audio) call.core.captureDevice else call.core.videoDevice + StatType.PLAYBACK -> if (stats.type == StreamType.Audio) call.core.playbackDevice else call.core.videoDisplayFilter + StatType.PAYLOAD -> "${payloadType.mimeType}/${payloadType.clockRate / 1000} kHz" + StatType.ENCODER -> call.core.mediastreamerFactory.getDecoderText(payloadType.mimeType) + StatType.DECODER -> call.core.mediastreamerFactory.getEncoderText(payloadType.mimeType) + StatType.DOWNLOAD_BW -> "${stats.downloadBandwidth} kbits/s" + StatType.UPLOAD_BW -> "${stats.uploadBandwidth} kbits/s" + StatType.ICE -> stats.iceState.toString() + StatType.IP_FAM -> if (stats.ipFamilyOfRemote == AddressFamily.Inet6) "IPv6" else "IPv4" + StatType.SENDER_LOSS -> DecimalFormat("##.##%").format(stats.senderLossRate) + StatType.RECEIVER_LOSS -> DecimalFormat("##.##%").format(stats.receiverLossRate) + StatType.JITTER -> DecimalFormat("##.## ms").format(stats.jitterBufferSizeMs) + StatType.SENT_RESOLUTION -> call.currentParams.sentVideoDefinition?.name + StatType.RECEIVED_RESOLUTION -> call.currentParams.receivedVideoDefinition?.name + StatType.SENT_FPS -> "${call.currentParams.sentFramerate}" + StatType.RECEIVED_FPS -> "${call.currentParams.receivedFramerate}" + StatType.ESTIMATED_AVAILABLE_DOWNLOAD_BW -> "${stats.estimatedDownloadBandwidth} kbit/s" + } + } +} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/StatisticsListViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/StatisticsListViewModel.kt new file mode 100644 index 000000000..2f49f30e0 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/viewmodels/StatisticsListViewModel.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Call +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub + +class StatisticsListViewModel : ViewModel() { + val callStatsList = MutableLiveData>() + + private val listener = object : CoreListenerStub() { + override fun onCallStateChanged( + core: Core, + call: Call, + state: Call.State, + message: String? + ) { + if (state == Call.State.End || state == Call.State.Error) { + for (stat in callStatsList.value.orEmpty()) { + if (stat.call == call) { + callStatsList.value?.remove(stat) + } + } + } + } + } + + init { + coreContext.core.addListener(listener) + + val list = arrayListOf() + for (call in coreContext.core.calls) { + if (call.state != Call.State.End && call.state != Call.State.Released && call.state != Call.State.Error) { + list.add(CallStatisticsViewModel(call)) + } + } + callStatsList.value = list + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + + super.onCleared() + } +} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/StatusViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/StatusViewModel.kt new file mode 100644 index 000000000..4943e865f --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/viewmodels/StatusViewModel.kt @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.viewmodels + +import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.viewmodels.StatusViewModel +import org.linphone.core.* +import org.linphone.utils.Event + +class StatusViewModel : StatusViewModel() { + val callQualityIcon = MutableLiveData() + val callQualityContentDescription = MutableLiveData() + + val encryptionIcon = MutableLiveData() + val encryptionContentDescription = MutableLiveData() + val encryptionIconVisible = MutableLiveData() + + val showZrtpDialogEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private val listener = object : CoreListenerStub() { + override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) { + updateCallQualityIcon() + } + + override fun onCallEncryptionChanged( + core: Core, + call: Call, + on: Boolean, + authenticationToken: String + ) { + if (call.params.mediaEncryption == MediaEncryption.ZRTP && !call.authenticationTokenVerified) { + showZrtpDialogEvent.value = Event(call) + } else { + updateEncryptionInfo(call) + } + } + + override fun onCallStateChanged( + core: Core, + call: Call, + state: Call.State, + message: String? + ) { + if (call == core.currentCall) { + updateEncryptionInfo(call) + } + } + } + + init { + coreContext.core.addListener(listener) + + updateCallQualityIcon() + + val currentCall = coreContext.core.currentCall + if (currentCall != null) { + updateEncryptionInfo(currentCall) + + if (currentCall.params.mediaEncryption == MediaEncryption.ZRTP && !currentCall.authenticationTokenVerified) { + showZrtpDialogEvent.value = Event(currentCall) + } + } + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + + super.onCleared() + } + + fun showZrtpDialog() { + val currentCall = coreContext.core.currentCall + if (currentCall?.params?.mediaEncryption == MediaEncryption.ZRTP) { + showZrtpDialogEvent.value = Event(currentCall) + } + } + + fun updateEncryptionInfo(call: Call) { + if (call.dir == Call.Dir.Incoming && call.state == Call.State.IncomingReceived && call.core.isMediaEncryptionMandatory) { + // If the incoming call view is displayed while encryption is mandatory, + // we can safely show the security_ok icon + encryptionIcon.value = R.drawable.security_ok + encryptionIconVisible.value = true + encryptionContentDescription.value = R.string.content_description_call_secured + return + } + + when (call.params.mediaEncryption ?: MediaEncryption.None) { + MediaEncryption.SRTP, MediaEncryption.DTLS -> { + encryptionIcon.value = R.drawable.security_ok + encryptionIconVisible.value = true + encryptionContentDescription.value = R.string.content_description_call_secured + } + MediaEncryption.ZRTP -> { + encryptionIcon.value = when (call.authenticationTokenVerified) { + true -> R.drawable.security_ok + else -> R.drawable.security_pending + } + encryptionContentDescription.value = when (call.authenticationTokenVerified) { + true -> R.string.content_description_call_secured + else -> R.string.content_description_call_security_pending + } + encryptionIconVisible.value = true + } + MediaEncryption.None -> { + encryptionIcon.value = R.drawable.security_ko + // Do not show unsecure icon if user doesn't want to do call encryption + encryptionIconVisible.value = call.core.mediaEncryption != MediaEncryption.None + encryptionContentDescription.value = R.string.content_description_call_not_secured + } + } + } + + private fun updateCallQualityIcon() { + val call = coreContext.core.currentCall + val quality = call?.currentQuality ?: 0f + callQualityIcon.value = when { + quality >= 4 -> R.drawable.call_quality_indicator_4 + quality >= 3 -> R.drawable.call_quality_indicator_3 + quality >= 2 -> R.drawable.call_quality_indicator_2 + quality >= 1 -> R.drawable.call_quality_indicator_1 + else -> R.drawable.call_quality_indicator_0 + } + callQualityContentDescription.value = when { + quality >= 4 -> R.string.content_description_call_quality_4 + quality >= 3 -> R.string.content_description_call_quality_3 + quality >= 2 -> R.string.content_description_call_quality_2 + quality >= 1 -> R.string.content_description_call_quality_1 + else -> R.string.content_description_call_quality_0 + } + } +} diff --git a/app/src/main/java/org/linphone/activities/call/views/AnswerDeclineIncomingCallButtons.kt b/app/src/main/java/org/linphone/activities/call/views/AnswerDeclineIncomingCallButtons.kt new file mode 100644 index 000000000..fb20b1184 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/views/AnswerDeclineIncomingCallButtons.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.views + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.View.OnTouchListener +import android.widget.LinearLayout +import androidx.databinding.DataBindingUtil +import org.linphone.R +import org.linphone.activities.call.viewmodels.IncomingCallViewModel +import org.linphone.core.tools.Log +import org.linphone.databinding.CallIncomingAnswerDeclineButtonsBinding + +class AnswerDeclineIncomingCallButtons : LinearLayout { + private lateinit var binding: CallIncomingAnswerDeclineButtonsBinding + private var mBegin = false + private var mDeclineX = 0f + private var mAnswerX = 0f + private var mOldSize = 0f + + private val mAnswerTouchListener = OnTouchListener { view, motionEvent -> + val curX: Float + + when (motionEvent.action) { + MotionEvent.ACTION_DOWN -> { + binding.declineButton.visibility = View.GONE + mAnswerX = motionEvent.x - view.width + mBegin = true + mOldSize = 0f + } + MotionEvent.ACTION_MOVE -> { + curX = motionEvent.x - view.width + view.scrollBy((mAnswerX - curX).toInt(), view.scrollY) + mOldSize -= mAnswerX - curX + mAnswerX = curX + if (mOldSize < -25) mBegin = false + if (curX < (width / 4) - view.width && !mBegin) { + binding.viewModel?.answer(true) + } + } + MotionEvent.ACTION_UP -> { + binding.declineButton.visibility = View.VISIBLE + view.scrollTo(0, view.scrollY) + } + } + true + } + private val mDeclineTouchListener = OnTouchListener { view, motionEvent -> + val curX: Float + + when (motionEvent.action) { + MotionEvent.ACTION_DOWN -> { + binding.answerButton.visibility = View.GONE + mDeclineX = motionEvent.x + } + MotionEvent.ACTION_MOVE -> { + curX = motionEvent.x + view.scrollBy((mDeclineX - curX).toInt(), view.scrollY) + mDeclineX = curX + if (curX > 3 * width / 4) { + binding.viewModel?.decline(true) + } + } + MotionEvent.ACTION_UP -> { + binding.answerButton.visibility = View.VISIBLE + view.scrollTo(0, view.scrollY) + } + } + true + } + + constructor(context: Context) : super(context) { + init(context) + } + + constructor(context: Context, attrs: AttributeSet) : super( + context, + attrs + ) { + init(context) + } + + constructor( + context: Context, + attrs: AttributeSet, + defStyleAttr: Int + ) : super(context, attrs, defStyleAttr) { + init(context) + } + + fun setViewModel(viewModel: IncomingCallViewModel) { + binding.viewModel = viewModel + + updateSlideMode() + } + + private fun init(context: Context) { + binding = DataBindingUtil.inflate( + LayoutInflater.from(context), R.layout.call_incoming_answer_decline_buttons, this, true + ) + + updateSlideMode() + } + + private fun updateSlideMode() { + val slideMode = binding.viewModel?.screenLocked?.value == true + Log.i("[Call Incoming Decline Button] Slide mode is $slideMode") + if (slideMode) { + binding.answerButton.setOnTouchListener(mAnswerTouchListener) + binding.declineButton.setOnTouchListener(mDeclineTouchListener) + } + } +} diff --git a/app/src/main/java/org/linphone/activities/call/views/ConferenceCallView.kt b/app/src/main/java/org/linphone/activities/call/views/ConferenceCallView.kt new file mode 100644 index 000000000..35d709ffa --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/views/ConferenceCallView.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.views + +import android.content.Context +import android.os.SystemClock +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import androidx.databinding.DataBindingUtil +import org.linphone.R +import org.linphone.activities.call.viewmodels.CallViewModel +import org.linphone.databinding.CallConferenceBinding + +class ConferenceCallView : LinearLayout { + private lateinit var binding: CallConferenceBinding + + constructor(context: Context) : super(context) { + init(context) + } + + constructor(context: Context, attrs: AttributeSet) : super( + context, + attrs + ) { + init(context) + } + + constructor( + context: Context, + attrs: AttributeSet, + defStyleAttr: Int + ) : super(context, attrs, defStyleAttr) { + init(context) + } + + fun init(context: Context) { + binding = DataBindingUtil.inflate( + LayoutInflater.from(context), R.layout.call_conference, this, true + ) + } + + fun setViewModel(viewModel: CallViewModel) { + binding.viewModel = viewModel + + binding.callTimer.base = + SystemClock.elapsedRealtime() - (1000 * viewModel.call.duration) // Linphone timestamps are in seconds + binding.callTimer.start() + } +} diff --git a/app/src/main/java/org/linphone/activities/call/views/PausedCallView.kt b/app/src/main/java/org/linphone/activities/call/views/PausedCallView.kt new file mode 100644 index 000000000..a5c9ffce4 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/call/views/PausedCallView.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.call.views + +import android.content.Context +import android.os.SystemClock +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import androidx.databinding.DataBindingUtil +import org.linphone.R +import org.linphone.activities.call.viewmodels.CallViewModel +import org.linphone.databinding.CallPausedBinding + +class PausedCallView : LinearLayout { + private lateinit var binding: CallPausedBinding + + constructor(context: Context) : super(context) { + init(context) + } + + constructor(context: Context, attrs: AttributeSet) : super( + context, + attrs + ) { + init(context) + } + + constructor( + context: Context, + attrs: AttributeSet, + defStyleAttr: Int + ) : super(context, attrs, defStyleAttr) { + init(context) + } + + fun init(context: Context) { + binding = DataBindingUtil.inflate( + LayoutInflater.from(context), R.layout.call_paused, this, true + ) + } + + fun setViewModel(viewModel: CallViewModel) { + binding.viewModel = viewModel + + binding.callTimer.base = + SystemClock.elapsedRealtime() - (1000 * viewModel.call.duration) // Linphone timestamps are in seconds + binding.callTimer.start() + } +} diff --git a/app/src/main/java/org/linphone/activities/launcher/LauncherActivity.kt b/app/src/main/java/org/linphone/activities/launcher/LauncherActivity.kt new file mode 100644 index 000000000..0e9783ba3 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/launcher/LauncherActivity.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.launcher + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.MainActivity +import org.linphone.core.tools.Log + +class LauncherActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.launcher_activity) + } + + override fun onStart() { + super.onStart() + coreContext.handler.postDelayed({ onReady() }, 500) + } + + private fun onReady() { + Log.i("[Launcher] Core is ready") + val intent = Intent() + intent.setClass(this, MainActivity::class.java) + + // Propagate current intent action, type and data + if (getIntent() != null) { + val extras = getIntent().extras + if (extras != null) intent.putExtras(extras) + } + intent.action = getIntent().action + intent.type = getIntent().type + intent.data = getIntent().data + + startActivity(intent) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/MainActivity.kt b/app/src/main/java/org/linphone/activities/main/MainActivity.kt new file mode 100644 index 000000000..9b71daa48 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/MainActivity.kt @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.Parcelable +import android.view.Gravity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.findNavController +import com.google.android.material.snackbar.Snackbar +import java.io.UnsupportedEncodingException +import java.net.URLDecoder +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.activities.GenericActivity +import org.linphone.activities.SnackBarActivity +import org.linphone.activities.assistant.AssistantActivity +import org.linphone.activities.call.CallActivity +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.compatibility.Compatibility +import org.linphone.contact.ContactsUpdatedListenerStub +import org.linphone.core.tools.Log +import org.linphone.databinding.MainActivityBinding +import org.linphone.utils.AppUtils +import org.linphone.utils.FileUtils + +class MainActivity : GenericActivity(), SnackBarActivity { + private lateinit var binding: MainActivityBinding + private lateinit var sharedViewModel: SharedMainViewModel + + private val listener = object : ContactsUpdatedListenerStub() { + override fun onContactsUpdated() { + if (corePreferences.contactsShortcuts) { + Log.i("[Main Activity] Contact(s) updated, update shortcuts") + Compatibility.createShortcutsToContacts(this@MainActivity) + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.main_activity) + binding.lifecycleOwner = this + + sharedViewModel = ViewModelProvider(this).get(SharedMainViewModel::class.java) + binding.viewModel = sharedViewModel + + sharedViewModel.toggleDrawerEvent.observe(this, Observer { + it.consume { + if (binding.sideMenu.isDrawerOpen(Gravity.LEFT)) { + binding.sideMenu.closeDrawer(binding.sideMenuContent, true) + } else { + binding.sideMenu.openDrawer(binding.sideMenuContent, true) + } + } + }) + + binding.setGoBackToCallClickListener { + val intent = Intent(this, CallActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + startActivity(intent) + } + + if (intent != null) handleIntentParams(intent) + + if (coreContext.core.proxyConfigList.isEmpty()) { + if (corePreferences.firstStart) { + corePreferences.firstStart = false + startActivity(Intent(this, AssistantActivity::class.java)) + } + } + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + + if (intent != null) handleIntentParams(intent) + } + + override fun onResume() { + super.onResume() + coreContext.contactsManager.addListener(listener) + } + + override fun onPause() { + coreContext.contactsManager.removeListener(listener) + super.onPause() + } + + override fun showSnackBar(resourceId: Int) { + Snackbar.make(binding.coordinator, resourceId, Snackbar.LENGTH_LONG).show() + } + + private fun handleIntentParams(intent: Intent) { + when (intent.action) { + Intent.ACTION_SEND -> { + handleSendImage(intent) + } + Intent.ACTION_SEND_MULTIPLE -> { + handleSendMultipleImages(intent) + } + Intent.ACTION_VIEW -> { + if (intent.type == AppUtils.getString(R.string.linphone_address_mime_type)) { + val contactUri = intent.data + if (contactUri != null) { + val contactId = coreContext.contactsManager.getAndroidContactIdFromUri(contactUri) + if (contactId != null) { + val deepLink = "linphone-android://contact/view/$contactId" + Log.i("[Main Activity] Found contact URI parameter in intent: $contactUri, starting deep link: $deepLink") + findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink)) + } + } + } + } + Intent.ACTION_DIAL, Intent.ACTION_CALL -> { + val uri = intent.data + if (uri != null) { + Log.i("[Main Activity] Found uri: $uri to call") + val stringUri = uri.toString() + var addressToCall: String = stringUri + try { + addressToCall = URLDecoder.decode(stringUri, "UTF-8") + } catch (e: UnsupportedEncodingException) {} + + if (addressToCall.startsWith("sip:")) { + addressToCall = addressToCall.substring("sip:".length) + } else if (addressToCall.startsWith("tel:")) { + addressToCall = addressToCall.substring("tel:".length) + } + + Log.i("[Main Activity] Starting dialer with pre-filled URI $addressToCall") + val args = Bundle() + args.putString("URI", addressToCall) + findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_dialerFragment, args) + } + } + else -> { + when { + intent.hasExtra("ContactId") -> { + val id = intent.getStringExtra("ContactId") + val deepLink = "linphone-android://contact/view/$id" + Log.i("[Main Activity] Found contact id parameter in intent: $id, starting deep link: $deepLink") + findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink)) + } + intent.hasExtra("Chat") -> { + Log.i("[Main Activity] Found chat intent extra, go to chat rooms list") + findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_masterChatRoomsFragment) + } + intent.hasExtra("Dialer") -> { + Log.i("[Main Activity] Found dialer intent extra, go to dialer") + val args = Bundle() + args.putBoolean("Transfer", intent.getBooleanExtra("Transfer", false)) + findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_dialerFragment, args) + } + } + } + } + } + + private fun handleSendImage(intent: Intent) { + (intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri)?.let { + val list = arrayListOf() + val path = FileUtils.getFilePath(this, it) + if (path != null) { + list.add(path) + Log.i("[Main Activity] Found single file to share: $path") + } + sharedViewModel.filesToShare.value = list + + val deepLink = "linphone-android://chat/" + Log.i("[Main Activity] Starting deep link: $deepLink") + findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink)) + } + } + + private fun handleSendMultipleImages(intent: Intent) { + intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)?.let { + val list = arrayListOf() + for (parcelable in it) { + val uri = parcelable as Uri + val path = FileUtils.getFilePath(this, uri) + Log.i("[Main Activity] Found file to share: $path") + if (path != null) list.add(path) + } + sharedViewModel.filesToShare.value = list + + val deepLink = "linphone-android://chat/" + Log.i("[Main Activity] Starting deep link: $deepLink") + findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink)) + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/about/AboutFragment.kt b/app/src/main/java/org/linphone/activities/main/about/AboutFragment.kt new file mode 100644 index 000000000..db7e329f1 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/about/AboutFragment.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.about + +import android.content.* +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.main.MainActivity +import org.linphone.core.tools.Log +import org.linphone.databinding.AboutFragmentBinding + +class AboutFragment : Fragment() { + private lateinit var binding: AboutFragmentBinding + private lateinit var viewModel: AboutViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AboutFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(AboutViewModel::class.java) + binding.viewModel = viewModel + + binding.setBackClickListener { findNavController().popBackStack() } + + binding.setPrivacyPolicyClickListener { + val browserIntent = Intent( + Intent.ACTION_VIEW, + Uri.parse(getString(R.string.about_privacy_policy_link)) + ) + startActivity(browserIntent) + } + + binding.setLicenseClickListener { + val browserIntent = Intent( + Intent.ACTION_VIEW, + Uri.parse(getString(R.string.about_license_link)) + ) + startActivity(browserIntent) + } + + viewModel.uploadFinishedEvent.observe(viewLifecycleOwner, Observer { + it.consume { url -> + val clipboard = + requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Logs url", url) + clipboard.setPrimaryClip(clip) + + val activity = requireActivity() as MainActivity + activity.showSnackBar(R.string.logs_url_copied_to_clipboard) + + shareUploadedLogsUrl(url) + } + }) + } + + // Logs + private fun shareUploadedLogsUrl(info: String) { + val appName = getString(R.string.app_name) + val intent = Intent(Intent.ACTION_SEND) + intent.putExtra( + Intent.EXTRA_EMAIL, + arrayOf(getString(R.string.about_bugreport_email)) + ) + intent.putExtra(Intent.EXTRA_SUBJECT, "$appName Logs") + intent.putExtra(Intent.EXTRA_TEXT, info) + intent.type = "application/zip" + + try { + startActivity(Intent.createChooser(intent, "Send mail...")) + } catch (ex: ActivityNotFoundException) { + Log.e(ex) + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/about/AboutViewModel.kt b/app/src/main/java/org/linphone/activities/main/about/AboutViewModel.kt new file mode 100644 index 000000000..5857bddb9 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/about/AboutViewModel.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.about + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub +import org.linphone.utils.Event + +class AboutViewModel : ViewModel() { + val appVersion: String = coreContext.appVersion + + val sdkVersion: String = coreContext.sdkVersion + + val showLogsButtons: Boolean = corePreferences.debugLogs + + val uploadInProgress = MutableLiveData() + + val uploadFinishedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private val listener = object : CoreListenerStub() { + override fun onLogCollectionUploadStateChanged( + core: Core, + state: Core.LogCollectionUploadState, + info: String + ) { + if (state == Core.LogCollectionUploadState.Delivered) { + uploadInProgress.value = false + uploadFinishedEvent.value = Event(info) + } else if (state == Core.LogCollectionUploadState.NotDelivered) { + uploadInProgress.value = false + } + } + } + + init { + coreContext.core.addListener(listener) + uploadInProgress.value = false + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + + super.onCleared() + } + + fun uploadLogs() { + uploadInProgress.value = true + coreContext.core.uploadLogCollection() + } + + fun resetLogs() { + coreContext.core.resetLogCollection() + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/ChatScrollListener.kt b/app/src/main/java/org/linphone/activities/main/chat/ChatScrollListener.kt new file mode 100644 index 000000000..7b2598b83 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/ChatScrollListener.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat + +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +internal abstract class ChatScrollListener(private val mLayoutManager: LinearLayoutManager) : + RecyclerView.OnScrollListener() { + // The total number of items in the data set after the last load + private var previousTotalItemCount = 0 + // True if we are still waiting for the last set of data to load. + private var loading = true + + // This happens many times a second during a scroll, so be wary of the code you place here. + // We are given a few useful parameters to help us work out if we need to load some more data, + // but first we check if we are waiting for the previous load to finish. + override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { + val totalItemCount = mLayoutManager.itemCount + val firstVisibleItemPosition: Int = mLayoutManager.findFirstVisibleItemPosition() + val lastVisibleItemPosition: Int = mLayoutManager.findLastVisibleItemPosition() + + // If the total item count is zero and the previous isn't, assume the + // list is invalidated and should be reset back to initial state + if (totalItemCount < previousTotalItemCount) { + previousTotalItemCount = totalItemCount + if (totalItemCount == 0) { + loading = true + } + } + + // If it’s still loading, we check to see if the data set count has + // changed, if so we conclude it has finished loading and update the current page + // number and total item count. + if (loading && totalItemCount > previousTotalItemCount) { + loading = false + previousTotalItemCount = totalItemCount + } + + // If it isn’t currently loading, we check to see if we have breached + // the mVisibleThreshold and need to reload more data. + // If we do need to reload some more data, we execute onLoadMore to fetch the data. + // threshold should reflect how many total columns there are too + if (!loading && firstVisibleItemPosition < mVisibleThreshold && firstVisibleItemPosition > 0 && lastVisibleItemPosition < totalItemCount - mVisibleThreshold) { + onLoadMore(totalItemCount) + loading = true + } + } + + // Defines the process for actually loading more data based on page + protected abstract fun onLoadMore(totalItemsCount: Int) + + companion object { + // The minimum amount of items to have below your current scroll position + // before loading more. + private const val mVisibleThreshold = 5 + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/GroupChatRoomMember.kt b/app/src/main/java/org/linphone/activities/main/chat/GroupChatRoomMember.kt new file mode 100644 index 000000000..9f8129cab --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/GroupChatRoomMember.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat + +import org.linphone.core.Address +import org.linphone.core.ChatRoomSecurityLevel + +data class GroupChatRoomMember( + val address: Address, + var isAdmin: Boolean = false, + val securityLevel: ChatRoomSecurityLevel = ChatRoomSecurityLevel.ClearText, + val hasLimeX3DHCapability: Boolean = false +) diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt new file mode 100644 index 000000000..74bdb9d89 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt @@ -0,0 +1,330 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.adapters + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DiffUtil +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.chat.viewmodels.ChatMessageViewModel +import org.linphone.activities.main.chat.viewmodels.EventViewModel +import org.linphone.activities.main.chat.viewmodels.OnContentClickedListener +import org.linphone.activities.main.viewmodels.ListTopBarViewModel +import org.linphone.core.ChatMessage +import org.linphone.core.ChatRoomCapabilities +import org.linphone.core.EventLog +import org.linphone.databinding.ChatEventListCellBinding +import org.linphone.databinding.ChatMessageListCellBinding +import org.linphone.utils.Event +import org.linphone.utils.LifecycleListAdapter +import org.linphone.utils.LifecycleViewHolder + +class ChatMessagesListAdapter(val selectionViewModel: ListTopBarViewModel) : LifecycleListAdapter(ChatMessageDiffCallback()) { + companion object { + const val MAX_TIME_TO_GROUP_MESSAGES = 300 // 5 minutes + } + + val resendMessageEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val deleteMessageEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val forwardMessageEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val showImdnForMessageEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val addSipUriToContactEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val openContentEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private val contentClickedListener = object : OnContentClickedListener { + override fun onContentClicked(path: String) { + openContentEvent.value = Event(path) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LifecycleViewHolder { + return when (viewType) { + EventLog.Type.ConferenceChatMessage.toInt() -> createChatMessageViewHolder(parent) + else -> createEventViewHolder(parent) + } + } + + private fun createChatMessageViewHolder(parent: ViewGroup): ChatMessageViewHolder { + val binding: ChatMessageListCellBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.chat_message_list_cell, parent, false + ) + val viewHolder = ChatMessageViewHolder(binding) + binding.lifecycleOwner = viewHolder + return viewHolder + } + + private fun createEventViewHolder(parent: ViewGroup): EventViewHolder { + val binding: ChatEventListCellBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.chat_event_list_cell, parent, false + ) + val viewHolder = EventViewHolder(binding) + binding.lifecycleOwner = viewHolder + return viewHolder + } + + override fun onBindViewHolder(holder: LifecycleViewHolder, position: Int) { + val eventLog = getItem(position) + when (holder) { + is ChatMessageViewHolder -> holder.bind(eventLog) + is EventViewHolder -> holder.bind(eventLog) + } + } + + override fun getItemViewType(position: Int): Int { + val eventLog = getItem(position) + return eventLog.type.toInt() + } + + inner class ChatMessageViewHolder( + private val binding: ChatMessageListCellBinding + ) : LifecycleViewHolder(binding), PopupMenu.OnMenuItemClickListener { + fun bind(eventLog: EventLog) { + with(binding) { + if (eventLog.type == EventLog.Type.ConferenceChatMessage) { + val chatMessage = eventLog.chatMessage + val chatMessageViewModel = ChatMessageViewModel(chatMessage, contentClickedListener) + viewModel = chatMessageViewModel + + // This is for item selection through ListTopBarFragment + selectionListViewModel = selectionViewModel + selectionViewModel.isEditionEnabled.observe(this@ChatMessageViewHolder, Observer { + position = adapterPosition + }) + + binding.setClickListener { + if (selectionViewModel.isEditionEnabled.value == true) { + selectionViewModel.onToggleSelect(adapterPosition) + } + } + + // Grouping + var hasPrevious = false + var hasNext = false + + if (adapterPosition > 0) { + val previousItem = getItem(adapterPosition - 1) + if (previousItem.type == EventLog.Type.ConferenceChatMessage) { + val previousMessage = previousItem.chatMessage + if (previousMessage.fromAddress.weakEqual(chatMessage.fromAddress)) { + if (chatMessage.time - previousMessage.time < MAX_TIME_TO_GROUP_MESSAGES) { + hasPrevious = true + } + } + } + } + + if (adapterPosition >= 0 && adapterPosition < itemCount - 1) { + val nextItem = getItem(adapterPosition + 1) + if (nextItem.type == EventLog.Type.ConferenceChatMessage) { + val nextMessage = nextItem.chatMessage + if (nextMessage.fromAddress.weakEqual(chatMessage.fromAddress)) { + if (nextMessage.time - chatMessage.time < MAX_TIME_TO_GROUP_MESSAGES) { + hasNext = true + } + } + } + } + + chatMessageViewModel.updateBubbleBackground(hasPrevious, hasNext) + + executePendingBindings() + + setContextMenuClickListener { + val popup = PopupMenu(root.context, background) + popup.setOnMenuItemClickListener(this@ChatMessageViewHolder) + popup.inflate(R.menu.chat_message_menu) + + if (!chatMessage.isOutgoing || + chatMessage.chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt()) || + chatMessage.state == ChatMessage.State.NotDelivered) { // No message id + popup.menu.removeItem(R.id.chat_message_menu_imdn_infos) + } + if (chatMessage.state != ChatMessage.State.NotDelivered) { + popup.menu.removeItem(R.id.chat_message_menu_resend) + } + if (!chatMessage.hasTextContent()) { + popup.menu.removeItem(R.id.chat_message_menu_copy_text) + } + if (chatMessageViewModel.contact.value != null) { + popup.menu.removeItem(R.id.chat_message_menu_add_to_contacts) + } + + popup.show() + true + } + } + } + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.chat_message_menu_imdn_infos -> { + showImdnDeliveryFragment() + true + } + R.id.chat_message_menu_resend -> { + resendMessage() + true + } + R.id.chat_message_menu_copy_text -> { + copyTextToClipboard() + true + } + R.id.chat_message_forward_message -> { + forwardMessage() + true + } + R.id.chat_message_menu_delete_message -> { + deleteMessage() + true + } + R.id.chat_message_menu_add_to_contacts -> { + addSenderToContacts() + true + } + else -> false + } + } + + private fun resendMessage() { + val chatMessage = binding.viewModel?.chatMessage + if (chatMessage != null) { + val viewHolder = binding.lifecycleOwner as ChatMessageViewHolder + chatMessage.userData = viewHolder.adapterPosition + resendMessageEvent.value = Event(chatMessage) + } + } + + private fun copyTextToClipboard() { + val chatMessage = binding.viewModel?.chatMessage + if (chatMessage != null && chatMessage.hasTextContent()) { + val clipboard: ClipboardManager = coreContext.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Message", chatMessage.textContent) + clipboard.setPrimaryClip(clip) + } + } + + private fun forwardMessage() { + val chatMessage = binding.viewModel?.chatMessage + if (chatMessage != null) { + forwardMessageEvent.value = Event(chatMessage) + } + } + + private fun showImdnDeliveryFragment() { + val chatMessage = binding.viewModel?.chatMessage + if (chatMessage != null) { + showImdnForMessageEvent.value = Event(chatMessage) + } + } + + private fun deleteMessage() { + val chatMessage = binding.viewModel?.chatMessage + if (chatMessage != null) { + val viewHolder = binding.lifecycleOwner as ChatMessageViewHolder + chatMessage.userData = viewHolder.adapterPosition + deleteMessageEvent.value = Event(chatMessage) + } + } + + private fun addSenderToContacts() { + val chatMessage = binding.viewModel?.chatMessage + if (chatMessage != null) { + chatMessage.fromAddress.clean() // To remove gruu if any + addSipUriToContactEvent.value = Event(chatMessage.fromAddress.asStringUriOnly()) + } + } + } + + inner class EventViewHolder( + private val binding: ChatEventListCellBinding + ) : LifecycleViewHolder(binding) { + fun bind(eventLog: EventLog) { + with(binding) { + val eventViewModel = EventViewModel(eventLog) + viewModel = eventViewModel + + // This is for item selection through ListTopBarFragment + selectionListViewModel = selectionViewModel + selectionViewModel.isEditionEnabled.observe(this@EventViewHolder, Observer { + position = adapterPosition + }) + + binding.setClickListener { + if (selectionViewModel.isEditionEnabled.value == true) { + selectionViewModel.onToggleSelect(adapterPosition) + } + } + + executePendingBindings() + } + } + } +} + +private class ChatMessageDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: EventLog, + newItem: EventLog + ): Boolean { + return if (oldItem.type == EventLog.Type.ConferenceChatMessage && + newItem.type == EventLog.Type.ConferenceChatMessage) { + oldItem.chatMessage.time == newItem.chatMessage.time && + oldItem.chatMessage.isOutgoing == newItem.chatMessage.isOutgoing + } else oldItem.notifyId == newItem.notifyId + } + + override fun areContentsTheSame( + oldItem: EventLog, + newItem: EventLog + ): Boolean { + return if (newItem.type == EventLog.Type.ConferenceChatMessage) { + newItem.chatMessage.state == ChatMessage.State.Displayed + } else false + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomCreationContactsAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomCreationContactsAdapter.kt new file mode 100644 index 000000000..c16d9ffa0 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomCreationContactsAdapter.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DiffUtil +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.chat.viewmodels.ChatRoomCreationContactViewModel +import org.linphone.core.Address +import org.linphone.core.FriendCapability +import org.linphone.core.SearchResult +import org.linphone.databinding.ChatRoomCreationContactCellBinding +import org.linphone.utils.Event +import org.linphone.utils.LifecycleListAdapter +import org.linphone.utils.LifecycleViewHolder + +class ChatRoomCreationContactsAdapter : LifecycleListAdapter(SearchResultDiffCallback()) { + val selectedContact = MutableLiveData>() + + val selectedAddresses = MutableLiveData>() + + var groupChatEnabled: Boolean = false + + val securityEnabled = MutableLiveData() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding: ChatRoomCreationContactCellBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.chat_room_creation_contact_cell, parent, false + ) + val viewHolder = ViewHolder(binding) + binding.lifecycleOwner = viewHolder + return viewHolder + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + inner class ViewHolder( + private val binding: ChatRoomCreationContactCellBinding + ) : LifecycleViewHolder(binding) { + fun bind(searchResult: SearchResult) { + with(binding) { + val searchResultViewModel = ChatRoomCreationContactViewModel(searchResult) + viewModel = searchResultViewModel + + securityEnabled.observe(this@ViewHolder, Observer { + updateSecurity(searchResult, searchResultViewModel, it) + }) + + selectedAddresses.observe(this@ViewHolder, Observer { + val selected = it.find { address -> + if (searchResult.address != null) address.weakEqual(searchResult.address) else false + } + searchResultViewModel.isSelected.value = selected != null + }) + + setClickListener { + selectedContact.value = Event(searchResult) + } + + executePendingBindings() + } + } + + private fun updateSecurity( + searchResult: SearchResult, + viewModel: ChatRoomCreationContactViewModel, + securityEnabled: Boolean + ) { + val isMyself = securityEnabled && searchResult.address != null && coreContext.core.defaultProxyConfig?.identityAddress?.weakEqual(searchResult.address) ?: false + val limeCheck = !securityEnabled || (securityEnabled && searchResult.hasCapability(FriendCapability.LimeX3Dh)) + val groupCheck = !groupChatEnabled || (groupChatEnabled && searchResult.hasCapability(FriendCapability.GroupChat)) + val disabled = if (searchResult.friend != null) !limeCheck || !groupCheck || isMyself else false // Generated entry from search filter + + viewModel.isDisabled.value = disabled + + if (disabled && viewModel.isSelected.value == true) { + // Remove item from selection if both selected and disabled + selectedContact.postValue(Event(searchResult)) + } + } + } +} + +private class SearchResultDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: SearchResult, + newItem: SearchResult + ): Boolean { + return if (oldItem.address != null && newItem.address != null) oldItem.address.weakEqual(newItem.address) else false + } + + override fun areContentsTheSame( + oldItem: SearchResult, + newItem: SearchResult + ): Boolean { + return newItem.friend != null + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomsListAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomsListAdapter.kt new file mode 100644 index 000000000..0c0786bf1 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomsListAdapter.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DiffUtil +import org.linphone.R +import org.linphone.activities.main.chat.viewmodels.ChatRoomViewModel +import org.linphone.activities.main.viewmodels.ListTopBarViewModel +import org.linphone.core.ChatRoom +import org.linphone.databinding.ChatRoomListCellBinding +import org.linphone.utils.Event +import org.linphone.utils.LifecycleListAdapter +import org.linphone.utils.LifecycleViewHolder + +class ChatRoomsListAdapter(val selectionViewModel: ListTopBarViewModel) : LifecycleListAdapter(ChatRoomDiffCallback()) { + val selectedChatRoomEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding: ChatRoomListCellBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.chat_room_list_cell, parent, false + ) + val viewHolder = ViewHolder(binding) + binding.lifecycleOwner = viewHolder + return viewHolder + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + inner class ViewHolder( + private val binding: ChatRoomListCellBinding + ) : LifecycleViewHolder(binding) { + fun bind(chatRoom: ChatRoom) { + with(binding) { + val chatRoomViewModel = ChatRoomViewModel(chatRoom) + viewModel = chatRoomViewModel + + // This is for item selection through ListTopBarFragment + selectionListViewModel = selectionViewModel + selectionViewModel.isEditionEnabled.observe(this@ViewHolder, Observer { + position = adapterPosition + }) + + binding.setClickListener { + if (selectionViewModel.isEditionEnabled.value == true) { + selectionViewModel.onToggleSelect(adapterPosition) + } else { + selectedChatRoomEvent.value = Event(chatRoom) + chatRoom.markAsRead() + } + } + + executePendingBindings() + } + } + } +} + +private class ChatRoomDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ChatRoom, + newItem: ChatRoom + ): Boolean { + return oldItem.localAddress.weakEqual(newItem.localAddress) && + oldItem.peerAddress.weakEqual(newItem.peerAddress) + } + + override fun areContentsTheSame( + oldItem: ChatRoom, + newItem: ChatRoom + ): Boolean { + return newItem.unreadMessagesCount == 0 + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/GroupInfoParticipantsAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/GroupInfoParticipantsAdapter.kt new file mode 100644 index 000000000..b5af4b208 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/GroupInfoParticipantsAdapter.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.MutableLiveData +import androidx.recyclerview.widget.DiffUtil +import org.linphone.R +import org.linphone.activities.main.chat.GroupChatRoomMember +import org.linphone.activities.main.chat.viewmodels.GroupInfoParticipantViewModel +import org.linphone.databinding.ChatRoomGroupInfoParticipantCellBinding +import org.linphone.utils.Event +import org.linphone.utils.LifecycleListAdapter +import org.linphone.utils.LifecycleViewHolder + +class GroupInfoParticipantsAdapter(private val isEncryptionEnabled: Boolean) : LifecycleListAdapter(ParticipantDiffCallback()) { + private var showAdmin: Boolean = false + + val participantRemovedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding: ChatRoomGroupInfoParticipantCellBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.chat_room_group_info_participant_cell, parent, false + ) + val viewHolder = ViewHolder(binding) + binding.lifecycleOwner = viewHolder + return viewHolder + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + fun showAdminControls(show: Boolean) { + showAdmin = show + notifyDataSetChanged() + } + + inner class ViewHolder( + private val binding: ChatRoomGroupInfoParticipantCellBinding + ) : LifecycleViewHolder(binding) { + fun bind(participant: GroupChatRoomMember) { + with(binding) { + val participantViewModel = GroupInfoParticipantViewModel(participant) + participantViewModel.showAdminControls.value = showAdmin + viewModel = participantViewModel + + setRemoveClickListener { + participantRemovedEvent.value = Event(participant) + } + isEncrypted = isEncryptionEnabled + + executePendingBindings() + } + } + } +} + +private class ParticipantDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: GroupChatRoomMember, + newItem: GroupChatRoomMember + ): Boolean { + return oldItem.address.weakEqual(newItem.address) + } + + override fun areContentsTheSame( + oldItem: GroupChatRoomMember, + newItem: GroupChatRoomMember + ): Boolean { + return false + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ImdnAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/ImdnAdapter.kt new file mode 100644 index 000000000..0100ab227 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/ImdnAdapter.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import org.linphone.R +import org.linphone.activities.main.chat.viewmodels.ImdnParticipantViewModel +import org.linphone.core.ChatMessage +import org.linphone.core.ParticipantImdnState +import org.linphone.databinding.ChatRoomImdnParticipantCellBinding +import org.linphone.databinding.ImdnListHeaderBinding +import org.linphone.utils.HeaderAdapter +import org.linphone.utils.LifecycleViewHolder + +class ImdnAdapter : ListAdapter(ParticipantImdnStateDiffCallback()), HeaderAdapter { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding: ChatRoomImdnParticipantCellBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.chat_room_imdn_participant_cell, parent, false + ) + val viewHolder = ViewHolder(binding) + binding.lifecycleOwner = viewHolder + return viewHolder + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + inner class ViewHolder( + private val binding: ChatRoomImdnParticipantCellBinding + ) : LifecycleViewHolder(binding) { + fun bind(participantImdnState: ParticipantImdnState) { + with(binding) { + val imdnViewModel = ImdnParticipantViewModel(participantImdnState) + viewModel = imdnViewModel + + executePendingBindings() + } + } + } + + override fun displayHeaderForPosition(position: Int): Boolean { + if (position >= itemCount) return false + val participantImdnState = getItem(position) + val previousPosition = position - 1 + return if (previousPosition >= 0) { + getItem(previousPosition).state != participantImdnState.state + } else true + } + + override fun getHeaderViewForPosition(context: Context, position: Int): View { + val participantImdnState = getItem(position) + val binding: ImdnListHeaderBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.imdn_list_header, null, false + ) + when (participantImdnState.state) { + ChatMessage.State.Displayed -> { + binding.title = R.string.chat_message_imdn_displayed + binding.textColor = R.color.imdn_read_color + binding.icon = R.drawable.message_read + } + ChatMessage.State.DeliveredToUser -> { + binding.title = R.string.chat_message_imdn_delivered + binding.textColor = R.color.grey_color + binding.icon = R.drawable.message_delivered + } + ChatMessage.State.Delivered -> { + binding.title = R.string.chat_message_imdn_sent + binding.textColor = R.color.grey_color + binding.icon = R.drawable.message_delivered + } + ChatMessage.State.NotDelivered -> { + binding.title = R.string.chat_message_imdn_undelivered + binding.textColor = R.color.red_color + binding.icon = R.drawable.message_undelivered + } + } + binding.executePendingBindings() + return binding.root + } +} + +private class ParticipantImdnStateDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ParticipantImdnState, + newItem: ParticipantImdnState + ): Boolean { + return oldItem.participant.address.weakEqual(newItem.participant.address) + } + + override fun areContentsTheSame( + oldItem: ParticipantImdnState, + newItem: ParticipantImdnState + ): Boolean { + return false + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt new file mode 100644 index 000000000..4f1285931 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.SearchView +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import org.linphone.R +import org.linphone.activities.main.MainActivity +import org.linphone.activities.main.chat.adapters.ChatRoomCreationContactsAdapter +import org.linphone.activities.main.chat.viewmodels.ChatRoomCreationViewModel +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.core.Address +import org.linphone.databinding.ChatRoomCreationFragmentBinding + +class ChatRoomCreationFragment : Fragment() { + private lateinit var binding: ChatRoomCreationFragmentBinding + private lateinit var viewModel: ChatRoomCreationViewModel + private lateinit var sharedViewModel: SharedMainViewModel + private lateinit var adapter: ChatRoomCreationContactsAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ChatRoomCreationFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + val createGroup = arguments?.getBoolean("createGroup") ?: false + + viewModel = ViewModelProvider(this).get(ChatRoomCreationViewModel::class.java) + viewModel.createGroupChat.value = createGroup + + viewModel.isEncrypted.value = sharedViewModel.createEncryptedChatRoom + + binding.viewModel = viewModel + + adapter = ChatRoomCreationContactsAdapter() + adapter.groupChatEnabled = viewModel.createGroupChat.value == true + adapter.securityEnabled.value = viewModel.isEncrypted.value == true + binding.contactsList.adapter = adapter + + val layoutManager = LinearLayoutManager(activity) + binding.contactsList.layoutManager = layoutManager + + // Divider between items + val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation) + dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null)) + binding.contactsList.addItemDecoration(dividerItemDecoration) + + binding.setBackClickListener { + findNavController().popBackStack() + } + binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE + + binding.setAllContactsToggleClickListener { + viewModel.sipContactsSelected.value = false + } + + binding.setSipContactsToggleClickListener { + viewModel.sipContactsSelected.value = true + } + + viewModel.contactsList.observe(viewLifecycleOwner, Observer { + adapter.submitList(it) + }) + + viewModel.isEncrypted.observe(viewLifecycleOwner, Observer { + adapter.securityEnabled.value = it + }) + + viewModel.sipContactsSelected.observe(viewLifecycleOwner, Observer { + viewModel.updateContactsList() + }) + + viewModel.selectedAddresses.observe(viewLifecycleOwner, Observer { + adapter.selectedAddresses.value = it + }) + + viewModel.chatRoomCreatedEvent.observe(viewLifecycleOwner, Observer { + it.consume { chatRoom -> + sharedViewModel.selectedChatRoom.value = chatRoom + if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) { + findNavController().navigate(R.id.action_chatRoomCreationFragment_to_detailChatRoomFragment) + } + } + }) + + adapter.selectedContact.observe(viewLifecycleOwner, Observer { + it.consume { searchResult -> + if (createGroup) { + viewModel.toggleSelectionForSearchResult(searchResult) + } else { + viewModel.createOneToOneChat(searchResult) + } + } + }) + + addParticipantsFromBundle() + + // Next button is only used to go to group chat info fragment + binding.setNextClickListener { + sharedViewModel.createEncryptedChatRoom = viewModel.isEncrypted.value == true + + if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) { + val args = Bundle() + args.putSerializable("participants", viewModel.selectedAddresses.value) + findNavController().navigate(R.id.action_chatRoomCreationFragment_to_groupInfoFragment, args) + } + } + + binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + viewModel.filter(newText ?: "") + return true + } + }) + + viewModel.onErrorEvent.observe(viewLifecycleOwner, Observer { + it.consume { messageResourceId -> + (activity as MainActivity).showSnackBar(messageResourceId) + } + }) + } + + @Suppress("UNCHECKED_CAST") + private fun addParticipantsFromBundle() { + val participants = arguments?.getSerializable("participants") as? ArrayList
+ if (participants != null && participants.size > 0) { + viewModel.selectedAddresses.value = participants + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt new file mode 100644 index 000000000..561e92cc4 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt @@ -0,0 +1,565 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.fragments + +import android.app.Activity +import android.app.Dialog +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Bundle +import android.os.Parcelable +import android.provider.MediaStore +import android.view.* +import android.webkit.MimeTypeMap +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.view.menu.MenuPopupHelper +import androidx.core.content.FileProvider +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.Navigation +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import java.io.File +import kotlinx.android.synthetic.main.tabs_fragment.* +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.activities.main.MainActivity +import org.linphone.activities.main.chat.ChatScrollListener +import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter +import org.linphone.activities.main.chat.viewmodels.* +import org.linphone.activities.main.fragments.MasterFragment +import org.linphone.activities.main.viewmodels.DialogViewModel +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.databinding.ChatRoomDetailFragmentBinding +import org.linphone.utils.* +import org.linphone.utils.Event + +class DetailChatRoomFragment : MasterFragment() { + private lateinit var binding: ChatRoomDetailFragmentBinding + private lateinit var viewModel: ChatRoomViewModel + private lateinit var chatSendingViewModel: ChatMessageSendingViewModel + private lateinit var listViewModel: ChatMessagesListViewModel + private lateinit var adapter: ChatMessagesListAdapter + private lateinit var sharedViewModel: SharedMainViewModel + private var chatRoomAddress: String? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ChatRoomDetailFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + val chatRoom = sharedViewModel.selectedChatRoom.value + chatRoom ?: return + chatRoomAddress = chatRoom.peerAddress.asStringUriOnly() + + viewModel = ViewModelProvider( + this, + ChatRoomViewModelFactory(chatRoom) + )[ChatRoomViewModel::class.java] + binding.viewModel = viewModel + + chatSendingViewModel = ViewModelProvider( + this, + ChatMessageSendingViewModelFactory(chatRoom) + )[ChatMessageSendingViewModel::class.java] + binding.chatSendingViewModel = chatSendingViewModel + + listViewModel = ViewModelProvider( + this, + ChatMessagesListViewModelFactory(chatRoom) + )[ChatMessagesListViewModel::class.java] + + adapter = ChatMessagesListAdapter(listSelectionViewModel) + // SubmitList is done on a background thread + // We need this adapter data observer to know when to scroll + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + if (positionStart == adapter.itemCount - 1) { + adapter.notifyItemChanged(positionStart - 1) // For grouping purposes + scrollToBottom() + } + } + }) + binding.chatMessagesList.adapter = adapter + + val layoutManager = LinearLayoutManager(activity) + layoutManager.stackFromEnd = true + binding.chatMessagesList.layoutManager = layoutManager + + val chatScrollListener: ChatScrollListener = object : ChatScrollListener(layoutManager) { + override fun onLoadMore(totalItemsCount: Int) { + listViewModel.loadMoreData(totalItemsCount) + } + } + binding.chatMessagesList.addOnScrollListener(chatScrollListener) + + listViewModel.events.observe(viewLifecycleOwner, Observer { events -> + adapter.submitList(events) + }) + + listViewModel.messageUpdatedEvent.observe(viewLifecycleOwner, Observer { + it.consume { position -> + adapter.notifyItemChanged(position) + } + }) + + listViewModel.requestWriteExternalStoragePermissionEvent.observe(viewLifecycleOwner, Observer { + it.consume { + requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), 1) + } + }) + + adapter.deleteMessageEvent.observe(viewLifecycleOwner, Observer { + it.consume { chatMessage -> + listViewModel.deleteMessage(chatMessage) + } + }) + + adapter.resendMessageEvent.observe(viewLifecycleOwner, Observer { + it.consume { chatMessage -> + listViewModel.resendMessage(chatMessage) + } + }) + + adapter.forwardMessageEvent.observe(viewLifecycleOwner, Observer { + it.consume { chatMessage -> + // Remove observer before setting the message to forward + // as we don't want to forward it in this chat room + sharedViewModel.messageToForwardEvent.removeObservers(viewLifecycleOwner) + sharedViewModel.messageToForwardEvent.value = Event(chatMessage) + + val deepLink = "linphone-android://chat/" + Log.i("[Chat Room] Forwarding message, starting deep link: $deepLink") + findNavController().navigate(Uri.parse(deepLink)) + } + }) + + adapter.showImdnForMessageEvent.observe(viewLifecycleOwner, Observer { + it.consume { chatMessage -> + val args = Bundle() + args.putString("MessageId", chatMessage.messageId) + Navigation.findNavController(binding.root).navigate(R.id.action_detailChatRoomFragment_to_imdnFragment, args) + } + }) + + adapter.addSipUriToContactEvent.observe(viewLifecycleOwner, Observer { + it.consume { sipUri -> + val deepLink = "linphone-android://contact/new/$sipUri" + Log.i("[Chat Room] Creating contact, starting deep link: $deepLink") + findNavController().navigate(Uri.parse(deepLink)) + } + }) + + adapter.openContentEvent.observe(viewLifecycleOwner, Observer { + it.consume { path -> + openFile(path) + } + }) + + binding.setBackClickListener { + findNavController().popBackStack() + } + binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE + + binding.setTitleClickListener { + binding.sipUri.visibility = if (!viewModel.oneToOneChatRoom || + binding.sipUri.visibility == View.VISIBLE) View.GONE else View.VISIBLE + } + + binding.setMenuClickListener { + showPopupMenu(chatRoom) + } + + binding.setEditClickListener { + enterEditionMode() + } + + binding.setSecurityIconClickListener { + showParticipantsDevices() + } + + binding.setAttachFileClickListener { + if (PermissionHelper.get().hasReadExternalStorage() && PermissionHelper.get().hasCameraPermission()) { + pickFile() + } else { + Log.i("[Chat Room] Asking for READ_EXTERNAL_STORAGE and CAMERA permissions") + requestPermissions(arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE, android.Manifest.permission.CAMERA), 0) + } + } + + binding.setSendMessageClickListener { + chatSendingViewModel.sendMessage() + binding.message.text?.clear() + } + + binding.setStartCallClickListener { + coreContext.startCall(viewModel.addressToCall) + } + + sharedViewModel.filesToShare.observe(viewLifecycleOwner, Observer { + if (it.isNotEmpty()) { + for (path in it) { + Log.i("[Chat Room] Found $path file to share") + chatSendingViewModel.addAttachment(path) + } + sharedViewModel.filesToShare.value = arrayListOf() + } + }) + + sharedViewModel.messageToForwardEvent.observe(viewLifecycleOwner, Observer { + it.consume { chatMessage -> + Log.i("[Chat Room] Found message to transfer") + showForwardConfirmationDialog(chatMessage) + } + }) + } + + override fun getItemCount(): Int { + return adapter.itemCount + } + + override fun deleteItems(indexesOfItemToDelete: ArrayList) { + val list = ArrayList() + for (index in indexesOfItemToDelete) { + val eventLog = adapter.getItemAt(index) + list.add(eventLog) + } + listViewModel.deleteEventLogs(list) + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + if (requestCode == 0) { + var atLeastOneGranted = false + for (result in grantResults) { + atLeastOneGranted = atLeastOneGranted || result == PackageManager.PERMISSION_GRANTED + } + if (atLeastOneGranted) { + pickFile() + } + } + } + + override fun onResume() { + super.onResume() + + // Prevent notifications for this chat room to be displayed + coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = chatRoomAddress + scrollToBottom() + } + + override fun onPause() { + coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null + + super.onPause() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (resultCode == Activity.RESULT_OK) { + var fileToUploadPath: String? = null + + val temporaryFileUploadPath = chatSendingViewModel.temporaryFileUploadPath + if (temporaryFileUploadPath != null) { + if (data != null) { + val dataUri = data.data + if (dataUri != null) { + fileToUploadPath = dataUri.toString() + Log.i("[Chat Room] Using data URI $fileToUploadPath") + } else if (temporaryFileUploadPath.exists()) { + fileToUploadPath = temporaryFileUploadPath.absolutePath + Log.i("[Chat Room] Data URI is null, using $fileToUploadPath") + } + } else if (temporaryFileUploadPath.exists()) { + fileToUploadPath = temporaryFileUploadPath.absolutePath + Log.i("[Chat Room] Data is null, using $fileToUploadPath") + } + } + + if (fileToUploadPath != null) { + if (fileToUploadPath.startsWith("content://") || + fileToUploadPath.startsWith("file://") + ) { + val uriToParse = Uri.parse(fileToUploadPath) + fileToUploadPath = FileUtils.getFilePath(requireContext(), uriToParse) + Log.i("[Chat] Path was using a content or file scheme, real path is: $fileToUploadPath") + if (fileToUploadPath == null) { + Log.e("[Chat] Failed to get access to file $uriToParse") + } + } + } + + if (fileToUploadPath != null) { + chatSendingViewModel.addAttachment(fileToUploadPath) + } + } + } + + private fun enterEditionMode() { + listSelectionViewModel.isEditionEnabled.value = true + } + + private fun showParticipantsDevices() { + if (corePreferences.limeSecurityPopupEnabled) { + val dialogViewModel = DialogViewModel(getString(R.string.dialog_lime_security_message)) + dialogViewModel.showDoNotAskAgain = true + val dialog = DialogUtils.getDialog(requireContext(), dialogViewModel) + + dialogViewModel.showCancelButton { doNotAskAgain -> + if (doNotAskAgain) corePreferences.limeSecurityPopupEnabled = false + dialog.dismiss() + } + + val okLabel = if (viewModel.oneParticipantOneDevice) getString(R.string.dialog_call) else getString(R.string.dialog_ok) + dialogViewModel.showOkButton({ doNotAskAgain -> + if (doNotAskAgain) corePreferences.limeSecurityPopupEnabled = false + + if (viewModel.oneParticipantOneDevice) { + coreContext.startCall(viewModel.onlyParticipantOnlyDeviceAddress, true) + } else { + if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) { + findNavController().navigate(R.id.action_detailChatRoomFragment_to_devicesFragment) + } + } + + dialog.dismiss() + }, okLabel) + + dialog.show() + } else { + if (viewModel.oneParticipantOneDevice) { + coreContext.startCall(viewModel.onlyParticipantOnlyDeviceAddress, true) + } else { + if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) { + findNavController().navigate(R.id.action_detailChatRoomFragment_to_devicesFragment) + } + } + } + } + + private fun showGroupInfo(chatRoom: ChatRoom) { + sharedViewModel.selectedGroupChatRoom.value = chatRoom + if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) { + findNavController().navigate(R.id.action_detailChatRoomFragment_to_groupInfoFragment) + } + } + + private fun showEphemeralMessages() { + if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) { + findNavController().navigate(R.id.action_detailChatRoomFragment_to_ephemeralFragment) + } + } + + private fun showForwardConfirmationDialog(chatMessage: ChatMessage) { + val viewModel = DialogViewModel(getString(R.string.chat_message_forward_confirmation_dialog)) + viewModel.iconResource = R.drawable.forward_message_dialog_default + viewModel.showIcon = true + val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel) + + viewModel.showCancelButton { + Log.i("[Chat Room] Transfer cancelled") + dialog.dismiss() + } + + viewModel.showOkButton({ + Log.i("[Chat Room] Transfer confirmed") + chatSendingViewModel.transferMessage(chatMessage) + dialog.dismiss() + }, getString(R.string.chat_message_context_menu_forward)) + + dialog.show() + } + + private fun showPopupMenu(chatRoom: ChatRoom) { + val builder = MenuBuilder(requireContext()) + val popupMenu = MenuPopupHelper(requireContext(), builder, binding.menu) + popupMenu.setForceShowIcon(true) + + MenuInflater(requireContext()).inflate(R.menu.chat_room_menu, builder) + if (viewModel.oneToOneChatRoom) { + builder.removeItem(R.id.chat_room_group_info) + + // If one participant one device, a click on security badge + // will directly start a call or show the dialog, so don't show this menu + if (viewModel.oneParticipantOneDevice) { + builder.removeItem(R.id.chat_room_participants_devices) + } + } + if (!viewModel.encryptedChatRoom) { + builder.removeItem(R.id.chat_room_participants_devices) + builder.removeItem(R.id.chat_room_ephemeral_messages) + } + // TODO: hide ephemeral menu if not all participants support the feature + + builder.setCallback(object : MenuBuilder.Callback { + override fun onMenuModeChange(menu: MenuBuilder?) {} + + override fun onMenuItemSelected(menu: MenuBuilder, item: MenuItem): Boolean { + return when (item.itemId) { + R.id.chat_room_group_info -> { + showGroupInfo(chatRoom) + true + } + R.id.chat_room_participants_devices -> { + showParticipantsDevices() + true + } + R.id.chat_room_ephemeral_messages -> { + showEphemeralMessages() + true + } + R.id.chat_room_delete_messages -> { + enterEditionMode() + true + } + else -> false + } + } + }) + + popupMenu.show() + } + + private fun scrollToBottom() { + if (adapter.itemCount > 0) { + binding.chatMessagesList.scrollToPosition(adapter.itemCount - 1) + } + } + + private fun pickFile() { + val cameraIntents = ArrayList() + + // Handles image & video picking + val galleryIntent = Intent(Intent.ACTION_PICK) + galleryIntent.type = "*/*" + galleryIntent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*")) + + if (PermissionHelper.get().hasCameraPermission()) { + // Allows to capture directly from the camera + val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + val tempFileName = System.currentTimeMillis().toString() + ".jpeg" + chatSendingViewModel.temporaryFileUploadPath = + FileUtils.getFileStoragePath(tempFileName) + val uri = Uri.fromFile(chatSendingViewModel.temporaryFileUploadPath) + captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri) + cameraIntents.add(captureIntent) + } + + if (PermissionHelper.get().hasReadExternalStorage()) { + // Finally allow any kind of file + val fileIntent = Intent(Intent.ACTION_GET_CONTENT) + fileIntent.type = "*/*" + cameraIntents.add(fileIntent) + } + + val chooserIntent = + Intent.createChooser(galleryIntent, getString(R.string.chat_message_pick_file_dialog)) + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toArray(arrayOf())) + + startActivityForResult(chooserIntent, 0) + } + + private fun openFile(contentFilePath: String) { + val intent = Intent(Intent.ACTION_VIEW) + val contentUri: Uri + var path = contentFilePath + + when { + path.startsWith("file://") -> { + path = path.substring("file://".length) + val file = File(path) + contentUri = FileProvider.getUriForFile( + requireContext(), + getString(R.string.file_provider), + file + ) + } + path.startsWith("content://") -> { + contentUri = Uri.parse(path) + } + else -> { + val file = File(path) + contentUri = try { + FileProvider.getUriForFile( + requireContext(), + getString(R.string.file_provider), + file + ) + } catch (e: Exception) { + Log.e( + "[Chat Message] Couldn't get URI for file $file using file provider ${getString(R.string.file_provider)}" + ) + Uri.parse(path) + } + } + } + + val filePath: String = contentUri.toString() + Log.i("[Chat Message] Trying to open file: $filePath") + var type: String? = null + val extension = FileUtils.getExtensionFromFileName(filePath) + + if (extension.isNotEmpty()) { + Log.i("[Chat Message] Found extension $extension") + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + } else { + Log.e("[Chat Message] Couldn't find extension") + } + + if (type != null) { + Log.i("[Chat Message] Found matching MIME type $type") + } else { + type = FileUtils.getMimeFromFile(filePath) + Log.e("[Chat Message] Can't get MIME type from extension: $extension, will use $type") + } + + intent.setDataAndType(contentUri, type) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + + try { + startActivity(intent) + } catch (anfe: ActivityNotFoundException) { + Log.e("[Chat Message] Couldn't find an activity to handle MIME type: $type") + val activity = requireActivity() as MainActivity + activity.showSnackBar(R.string.chat_room_cant_open_file_no_app_found) + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/DevicesFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/DevicesFragment.kt new file mode 100644 index 000000000..8bff9f38b --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/DevicesFragment.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.activities.main.chat.viewmodels.DevicesListViewModel +import org.linphone.activities.main.chat.viewmodels.DevicesListViewModelFactory +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.databinding.ChatRoomDevicesFragmentBinding + +class DevicesFragment : Fragment() { + private lateinit var binding: ChatRoomDevicesFragmentBinding + private lateinit var listViewModel: DevicesListViewModel + private lateinit var sharedViewModel: SharedMainViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ChatRoomDevicesFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + val chatRoom = sharedViewModel.selectedChatRoom.value + chatRoom ?: return + + listViewModel = ViewModelProvider( + this, + DevicesListViewModelFactory(chatRoom) + )[DevicesListViewModel::class.java] + binding.viewModel = listViewModel + + binding.setBackClickListener { + findNavController().popBackStack() + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt new file mode 100644 index 000000000..5aa15a1a0 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.activities.main.chat.viewmodels.EphemeralViewModel +import org.linphone.activities.main.chat.viewmodels.EphemeralViewModelFactory +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.databinding.ChatRoomEphemeralFragmentBinding + +class EphemeralFragment : Fragment() { + private lateinit var binding: ChatRoomEphemeralFragmentBinding + private lateinit var viewModel: EphemeralViewModel + private lateinit var sharedViewModel: SharedMainViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ChatRoomEphemeralFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + val chatRoom = sharedViewModel.selectedChatRoom.value + chatRoom ?: return + + viewModel = ViewModelProvider( + this, + EphemeralViewModelFactory(chatRoom) + )[EphemeralViewModel::class.java] + binding.viewModel = viewModel + + binding.setBackClickListener { + findNavController().popBackStack() + } + + binding.setValidClickListener { + viewModel.updateChatRoomEphemeralDuration() + findNavController().popBackStack() + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt new file mode 100644 index 000000000..2cd186b44 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.fragments + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import org.linphone.R +import org.linphone.activities.main.MainActivity +import org.linphone.activities.main.chat.GroupChatRoomMember +import org.linphone.activities.main.chat.adapters.GroupInfoParticipantsAdapter +import org.linphone.activities.main.chat.viewmodels.GroupInfoViewModel +import org.linphone.activities.main.chat.viewmodels.GroupInfoViewModelFactory +import org.linphone.activities.main.viewmodels.DialogViewModel +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.core.Address +import org.linphone.core.ChatRoom +import org.linphone.core.ChatRoomCapabilities +import org.linphone.databinding.ChatRoomGroupInfoFragmentBinding +import org.linphone.utils.DialogUtils + +class GroupInfoFragment : Fragment() { + private lateinit var binding: ChatRoomGroupInfoFragmentBinding + private lateinit var viewModel: GroupInfoViewModel + private lateinit var sharedViewModel: SharedMainViewModel + private lateinit var adapter: GroupInfoParticipantsAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ChatRoomGroupInfoFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + val chatRoom: ChatRoom? = sharedViewModel.selectedGroupChatRoom.value + + viewModel = ViewModelProvider( + this, + GroupInfoViewModelFactory(chatRoom) + )[GroupInfoViewModel::class.java] + binding.viewModel = viewModel + + viewModel.isEncrypted.value = sharedViewModel.createEncryptedChatRoom + + adapter = GroupInfoParticipantsAdapter(chatRoom?.hasCapability(ChatRoomCapabilities.Encrypted.toInt()) ?: viewModel.isEncrypted.value == true) + binding.participants.adapter = adapter + + val layoutManager = LinearLayoutManager(activity) + binding.participants.layoutManager = layoutManager + + // Divider between items + val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation) + dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null)) + binding.participants.addItemDecoration(dividerItemDecoration) + + viewModel.participants.observe(viewLifecycleOwner, Observer { + adapter.submitList(it) + }) + + viewModel.isMeAdmin.observe(viewLifecycleOwner, Observer { + adapter.showAdminControls(it && chatRoom != null) + }) + + adapter.participantRemovedEvent.observe(viewLifecycleOwner, Observer { + it.consume { participant -> + viewModel.removeParticipant(participant) + } + }) + + addParticipantsFromBundle() + + binding.setBackClickListener { + findNavController().popBackStack() + } + + viewModel.createdChatRoomEvent.observe(viewLifecycleOwner, Observer { + it.consume { chatRoom -> + sharedViewModel.selectedChatRoom.value = chatRoom + goToChatRoom() + } + }) + + binding.setNextClickListener { + if (viewModel.chatRoom != null) { + viewModel.updateRoom() + } else { + viewModel.createChatRoom() + } + } + + binding.setParticipantsClickListener { + sharedViewModel.createEncryptedChatRoom = viewModel.isEncrypted.value == true + + if (findNavController().currentDestination?.id == R.id.groupInfoFragment) { + val args = Bundle() + args.putBoolean("createGroup", true) + + val list = arrayListOf
() + for (participant in viewModel.participants.value.orEmpty()) { + list.add(participant.address) + } + args.putSerializable("participants", list) + + findNavController().navigate(R.id.action_groupInfoFragment_to_chatRoomCreationFragment, args) + } + } + + binding.setLeaveClickListener { + val dialogViewModel = DialogViewModel(getString(R.string.chat_room_group_info_leave_dialog_message)) + val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel) + + dialogViewModel.showDeleteButton({ + viewModel.leaveGroup() + dialog.dismiss() + }, getString(R.string.chat_room_group_info_leave_dialog_button)) + + dialogViewModel.showCancelButton { + dialog.dismiss() + } + + dialog.show() + } + + viewModel.onErrorEvent.observe(viewLifecycleOwner, Observer { + it.consume { messageResourceId -> + (activity as MainActivity).showSnackBar(messageResourceId) + } + }) + } + + @Suppress("UNCHECKED_CAST") + private fun addParticipantsFromBundle() { + val participants = arguments?.getSerializable("participants") as? ArrayList
+ if (participants != null && participants.size > 0) { + val list = arrayListOf() + + for (address in participants) { + val exists = viewModel.participants.value?.find { + it.address.weakEqual(address) + } + + if (exists != null) { + list.add(exists) + } else { + list.add(GroupChatRoomMember(address, false, hasLimeX3DHCapability = viewModel.isEncrypted.value == true)) + } + } + + viewModel.participants.value = list + } + } + + private fun goToChatRoom() { + if (findNavController().currentDestination?.id == R.id.groupInfoFragment) { + findNavController().navigate(R.id.action_groupInfoFragment_to_detailChatRoomFragment) + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt new file mode 100644 index 000000000..febfabe25 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import org.linphone.R +import org.linphone.activities.main.chat.adapters.ImdnAdapter +import org.linphone.activities.main.chat.viewmodels.ImdnViewModel +import org.linphone.activities.main.chat.viewmodels.ImdnViewModelFactory +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.core.tools.Log +import org.linphone.databinding.ChatRoomImdnFragmentBinding +import org.linphone.utils.RecyclerViewHeaderDecoration + +class ImdnFragment : Fragment() { + private lateinit var binding: ChatRoomImdnFragmentBinding + private lateinit var viewModel: ImdnViewModel + private lateinit var adapter: ImdnAdapter + private lateinit var sharedViewModel: SharedMainViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ChatRoomImdnFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + val chatRoom = sharedViewModel.selectedChatRoom.value + chatRoom ?: return + + if (arguments != null) { + val messageId = arguments?.getString("MessageId") + val message = chatRoom.findMessage(messageId) + if (message != null) { + Log.i("[IMDN] Found message $message with id $messageId") + viewModel = ViewModelProvider( + this, + ImdnViewModelFactory(message) + )[ImdnViewModel::class.java] + binding.viewModel = viewModel + } else { + Log.e("[IMDN] Couldn't find message with id $messageId in chat room $chatRoom") + findNavController().popBackStack() + return + } + } else { + Log.e("[IMDN] Couldn't find message id in intent arguments") + findNavController().popBackStack() + return + } + + adapter = ImdnAdapter() + binding.participantsList.adapter = adapter + + val layoutManager = LinearLayoutManager(activity) + binding.participantsList.layoutManager = layoutManager + + // Divider between items + val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation) + dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null)) + binding.participantsList.addItemDecoration(dividerItemDecoration) + + // Displays state header + val headerItemDecoration = RecyclerViewHeaderDecoration(adapter) + binding.participantsList.addItemDecoration(headerItemDecoration) + + viewModel.participants.observe(viewLifecycleOwner, Observer { + adapter.submitList(it) + }) + + binding.setBackClickListener { + findNavController().popBackStack() + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt new file mode 100644 index 000000000..510a031b6 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.fragments + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.MainActivity +import org.linphone.activities.main.chat.adapters.ChatRoomsListAdapter +import org.linphone.activities.main.chat.viewmodels.ChatRoomsListViewModel +import org.linphone.activities.main.fragments.MasterFragment +import org.linphone.activities.main.viewmodels.DialogViewModel +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.core.ChatRoom +import org.linphone.core.Factory +import org.linphone.core.tools.Log +import org.linphone.databinding.ChatRoomMasterFragmentBinding +import org.linphone.utils.* + +class MasterChatRoomsFragment : MasterFragment() { + private lateinit var binding: ChatRoomMasterFragmentBinding + private lateinit var listViewModel: ChatRoomsListViewModel + private lateinit var adapter: ChatRoomsListAdapter + private lateinit var sharedViewModel: SharedMainViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ChatRoomMasterFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + listViewModel = ViewModelProvider(this).get(ChatRoomsListViewModel::class.java) + binding.viewModel = listViewModel + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + adapter = ChatRoomsListAdapter(listSelectionViewModel) + // SubmitList is done on a background thread + // We need this adapter data observer to know when to scroll + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onChanged() { + scrollToTop() + } + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + scrollToTop() + } + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + scrollToTop() + } + }) + binding.chatList.adapter = adapter + + val layoutManager = LinearLayoutManager(activity) + binding.chatList.layoutManager = layoutManager + + val swipeConfiguration = RecyclerViewSwipeConfiguration() + val white = ContextCompat.getColor(requireContext(), R.color.white_color) + + swipeConfiguration.rightToLeftAction = RecyclerViewSwipeConfiguration.Action("Delete", white, ContextCompat.getColor(requireContext(), R.color.red_color)) + val swipeListener = object : RecyclerViewSwipeListener { + override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {} + + override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) { + val viewModel = DialogViewModel(getString(R.string.dialog_default_delete_message)) + val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel) + + viewModel.showCancelButton { + adapter.notifyItemChanged(viewHolder.adapterPosition) + dialog.dismiss() + } + + viewModel.showDeleteButton({ + listViewModel.deleteChatRoom(listViewModel.chatRooms.value?.get(viewHolder.adapterPosition)) + dialog.dismiss() + }, getString(R.string.dialog_delete)) + + dialog.show() + } + } + RecyclerViewSwipeUtils(ItemTouchHelper.LEFT, swipeConfiguration, swipeListener) + .attachToRecyclerView(binding.chatList) + + // Divider between items + val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation) + dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null)) + binding.chatList.addItemDecoration(dividerItemDecoration) + + listViewModel.chatRooms.observe(viewLifecycleOwner, Observer { chatRooms -> + adapter.submitList(chatRooms) + }) + + listViewModel.latestUpdatedChatRoomId.observe(viewLifecycleOwner, Observer { position -> + adapter.notifyItemChanged(position) + }) + + listViewModel.contactsUpdatedEvent.observe(viewLifecycleOwner, Observer { + it.consume { + adapter.notifyDataSetChanged() + } + }) + + adapter.selectedChatRoomEvent.observe(viewLifecycleOwner, Observer { + it.consume { chatRoom -> + sharedViewModel.selectedChatRoom.value = chatRoom + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.masterChatRoomsFragment) { + findNavController().navigate(R.id.action_masterChatRoomsFragment_to_detailChatRoomFragment) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_detailChatRoomFragment) + } + } + }) + + binding.setEditClickListener { + listSelectionViewModel.isEditionEnabled.value = true + } + + binding.setNewOneToOneChatRoomClickListener { + val bundle = bundleOf("createGroup" to false) + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.masterChatRoomsFragment) { + findNavController().navigate( + R.id.action_masterChatRoomsFragment_to_chatRoomCreationFragment, + bundle + ) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_chatRoomCreationFragment, bundle) + } + } + + binding.setNewGroupChatRoomClickListener { + sharedViewModel.selectedGroupChatRoom.value = null + + val bundle = bundleOf("createGroup" to true) + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.masterChatRoomsFragment) { + findNavController().navigate( + R.id.action_masterChatRoomsFragment_to_chatRoomCreationFragment, + bundle + ) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_chatRoomCreationFragment, bundle) + } + } + + val localSipUri = arguments?.getString("LocalSipUri") + val remoteSipUri = arguments?.getString("RemoteSipUri") + if (localSipUri != null && remoteSipUri != null) { + Log.i("[Chat] Found local ($localSipUri) & remote addresses ($remoteSipUri) in arguments") + arguments?.clear() + val localAddress = Factory.instance().createAddress(localSipUri) + val remoteSipAddress = Factory.instance().createAddress(remoteSipUri) + val chatRoom = coreContext.core.getChatRoom(remoteSipAddress, localAddress) + if (chatRoom != null) { + Log.i("[Chat] Found matching chat room $chatRoom") + chatRoom.markAsRead() + adapter.selectedChatRoomEvent.value = Event(chatRoom) + } + } else { + sharedViewModel.filesToShare.observe(viewLifecycleOwner, Observer { + if (it.isNotEmpty()) { + Log.i("[Chat] Found ${it.size} files to share") + val activity = requireActivity() as MainActivity + activity.showSnackBar(R.string.chat_room_toast_choose_for_sharing) + } + }) + sharedViewModel.messageToForwardEvent.observe(viewLifecycleOwner, Observer { + if (!it.consumed()) { + Log.i("[Chat] Found chat message to transfer") + + val activity = requireActivity() as MainActivity + activity.showSnackBar(R.string.chat_room_toast_choose_for_sharing) + } + }) + + listViewModel.onErrorEvent.observe(viewLifecycleOwner, Observer { + it.consume { messageResourceId -> + (activity as MainActivity).showSnackBar(messageResourceId) + } + }) + } + } + + override fun getItemCount(): Int { + return adapter.itemCount + } + + override fun deleteItems(indexesOfItemToDelete: ArrayList) { + val list = ArrayList() + for (index in indexesOfItemToDelete) { + val chatRoom = adapter.getItemAt(index) + list.add(chatRoom) + } + listViewModel.deleteChatRooms(list) + } + + private fun scrollToTop() { + binding.chatList.scrollToPosition(0) + } +} diff --git a/app/src/main/java/org/linphone/chat/DeviceChildViewHolder.java b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageAttachmentViewModel.kt similarity index 60% rename from app/src/main/java/org/linphone/chat/DeviceChildViewHolder.java rename to app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageAttachmentViewModel.kt index b0f9e06c5..ad597cf48 100644 --- a/app/src/main/java/org/linphone/chat/DeviceChildViewHolder.java +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageAttachmentViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2019 Belledonne Communications SARL. + * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,19 +17,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.chat; +package org.linphone.activities.main.chat.viewmodels -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; -import org.linphone.R; +import androidx.lifecycle.ViewModel +import org.linphone.utils.FileUtils -class DeviceChildViewHolder { - public final TextView deviceName; - public final ImageView securityLevel; +class ChatMessageAttachmentViewModel( + val path: String, + val isImage: Boolean, + private val deleteCallback: (attachment: ChatMessageAttachmentViewModel) -> Unit +) : ViewModel() { + val fileName: String = FileUtils.getNameFromFilePath(path) - public DeviceChildViewHolder(View v) { - deviceName = v.findViewById(R.id.name); - securityLevel = v.findViewById(R.id.security_level); + fun delete() { + deleteCallback(this) } } diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageContentViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageContentViewModel.kt new file mode 100644 index 000000000..523856702 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageContentViewModel.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.core.ChatMessage +import org.linphone.core.Content +import org.linphone.core.tools.Log +import org.linphone.utils.FileUtils + +class ChatMessageContentViewModel( + val content: Content, + private val chatMessage: ChatMessage, + private val listener: OnContentClickedListener? +) : ViewModel() { + val isImage = MutableLiveData() + + val downloadable = MutableLiveData() + + val downloadEnabled = MutableLiveData() + + val isAlone: Boolean + get() { + var count = 0 + for (content in chatMessage.contents) { + if (content.isFileTransfer || content.isFile) { + count += 1 + } + } + return count == 1 + } + + init { + if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) { + downloadable.value = content.filePath.isEmpty() + + if (content.filePath.isNotEmpty()) { + Log.i("[Content] Found displayable content: ${content.filePath}") + isImage.value = FileUtils.isExtensionImage(content.filePath) + } else { + Log.w("[Content] Found content with empty path...") + isImage.value = false + } + } else { + Log.i("[Content] Found downloadable content: ${content.name}") + downloadable.value = true + isImage.value = false + } + + downloadEnabled.value = downloadable.value + } + + fun download() { + if (content.isFileTransfer && (content.filePath == null || content.filePath.isEmpty())) { + val file = FileUtils.getFileStoragePath(content.name) + content.filePath = file.path + downloadEnabled.value = false + + Log.i("[Content] Started downloading ${content.name} into ${content.filePath}") + chatMessage.downloadContent(content) + } + } + + fun openFile() { + listener?.onContentClicked(content.filePath) + } +} + +interface OnContentClickedListener { + fun onContentClicked(path: String) +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt new file mode 100644 index 000000000..f57eb4b7d --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import java.io.File +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.core.* +import org.linphone.utils.FileUtils + +class ChatMessageSendingViewModelFactory(private val chatRoom: ChatRoom) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return ChatMessageSendingViewModel(chatRoom) as T + } +} + +class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel() { + var temporaryFileUploadPath: File? = null + + val attachments = MutableLiveData>() + + val attachFileEnabled = MutableLiveData() + + val sendMessageEnabled = MutableLiveData() + + val isReadOnly = MutableLiveData() + + var textToSend: String = "" + set(value) { + sendMessageEnabled.value = value.isNotEmpty() || attachments.value?.isNotEmpty() ?: false + if (value.isNotEmpty()) { + if (!corePreferences.allowMultipleFilesAndTextInSameMessage) { + attachFileEnabled.value = false + } + chatRoom.compose() + } else { + if (!corePreferences.allowMultipleFilesAndTextInSameMessage) { + attachFileEnabled.value = attachments.value?.isEmpty() ?: true + } + } + field = value + } + + init { + attachments.value = arrayListOf() + + attachFileEnabled.value = true + sendMessageEnabled.value = false + isReadOnly.value = chatRoom.hasBeenLeft() + } + + fun addAttachment(path: String) { + val list = arrayListOf() + list.addAll(attachments.value.orEmpty()) + list.add(ChatMessageAttachmentViewModel(path, FileUtils.isExtensionImage(path)) { + removeAttachment(it) + }) + attachments.value = list + + sendMessageEnabled.value = textToSend.isNotEmpty() || list.isNotEmpty() + if (!corePreferences.allowMultipleFilesAndTextInSameMessage) { + attachFileEnabled.value = false + } + } + + private fun removeAttachment(attachment: ChatMessageAttachmentViewModel) { + val list = arrayListOf() + list.addAll(attachments.value.orEmpty()) + list.remove(attachment) + attachments.value = list + + sendMessageEnabled.value = textToSend.isNotEmpty() || list.isNotEmpty() + if (!corePreferences.allowMultipleFilesAndTextInSameMessage) { + attachFileEnabled.value = list.isEmpty() + } + } + + fun sendMessage() { + val isBasicChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt()) + val message: ChatMessage = chatRoom.createEmptyMessage() + + if (textToSend.isNotEmpty()) { + message.addTextContent(textToSend) + } + + for (attachment in attachments.value.orEmpty()) { + val content = Factory.instance().createContent() + + if (attachment.isImage) { + content.type = "image" + } else { + content.type = "file" + } + content.subtype = FileUtils.getExtensionFromFileName(attachment.fileName) + content.name = attachment.fileName + content.filePath = attachment.path // Let the file body handler take care of the upload + + if (isBasicChatRoom) { + val fileMessage: ChatMessage = chatRoom.createFileTransferMessage(content) + fileMessage.send() + } else { + message.addFileContent(content) + } + } + + if (message.contents.isNotEmpty()) { + message.send() + } + + attachments.value = arrayListOf() + } + + fun transferMessage(chatMessage: ChatMessage) { + val message = chatRoom.createForwardMessage(chatMessage) + message?.send() + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageViewModel.kt new file mode 100644 index 000000000..949f3dccf --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageViewModel.kt @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import android.os.CountDownTimer +import android.text.Spanned +import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.compatibility.Compatibility +import org.linphone.contact.GenericContactViewModel +import org.linphone.core.ChatMessage +import org.linphone.core.ChatMessageListenerStub +import org.linphone.core.Content +import org.linphone.core.tools.Log +import org.linphone.mediastream.Version +import org.linphone.utils.AppUtils +import org.linphone.utils.PermissionHelper +import org.linphone.utils.TimestampUtils + +class ChatMessageViewModel( + val chatMessage: ChatMessage, + private val contentListener: OnContentClickedListener? = null +) : GenericContactViewModel(chatMessage.fromAddress) { + val sendInProgress = MutableLiveData() + + val transferInProgress = MutableLiveData() + + val showImdn = MutableLiveData() + + val imdnIcon = MutableLiveData() + + val backgroundRes = MutableLiveData() + + val hideAvatar = MutableLiveData() + + val hideTime = MutableLiveData() + + val contents = MutableLiveData>() + + val time = MutableLiveData() + + val ephemeralLifetime = MutableLiveData() + + val text: Spanned? by lazy { + if (chatMessage.textContent != null) AppUtils.getTextWithHttpLinks(chatMessage.textContent) else null + } + + private var countDownTimer: CountDownTimer? = null + + private val listener = object : ChatMessageListenerStub() { + override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) { + time.value = TimestampUtils.toString(chatMessage.time) + updateChatMessageState(state) + + // TODO FIXME : find a way to refresh outgoing message downloaded + if (state == ChatMessage.State.FileTransferDone && !message.isOutgoing) { + Log.i("[Chat Message] File transfer done") + // No need to refresh content lists on outgoing messages after file transfer is done + // It will even cause the app to crash if updateContentsList is not call right after + updateContentsList() + + if (!message.isEphemeral && corePreferences.makePublicDownloadedImages) { + if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10) || PermissionHelper.get().hasWriteExternalStorage()) { + for (content in message.contents) { + if (content.isFile && content.filePath != null && content.userData == null) { + addContentToMediaStore(content) + } + } + } else { + Log.e("[Chat Message] Can't make file public, app doesn't have WRITE_EXTERNAL_STORAGE permission") + } + } + } + } + + override fun onEphemeralMessageTimerStarted(message: ChatMessage) { + updateEphemeralTimer() + } + } + + init { + chatMessage.addListener(listener) + + backgroundRes.value = if (chatMessage.isOutgoing) R.drawable.chat_bubble_outgoing_full else R.drawable.chat_bubble_incoming_full + hideAvatar.value = false + time.value = TimestampUtils.toString(chatMessage.time) + updateEphemeralTimer() + + updateChatMessageState(chatMessage.state) + updateContentsList() + } + + override fun onCleared() { + chatMessage.removeListener(listener) + + super.onCleared() + } + + fun updateBubbleBackground(hasPrevious: Boolean, hasNext: Boolean) { + if (hasPrevious) { + hideTime.value = true + } + + if (chatMessage.isOutgoing) { + if (hasNext && hasPrevious) { + backgroundRes.value = R.drawable.chat_bubble_outgoing_split_2 + } else if (hasNext) { + backgroundRes.value = R.drawable.chat_bubble_outgoing_split_1 + } else if (hasPrevious) { + backgroundRes.value = R.drawable.chat_bubble_outgoing_split_3 + } else { + backgroundRes.value = R.drawable.chat_bubble_outgoing_full + } + } else { + if (hasNext && hasPrevious) { + hideAvatar.value = true + backgroundRes.value = R.drawable.chat_bubble_incoming_split_2 + } else if (hasNext) { + backgroundRes.value = R.drawable.chat_bubble_incoming_split_1 + } else if (hasPrevious) { + hideAvatar.value = true + backgroundRes.value = R.drawable.chat_bubble_incoming_split_3 + } else { + backgroundRes.value = R.drawable.chat_bubble_incoming_full + } + } + } + + private fun updateChatMessageState(state: ChatMessage.State) { + transferInProgress.value = state == ChatMessage.State.FileTransferInProgress + + sendInProgress.value = state == ChatMessage.State.InProgress || state == ChatMessage.State.FileTransferInProgress + + showImdn.value = when (state) { + ChatMessage.State.DeliveredToUser, ChatMessage.State.Displayed, ChatMessage.State.NotDelivered -> true + else -> false + } + + imdnIcon.value = when (state) { + ChatMessage.State.DeliveredToUser -> R.drawable.imdn_received + ChatMessage.State.Displayed -> R.drawable.imdn_read + else -> R.drawable.imdn_error + } + } + + private fun updateContentsList() { + val list = arrayListOf() + for (content in chatMessage.contents) { + if (content.isFileTransfer || content.isFile) { + list.add(ChatMessageContentViewModel(content, chatMessage, contentListener)) + } + } + contents.value = list + } + + private fun updateEphemeralTimer() { + if (chatMessage.isEphemeral) { + if (chatMessage.ephemeralExpireTime == 0L) { + // This means the message hasn't been read by all participants yet, so the countdown hasn't started + // In this case we simply display the configured value for lifetime + ephemeralLifetime.value = formatLifetime(chatMessage.ephemeralLifetime) + } else { + // Countdown has started, display remaining time + val remaining = chatMessage.ephemeralExpireTime - (System.currentTimeMillis() / 1000) + ephemeralLifetime.value = formatLifetime(remaining) + if (countDownTimer == null) { + countDownTimer = object : CountDownTimer(remaining * 1000, 1000) { + override fun onFinish() {} + + override fun onTick(millisUntilFinished: Long) { + ephemeralLifetime.postValue(formatLifetime(millisUntilFinished / 1000)) + } + } + countDownTimer?.start() + } + } + } + } + + private fun formatLifetime(seconds: Long): String { + val days = seconds / 86400 + return when { + days >= 1L -> AppUtils.getStringWithPlural(R.plurals.days, days.toInt()) + else -> String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, (seconds % 60)) + } + } + + private fun addContentToMediaStore(content: Content) { + when (content.type) { + "image" -> { + if (Compatibility.addImageToMediaStore(coreContext.context, content)) { + Log.i("[Chat Message] Adding image ${content.name} terminated") + } else { + Log.e("[Chat Message] Something went wrong while copying file...") + } + } + "video" -> { + if (Compatibility.addVideoToMediaStore(coreContext.context, content)) { + Log.i("[Chat Message] Adding video ${content.name} terminated") + } else { + Log.e("[Chat Message] Something went wrong while copying file...") + } + } + "audio" -> { + if (Compatibility.addAudioToMediaStore(coreContext.context, content)) { + Log.i("[Chat Message] Adding audio ${content.name} terminated") + } else { + Log.e("[Chat Message] Something went wrong while copying file...") + } + } + else -> { + Log.w("[Chat Message] File ${content.name} isn't either an image, an audio file or a video, can't add it to the Media Store") + } + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessagesListViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessagesListViewModel.kt new file mode 100644 index 000000000..0951ed904 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessagesListViewModel.kt @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import java.util.* +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.mediastream.Version +import org.linphone.utils.Event +import org.linphone.utils.LinphoneUtils +import org.linphone.utils.PermissionHelper + +class ChatMessagesListViewModelFactory(private val chatRoom: ChatRoom) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return ChatMessagesListViewModel(chatRoom) as T + } +} + +class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() { + companion object { + private const val MESSAGES_PER_PAGE = 20 + } + + val events = MutableLiveData>() + + val messageUpdatedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val requestWriteExternalStoragePermissionEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private val chatMessageListener: ChatMessageListenerStub = object : ChatMessageListenerStub() { + override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) { + if (state == ChatMessage.State.Displayed) { + message.removeListener(this) + } + + val position: Int? = message.userData as? Int? + if (position != null) { + messageUpdatedEvent.value = Event(position) + } + } + } + + private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() { + override fun onChatMessageReceived(chatRoom: ChatRoom, eventLog: EventLog) { + chatRoom.markAsRead() + + val position = events.value?.size ?: 0 + + if (eventLog.type == EventLog.Type.ConferenceChatMessage) { + val chatMessage = eventLog.chatMessage + chatMessage.userData = position + chatMessage.addListener(chatMessageListener) + + if (Version.sdkStrictlyBelow(Version.API29_ANDROID_10) && !PermissionHelper.get().hasWriteExternalStorage()) { + for (content in chatMessage.contents) { + if (content.isFileTransfer) { + Log.i("[Chat Messages] Android < 10 detected and WRITE_EXTERNAL_STORAGE permission isn't granted yet") + requestWriteExternalStoragePermissionEvent.value = Event(true) + } + } + } + } + + addEvent(eventLog) + } + + override fun onChatMessageSent(chatRoom: ChatRoom, eventLog: EventLog) { + val position = events.value?.size ?: 0 + + if (eventLog.type == EventLog.Type.ConferenceChatMessage) { + val chatMessage = eventLog.chatMessage + chatMessage.userData = position + chatMessage.addListener(chatMessageListener) + } + + addEvent(eventLog) + } + + override fun onSecurityEvent(chatRoom: ChatRoom, eventLog: EventLog) { + addEvent(eventLog) + } + + override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) { + addEvent(eventLog) + } + + override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) { + addEvent(eventLog) + } + + override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) { + addEvent(eventLog) + } + + override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) { + addEvent(eventLog) + } + + override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) { + addEvent(eventLog) + } + + override fun onConferenceLeft(chatRoom: ChatRoom, eventLog: EventLog) { + addEvent(eventLog) + } + + override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) { + Log.i("[Chat Messages] An ephemeral chat message has expired, removing it from event list") + deleteMessage(eventLog.chatMessage) + } + + override fun onEphemeralEvent(chatRoom: ChatRoom, eventLog: EventLog) { + addEvent(eventLog) + } + } + + init { + chatRoom.addListener(chatRoomListener) + + events.value = getEvents() + } + + override fun onCleared() { + chatRoom.removeListener(chatRoomListener) + + super.onCleared() + } + + fun resendMessage(chatMessage: ChatMessage) { + val position: Int = chatMessage.userData as Int + chatMessage.resend() + messageUpdatedEvent.value = Event(position) + } + + fun deleteMessage(chatMessage: ChatMessage) { + val position: Int = chatMessage.userData as Int + LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage) + chatRoom.deleteMessage(chatMessage) + + val list = arrayListOf() + list.addAll(events.value.orEmpty()) + list.removeAt(position) + events.value = list + } + + fun deleteEventLogs(listToDelete: ArrayList) { + val list = arrayListOf() + list.addAll(events.value.orEmpty()) + + for (eventLog in listToDelete) { + LinphoneUtils.deleteFilesAttachedToEventLog(eventLog) + eventLog.deleteFromDatabase() + list.remove(eventLog) + } + + events.value = list + } + + fun loadMoreData(totalItemsCount: Int) { + Log.i("[Chat Messages] Load more data, current total is $totalItemsCount") + val maxSize: Int = chatRoom.historyEventsSize + + if (totalItemsCount < maxSize) { + var upperBound: Int = totalItemsCount + MESSAGES_PER_PAGE + if (upperBound > maxSize) { + upperBound = maxSize + } + + val history: Array = chatRoom.getHistoryRangeEvents(totalItemsCount, upperBound) + val list = arrayListOf() + for (message in history) { + list.add(message) + } + list.addAll(events.value.orEmpty()) + events.value = list + } + } + + private fun addEvent(eventLog: EventLog) { + val list = arrayListOf() + list.addAll(events.value.orEmpty()) + list.add(eventLog) + events.value = list + } + + private fun getEvents(): ArrayList { + val list = arrayListOf() + val history = chatRoom.getHistoryEvents(MESSAGES_PER_PAGE) + for (message in history) { + list.add(message) + } + return list + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationContactViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationContactViewModel.kt new file mode 100644 index 000000000..1346f53e8 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationContactViewModel.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.contact.Contact +import org.linphone.contact.ContactViewModelInterface +import org.linphone.core.* +import org.linphone.utils.LinphoneUtils + +class ChatRoomCreationContactViewModel(private val searchResult: SearchResult) : ViewModel(), ContactViewModelInterface { + override val contact = MutableLiveData() + + override val displayName: String by lazy { + when { + searchResult.friend != null -> searchResult.friend.name + searchResult.address != null -> LinphoneUtils.getDisplayName(searchResult.address) + else -> searchResult.phoneNumber + } + } + + val isDisabled: MutableLiveData by lazy { + MutableLiveData() + } + + val isSelected: MutableLiveData by lazy { + MutableLiveData() + } + + val isLinphoneUser: Boolean by lazy { + searchResult.friend?.getPresenceModelForUriOrTel(searchResult.phoneNumber ?: searchResult.address.asStringUriOnly())?.basicStatus == PresenceBasicStatus.Open + } + + val sipUri: String by lazy { + searchResult.phoneNumber ?: searchResult.address.asStringUriOnly() + } + + val address: Address by lazy { + searchResult.address + } + + val hasLimeX3DHCapability: Boolean + get() = searchResult.hasCapability(FriendCapability.LimeX3Dh) + + var listener: ChatRoomCreationContactSelectionListener? = null + + init { + isDisabled.value = false + isSelected.value = false + searchMatchingContact() + } + + private fun searchMatchingContact() { + if (searchResult.address != null) { + contact.value = + coreContext.contactsManager.findContactByAddress(searchResult.address) + } else if (searchResult.phoneNumber != null) { + contact.value = coreContext.contactsManager.findContactByPhoneNumber(searchResult.phoneNumber) + } + } +} + +interface ChatRoomCreationContactSelectionListener { + fun onUnSelected(searchResult: SearchResult) +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt new file mode 100644 index 000000000..ba7e77422 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.viewmodels.ErrorReportingViewModel +import org.linphone.contact.ContactsUpdatedListenerStub +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils +import org.linphone.utils.Event +import org.linphone.utils.LinphoneUtils + +class ChatRoomCreationViewModel : ErrorReportingViewModel() { + val chatRoomCreatedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val createGroupChat = MutableLiveData() + + val sipContactsSelected = MutableLiveData() + + val isEncrypted = MutableLiveData() + + val contactsList = MutableLiveData>() + + val waitForChatRoomCreation = MutableLiveData() + + val selectedAddresses = MutableLiveData>() + + val limeAvailable: Boolean = LinphoneUtils.isLimeAvailable() + + private var filter: String = "" + + private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { + override fun onContactsUpdated() { + Log.i("[Chat Room Creation] Contacts have changed") + updateContactsList() + } + } + + private val listener = object : ChatRoomListenerStub() { + override fun onStateChanged(room: ChatRoom, state: ChatRoom.State) { + if (state == ChatRoom.State.Created) { + waitForChatRoomCreation.value = false + Log.i("[Chat Room Creation] Chat room created") + chatRoomCreatedEvent.value = Event(room) + } else if (state == ChatRoom.State.CreationFailed) { + Log.e("[Chat Room Creation] Group chat room creation has failed !") + waitForChatRoomCreation.value = false + onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack) + } + } + } + + init { + createGroupChat.value = false + sipContactsSelected.value = true + isEncrypted.value = false + + selectedAddresses.value = arrayListOf() + + updateContactsList() + + coreContext.contactsManager.addListener(contactsUpdatedListener) + waitForChatRoomCreation.value = false + } + + override fun onCleared() { + coreContext.contactsManager.removeListener(contactsUpdatedListener) + + super.onCleared() + } + + fun updateEncryption(encrypted: Boolean) { + isEncrypted.value = encrypted + } + + fun filter(search: String) { + if (filter.isNotEmpty() && filter.length > search.length) { + coreContext.contactsManager.magicSearch.resetSearchCache() + } + filter = search + + updateContactsList() + } + + fun updateContactsList() { + val domain = if (sipContactsSelected.value == true) coreContext.core.defaultProxyConfig?.domain ?: "" else "" + val results = coreContext.contactsManager.magicSearch.getContactListFromFilter(filter, domain) + + val list = arrayListOf() + for (result in results) { + list.add(result) + } + contactsList.value = list + } + + fun toggleSelectionForSearchResult(searchResult: SearchResult) { + if (searchResult.address != null) { + toggleSelectionForAddress(searchResult.address) + } + } + + fun toggleSelectionForAddress(address: Address) { + val list = arrayListOf
() + list.addAll(selectedAddresses.value.orEmpty()) + + val found = list.find { + if (address != null) it.weakEqual(address) else false + } + + if (found != null) { + list.remove(found) + } else { + val contact = coreContext.contactsManager.findContactByAddress(address) + if (contact != null) address.displayName = contact.fullName + list.add(address) + } + + selectedAddresses.value = list + } + + fun createOneToOneChat(searchResult: SearchResult) { + waitForChatRoomCreation.value = true + val defaultProxyConfig = coreContext.core.defaultProxyConfig + var room: ChatRoom? + + if (defaultProxyConfig == null) { + val address = searchResult.address ?: coreContext.core.interpretUrl(searchResult.phoneNumber) + if (address == null) { + Log.e("[Chat Room Creation] Can't get a valid address from search result $searchResult") + onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack) + waitForChatRoomCreation.value = false + return + } + + Log.w("[Chat Room Creation] No default proxy config found, creating basic chat room without local identity with ${address.asStringUriOnly()}") + room = coreContext.core.getChatRoom(address) + if (room != null) { + chatRoomCreatedEvent.value = Event(room) + } else { + Log.e("[Chat Room Creation] Couldn't create chat room with remote ${address.asStringUriOnly()}") + } + waitForChatRoomCreation.value = false + return + } + + val encrypted = isEncrypted.value == true + room = coreContext.core.findOneToOneChatRoom(defaultProxyConfig.identityAddress, searchResult.address, encrypted) + if (room == null) { + Log.w("[Chat Room Creation] Couldn't find existing 1-1 chat room with remote ${searchResult.address.asStringUriOnly()}, encryption=$encrypted and local identity ${defaultProxyConfig.identityAddress.asStringUriOnly()}") + if (encrypted) { + val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams() + // This will set the backend to FlexisipChat automatically + params.enableEncryption(true) + params.enableGroup(false) + + val participants = arrayOfNulls
(1) + participants[0] = searchResult.address + + room = coreContext.core.createChatRoom( + params, + AppUtils.getString(R.string.chat_room_dummy_subject), + participants + ) + room?.addListener(listener) + } else { + room = coreContext.core.getChatRoom(searchResult.address, defaultProxyConfig.identityAddress) + if (room != null) { + chatRoomCreatedEvent.value = Event(room) + } else { + Log.e("[Chat Room Creation] Couldn't create chat room with remote ${searchResult.address.asStringUriOnly()} and local identity ${defaultProxyConfig.identityAddress.asStringUriOnly()}") + } + waitForChatRoomCreation.value = false + } + } else { + Log.i("[Chat Room Creation] Found existing 1-1 chat room with remote ${searchResult.address.asStringUriOnly()}, encryption=$encrypted and local identity ${defaultProxyConfig.identityAddress.asStringUriOnly()}") + chatRoomCreatedEvent.value = Event(room) + waitForChatRoomCreation.value = false + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt new file mode 100644 index 000000000..bf6a2cb01 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.contact.Contact +import org.linphone.contact.ContactViewModelInterface +import org.linphone.contact.ContactsUpdatedListenerStub +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils +import org.linphone.utils.LinphoneUtils +import org.linphone.utils.TimestampUtils + +class ChatRoomViewModelFactory(private val chatRoom: ChatRoom) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return ChatRoomViewModel(chatRoom) as T + } +} + +class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactViewModelInterface { + override val contact = MutableLiveData() + + override val displayName: String by lazy { + when { + chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt()) -> LinphoneUtils.getDisplayName(chatRoom.peerAddress) + chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) -> LinphoneUtils.getDisplayName(chatRoom.participants.first()?.address ?: chatRoom.peerAddress) + chatRoom.hasCapability(ChatRoomCapabilities.Conference.toInt()) -> chatRoom.subject + else -> chatRoom.peerAddress.asStringUriOnly() + } + } + + override val securityLevel: ChatRoomSecurityLevel + get() = chatRoom.securityLevel + + override val showGroupChatAvatar: Boolean + get() = chatRoom.hasCapability(ChatRoomCapabilities.Conference.toInt()) && !chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) + + val subject = MutableLiveData() + + val participants = MutableLiveData() + + val unreadMessagesCount = MutableLiveData() + + val lastUpdate = MutableLiveData() + + val lastMessageText = MutableLiveData() + + val callInProgress = MutableLiveData() + + val remoteIsComposing = MutableLiveData() + + val composingList = MutableLiveData() + + val securityLevelIcon = MutableLiveData() + + val securityLevelContentDescription = MutableLiveData() + + val oneToOneChatRoom: Boolean + get() = chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) + + val encryptedChatRoom: Boolean + get() = chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt()) + + val basicChatRoom: Boolean + get() = chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt()) + + val peerSipUri: String + get() = chatRoom.peerAddress.asStringUriOnly() + + val oneParticipantOneDevice: Boolean + get() { + return chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) && + chatRoom.me.devices.size == 1 && + chatRoom.participants.first().devices.size == 1 + } + + val addressToCall: Address + get() { + return if (chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())) + chatRoom.peerAddress + else + chatRoom.participants.first().address + } + + val onlyParticipantOnlyDeviceAddress: Address + get() = chatRoom.participants.first().devices.first().address + + private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { + override fun onContactsUpdated() { + Log.i("[Chat Room] Contacts have changed") + contactLookup() + } + } + + private val coreListener: CoreListenerStub = object : CoreListenerStub() { + override fun onCallStateChanged( + core: Core, + call: Call, + state: Call.State, + message: String + ) { + callInProgress.value = core.callsNb > 0 + } + } + + private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() { + override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) { + Log.i("[Chat Room] $chatRoom state changed: $state") + } + + override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) { + subject.value = chatRoom.subject + } + + override fun onChatMessageReceived(chatRoom: ChatRoom, eventLog: EventLog) { + unreadMessagesCount.value = chatRoom.unreadMessagesCount + lastMessageText.value = formatLastMessage(eventLog.chatMessage) + } + + override fun onChatMessageSent(chatRoom: ChatRoom, eventLog: EventLog) { + lastMessageText.value = formatLastMessage(eventLog.chatMessage) + } + + override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) { + contactLookup() + updateSecurityIcon() + } + + override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) { + contactLookup() + updateSecurityIcon() + } + + override fun onIsComposingReceived( + chatRoom: ChatRoom, + remoteAddr: Address, + isComposing: Boolean + ) { + updateRemotesComposing() + } + + override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) { + contactLookup() + updateSecurityIcon() + } + + override fun onSecurityEvent(chatRoom: ChatRoom, eventLog: EventLog) { + updateSecurityIcon() + } + + override fun onParticipantDeviceAdded(chatRoom: ChatRoom, eventLog: EventLog) { + updateSecurityIcon() + } + + override fun onParticipantDeviceRemoved(chatRoom: ChatRoom, eventLog: EventLog) { + updateSecurityIcon() + } + + override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) { + Log.i("[Chat Room] Ephemeral message deleted, updated last message displayed") + lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory) + } + } + + init { + chatRoom.core.addListener(coreListener) + chatRoom.addListener(chatRoomListener) + coreContext.contactsManager.addListener(contactsUpdatedListener) + + lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory) + unreadMessagesCount.value = chatRoom.unreadMessagesCount + lastUpdate.value = TimestampUtils.toString(chatRoom.lastUpdateTime, true) + + subject.value = chatRoom.subject + updateSecurityIcon() + + contactLookup() + + callInProgress.value = chatRoom.core.callsNb > 0 + updateRemotesComposing() + } + + override fun onCleared() { + coreContext.contactsManager.removeListener(contactsUpdatedListener) + chatRoom.removeListener(chatRoomListener) + chatRoom.core.removeListener(coreListener) + + super.onCleared() + } + + fun contactLookup() { + if (chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { + searchMatchingContact() + } else { + getParticipantsNames() + } + } + + private fun formatLastMessage(msg: ChatMessage?): String { + if (msg == null) return "" + + val sender: String = + coreContext.contactsManager.findContactByAddress(msg.fromAddress)?.fullName + ?: LinphoneUtils.getDisplayName(msg.fromAddress) + var body = "" + for (content in msg.contents) { + if (content.isFile || content.isFileTransfer) body += content.name + " " + else if (content.isText) body += content.stringBuffer + " " + } + + return "$sender: $body" + } + + private fun searchMatchingContact() { + val remoteAddress = if (chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())) { + chatRoom.peerAddress + } else { + if (chatRoom.participants.isNotEmpty()) { + chatRoom.participants[0].address + } else { + Log.e("[Chat Room] $chatRoom doesn't have any participant in state ${chatRoom.state}!") + return + } + } + contact.value = coreContext.contactsManager.findContactByAddress(remoteAddress) + } + + private fun getParticipantsNames() { + if (oneToOneChatRoom) return + + var participantsList = "" + var index = 0 + for (participant in chatRoom.participants) { + val contact: Contact? = + coreContext.contactsManager.findContactByAddress(participant.address) + participantsList += contact?.fullName ?: LinphoneUtils.getDisplayName(participant.address) + index++ + if (index != chatRoom.nbParticipants) participantsList += ", " + } + participants.value = participantsList + } + + private fun updateSecurityIcon() { + securityLevelIcon.value = when (chatRoom.securityLevel) { + ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator + ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator + else -> R.drawable.security_alert_indicator + } + securityLevelContentDescription.value = when (chatRoom.securityLevel) { + ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe + ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted + else -> R.string.content_description_security_level_unsafe + } + } + + private fun updateRemotesComposing() { + remoteIsComposing.value = chatRoom.isRemoteComposing + + var composing = "" + for (address in chatRoom.composingAddresses) { + val contact: Contact? = coreContext.contactsManager.findContactByAddress(address) + composing += if (composing.isNotEmpty()) ", " else "" + composing += contact?.fullName ?: LinphoneUtils.getDisplayName(address) + } + composingList.value = AppUtils.getStringWithPlural(R.plurals.chat_room_remote_composing, chatRoom.composingAddresses.size, composing) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt new file mode 100644 index 000000000..3999cfd1e --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.viewmodels.ErrorReportingViewModel +import org.linphone.contact.ContactsUpdatedListenerStub +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.Event +import org.linphone.utils.LinphoneUtils + +class ChatRoomsListViewModel : ErrorReportingViewModel() { + val chatRooms = MutableLiveData>() + + val latestUpdatedChatRoomId = MutableLiveData() + + val contactsUpdatedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val groupChatAvailable: Boolean = LinphoneUtils.isGroupChatAvailable() + + private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { + override fun onContactsUpdated() { + Log.i("[Chat Rooms] Contacts have changed") + contactsUpdatedEvent.value = Event(true) + } + } + + private val listener: CoreListenerStub = object : CoreListenerStub() { + override fun onChatRoomStateChanged(core: Core, chatRoom: ChatRoom, state: ChatRoom.State) { + if (state == ChatRoom.State.Created) { + updateChatRooms() + } else if (state == ChatRoom.State.TerminationFailed) { + Log.e("[Chat Rooms] Group chat room removal for address ${chatRoom.peerAddress.asStringUriOnly()} has failed !") + onErrorEvent.value = Event(R.string.chat_room_removal_failed_snack) + } + } + + override fun onChatRoomSubjectChanged(core: Core, chatRoom: ChatRoom) { + updateChatRoom(chatRoom) + } + + override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) { + updateChatRoom(chatRoom) + } + + override fun onMessageSent(core: Core, chatRoom: ChatRoom, message: ChatMessage) { + if (chatRooms.value?.indexOf(chatRoom) == 0) updateChatRoom(chatRoom) + else updateChatRooms() + } + + override fun onMessageReceived(core: Core, chatRoom: ChatRoom, message: ChatMessage) { + if (chatRooms.value?.indexOf(chatRoom) == 0) updateChatRoom(chatRoom) + else updateChatRooms() + } + + override fun onMessageReceivedUnableDecrypt( + core: Core, + chatRoom: ChatRoom, + message: ChatMessage + ) { + updateChatRooms() + } + } + + private val chatRoomListener = object : ChatRoomListenerStub() { + override fun onStateChanged(chatRoom: ChatRoom, newState: ChatRoom.State) { + if (newState == ChatRoom.State.Deleted) { + val list = arrayListOf() + list.addAll(chatRooms.value.orEmpty()) + list.remove(chatRoom) + chatRooms.value = list + } + } + } + + private var chatRoomsToDeleteCount = 0 + + init { + chatRooms.value = getChatRooms() + coreContext.core.addListener(listener) + coreContext.contactsManager.addListener(contactsUpdatedListener) + } + + override fun onCleared() { + coreContext.contactsManager.removeListener(contactsUpdatedListener) + coreContext.core.removeListener(listener) + + super.onCleared() + } + + fun deleteChatRoom(chatRoom: ChatRoom?) { + for (eventLog in chatRoom?.getHistoryMessageEvents(0).orEmpty()) { + LinphoneUtils.deleteFilesAttachedToEventLog(eventLog) + } + + chatRoomsToDeleteCount = 1 + chatRoom?.addListener(chatRoomListener) + chatRoom?.core?.deleteChatRoom(chatRoom) + } + + fun deleteChatRooms(chatRooms: ArrayList) { + chatRoomsToDeleteCount = chatRooms.size + for (chatRoom in chatRooms) { + for (eventLog in chatRoom.getHistoryMessageEvents(0).orEmpty()) { + LinphoneUtils.deleteFilesAttachedToEventLog(eventLog) + } + + chatRoom.addListener(chatRoomListener) + chatRoom.core?.deleteChatRoom(chatRoom) + } + } + + private fun updateChatRoom(chatRoom: ChatRoom) { + latestUpdatedChatRoomId.value = chatRooms.value?.indexOf(chatRoom) + } + + private fun updateChatRooms() { + chatRooms.value = getChatRooms() + } + + private fun getChatRooms(): ArrayList { + val list = arrayListOf() + + for (room in coreContext.core.chatRooms) { + list.add(room) + } + + return list + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListChildViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListChildViewModel.kt new file mode 100644 index 000000000..3b192255e --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListChildViewModel.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.core.ChatRoomSecurityLevel +import org.linphone.core.ParticipantDevice + +class DevicesListChildViewModel(private val device: ParticipantDevice) : ViewModel() { + val deviceName: String = device.name + + val securityLevelIcon: Int by lazy { + when (device.securityLevel) { + ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator + ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator + else -> R.drawable.security_alert_indicator + } + } + + val securityContentDescription: Int by lazy { + when (device.securityLevel) { + ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe + ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted + else -> R.string.content_description_security_level_unsafe + } + } + + fun onClick() { + coreContext.startCall(device.address, true) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListGroupViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListGroupViewModel.kt new file mode 100644 index 000000000..693d9203d --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListGroupViewModel.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.contact.GenericContactViewModel +import org.linphone.core.ChatRoomSecurityLevel +import org.linphone.core.Participant + +class DevicesListGroupViewModel(private val participant: Participant) : GenericContactViewModel(participant.address) { + override val securityLevel: ChatRoomSecurityLevel + get() = participant.securityLevel + + private val device = if (participant.devices.isEmpty()) null else participant.devices.first() + + val securityLevelIcon: Int by lazy { + when (device?.securityLevel) { + ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator + ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator + else -> R.drawable.security_alert_indicator + } + } + + val securityLevelContentDescription: Int by lazy { + when (device?.securityLevel) { + ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe + ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted + else -> R.string.content_description_security_level_unsafe + } + } + + val sipUri: String = participant.address.asStringUriOnly() + + val isExpanded = MutableLiveData() + + val devices = MutableLiveData>() + + init { + isExpanded.value = false + + val list = arrayListOf() + for (device in participant.devices) { + list.add(DevicesListChildViewModel((device))) + } + devices.value = list + } + + fun toggleExpanded() { + isExpanded.value = isExpanded.value != true + } + + fun onClick() { + if (device?.address != null) coreContext.startCall(device.address, true) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListViewModel.kt new file mode 100644 index 000000000..14ddbb4ae --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListViewModel.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.linphone.core.ChatRoom +import org.linphone.core.ChatRoomListenerStub +import org.linphone.core.EventLog + +class DevicesListViewModelFactory(private val chatRoom: ChatRoom) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return DevicesListViewModel(chatRoom) as T + } +} + +class DevicesListViewModel(private val chatRoom: ChatRoom) : ViewModel() { + val participants = MutableLiveData>() + + private val listener = object : ChatRoomListenerStub() { + override fun onParticipantDeviceAdded(chatRoom: ChatRoom?, eventLog: EventLog?) { + updateParticipants() + } + + override fun onParticipantDeviceRemoved(chatRoom: ChatRoom?, eventLog: EventLog?) { + updateParticipants() + } + + override fun onParticipantAdded(chatRoom: ChatRoom?, eventLog: EventLog?) { + updateParticipants() + } + + override fun onParticipantRemoved(chatRoom: ChatRoom?, eventLog: EventLog?) { + updateParticipants() + } + } + + init { + chatRoom.addListener(listener) + updateParticipants() + } + + override fun onCleared() { + chatRoom.removeListener(listener) + super.onCleared() + } + + private fun updateParticipants() { + val list = arrayListOf() + list.add(DevicesListGroupViewModel(chatRoom.me)) + for (participant in chatRoom.participants) { + list.add(DevicesListGroupViewModel(participant)) + } + participants.value = list + } +} diff --git a/app/src/main/java/org/linphone/call/CallStatsViewHolder.java b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EphemeralDurationViewModel.kt similarity index 57% rename from app/src/main/java/org/linphone/call/CallStatsViewHolder.java rename to app/src/main/java/org/linphone/activities/main/chat/viewmodels/EphemeralDurationViewModel.kt index 77d558b23..10110fb74 100644 --- a/app/src/main/java/org/linphone/call/CallStatsViewHolder.java +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EphemeralDurationViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2019 Belledonne Communications SARL. + * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,21 +17,23 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.call; +package org.linphone.activities.main.chat.viewmodels -import android.view.View; -import android.widget.RelativeLayout; -import android.widget.TextView; -import org.linphone.R; +import androidx.lifecycle.ViewModel -public class CallStatsViewHolder { +class EphemeralDurationViewModel( + val textResource: Int, + private val selectedDuration: Long, + private val duration: Long, + private val listener: DurationItemClicked +) : ViewModel() { + val selected: Boolean = selectedDuration == duration - public final RelativeLayout avatarLayout; - public final TextView participantName, sipUri; - - public CallStatsViewHolder(View v) { - avatarLayout = v.findViewById(R.id.avatar_layout); - participantName = v.findViewById(R.id.name); - sipUri = v.findViewById(R.id.sipUri); + fun setSelected() { + listener.onDurationValueChanged(duration) } } + +interface DurationItemClicked { + fun onDurationValueChanged(duration: Long) +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EphemeralViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EphemeralViewModel.kt new file mode 100644 index 000000000..5f7a14eb4 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EphemeralViewModel.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.linphone.R +import org.linphone.core.ChatRoom +import org.linphone.core.tools.Log + +class EphemeralViewModelFactory(private val chatRoom: ChatRoom) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return EphemeralViewModel(chatRoom) as T + } +} + +class EphemeralViewModel(private val chatRoom: ChatRoom) : ViewModel() { + val durationsList = MutableLiveData>() + + var currentSelectedDuration: Long = 0 + + private val listener = object : DurationItemClicked { + override fun onDurationValueChanged(duration: Long) { + currentSelectedDuration = duration + computeEphemeralDurationValues() + } + } + + init { + Log.i("[Ephemeral Messages] Current duration is ${chatRoom.ephemeralLifetime}, ephemeral enabled? ${chatRoom.ephemeralEnabled()}") + currentSelectedDuration = if (chatRoom.ephemeralEnabled()) chatRoom.ephemeralLifetime else 0 + computeEphemeralDurationValues() + } + + fun updateChatRoomEphemeralDuration() { + Log.i("[Ephemeral Messages] Selected value is $currentSelectedDuration") + if (currentSelectedDuration > 0) { + if (chatRoom.ephemeralLifetime != currentSelectedDuration) { + Log.i("[Ephemeral Messages] Setting new lifetime for ephemeral messages to $currentSelectedDuration") + chatRoom.ephemeralLifetime = currentSelectedDuration + } else { + Log.i("[Ephemeral Messages] Configured lifetime for ephemeral messages was already $currentSelectedDuration") + } + + if (!chatRoom.ephemeralEnabled()) { + Log.i("[Ephemeral Messages] Ephemeral messages were disabled, enable them") + chatRoom.enableEphemeral(true) + } + } else if (chatRoom.ephemeralEnabled()) { + Log.i("[Ephemeral Messages] Ephemeral messages were enabled, disable them") + chatRoom.enableEphemeral(false) + } + } + + private fun computeEphemeralDurationValues() { + val list = arrayListOf() + list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_disabled, currentSelectedDuration, 0, listener)) + list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_one_minute, currentSelectedDuration, 60, listener)) + list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_one_hour, currentSelectedDuration, 3600, listener)) + list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_one_day, currentSelectedDuration, 86400, listener)) + list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_three_days, currentSelectedDuration, 259200, listener)) + list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_one_week, currentSelectedDuration, 604800, listener)) + durationsList.value = list + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EventViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EventViewModel.kt new file mode 100644 index 000000000..d4ac0fa48 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EventViewModel.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import android.content.Context +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.contact.Contact +import org.linphone.core.EventLog +import org.linphone.core.tools.Log +import org.linphone.utils.LinphoneUtils + +class EventViewModel(private val eventLog: EventLog) : ViewModel() { + val text = MutableLiveData() + + val isSecurity: Boolean by lazy { + when (eventLog.type) { + EventLog.Type.ConferenceSecurityEvent -> true + else -> false + } + } + + private val contact: Contact? by lazy { + val address = eventLog.participantAddress ?: eventLog.securityEventFaultyDeviceAddress + if (address != null) { + coreContext.contactsManager.findContactByAddress(address) + } else { + Log.e("[Event ViewModel] Unexpected null address for event $eventLog") + null + } + } + + private val displayName: String by lazy { + val address = eventLog.participantAddress ?: eventLog.securityEventFaultyDeviceAddress + if (address != null) { + LinphoneUtils.getDisplayName(address) + } else { + Log.e("[Event ViewModel] Unexpected null address for event $eventLog") + "" + } + } + + init { + updateEventText() + } + + private fun getName(): String { + return contact?.fullName ?: displayName + } + + private fun updateEventText() { + val context: Context = coreContext.context + + text.value = when (eventLog.type) { + EventLog.Type.ConferenceCreated -> context.getString(R.string.chat_event_conference_created) + EventLog.Type.ConferenceTerminated -> context.getString(R.string.chat_event_conference_destroyed) + EventLog.Type.ConferenceParticipantAdded -> context.getString(R.string.chat_event_participant_added).format(getName()) + EventLog.Type.ConferenceParticipantRemoved -> context.getString(R.string.chat_event_participant_removed).format(getName()) + EventLog.Type.ConferenceSubjectChanged -> context.getString(R.string.chat_event_subject_changed).format(eventLog.subject) + EventLog.Type.ConferenceParticipantSetAdmin -> context.getString(R.string.chat_event_admin_set).format(getName()) + EventLog.Type.ConferenceParticipantUnsetAdmin -> context.getString(R.string.chat_event_admin_unset).format(getName()) + EventLog.Type.ConferenceParticipantDeviceAdded -> context.getString(R.string.chat_event_device_added).format(getName()) + EventLog.Type.ConferenceParticipantDeviceRemoved -> context.getString(R.string.chat_event_device_removed).format(getName()) + EventLog.Type.ConferenceSecurityEvent -> { + val name = getName() + when (eventLog.securityEventType) { + EventLog.SecurityEventType.EncryptionIdentityKeyChanged -> context.getString(R.string.chat_security_event_lime_identity_key_changed).format(name) + EventLog.SecurityEventType.ManInTheMiddleDetected -> context.getString(R.string.chat_security_event_man_in_the_middle_detected).format(name) + EventLog.SecurityEventType.SecurityLevelDowngraded -> context.getString(R.string.chat_security_event_security_level_downgraded).format(name) + EventLog.SecurityEventType.ParticipantMaxDeviceCountExceeded -> context.getString(R.string.chat_security_event_participant_max_count_exceeded).format(name) + else -> "Unexpected security event for $name: ${eventLog.securityEventType}" + } + } + EventLog.Type.ConferenceEphemeralMessageDisabled -> context.getString(R.string.chat_event_ephemeral_disabled) + EventLog.Type.ConferenceEphemeralMessageEnabled -> context.getString(R.string.chat_event_ephemeral_enabled).format(formatEphemeralExpiration(context, eventLog.ephemeralMessageLifetime)) + EventLog.Type.ConferenceEphemeralMessageLifetimeChanged -> context.getString(R.string.chat_event_ephemeral_lifetime_changed).format(formatEphemeralExpiration(context, eventLog.ephemeralMessageLifetime)) + else -> "Unexpected event: ${eventLog.type}" + } + } + + private fun formatEphemeralExpiration(context: Context, duration: Long): String { + return when (duration) { + 0L -> context.getString(R.string.chat_room_ephemeral_message_disabled) + 60L -> context.getString(R.string.chat_room_ephemeral_message_one_minute) + 3600L -> context.getString(R.string.chat_room_ephemeral_message_one_hour) + 86400L -> context.getString(R.string.chat_room_ephemeral_message_one_day) + 259200L -> context.getString(R.string.chat_room_ephemeral_message_three_days) + 604800L -> context.getString(R.string.chat_room_ephemeral_message_one_week) + else -> "Unexpected duration" + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoParticipantViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoParticipantViewModel.kt new file mode 100644 index 000000000..b65a570e3 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoParticipantViewModel.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import androidx.lifecycle.MutableLiveData +import org.linphone.activities.main.chat.GroupChatRoomMember +import org.linphone.contact.GenericContactViewModel +import org.linphone.core.ChatRoomSecurityLevel + +class GroupInfoParticipantViewModel(private val participant: GroupChatRoomMember) : GenericContactViewModel(participant.address) { + override val securityLevel: ChatRoomSecurityLevel + get() = participant.securityLevel + + val sipUri: String = participant.address.asStringUriOnly() + + val isAdmin = MutableLiveData() + + val showAdminControls = MutableLiveData() + + init { + isAdmin.value = participant.isAdmin + showAdminControls.value = false + } + + fun setAdmin() { + isAdmin.value = true + participant.isAdmin = true + } + + fun unSetAdmin() { + isAdmin.value = false + participant.isAdmin = false + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt new file mode 100644 index 000000000..e46323c2a --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.chat.GroupChatRoomMember +import org.linphone.activities.main.viewmodels.ErrorReportingViewModel +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class GroupInfoViewModelFactory(private val chatRoom: ChatRoom?) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return GroupInfoViewModel(chatRoom) as T + } +} + +class GroupInfoViewModel(val chatRoom: ChatRoom?) : ErrorReportingViewModel() { + val createdChatRoomEvent = MutableLiveData>() + + val subject = MutableLiveData() + + val participants = MutableLiveData>() + + val isEncrypted = MutableLiveData() + + val isMeAdmin = MutableLiveData() + + val canLeaveGroup = MutableLiveData() + + val waitForChatRoomCreation = MutableLiveData() + + private val listener = object : ChatRoomListenerStub() { + override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) { + if (state == ChatRoom.State.Created) { + waitForChatRoomCreation.value = false + createdChatRoomEvent.value = Event(chatRoom) // To trigger going to the chat room + } else if (state == ChatRoom.State.CreationFailed) { + Log.e("[Chat Room Group Info] Group chat room creation has failed !") + waitForChatRoomCreation.value = false + onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack) + } + } + + override fun onSubjectChanged(chatRoom: ChatRoom?, eventLog: EventLog?) { + subject.value = chatRoom?.subject + } + + override fun onParticipantAdded(chatRoom: ChatRoom?, eventLog: EventLog?) { + updateParticipants() + } + + override fun onParticipantRemoved(chatRoom: ChatRoom?, eventLog: EventLog?) { + updateParticipants() + } + + override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom?, eventLog: EventLog?) { + isMeAdmin.value = chatRoom?.me?.isAdmin + updateParticipants() + } + } + + init { + subject.value = chatRoom?.subject + isMeAdmin.value = chatRoom == null || (chatRoom.me.isAdmin && !chatRoom.hasBeenLeft()) + canLeaveGroup.value = chatRoom != null && !chatRoom.hasBeenLeft() + isEncrypted.value = chatRoom?.hasCapability(ChatRoomCapabilities.Encrypted.toInt()) + + if (chatRoom != null) updateParticipants() + + chatRoom?.addListener(listener) + waitForChatRoomCreation.value = false + } + + override fun onCleared() { + chatRoom?.removeListener(listener) + + super.onCleared() + } + + fun createChatRoom() { + waitForChatRoomCreation.value = true + val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams() + params.enableEncryption(isEncrypted.value == true) + params.enableGroup(true) + + val addresses = arrayOfNulls
(participants.value.orEmpty().size) + var index = 0 + for (participant in participants.value.orEmpty()) { + addresses[index] = participant.address + Log.i("[Chat Room Group Info] Participant ${participant.address.asStringUriOnly()} will be added to group") + index += 1 + } + + val chatRoom: ChatRoom? = coreContext.core.createChatRoom(params, subject.value, addresses) + chatRoom?.addListener(listener) + } + + fun updateRoom() { + if (chatRoom != null) { + // Subject + val newSubject = subject.value.orEmpty() + if (newSubject.isNotEmpty() && newSubject != chatRoom.subject) { + Log.i("[Chat Room Group Info] Subject changed to $newSubject") + chatRoom.subject = newSubject + } + + // Removed participants + val participantsToRemove = arrayListOf() + for (participant in chatRoom.participants) { + val member = participants.value.orEmpty().find { member -> + participant.address.weakEqual(member.address) + } + if (member == null) { + Log.w("[Chat Room Group Info] Participant ${participant.address.asStringUriOnly()} will be removed from group") + participantsToRemove.add(participant) + } + } + val toRemove = arrayOfNulls(participantsToRemove.size) + participantsToRemove.toArray(toRemove) + chatRoom.removeParticipants(toRemove) + + // Added participants & new admins + val participantsToAdd = arrayListOf
() + for (member in participants.value.orEmpty()) { + val participant = chatRoom.participants.find { participant -> + participant.address.weakEqual(member.address) + } + if (participant != null) { + // Participant found, check if admin status needs to be updated + if (member.isAdmin != participant.isAdmin) { + if (chatRoom.me.isAdmin) { + Log.i("[Chat Room Group Info] Participant ${member.address.asStringUriOnly()} will be admin? ${member.isAdmin}") + chatRoom.setParticipantAdminStatus(participant, member.isAdmin) + } + } + } else { + Log.i("[Chat Room Group Info] Participant ${member.address.asStringUriOnly()} will be added to group") + participantsToAdd.add(member.address) + } + } + val toAdd = arrayOfNulls
(participantsToAdd.size) + participantsToAdd.toArray(toAdd) + chatRoom.addParticipants(toAdd) + + // Go back to chat room + createdChatRoomEvent.value = Event(chatRoom) + } + } + + fun leaveGroup() { + if (chatRoom != null) { + Log.w("[Chat Room Group Info] Leaving group") + chatRoom.leave() + createdChatRoomEvent.value = Event(chatRoom) + } + } + + fun removeParticipant(participant: GroupChatRoomMember) { + val list = arrayListOf() + list.addAll(participants.value.orEmpty()) + list.remove(participant) + participants.value = list + } + + private fun updateParticipants() { + val list = arrayListOf() + + if (chatRoom != null) { + for (participant in chatRoom.participants) { + list.add(GroupChatRoomMember(participant.address, participant.isAdmin, participant.securityLevel)) + } + } + + participants.value = list + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ImdnParticipantViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ImdnParticipantViewModel.kt new file mode 100644 index 000000000..fb94ef186 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ImdnParticipantViewModel.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import org.linphone.contact.GenericContactViewModel +import org.linphone.core.ParticipantImdnState +import org.linphone.utils.TimestampUtils + +class ImdnParticipantViewModel(private val imdnState: ParticipantImdnState) : GenericContactViewModel(imdnState.participant.address) { + val sipUri: String = imdnState.participant.address.asStringUriOnly() + + val time: String = TimestampUtils.toString(imdnState.stateChangeTime) +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ImdnViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ImdnViewModel.kt new file mode 100644 index 000000000..ebdebe520 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ImdnViewModel.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.linphone.core.ChatMessage +import org.linphone.core.ChatMessageListenerStub +import org.linphone.core.ParticipantImdnState + +class ImdnViewModelFactory(private val chatMessage: ChatMessage) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return ImdnViewModel(chatMessage) as T + } +} + +class ImdnViewModel(private val chatMessage: ChatMessage) : ViewModel() { + val participants = MutableLiveData>() + + val chatMessageViewModel = ChatMessageViewModel(chatMessage) + + private val listener = object : ChatMessageListenerStub() { + override fun onParticipantImdnStateChanged( + message: ChatMessage?, + state: ParticipantImdnState? + ) { + updateParticipantsLists() + } + } + + init { + chatMessage.addListener(listener) + updateParticipantsLists() + } + + override fun onCleared() { + chatMessage.removeListener(listener) + super.onCleared() + } + + private fun updateParticipantsLists() { + val list = arrayListOf() + list.addAll(chatMessage.getParticipantsByImdnState(ChatMessage.State.Displayed)) + list.addAll(chatMessage.getParticipantsByImdnState(ChatMessage.State.DeliveredToUser)) + list.addAll(chatMessage.getParticipantsByImdnState(ChatMessage.State.Delivered)) + list.addAll(chatMessage.getParticipantsByImdnState(ChatMessage.State.NotDelivered)) + participants.value = list + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt b/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt new file mode 100644 index 000000000..b682b7b36 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.views + +import android.content.Context +import android.text.Layout +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import kotlin.math.ceil + +/** + * The purpose of this class is to have a TextView declared with wrap_content as width that won't + * fill it's parent if it is multi line. + */ +class MultiLineWrapContentWidthTextView : AppCompatTextView { + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int + ) : super(context, attrs, defStyleAttr) + + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + var wSpec = widthSpec + val widthMode = MeasureSpec.getMode(wSpec) + + if (widthMode == MeasureSpec.AT_MOST) { + val layout = layout + if (layout != null) { + val maxWidth = (ceil(getMaxLineWidth(layout).toDouble()).toInt() + + totalPaddingLeft + + totalPaddingRight) + wSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST) + } + } + + super.onMeasure(wSpec, heightSpec) + } + + private fun getMaxLineWidth(layout: Layout): Float { + var maxWidth = 0.0f + val lines = layout.lineCount + for (i in 0 until lines) { + if (layout.getLineWidth(i) > maxWidth) { + maxWidth = layout.getLineWidth(i) + } + } + return maxWidth + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/views/RichEditText.kt b/app/src/main/java/org/linphone/activities/main/chat/views/RichEditText.kt new file mode 100644 index 000000000..bc525cff8 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/views/RichEditText.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.chat.views + +import android.content.Context +import android.os.Bundle +import android.util.AttributeSet +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputConnection +import androidx.appcompat.widget.AppCompatEditText +import androidx.core.view.inputmethod.EditorInfoCompat +import androidx.core.view.inputmethod.InputConnectionCompat +import androidx.core.view.inputmethod.InputContentInfoCompat + +/** + * Allows for image input inside an EditText, usefull for keyboards with gif support for example. + */ +class RichEditText : AppCompatEditText { + private var mListener: RichInputListener? = null + private var mSupportedMimeTypes: Array? = null + + interface RichInputListener { + fun onCommitContent( + inputContentInfo: InputContentInfoCompat, + flags: Int, + opts: Bundle, + contentMimeTypes: Array? + ): Boolean + } + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + fun setListener(listener: RichInputListener) { + mListener = listener + mSupportedMimeTypes = arrayOf("image/png", "image/gif", "image/jpeg") + } + + override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? { + val ic = super.onCreateInputConnection(editorInfo) + EditorInfoCompat.setContentMimeTypes(editorInfo, mSupportedMimeTypes) + + val callback = + InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, opts -> + val listener = mListener + listener?.onCommitContent( + inputContentInfo, flags, opts, mSupportedMimeTypes + ) ?: false + } + + return if (ic != null) { + InputConnectionCompat.createWrapper(ic, editorInfo, callback) + } else null + } +} diff --git a/app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt b/app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt new file mode 100644 index 000000000..d922d86cb --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.contact.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DiffUtil +import org.linphone.R +import org.linphone.activities.main.contact.viewmodels.ContactViewModel +import org.linphone.activities.main.viewmodels.ListTopBarViewModel +import org.linphone.contact.Contact +import org.linphone.databinding.ContactListCellBinding +import org.linphone.databinding.GenericListHeaderBinding +import org.linphone.utils.Event +import org.linphone.utils.HeaderAdapter +import org.linphone.utils.LifecycleListAdapter +import org.linphone.utils.LifecycleViewHolder + +class ContactsListAdapter(val selectionViewModel: ListTopBarViewModel) : LifecycleListAdapter(ContactDiffCallback()), HeaderAdapter { + val selectedContactEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding: ContactListCellBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.contact_list_cell, parent, false + ) + val viewHolder = ViewHolder(binding) + binding.lifecycleOwner = viewHolder + return viewHolder + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + inner class ViewHolder( + private val binding: ContactListCellBinding + ) : LifecycleViewHolder(binding) { + fun bind(contact: Contact) { + with(binding) { + val contactViewModel = ContactViewModel(contact) + viewModel = contactViewModel + + // This is for item selection through ListTopBarFragment + selectionListViewModel = selectionViewModel + selectionViewModel.isEditionEnabled.observe(this@ViewHolder, Observer { + position = adapterPosition + }) + + binding.setClickListener { + if (selectionViewModel.isEditionEnabled.value == true) { + selectionViewModel.onToggleSelect(adapterPosition) + } else { + selectedContactEvent.value = Event(contact) + } + } + + executePendingBindings() + } + } + } + + override fun displayHeaderForPosition(position: Int): Boolean { + if (position >= itemCount) return false + val contact = getItem(position) + val firstLetter = contact.fullName?.first().toString() + val previousPosition = position - 1 + return if (previousPosition >= 0) { + val previousItemFirstLetter = getItem(previousPosition).fullName?.first().toString() + previousItemFirstLetter != firstLetter + } else true + } + + override fun getHeaderViewForPosition(context: Context, position: Int): View { + val contact = getItem(position) + val firstLetter = contact.fullName?.first().toString() + val binding: GenericListHeaderBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.generic_list_header, null, false + ) + binding.title = firstLetter + binding.executePendingBindings() + return binding.root + } +} + +private class ContactDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: Contact, + newItem: Contact + ): Boolean { + return oldItem.compareTo(newItem) == 0 + } + + override fun areContentsTheSame( + oldItem: Contact, + newItem: Contact + ): Boolean { + return false // For headers + } +} diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt new file mode 100644 index 000000000..bd05764d3 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.contact.fragments + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.Parcelable +import android.provider.MediaStore +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import java.io.File +import org.linphone.R +import org.linphone.activities.main.contact.viewmodels.* +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.contact.NativeContact +import org.linphone.core.tools.Log +import org.linphone.databinding.ContactEditorFragmentBinding +import org.linphone.utils.FileUtils +import org.linphone.utils.PermissionHelper + +class ContactEditorFragment : Fragment() { + private lateinit var binding: ContactEditorFragmentBinding + private lateinit var viewModel: ContactEditorViewModel + private lateinit var sharedViewModel: SharedMainViewModel + private var temporaryPicturePath: File? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ContactEditorFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + viewModel = ViewModelProvider( + this, + ContactEditorViewModelFactory(sharedViewModel.selectedContact.value) + )[ContactEditorViewModel::class.java] + binding.viewModel = viewModel + + binding.setBackClickListener { + findNavController().popBackStack() + } + + binding.setAvatarClickListener { + pickFile() + } + + binding.setSaveChangesClickListener { + val savedContact = viewModel.save() + if (savedContact is NativeContact) { + val deepLink = "linphone-android://contact/view/${savedContact.nativeId}" + Log.i("[Contact Editor] Displaying contact, starting deep link: $deepLink") + findNavController().navigate(Uri.parse(deepLink)) + } else { + findNavController().popBackStack() + } + } + + val sipUri = arguments?.getString("SipUri") + if (sipUri != null) { + Log.i("[Contact Editor] Found SIP URI in arguments: $sipUri") + val newSipUri = NumberOrAddressEditorViewModel("", true) + newSipUri.newValue.value = sipUri + + val list = arrayListOf() + list.addAll(viewModel.addresses.value.orEmpty()) + list.add(newSipUri) + viewModel.addresses.value = list + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (resultCode == Activity.RESULT_OK) { + var fileToUploadPath: String? = null + + val temporaryFileUploadPath = temporaryPicturePath + if (temporaryFileUploadPath != null) { + if (data != null) { + val dataUri = data.data + if (dataUri != null) { + fileToUploadPath = dataUri.toString() + Log.i("[Chat Room] Using data URI $fileToUploadPath") + } else if (temporaryFileUploadPath.exists()) { + fileToUploadPath = temporaryFileUploadPath.absolutePath + Log.i("[Chat Room] Data URI is null, using $fileToUploadPath") + } + } else if (temporaryFileUploadPath.exists()) { + fileToUploadPath = temporaryFileUploadPath.absolutePath + Log.i("[Chat Room] Data is null, using $fileToUploadPath") + } + } + + if (fileToUploadPath != null) { + if (fileToUploadPath.startsWith("content://") || + fileToUploadPath.startsWith("file://") + ) { + val uriToParse = Uri.parse(fileToUploadPath) + fileToUploadPath = FileUtils.getFilePath(requireContext(), uriToParse) + Log.i("[Chat] Path was using a content or file scheme, real path is: $fileToUploadPath") + if (fileToUploadPath == null) { + Log.e("[Chat] Failed to get access to file $uriToParse") + } + } + } + + if (fileToUploadPath != null) { + viewModel.setPictureFromPath(fileToUploadPath) + } + } + } + + private fun pickFile() { + val cameraIntents = ArrayList() + + // Handles image & video picking + val galleryIntent = Intent(Intent.ACTION_PICK) + galleryIntent.type = "image/*" + + if (PermissionHelper.get().hasCameraPermission()) { + // Allows to capture directly from the camera + val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + val tempFileName = System.currentTimeMillis().toString() + ".jpeg" + temporaryPicturePath = FileUtils.getFileStoragePath(tempFileName) + val uri = Uri.fromFile(temporaryPicturePath) + captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri) + cameraIntents.add(captureIntent) + } + + val chooserIntent = + Intent.createChooser(galleryIntent, getString(R.string.chat_message_pick_file_dialog)) + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toArray(arrayOf())) + + startActivityForResult(chooserIntent, 0) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt new file mode 100644 index 000000000..8c7d49437 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.contact.fragments + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.MainActivity +import org.linphone.activities.main.contact.viewmodels.ContactViewModel +import org.linphone.activities.main.contact.viewmodels.ContactViewModelFactory +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.core.tools.Log +import org.linphone.databinding.ContactDetailFragmentBinding + +class DetailContactFragment : Fragment() { + private lateinit var binding: ContactDetailFragmentBinding + private lateinit var viewModel: ContactViewModel + private lateinit var sharedViewModel: SharedMainViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ContactDetailFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + val contact = sharedViewModel.selectedContact.value + contact ?: return + + viewModel = ViewModelProvider( + this, + ContactViewModelFactory(contact) + )[ContactViewModel::class.java] + binding.viewModel = viewModel + + viewModel.sendSmsToEvent.observe(viewLifecycleOwner, Observer { + it.consume { number -> + sendSms(number) + } + }) + + viewModel.startCallToEvent.observe(viewLifecycleOwner, Observer { + it.consume { address -> + if (coreContext.core.callsNb > 0) { + Log.i("[Contact] Starting dialer with pre-filled URI ${address.asStringUriOnly()}") + val args = Bundle() + args.putString("URI", address.asStringUriOnly()) + args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer) + findNavController().navigate( + R.id.action_global_dialerFragment, + args + ) + } else { + coreContext.startCall(address) + } + } + }) + + viewModel.chatRoomCreatedEvent.observe(viewLifecycleOwner, Observer { + it.consume { chatRoom -> + if (findNavController().currentDestination?.id == R.id.detailContactFragment) { + val args = Bundle() + args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly()) + args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly()) + findNavController().navigate(R.id.action_global_masterChatRoomsFragment, args) + } + } + }) + + binding.setBackClickListener { + findNavController().popBackStack() + } + binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE + + binding.setEditClickListener { + if (findNavController().currentDestination?.id == R.id.detailContactFragment) { + findNavController().navigate(R.id.action_detailContactFragment_to_contactEditorFragment) + } + } + + viewModel.onErrorEvent.observe(viewLifecycleOwner, Observer { + it.consume { messageResourceId -> + (activity as MainActivity).showSnackBar(messageResourceId) + } + }) + } + + private fun sendSms(number: String) { + val smsIntent = Intent(Intent.ACTION_SENDTO) + smsIntent.putExtra("address", number) + smsIntent.data = Uri.parse("smsto:$number") + val text = getString(R.string.contact_send_sms_invite_text).format(getString(R.string.contact_send_sms_invite_download_link)) + smsIntent.putExtra("sms_body", text) + startActivity(smsIntent) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt new file mode 100644 index 000000000..031ecbd4c --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.contact.fragments + +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.SearchView +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.MainActivity +import org.linphone.activities.main.contact.adapters.ContactsListAdapter +import org.linphone.activities.main.contact.viewmodels.ContactsListViewModel +import org.linphone.activities.main.fragments.MasterFragment +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.contact.Contact +import org.linphone.core.Factory +import org.linphone.core.tools.Log +import org.linphone.databinding.ContactMasterFragmentBinding +import org.linphone.utils.Event +import org.linphone.utils.PermissionHelper +import org.linphone.utils.RecyclerViewHeaderDecoration + +class MasterContactsFragment : MasterFragment() { + private lateinit var binding: ContactMasterFragmentBinding + private lateinit var listViewModel: ContactsListViewModel + private lateinit var adapter: ContactsListAdapter + private lateinit var sharedViewModel: SharedMainViewModel + + private var sipUriToAdd: String? = null + private var editOnClick: Boolean = false + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ContactMasterFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + listViewModel = ViewModelProvider(this).get(ContactsListViewModel::class.java) + binding.viewModel = listViewModel + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + adapter = ContactsListAdapter(listSelectionViewModel) + binding.contactsList.adapter = adapter + + binding.setEditClickListener { + if (PermissionHelper.get().hasWriteContactsPermission()) { + listSelectionViewModel.isEditionEnabled.value = true + } else { + Log.i("[Contacts] Asking for WRITE_CONTACTS permission") + requestPermissions(arrayOf(android.Manifest.permission.WRITE_CONTACTS), 1) + } + } + + val layoutManager = LinearLayoutManager(activity) + binding.contactsList.layoutManager = layoutManager + + // Divider between items + val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation) + dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null)) + binding.contactsList.addItemDecoration(dividerItemDecoration) + + // Displays the first letter header + val headerItemDecoration = RecyclerViewHeaderDecoration(adapter) + binding.contactsList.addItemDecoration(headerItemDecoration) + + adapter.selectedContactEvent.observe(viewLifecycleOwner, Observer { + it.consume { contact -> + Log.i("[Contacts] Selected item in list changed: $contact") + sharedViewModel.selectedContact.value = contact + + if (editOnClick) { + goToContactEditor() + editOnClick = false + sipUriToAdd = null + } else { + goToContactDetails() + } + } + }) + + listViewModel.contactsList.observe(viewLifecycleOwner, Observer { + adapter.submitList(it) + }) + + binding.setAllContactsToggleClickListener { + listViewModel.sipContactsSelected.value = false + } + binding.setSipContactsToggleClickListener { + listViewModel.sipContactsSelected.value = true + } + + listViewModel.sipContactsSelected.observe(viewLifecycleOwner, Observer { + listViewModel.updateContactsList() + }) + + binding.setNewContactClickListener { + // Remove any previously selected contact + sharedViewModel.selectedContact.value = null + goToContactEditor() + } + + binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + listViewModel.filter(newText ?: "") + return true + } + }) + + val id = arguments?.getString("id") + if (id != null) { + Log.i("[Contacts] Found contact id parameter in arguments: $id") + arguments?.clear() + val contact = coreContext.contactsManager.findContactById(id) + if (contact != null) { + Log.i("[Contacts] Found matching contact $contact") + adapter.selectedContactEvent.value = Event(contact) + } + } else { + val sipUri = arguments?.getString("sipUri") + if (sipUri != null) { + Log.i("[Contacts] Found sipUri parameter in arguments: $sipUri") + sipUriToAdd = sipUri + arguments?.clear() + + val activity = requireActivity() as MainActivity + activity.showSnackBar(R.string.contact_choose_existing_or_new_to_add_number) + editOnClick = true + } else { + // When trying to display a non-native contact from history + val addressString = arguments?.getString("address") + val address = Factory.instance().createAddress(addressString) + if (address != null) { + Log.i("[Contacts] Found friend native pointer parameter in arguments: ${address.asStringUriOnly()}") + arguments?.clear() + + val contact = coreContext.contactsManager.findContactByAddress(address) + if (contact != null) { + Log.i("[Contacts] Found matching contact $contact") + adapter.selectedContactEvent.value = Event(contact) + } + } + } + } + + if (!PermissionHelper.get().hasReadContactsPermission()) { + Log.i("[Contacts] Asking for READ_CONTACTS permission") + requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 0) + } + } + + override fun getItemCount(): Int { + return adapter.itemCount + } + + override fun deleteItems(indexesOfItemToDelete: ArrayList) { + val list = ArrayList() + for (index in indexesOfItemToDelete) { + val contact = adapter.getItemAt(index) + list.add(contact) + } + listViewModel.deleteContacts(list) + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + if (requestCode == 0) { + val granted = grantResults[0] == PackageManager.PERMISSION_GRANTED + if (granted) { + Log.i("[Contacts] READ_CONTACTS permission granted") + coreContext.contactsManager.fetchContactsAsync() + } else { + Log.w("[Contacts] READ_CONTACTS permission denied") + } + } else if (requestCode == 1) { + val granted = grantResults[0] == PackageManager.PERMISSION_GRANTED + if (granted) { + Log.i("[Contacts] WRITE_CONTACTS permission granted") + listSelectionViewModel.isEditionEnabled.value = true + } else { + Log.w("[Contacts] WRITE_CONTACTS permission denied") + } + } + } + + private fun goToContactDetails() { + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.masterContactsFragment) { + findNavController().navigate(R.id.action_masterContactsFragment_to_detailContactFragment) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_detailContactFragment) + } + } + + private fun goToContactEditor() { + val args = Bundle() + if (sipUriToAdd != null) args.putString("SipUri", sipUriToAdd) + + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.masterContactsFragment) { + findNavController().navigate(R.id.action_masterContactsFragment_to_contactEditorFragment, args) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_contactEditorFragment, args) + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactEditorViewModel.kt b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactEditorViewModel.kt new file mode 100644 index 000000000..46edd12a4 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactEditorViewModel.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.contact.viewmodels + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.ExifInterface +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import java.io.ByteArrayOutputStream +import java.io.IOException +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.contact.* +import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils +import org.linphone.utils.ImageUtils +import org.linphone.utils.PermissionHelper + +class ContactEditorViewModelFactory(private val contact: Contact?) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return ContactEditorViewModel(contact) as T + } +} + +class ContactEditorViewModel(val c: Contact?) : ViewModel(), ContactViewModelInterface { + override val contact = MutableLiveData() + + override val displayName: String + get() = if (c == null) "" else c.fullName ?: c.firstName + " " + c.lastName + + val firstName = MutableLiveData() + + val lastName = MutableLiveData() + + val organization = MutableLiveData() + + val displayOrganization = corePreferences.contactOrganizationVisible + + val tempPicturePath = MutableLiveData() + private var picture: ByteArray? = null + + val numbers = MutableLiveData>() + + val addresses = MutableLiveData>() + + init { + if (c != null) { + contact.value = c + firstName.value = c.firstName + lastName.value = c.lastName + organization.value = c.organization + } + + updateNumbersAndAddresses() + } + + fun save(): Contact { + var contact = c + if (contact == null) { + contact = if (PermissionHelper.get().hasWriteContactsPermission()) { + NativeContact(AppUtils.createAndroidContact().toString()) + } else { + Contact() + } + } + + if (contact is NativeContact) { + NativeContactEditor(contact) + .setFirstAndLastNames(firstName.value.orEmpty(), lastName.value.orEmpty()) + .setOrganization(organization.value.orEmpty()) + .setPhoneNumbers(numbers.value.orEmpty()) + .setSipAddresses(addresses.value.orEmpty()) + .setPicture(picture) + .commit() + } else { + val friend = contact.friend ?: coreContext.core.createFriend() + if (friend != null) { + friend.edit() + // TODO edit friend + friend.done() + + if (contact.friend == null) { + contact.friend = friend + coreContext.core.defaultFriendList.addLocalFriend(friend) + } + } + } + return contact + } + + fun setPictureFromPath(picturePath: String) { + var orientation = ExifInterface.ORIENTATION_NORMAL + var image = BitmapFactory.decodeFile(picturePath) + + try { + val ei = ExifInterface(picturePath) + orientation = ei.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_UNDEFINED + ) + Log.i("[Contact Editor] Exif rotation is $orientation") + } catch (e: IOException) { + Log.e("[Contact Editor] Failed to get Exif rotation, exception raised: $e") + } + + if (image == null) { + Log.e("[Contact Editor] Couldn't get bitmap from filePath: $picturePath") + return + } + + when (orientation) { + ExifInterface.ORIENTATION_ROTATE_90 -> image = + ImageUtils.rotateImage(image, 90f) + ExifInterface.ORIENTATION_ROTATE_180 -> image = + ImageUtils.rotateImage(image, 180f) + ExifInterface.ORIENTATION_ROTATE_270 -> image = + ImageUtils.rotateImage(image, 270f) + } + + val stream = ByteArrayOutputStream() + image.compress(Bitmap.CompressFormat.JPEG, 100, stream) + picture = stream.toByteArray() + tempPicturePath.value = picturePath + image.recycle() + stream.close() + } + + fun addEmptySipAddress() { + val list = arrayListOf() + list.addAll(addresses.value.orEmpty()) + list.add(NumberOrAddressEditorViewModel("", true)) + addresses.value = list + } + + fun addEmptyPhoneNumber() { + val list = arrayListOf() + list.addAll(numbers.value.orEmpty()) + list.add(NumberOrAddressEditorViewModel("", false)) + numbers.value = list + } + + private fun updateNumbersAndAddresses() { + val phoneNumbers = arrayListOf() + for (number in c?.phoneNumbers.orEmpty()) { + phoneNumbers.add(NumberOrAddressEditorViewModel(number, false)) + } + if (phoneNumbers.isEmpty()) { + phoneNumbers.add(NumberOrAddressEditorViewModel("", false)) + } + numbers.value = phoneNumbers + + val sipAddresses = arrayListOf() + for (address in c?.rawSipAddresses.orEmpty()) { + sipAddresses.add(NumberOrAddressEditorViewModel(address, true)) + } + if (sipAddresses.isEmpty()) { + sipAddresses.add(NumberOrAddressEditorViewModel("", true)) + } + addresses.value = sipAddresses + } +} diff --git a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactNumberOrAddressViewModel.kt b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactNumberOrAddressViewModel.kt new file mode 100644 index 000000000..e81255a06 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactNumberOrAddressViewModel.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.contact.viewmodels + +import androidx.lifecycle.ViewModel +import org.linphone.core.Address + +class ContactNumberOrAddressViewModel( + val address: Address, + val hasPresence: Boolean, + val displayedValue: String, + val isSip: Boolean = true, + val showSecureChat: Boolean = false, + private val listener: ContactNumberOrAddressClickListener +) : ViewModel() { + val showInvite = !hasPresence && !isSip + + fun startCall() { + listener.onCall(address) + } + + fun startChat(secured: Boolean) { + listener.onChat(address, secured) + } + + fun smsInvite() { + listener.onSmsInvite(displayedValue) + } +} + +interface ContactNumberOrAddressClickListener { + fun onCall(address: Address) + + fun onChat(address: Address, isSecured: Boolean) + + fun onSmsInvite(number: String) +} diff --git a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt new file mode 100644 index 000000000..db285871d --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.contact.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.activities.main.viewmodels.ErrorReportingViewModel +import org.linphone.contact.Contact +import org.linphone.contact.ContactViewModelInterface +import org.linphone.contact.ContactsUpdatedListenerStub +import org.linphone.contact.NativeContact +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.Event +import org.linphone.utils.LinphoneUtils + +class ContactViewModelFactory(private val contact: Contact) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return ContactViewModel(contact) as T + } +} + +class ContactViewModel(private val c: Contact) : ErrorReportingViewModel(), ContactViewModelInterface { + override val contact = MutableLiveData() + + override val displayName: String by lazy { + c.fullName ?: c.firstName + " " + c.lastName + } + + val displayOrganization = corePreferences.displayOrganization + + val numbersAndAddresses = MutableLiveData>() + + val sendSmsToEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val startCallToEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val chatRoomCreatedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val waitForChatRoomCreation = MutableLiveData() + + private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { + override fun onContactUpdated(contact: Contact) { + if (c is NativeContact && contact is NativeContact && c.nativeId == contact.nativeId) { + Log.i("[Contact] $contact has changed") + updateNumbersAndAddresses() + } + } + } + + private val chatRoomListener = object : ChatRoomListenerStub() { + override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) { + if (state == ChatRoom.State.Created) { + waitForChatRoomCreation.value = false + chatRoomCreatedEvent.value = Event(chatRoom) + } else if (state == ChatRoom.State.CreationFailed) { + Log.e("[Contact Detail] Group chat room creation has failed !") + waitForChatRoomCreation.value = false + onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack) + } + } + } + + private val listener = object : ContactNumberOrAddressClickListener { + override fun onCall(address: Address) { + startCallToEvent.value = Event(address) + } + + override fun onChat(address: Address, isSecured: Boolean) { + waitForChatRoomCreation.value = true + val chatRoom = LinphoneUtils.createOneToOneChatRoom(address, isSecured) + + if (chatRoom != null) { + if (chatRoom.state == ChatRoom.State.Created) { + chatRoomCreatedEvent.value = Event(chatRoom) + } else { + chatRoom.addListener(chatRoomListener) + } + } else { + waitForChatRoomCreation.value = false + Log.e("[Contact Detail] Couldn't create chat room with address $address") + } + } + + override fun onSmsInvite(number: String) { + sendSmsToEvent.value = Event(number) + } + } + + init { + contact.value = c + updateNumbersAndAddresses() + coreContext.contactsManager.addListener(contactsUpdatedListener) + waitForChatRoomCreation.value = false + } + + override fun onCleared() { + coreContext.contactsManager.removeListener(contactsUpdatedListener) + super.onCleared() + } + + private fun updateNumbersAndAddresses() { + val list = arrayListOf() + for (address in c.sipAddresses) { + val value = address.asStringUriOnly() + val presenceModel = c.friend?.getPresenceModelForUriOrTel(value) + val hasPresence = presenceModel?.basicStatus == PresenceBasicStatus.Open + val isMe = coreContext.core.defaultProxyConfig?.identityAddress?.weakEqual(address) ?: false + val secureChatAllowed = !isMe && c.friend?.getPresenceModelForUriOrTel(value)?.hasCapability(FriendCapability.LimeX3Dh) ?: false + val noa = ContactNumberOrAddressViewModel(address, hasPresence, LinphoneUtils.getDisplayName(address), showSecureChat = secureChatAllowed, listener = listener) + list.add(noa) + } + for (number in c.phoneNumbers) { + val presenceModel = c.friend?.getPresenceModelForUriOrTel(number) + val hasPresence = presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open + val contactAddress = presenceModel?.contact ?: number + val address = coreContext.core.interpretUrl(contactAddress) ?: continue + val isMe = coreContext.core.defaultProxyConfig?.identityAddress?.weakEqual(address) ?: false + val secureChatAllowed = !isMe && c.friend?.getPresenceModelForUriOrTel(number)?.hasCapability(FriendCapability.LimeX3Dh) ?: false + val noa = ContactNumberOrAddressViewModel(address, hasPresence, number, isSip = false, showSecureChat = secureChatAllowed, listener = listener) + list.add(noa) + } + numbersAndAddresses.value = list + } +} diff --git a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt new file mode 100644 index 000000000..30e0a63cc --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.contact.viewmodels + +import android.content.ContentProviderOperation +import android.provider.ContactsContract +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import java.util.* +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.contact.Contact +import org.linphone.contact.ContactsUpdatedListenerStub +import org.linphone.contact.NativeContact +import org.linphone.core.tools.Log + +class ContactsListViewModel : ViewModel() { + val sipContactsSelected = MutableLiveData() + + val contactsList = MutableLiveData>() + + private var filter: String = "" + + private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { + override fun onContactsUpdated() { + Log.i("[Contacts] Contacts have changed") + updateContactsList() + } + } + + init { + sipContactsSelected.value = true + filter = "" + + coreContext.contactsManager.addListener(contactsUpdatedListener) + } + + override fun onCleared() { + coreContext.contactsManager.removeListener(contactsUpdatedListener) + + super.onCleared() + } + + fun updateContactsList() { + var list = arrayListOf() + list.addAll(if (sipContactsSelected.value == true) coreContext.contactsManager.sipContacts else coreContext.contactsManager.contacts) + + if (filter.isNotEmpty()) { + val filter = filter.toLowerCase(Locale.getDefault()) + list = list.filter { contact -> + contact.fullName?.toLowerCase(Locale.getDefault())?.contains(filter) ?: false + } as ArrayList + } + + // Prevent blinking items when list hasn't changed + if (list.isEmpty() || list.size != contactsList.value.orEmpty().size) { + contactsList.value = list + } + } + + fun filter(search: String) { + filter = search + updateContactsList() + } + + fun deleteContacts(list: ArrayList) { + val select = ContactsContract.Data.CONTACT_ID + " = ?" + val ops = ArrayList() + + for (contact in list) { + if (contact is NativeContact) { + val nativeContact: NativeContact = contact + Log.i("[Contacts] Adding Android contact id ${nativeContact.nativeId} to batch removal") + val args = arrayOf(nativeContact.nativeId) + ops.add( + ContentProviderOperation.newDelete(ContactsContract.RawContacts.CONTENT_URI) + .withSelection(select, args) + .build() + ) + } + + if (contact.friend != null) { + Log.i("[Contacts] Removing friend") + contact.friend?.remove() + } + } + + if (ops.isNotEmpty()) { + try { + Log.i("[Contacts] Removing ${ops.size} contacts") + coreContext.context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) + } catch (e: Exception) { + Log.e("[Contacts] $e") + } + } + } +} diff --git a/app/src/main/java/org/linphone/sync/AuthenticationService.java b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/NumberOrAddressEditorViewModel.kt similarity index 61% rename from app/src/main/java/org/linphone/sync/AuthenticationService.java rename to app/src/main/java/org/linphone/activities/main/contact/viewmodels/NumberOrAddressEditorViewModel.kt index e666fc8ed..04db7d963 100644 --- a/app/src/main/java/org/linphone/sync/AuthenticationService.java +++ b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/NumberOrAddressEditorViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2019 Belledonne Communications SARL. + * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,23 +17,21 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.sync; +package org.linphone.activities.main.contact.viewmodels -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; +import androidx.lifecycle.MutableLiveData -public class AuthenticationService extends Service { +class NumberOrAddressEditorViewModel(val currentValue: String, val isSipAddress: Boolean) { + val newValue = MutableLiveData() - private Authenticator mAuthenticator; + val toRemove = MutableLiveData() - @Override - public void onCreate() { - mAuthenticator = new Authenticator(this); + init { + newValue.value = currentValue + toRemove.value = false } - @Override - public IBinder onBind(Intent intent) { - return mAuthenticator.getIBinder(); + fun remove() { + toRemove.value = true } } diff --git a/app/src/main/java/org/linphone/call/CallActivityInterface.java b/app/src/main/java/org/linphone/activities/main/dialer/NumpadDigitListener.kt similarity index 77% rename from app/src/main/java/org/linphone/call/CallActivityInterface.java rename to app/src/main/java/org/linphone/activities/main/dialer/NumpadDigitListener.kt index 1da80055a..49cfa6437 100644 --- a/app/src/main/java/org/linphone/call/CallActivityInterface.java +++ b/app/src/main/java/org/linphone/activities/main/dialer/NumpadDigitListener.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2019 Belledonne Communications SARL. + * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,10 +17,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.call; +package org.linphone.activities.main.dialer -public interface CallActivityInterface { - void refreshInCallActions(); - - void resetCallControlsHidingTimer(); +interface NumpadDigitListener { + fun handleClick(key: Char) + fun handleLongClick(key: Char): Boolean } diff --git a/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt b/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt new file mode 100644 index 000000000..dad3492d2 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.dialer.fragments + +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.activities.main.dialer.viewmodels.DialerViewModel +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.core.tools.Log +import org.linphone.databinding.DialerFragmentBinding + +class DialerFragment : Fragment() { + private lateinit var binding: DialerFragmentBinding + private lateinit var viewModel: DialerViewModel + private lateinit var sharedViewModel: SharedMainViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = DialerFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(DialerViewModel::class.java) + binding.viewModel = viewModel + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + binding.setEraseClickListener { + viewModel.eraseLastChar() + } + + binding.setEraseLongClickListener { + viewModel.eraseAll() + } + + binding.setNewContactClickListener { + val deepLink = "linphone-android://contact/new/${viewModel.enteredUri.value}" + Log.i("[Dialer] Creating contact, starting deep link: $deepLink") + findNavController().navigate(Uri.parse(deepLink)) + } + + binding.setStartCallClickListener { + viewModel.startCall() + } + + binding.setAddCallClickListener { + viewModel.startCall() + } + + binding.setTransferCallClickListener { + viewModel.transferCall() + } + + if (arguments?.containsKey("Transfer") == true) { + sharedViewModel.pendingCallTransfer = arguments?.getBoolean("Transfer") ?: false + } + if (arguments?.containsKey("URI") == true) { + viewModel.enteredUri.value = arguments?.getString("URI") + } + + Log.i("[Dialer] Pending call transfer mode = ${sharedViewModel.pendingCallTransfer}") + viewModel.transferVisibility.value = sharedViewModel.pendingCallTransfer + } +} diff --git a/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt b/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt new file mode 100644 index 000000000..c36324d7b --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.dialer.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.activities.main.dialer.NumpadDigitListener +import org.linphone.core.* + +class DialerViewModel : ViewModel() { + val enteredUri = MutableLiveData() + + val atLeastOneCall = MutableLiveData() + + val transferVisibility = MutableLiveData() + + val onKeyClick: NumpadDigitListener = object : NumpadDigitListener { + override fun handleClick(key: Char) { + enteredUri.value += key.toString() + coreContext.core.playDtmf(key, 1) + } + + override fun handleLongClick(key: Char): Boolean { + if (key == '1') { + val voiceMailUri = corePreferences.voiceMailUri + if (voiceMailUri != null) { + coreContext.startCall(voiceMailUri) + } + } else { + enteredUri.value += key.toString() + } + return true + } + } + + private val listener = object : CoreListenerStub() { + override fun onCallStateChanged( + core: Core, + call: Call, + state: Call.State, + message: String? + ) { + atLeastOneCall.value = core.callsNb > 0 + } + } + + init { + coreContext.core.addListener(listener) + + enteredUri.value = "" + atLeastOneCall.value = coreContext.core.callsNb > 0 + transferVisibility.value = false + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + + super.onCleared() + } + + fun eraseLastChar() { + enteredUri.value = enteredUri.value?.dropLast(1) + } + + fun eraseAll(): Boolean { + enteredUri.value = "" + return true + } + + fun startCall() { + val addressToCall = enteredUri.value.orEmpty() + if (addressToCall.isNotEmpty()) { + coreContext.startCall(addressToCall) + } else { + setLastOutgoingCallAddress() + } + } + + fun transferCall() { + val addressToCall = enteredUri.value.orEmpty() + if (addressToCall.isNotEmpty()) { + coreContext.transferCallTo(addressToCall) + } else { + setLastOutgoingCallAddress() + } + } + + private fun setLastOutgoingCallAddress() { + val callLog = coreContext.core.lastOutgoingCallLog + if (callLog != null) { + enteredUri.value = callLog.remoteAddress.asStringUriOnly() + } + } +} diff --git a/app/src/main/java/org/linphone/fragments/EmptyFragment.java b/app/src/main/java/org/linphone/activities/main/fragments/EmptyFragment.kt similarity index 63% rename from app/src/main/java/org/linphone/fragments/EmptyFragment.java rename to app/src/main/java/org/linphone/activities/main/fragments/EmptyFragment.kt index 945a68d22..a14f7e32c 100644 --- a/app/src/main/java/org/linphone/fragments/EmptyFragment.java +++ b/app/src/main/java/org/linphone/activities/main/fragments/EmptyFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2019 Belledonne Communications SARL. + * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,21 +17,21 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.fragments; +package org.linphone.activities.main.fragments -import android.app.Fragment; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import org.linphone.R; +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import org.linphone.R -public class EmptyFragment extends Fragment { - - @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - - return inflater.inflate(R.layout.empty_fragment, container, false); +class EmptyFragment : Fragment() { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.empty_fragment, container, false) } } diff --git a/app/src/main/java/org/linphone/activities/main/fragments/ListTopBarFragment.kt b/app/src/main/java/org/linphone/activities/main/fragments/ListTopBarFragment.kt new file mode 100644 index 000000000..12d783bd6 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/fragments/ListTopBarFragment.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import org.linphone.activities.main.viewmodels.ListTopBarViewModel +import org.linphone.databinding.ListEditTopBarFragmentBinding +import org.linphone.utils.Event + +class ListTopBarFragment : Fragment() { + private lateinit var binding: ListEditTopBarFragmentBinding + private lateinit var viewModel: ListTopBarViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ListEditTopBarFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(parentFragment ?: this)[ListTopBarViewModel::class.java] + binding.viewModel = viewModel + + binding.setCancelClickListener { + viewModel.isEditionEnabled.value = false + } + + binding.setSelectAllClickListener { + viewModel.selectAllEvent.value = Event(true) + } + + binding.setUnSelectAllClickListener { + viewModel.unSelectAllEvent.value = Event(true) + } + + binding.setDeleteClickListener { + viewModel.deleteSelectionEvent.value = Event(true) + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/fragments/MasterFragment.kt b/app/src/main/java/org/linphone/activities/main/fragments/MasterFragment.kt new file mode 100644 index 000000000..f5853c06b --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/fragments/MasterFragment.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.fragments + +import android.app.Dialog +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import org.linphone.R +import org.linphone.activities.main.viewmodels.DialogViewModel +import org.linphone.activities.main.viewmodels.ListTopBarViewModel +import org.linphone.utils.DialogUtils + +/** + * This fragment can be inherited by all fragments that will display a list + * where items can be selected for removal through the ListTopBarFragment + */ +abstract class MasterFragment : Fragment() { + protected lateinit var listSelectionViewModel: ListTopBarViewModel + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + // List selection + listSelectionViewModel = ViewModelProvider(this).get(ListTopBarViewModel::class.java) + + listSelectionViewModel.isEditionEnabled.observe(viewLifecycleOwner, Observer { + if (!it) listSelectionViewModel.onUnSelectAll() + }) + + listSelectionViewModel.selectAllEvent.observe(viewLifecycleOwner, Observer { + it.consume { + listSelectionViewModel.onSelectAll(getItemCount() - 1) + } + }) + + listSelectionViewModel.unSelectAllEvent.observe(viewLifecycleOwner, Observer { + it.consume { + listSelectionViewModel.onUnSelectAll() + } + }) + + listSelectionViewModel.deleteSelectionEvent.observe(viewLifecycleOwner, Observer { + it.consume { + val viewModel = DialogViewModel(getString(R.string.dialog_default_delete_message)) + val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel) + + viewModel.showCancelButton { + dialog.dismiss() + listSelectionViewModel.isEditionEnabled.value = false + } + + viewModel.showDeleteButton({ + delete() + dialog.dismiss() + listSelectionViewModel.isEditionEnabled.value = false + }, getString(R.string.dialog_delete)) + + dialog.show() + } + }) + } + + private fun delete() { + val list = listSelectionViewModel.selectedItems.value ?: arrayListOf() + deleteItems(list) + } + + abstract fun getItemCount(): Int + + abstract fun deleteItems(indexesOfItemToDelete: ArrayList) +} diff --git a/app/src/main/java/org/linphone/activities/main/fragments/StatusFragment.kt b/app/src/main/java/org/linphone/activities/main/fragments/StatusFragment.kt new file mode 100644 index 000000000..dec02a69a --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/fragments/StatusFragment.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.activities.main.viewmodels.StatusViewModel +import org.linphone.core.tools.Log +import org.linphone.databinding.StatusFragmentBinding +import org.linphone.utils.Event + +class StatusFragment : Fragment() { + private lateinit var binding: StatusFragmentBinding + private lateinit var viewModel: StatusViewModel + private lateinit var sharedViewModel: SharedMainViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = StatusFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(StatusViewModel::class.java) + binding.viewModel = viewModel + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + sharedViewModel.proxyConfigRemoved.observe(viewLifecycleOwner, Observer { + Log.i("[Status Fragment] A proxy config was removed, update default proxy state") + val defaultProxy = coreContext.core.defaultProxyConfig + if (defaultProxy != null) { + viewModel.updateDefaultProxyConfigRegistrationStatus(defaultProxy.state) + } + }) + + binding.setMenuClickListener { + sharedViewModel.toggleDrawerEvent.value = Event(true) + } + + binding.setRefreshClickListener { + viewModel.refreshRegister() + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/fragments/TabsFragment.kt b/app/src/main/java/org/linphone/activities/main/fragments/TabsFragment.kt new file mode 100644 index 000000000..2680c74c8 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/fragments/TabsFragment.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.fragments + +import android.content.Context +import android.os.Bundle +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.main.viewmodels.TabsViewModel +import org.linphone.databinding.TabsFragmentBinding + +class TabsFragment : Fragment() { + private lateinit var binding: TabsFragmentBinding + private lateinit var viewModel: TabsViewModel + + private var dialerSelected: Boolean = false + private var contactsSelected: Boolean = false + private var chatSelected: Boolean = false + private var historySelected: Boolean = false + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = TabsFragmentBinding.inflate(inflater, container, false) + + binding.historySelect.visibility = if (historySelected) View.VISIBLE else View.GONE + binding.contactsSelect.visibility = if (contactsSelected) View.VISIBLE else View.GONE + binding.dialerSelect.visibility = if (dialerSelected) View.VISIBLE else View.GONE + binding.chatSelect.visibility = if (chatSelected) View.VISIBLE else View.GONE + + return binding.root + } + + override fun onInflate(context: Context, attrs: AttributeSet, savedInstanceState: Bundle?) { + super.onInflate(context, attrs, savedInstanceState) + + val attributes = context.obtainStyledAttributes(attrs, R.styleable.TabsFragment) + historySelected = attributes.getBoolean(R.styleable.TabsFragment_history_selected, false) + contactsSelected = attributes.getBoolean(R.styleable.TabsFragment_contacts_selected, false) + dialerSelected = attributes.getBoolean(R.styleable.TabsFragment_dialer_selected, false) + chatSelected = attributes.getBoolean(R.styleable.TabsFragment_chat_selected, false) + + attributes.recycle() + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = activity?.run { + ViewModelProvider(this).get(TabsViewModel::class.java) + } ?: throw Exception("Invalid Activity") + binding.viewModel = viewModel + + binding.setHistoryClickListener { + when (findNavController().currentDestination?.id) { + R.id.masterContactsFragment -> findNavController().navigate(R.id.action_masterContactsFragment_to_masterCallLogsFragment) + R.id.dialerFragment -> findNavController().navigate(R.id.action_dialerFragment_to_masterCallLogsFragment) + R.id.masterChatRoomsFragment -> findNavController().navigate(R.id.action_masterChatRoomsFragment_to_masterCallLogsFragment) + } + } + + binding.setContactsClickListener { + when (findNavController().currentDestination?.id) { + R.id.masterCallLogsFragment -> findNavController().navigate(R.id.action_masterCallLogsFragment_to_masterContactsFragment) + R.id.dialerFragment -> findNavController().navigate(R.id.action_dialerFragment_to_masterContactsFragment) + R.id.masterChatRoomsFragment -> findNavController().navigate(R.id.action_masterChatRoomsFragment_to_masterContactsFragment) + } + } + + binding.setDialerClickListener { + when (findNavController().currentDestination?.id) { + R.id.masterCallLogsFragment -> findNavController().navigate(R.id.action_masterCallLogsFragment_to_dialerFragment) + R.id.masterContactsFragment -> findNavController().navigate(R.id.action_masterContactsFragment_to_dialerFragment) + R.id.masterChatRoomsFragment -> findNavController().navigate(R.id.action_masterChatRoomsFragment_to_dialerFragment) + } + } + + binding.setChatClickListener { + when (findNavController().currentDestination?.id) { + R.id.masterCallLogsFragment -> findNavController().navigate(R.id.action_masterCallLogsFragment_to_masterChatRoomsFragment) + R.id.masterContactsFragment -> findNavController().navigate(R.id.action_masterContactsFragment_to_masterChatRoomsFragment) + R.id.dialerFragment -> findNavController().navigate(R.id.action_dialerFragment_to_masterChatRoomsFragment) + } + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/history/adapters/CallLogsListAdapter.kt b/app/src/main/java/org/linphone/activities/main/history/adapters/CallLogsListAdapter.kt new file mode 100644 index 000000000..b455e70ab --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/history/adapters/CallLogsListAdapter.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.history.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DiffUtil +import java.text.SimpleDateFormat +import java.util.* +import org.linphone.R +import org.linphone.activities.main.history.viewmodels.CallLogViewModel +import org.linphone.activities.main.viewmodels.ListTopBarViewModel +import org.linphone.core.Address +import org.linphone.core.CallLog +import org.linphone.databinding.GenericListHeaderBinding +import org.linphone.databinding.HistoryListCellBinding +import org.linphone.utils.* + +class CallLogsListAdapter(val selectionViewModel: ListTopBarViewModel) : LifecycleListAdapter(CallLogDiffCallback()), HeaderAdapter { + val selectedCallLogEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val startCallToEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding: HistoryListCellBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.history_list_cell, parent, false + ) + val viewHolder = ViewHolder(binding) + binding.lifecycleOwner = viewHolder + return viewHolder + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + inner class ViewHolder( + private val binding: HistoryListCellBinding + ) : LifecycleViewHolder(binding) { + fun bind(callLog: CallLog) { + with(binding) { + val callLogViewModel = CallLogViewModel(callLog) + viewModel = callLogViewModel + + // This is for item selection through ListTopBarFragment + selectionListViewModel = selectionViewModel + selectionViewModel.isEditionEnabled.observe(this@ViewHolder, Observer { + position = adapterPosition + }) + + setClickListener { + if (selectionViewModel.isEditionEnabled.value == true) { + selectionViewModel.onToggleSelect(adapterPosition) + } else { + startCallToEvent.value = Event(callLog.remoteAddress) + } + } + + // This listener is disabled when in edition mode + setDetailsClickListener { + selectedCallLogEvent.value = Event(callLog) + } + + executePendingBindings() + } + } + } + + override fun displayHeaderForPosition(position: Int): Boolean { + if (position >= itemCount) return false + val callLog = getItem(position) + val date = callLog.startDate + val previousPosition = position - 1 + return if (previousPosition >= 0) { + val previousItemDate = getItem(previousPosition).startDate + !TimestampUtils.isSameDay(date, previousItemDate) + } else true + } + + override fun getHeaderViewForPosition(context: Context, position: Int): View { + val callLog = getItem(position) + val date = formatDate(context, callLog.startDate) + val binding: GenericListHeaderBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.generic_list_header, null, false + ) + binding.title = date + binding.executePendingBindings() + return binding.root + } + + private fun formatDate(context: Context, date: Long): String { + if (TimestampUtils.isToday(date)) { + return context.getString(R.string.today) + } else if (TimestampUtils.isYesterday(date)) { + return context.getString(R.string.yesterday) + } + return SimpleDateFormat("EEE d MMM", Locale.getDefault()).format(Date(date * 1000)) + } +} + +private class CallLogDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: CallLog, + newItem: CallLog + ): Boolean { + return oldItem.callId == newItem.callId + } + + override fun areContentsTheSame( + oldItem: CallLog, + newItem: CallLog + ): Boolean { + return false // For headers + } +} diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt new file mode 100644 index 000000000..d4c0c3a69 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.history.fragments + +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.MainActivity +import org.linphone.activities.main.history.viewmodels.CallLogViewModel +import org.linphone.activities.main.history.viewmodels.CallLogViewModelFactory +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.contact.NativeContact +import org.linphone.core.tools.Log +import org.linphone.databinding.HistoryDetailFragmentBinding + +class DetailCallLogFragment : Fragment() { + private lateinit var binding: HistoryDetailFragmentBinding + private lateinit var viewModel: CallLogViewModel + private lateinit var sharedViewModel: SharedMainViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = HistoryDetailFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + val callLog = sharedViewModel.selectedCallLog.value + callLog ?: return + + viewModel = ViewModelProvider( + this, + CallLogViewModelFactory(callLog) + )[CallLogViewModel::class.java] + binding.viewModel = viewModel + + binding.setBackClickListener { + findNavController().popBackStack() + } + binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE + + binding.setNewContactClickListener { + viewModel.callLog.remoteAddress.clean() + val deepLink = "linphone-android://contact/new/${viewModel.callLog.remoteAddress.asStringUriOnly()}" + Log.i("[History] Creating contact, starting deep link: $deepLink") + findNavController().navigate(Uri.parse(deepLink)) + } + + binding.setContactClickListener { + val contact = viewModel.contact.value as? NativeContact + if (contact != null) { + val deepLink = "linphone-android://contact/view/${contact.nativeId}" + Log.i("[History] Displaying contact, starting deep link: $deepLink") + findNavController().navigate(Uri.parse(deepLink)) + } else { + val address = viewModel.callLog.remoteAddress + address.clean() + val deepLink = "linphone-android://contact/view-friend/${address.asStringUriOnly()}" + Log.i("[History] Displaying friend, starting deep link: $deepLink") + findNavController().navigate(Uri.parse(deepLink)) + } + } + + viewModel.startCallEvent.observe(viewLifecycleOwner, Observer { + it.consume { address -> + if (coreContext.core.callsNb > 0) { + Log.i("[History] Starting dialer with pre-filled URI ${address.asStringUriOnly()}") + val args = Bundle() + args.putString("URI", address.asStringUriOnly()) + args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer) + findNavController().navigate( + R.id.action_global_dialerFragment, + args + ) + } else { + coreContext.startCall(address) + } + } + }) + + viewModel.chatRoomCreatedEvent.observe(viewLifecycleOwner, Observer { + it.consume { chatRoom -> + if (findNavController().currentDestination?.id == R.id.detailCallLogFragment) { + val args = Bundle() + args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly()) + args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly()) + findNavController().navigate(R.id.action_global_masterChatRoomsFragment, args) + } + } + }) + + viewModel.onErrorEvent.observe(viewLifecycleOwner, Observer { + it.consume { messageResourceId -> + (activity as MainActivity).showSnackBar(messageResourceId) + } + }) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt new file mode 100644 index 000000000..c3e3820f1 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.history.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.fragments.MasterFragment +import org.linphone.activities.main.history.adapters.CallLogsListAdapter +import org.linphone.activities.main.history.viewmodels.CallLogsListViewModel +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.activities.main.viewmodels.TabsViewModel +import org.linphone.core.CallLog +import org.linphone.core.tools.Log +import org.linphone.databinding.HistoryMasterFragmentBinding +import org.linphone.utils.RecyclerViewHeaderDecoration + +class MasterCallLogsFragment : MasterFragment() { + private lateinit var binding: HistoryMasterFragmentBinding + private lateinit var listViewModel: CallLogsListViewModel + private lateinit var adapter: CallLogsListAdapter + private lateinit var sharedViewModel: SharedMainViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = HistoryMasterFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + listViewModel = ViewModelProvider(this).get(CallLogsListViewModel::class.java) + binding.viewModel = listViewModel + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + adapter = CallLogsListAdapter(listSelectionViewModel) + // SubmitList is done on a background thread + // We need this adapter data observer to know when to scroll + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + scrollToTop() + } + }) + binding.callLogsList.adapter = adapter + + binding.setEditClickListener { + listSelectionViewModel.isEditionEnabled.value = true + } + + val layoutManager = LinearLayoutManager(activity) + binding.callLogsList.layoutManager = layoutManager + + // Divider between items + val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation) + dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null)) + binding.callLogsList.addItemDecoration(dividerItemDecoration) + + // Displays formatted date header + val headerItemDecoration = RecyclerViewHeaderDecoration(adapter) + binding.callLogsList.addItemDecoration(headerItemDecoration) + + listViewModel.callLogs.observe(viewLifecycleOwner, Observer { callLogs -> + if (listViewModel.missedCallLogsSelected.value == false) { + adapter.submitList(callLogs) + } + }) + + listViewModel.missedCallLogs.observe(viewLifecycleOwner, Observer { callLogs -> + if (listViewModel.missedCallLogsSelected.value == true) { + adapter.submitList(callLogs) + } + }) + + listViewModel.missedCallLogsSelected.observe(viewLifecycleOwner, Observer { + if (it) { + adapter.submitList(listViewModel.missedCallLogs.value) + } else { + adapter.submitList(listViewModel.callLogs.value) + } + }) + + listViewModel.contactsUpdatedEvent.observe(viewLifecycleOwner, Observer { + it.consume { + adapter.notifyDataSetChanged() + } + }) + + adapter.selectedCallLogEvent.observe(viewLifecycleOwner, Observer { + it.consume { callLog -> + sharedViewModel.selectedCallLog.value = callLog + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.masterCallLogsFragment) { + findNavController().navigate(R.id.action_masterCallLogsFragment_to_detailCallLogFragment) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.history_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_detailCallLogFragment) + } + } + }) + + adapter.startCallToEvent.observe(viewLifecycleOwner, Observer { + it.consume { address -> + if (coreContext.core.callsNb > 0) { + Log.i("[History] Starting dialer with pre-filled URI ${address.asStringUriOnly()}") + val args = Bundle() + args.putString("URI", address.asStringUriOnly()) + args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer) + findNavController().navigate( + R.id.action_global_dialerFragment, + args + ) + } else { + coreContext.startCall(address) + } + } + }) + + binding.setAllCallLogsToggleClickListener { + listViewModel.missedCallLogsSelected.value = false + } + binding.setMissedCallLogsToggleClickListener { + listViewModel.missedCallLogsSelected.value = true + } + } + + override fun onResume() { + super.onResume() + + coreContext.core.resetMissedCallsCount() + coreContext.notificationsManager.dismissMissedCallNotification() + + val tabsViewModel = activity?.run { + ViewModelProvider(this).get(TabsViewModel::class.java) + } + tabsViewModel?.updateMissedCallCount() + } + + override fun getItemCount(): Int { + return adapter.itemCount + } + + override fun deleteItems(indexesOfItemToDelete: ArrayList) { + val list = ArrayList() + for (index in indexesOfItemToDelete) { + val callLog = adapter.getItemAt(index) + list.add(callLog) + } + listViewModel.deleteCallLogs(list) + } + + private fun scrollToTop() { + binding.callLogsList.scrollToPosition(0) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt b/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt new file mode 100644 index 000000000..bf93ff04a --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.history.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import java.text.SimpleDateFormat +import java.util.* +import kotlin.collections.ArrayList +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.contact.GenericContactViewModel +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.Event +import org.linphone.utils.LinphoneUtils +import org.linphone.utils.TimestampUtils + +class CallLogViewModelFactory(private val callLog: CallLog) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return CallLogViewModel(callLog) as T + } +} + +class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.remoteAddress) { + val peerSipUri: String by lazy { + callLog.remoteAddress.clean() // To remove gruu if any + callLog.remoteAddress.asStringUriOnly() + } + + val statusIconResource: Int by lazy { + if (callLog.dir == Call.Dir.Incoming) { + if (callLog.status == Call.Status.Missed) { + R.drawable.call_status_missed + } else { + R.drawable.call_status_incoming + } + } else { + R.drawable.call_status_outgoing + } + } + + val iconContentDescription: Int by lazy { + if (callLog.dir == Call.Dir.Incoming) { + if (callLog.status == Call.Status.Missed) { + R.string.content_description_missed_call + } else { + R.string.content_description_incoming_call + } + } else { + R.string.content_description_outgoing_call + } + } + + val directionIconResource: Int by lazy { + if (callLog.dir == Call.Dir.Incoming) { + if (callLog.status == Call.Status.Missed) { + R.drawable.call_missed + } else { + R.drawable.call_incoming + } + } else { + R.drawable.call_outgoing + } + } + + val duration: String by lazy { + val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) + val cal = Calendar.getInstance() + cal[0, 0, 0, 0, 0] = callLog.duration + dateFormat.format(cal.time) + } + + val date: String by lazy { + val pattern = if (TimestampUtils.isToday(callLog.startDate)) "HH:mm" else "yyyy/MM/dd - HH:mm" + SimpleDateFormat(pattern, Locale.getDefault()).format(Date(callLog.startDate * 1000)) + } + + val startCallEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val chatRoomCreatedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val waitForChatRoomCreation = MutableLiveData() + + val secureChatAllowed = contact.value?.friend?.getPresenceModelForUriOrTel(peerSipUri)?.hasCapability(FriendCapability.LimeX3Dh) ?: false + + private val chatRoomListener = object : ChatRoomListenerStub() { + override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) { + if (state == ChatRoom.State.Created) { + waitForChatRoomCreation.value = false + chatRoomCreatedEvent.value = Event(chatRoom) + } else if (state == ChatRoom.State.CreationFailed) { + Log.e("[History Detail] Group chat room creation has failed !") + waitForChatRoomCreation.value = false + onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack) + } + } + } + + init { + waitForChatRoomCreation.value = false + } + + fun startCall() { + startCallEvent.value = Event(callLog.remoteAddress) + } + + fun startChat(isSecured: Boolean) { + waitForChatRoomCreation.value = true + val chatRoom = LinphoneUtils.createOneToOneChatRoom(callLog.remoteAddress, isSecured) + if (chatRoom != null) { + if (chatRoom.state == ChatRoom.State.Created) { + chatRoomCreatedEvent.value = Event(chatRoom) + } else { + chatRoom.addListener(chatRoomListener) + } + } else { + waitForChatRoomCreation.value = false + Log.e("[History Detail] Couldn't create chat room with address ${callLog.remoteAddress}") + } + } + + fun getCallsHistory(): ArrayList { + val callsHistory = ArrayList() + val logs = coreContext.core.getCallHistory(callLog.remoteAddress, coreContext.core.defaultProxyConfig?.identityAddress) + for (log in logs) { + callsHistory.add(CallLogViewModel(log)) + } + return callsHistory + } +} diff --git a/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogsListViewModel.kt b/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogsListViewModel.kt new file mode 100644 index 000000000..3f7ecb097 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogsListViewModel.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.history.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.contact.ContactsUpdatedListenerStub +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class CallLogsListViewModel : ViewModel() { + val callLogs = MutableLiveData>() + val missedCallLogs = MutableLiveData>() + + val missedCallLogsSelected = MutableLiveData() + + val contactsUpdatedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private val listener: CoreListenerStub = object : CoreListenerStub() { + override fun onCallStateChanged( + core: Core, + call: Call, + state: Call.State, + message: String + ) { + updateCallLogs() + } + } + + private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { + override fun onContactsUpdated() { + Log.i("[Call Logs] Contacts have changed") + contactsUpdatedEvent.value = Event(true) + } + } + + init { + missedCallLogsSelected.value = false + updateCallLogs() + + coreContext.core.addListener(listener) + coreContext.contactsManager.addListener(contactsUpdatedListener) + } + + override fun onCleared() { + coreContext.contactsManager.removeListener(contactsUpdatedListener) + coreContext.core.removeListener(listener) + + super.onCleared() + } + + fun deleteCallLogs(listToDelete: ArrayList) { + val list = arrayListOf() + list.addAll(callLogs.value.orEmpty()) + + val missedList = arrayListOf() + missedList.addAll(missedCallLogs.value.orEmpty()) + + for (callLog in listToDelete) { + coreContext.core.removeCallLog(callLog) + list.remove(callLog) + missedList.remove(callLog) + } + + callLogs.value = list + missedCallLogs.value = missedList + } + + private fun updateCallLogs() { + val list = arrayListOf() + val missedList = arrayListOf() + + for (callLog in coreContext.core.callLogs) { + list.add(callLog) + if (callLog.status == Call.Status.Missed) missedList.add(callLog) + } + + callLogs.value = list + missedCallLogs.value = missedList + } +} diff --git a/app/src/main/java/org/linphone/activities/main/recordings/adapters/RecordingsListAdapter.kt b/app/src/main/java/org/linphone/activities/main/recordings/adapters/RecordingsListAdapter.kt new file mode 100644 index 000000000..b47f75b37 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/recordings/adapters/RecordingsListAdapter.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.recordings.adapters + +import android.content.Context +import android.os.SystemClock +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DiffUtil +import java.text.SimpleDateFormat +import java.util.* +import org.linphone.R +import org.linphone.activities.main.recordings.viewmodels.RecordingViewModel +import org.linphone.activities.main.viewmodels.ListTopBarViewModel +import org.linphone.databinding.GenericListHeaderBinding +import org.linphone.databinding.RecordingListCellBinding +import org.linphone.utils.HeaderAdapter +import org.linphone.utils.LifecycleListAdapter +import org.linphone.utils.LifecycleViewHolder +import org.linphone.utils.TimestampUtils + +class RecordingsListAdapter(val selectionViewModel: ListTopBarViewModel) : LifecycleListAdapter( + RecordingDiffCallback() +), HeaderAdapter { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding: RecordingListCellBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.recording_list_cell, parent, false + ) + val viewHolder = ViewHolder(binding) + binding.lifecycleOwner = viewHolder + return viewHolder + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + inner class ViewHolder( + private val binding: RecordingListCellBinding + ) : LifecycleViewHolder(binding) { + fun bind(recording: RecordingViewModel) { + with(binding) { + viewModel = recording + + // This is for item selection through ListTopBarFragment + selectionListViewModel = selectionViewModel + selectionViewModel.isEditionEnabled.observe(this@ViewHolder, Observer { + position = adapterPosition + }) + + setClickListener { + if (selectionViewModel.isEditionEnabled.value == true) { + selectionViewModel.onToggleSelect(adapterPosition) + } + } + + recording.playStartedEvent.observe(this@ViewHolder, Observer { + it.consume { playing -> + recordCurrentTime.base = SystemClock.elapsedRealtime() + + if (playing) { + recordCurrentTime.start() + } else { + recordCurrentTime.stop() + recordProgressionBar.progress = 0 + } + } + }) + + recordCurrentTime.setOnChronometerTickListener { + recordProgressionBar.progress = (SystemClock.elapsedRealtime() - it.base).toInt() + } + + executePendingBindings() + } + } + } + + override fun displayHeaderForPosition(position: Int): Boolean { + if (position >= itemCount) return false + + val recording = getItem(position) + val date = recording.date + val previousPosition = position - 1 + + return if (previousPosition >= 0) { + val previousItemDate = getItem(previousPosition).date + !TimestampUtils.isSameDay(date, previousItemDate) + } else { + true + } + } + + override fun getHeaderViewForPosition(context: Context, position: Int): View { + val recording = getItem(position) + val date = formatDate(context, recording.date.time) + val binding: GenericListHeaderBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.generic_list_header, null, false + ) + binding.title = date + binding.executePendingBindings() + return binding.root + } + + private fun formatDate(context: Context, date: Long): String { + // Recordings is one of the few items in Linphone that is already in milliseconds + if (TimestampUtils.isToday(date, false)) { + return context.getString(R.string.today) + } else if (TimestampUtils.isYesterday(date, false)) { + return context.getString(R.string.yesterday) + } + return SimpleDateFormat("EEE d MMM", Locale.getDefault()).format(Date(date)) + } +} + +private class RecordingDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: RecordingViewModel, + newItem: RecordingViewModel + ): Boolean { + return oldItem.compareTo(newItem) == 0 + } + + override fun areContentsTheSame( + oldItem: RecordingViewModel, + newItem: RecordingViewModel + ): Boolean { + return false // for headers + } +} diff --git a/app/src/main/java/org/linphone/activities/main/recordings/fragments/RecordingsFragment.kt b/app/src/main/java/org/linphone/activities/main/recordings/fragments/RecordingsFragment.kt new file mode 100644 index 000000000..31d5af55c --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/recordings/fragments/RecordingsFragment.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.recordings.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import org.linphone.R +import org.linphone.activities.main.fragments.MasterFragment +import org.linphone.activities.main.recordings.adapters.RecordingsListAdapter +import org.linphone.activities.main.recordings.viewmodels.RecordingViewModel +import org.linphone.activities.main.recordings.viewmodels.RecordingsViewModel +import org.linphone.databinding.RecordingsFragmentBinding +import org.linphone.utils.RecyclerViewHeaderDecoration + +class RecordingsFragment : MasterFragment() { + private lateinit var binding: RecordingsFragmentBinding + private lateinit var viewModel: RecordingsViewModel + private lateinit var adapter: RecordingsListAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = RecordingsFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(RecordingsViewModel::class.java) + binding.viewModel = viewModel + + adapter = + RecordingsListAdapter( + listSelectionViewModel + ) + binding.recordingsList.adapter = adapter + + val layoutManager = LinearLayoutManager(activity) + binding.recordingsList.layoutManager = layoutManager + + // Divider between items + val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation) + dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null)) + binding.recordingsList.addItemDecoration(dividerItemDecoration) + + // Displays the first letter header + val headerItemDecoration = RecyclerViewHeaderDecoration(adapter) + binding.recordingsList.addItemDecoration(headerItemDecoration) + + viewModel.recordingsList.observe(viewLifecycleOwner, Observer { recordings -> + adapter.submitList(recordings) + }) + + binding.setBackClickListener { findNavController().popBackStack() } + + binding.setEditClickListener { listSelectionViewModel.isEditionEnabled.value = true } + } + + override fun getItemCount(): Int { + return adapter.itemCount + } + + override fun deleteItems(indexesOfItemToDelete: ArrayList) { + val list = ArrayList() + for (index in indexesOfItemToDelete) { + val recording = adapter.getItemAt(index) + list.add(recording) + } + viewModel.deleteRecordings(list) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/recordings/viewmodels/RecordingViewModel.kt b/app/src/main/java/org/linphone/activities/main/recordings/viewmodels/RecordingViewModel.kt new file mode 100644 index 000000000..e9cb2d631 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/recordings/viewmodels/RecordingViewModel.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.recordings.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import java.text.SimpleDateFormat +import java.util.* +import java.util.regex.Pattern +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Player +import org.linphone.core.PlayerListener +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class RecordingViewModel(val path: String) : ViewModel(), Comparable { + companion object { + val RECORD_PATTERN: Pattern = + Pattern.compile(".*/(.*)_(\\d{2}-\\d{2}-\\d{4}-\\d{2}-\\d{2}-\\d{2})\\..*") + } + + lateinit var name: String + lateinit var date: Date + + val duration: Int + get() { + if (isClosed()) player.open(path) + return player.duration + } + + val formattedDuration: String + get() = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration) // is already in milliseconds + + val formattedDate: String + get() = SimpleDateFormat("HH:mm", Locale.getDefault()).format(date) + + val playStartedEvent = MutableLiveData>() + + val isPlaying = MutableLiveData() + + private var player: Player + private val listener = PlayerListener { + Log.i("[Recording] End of file reached") + pause() + } + + init { + val m = RECORD_PATTERN.matcher(path) + if (m.matches()) { + name = m.group(1) + date = SimpleDateFormat("dd-MM-yyyy-HH-mm-ss", Locale.getDefault()).parse(m.group(2)) + } + isPlaying.value = false + + player = coreContext.core.createLocalPlayer(null, null, null) + player.addListener(listener) + } + + override fun onCleared() { + if (!isClosed()) player.close() + player.removeListener(listener) + + super.onCleared() + } + + override fun compareTo(other: RecordingViewModel): Int { + return -date.compareTo(other.date) + } + + fun play() { + if (isClosed()) player.open(path) + seek(0) + player.start() + playStartedEvent.value = Event(true) + isPlaying.value = true + } + + fun pause() { + player.pause() + isPlaying.value = false + playStartedEvent.value = Event(false) + } + + private fun seek(position: Int) { + if (!isClosed()) player.seek(position) + } + + private fun isClosed(): Boolean { + return player.state == Player.State.Closed + } +} diff --git a/app/src/main/java/org/linphone/activities/main/recordings/viewmodels/RecordingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/recordings/viewmodels/RecordingsViewModel.kt new file mode 100644 index 000000000..d23ea23b7 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/recordings/viewmodels/RecordingsViewModel.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.recordings.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import kotlin.collections.ArrayList +import org.linphone.core.tools.Log +import org.linphone.utils.FileUtils + +class RecordingsViewModel : ViewModel() { + val recordingsList = MutableLiveData>() + + init { + getRecordings() + } + + fun deleteRecordings(list: ArrayList) { + for (recording in list) { + Log.i("[Recordings] Deleting recording ${recording.path}") + FileUtils.deleteFile(recording.path) + } + getRecordings() + } + + private fun getRecordings() { + val list = arrayListOf() + + for (f in FileUtils.getFileStorageDir().listFiles().orEmpty()) { + Log.i("[Recordings] Found file ${f.path}") + if (RecordingViewModel.RECORD_PATTERN.matcher(f.path).matches()) { + list.add( + RecordingViewModel( + f.path + ) + ) + Log.i("[Recordings] Found record ${f.path}") + } + } + + list.sort() + recordingsList.value = list + } +} diff --git a/app/src/main/java/org/linphone/settings/widget/SettingListener.java b/app/src/main/java/org/linphone/activities/main/settings/SettingListener.kt similarity index 71% rename from app/src/main/java/org/linphone/settings/widget/SettingListener.java rename to app/src/main/java/org/linphone/activities/main/settings/SettingListener.kt index 7b184f670..16708c9df 100644 --- a/app/src/main/java/org/linphone/settings/widget/SettingListener.java +++ b/app/src/main/java/org/linphone/activities/main/settings/SettingListener.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2019 Belledonne Communications SARL. + * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,14 +17,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.settings.widget; +package org.linphone.activities.main.settings interface SettingListener { - void onClicked(); + fun onClicked() - void onTextValueChanged(String newValue); + fun onAccountClicked(identity: String) - void onBoolValueChanged(boolean newValue); + fun onTextValueChanged(newValue: String) - void onListValueChanged(int position, String newLabel, String newValue); + fun onBoolValueChanged(newValue: Boolean) + + fun onListValueChanged(position: Int) } diff --git a/app/src/main/java/org/linphone/call/views/LinphoneOverlay.java b/app/src/main/java/org/linphone/activities/main/settings/SettingListenerStub.kt similarity index 64% rename from app/src/main/java/org/linphone/call/views/LinphoneOverlay.java rename to app/src/main/java/org/linphone/activities/main/settings/SettingListenerStub.kt index 0172957d1..2bb5fa46c 100644 --- a/app/src/main/java/org/linphone/call/views/LinphoneOverlay.java +++ b/app/src/main/java/org/linphone/activities/main/settings/SettingListenerStub.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2019 Belledonne Communications SARL. + * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,16 +17,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.call.views; +package org.linphone.activities.main.settings -import android.view.WindowManager; +open class SettingListenerStub : SettingListener { + override fun onClicked() {} -public interface LinphoneOverlay { - WindowManager.LayoutParams getWindowManagerLayoutParams(); + override fun onAccountClicked(identity: String) {} - void addToWindowManager(WindowManager mWindowManager, WindowManager.LayoutParams params); + override fun onTextValueChanged(newValue: String) {} - void removeFromWindowManager(WindowManager mWindowManager); + override fun onBoolValueChanged(newValue: Boolean) {} - void destroy(); + override fun onListValueChanged(position: Int) {} } diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt new file mode 100644 index 000000000..f73c1ab1a --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.main.settings.viewmodels.AccountSettingsViewModel +import org.linphone.activities.main.settings.viewmodels.AccountSettingsViewModelFactory +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.core.tools.Log +import org.linphone.databinding.SettingsAccountFragmentBinding + +class AccountSettingsFragment : Fragment() { + private lateinit var binding: SettingsAccountFragmentBinding + private lateinit var sharedViewModel: SharedMainViewModel + private lateinit var viewModel: AccountSettingsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SettingsAccountFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + val identity = arguments?.getString("Identity") ?: "" + viewModel = ViewModelProvider(this, AccountSettingsViewModelFactory(identity)).get(AccountSettingsViewModel::class.java) + binding.viewModel = viewModel + + binding.setBackClickListener { findNavController().popBackStack() } + binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE + + viewModel.linkPhoneNumberEvent.observe(viewLifecycleOwner, Observer { + it.consume { + if (findNavController().currentDestination?.id == R.id.accountSettingsFragment) { + val authInfo = viewModel.proxyConfig.findAuthInfo() + if (authInfo == null) { + Log.e("[Account Settings] Failed to find auth info for proxy config ${viewModel.proxyConfig}") + } else { + val args = Bundle() + args.putString("Username", authInfo.username) + args.putString("Password", authInfo.password) + args.putString("HA1", authInfo.ha1) + findNavController().navigate( + R.id.action_accountSettingsFragment_to_phoneAccountLinkingFragment, + args + ) + } + } + } + }) + + viewModel.proxyConfigRemovedEvent.observe(viewLifecycleOwner, Observer { + it.consume { + sharedViewModel.proxyConfigRemoved.value = true + findNavController().navigateUp() + } + }) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/AdvancedSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/AdvancedSettingsFragment.kt new file mode 100644 index 000000000..e8e7ef6d0 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/AdvancedSettingsFragment.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.fragments + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.Settings +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.main.settings.viewmodels.AdvancedSettingsViewModel +import org.linphone.databinding.SettingsAdvancedFragmentBinding + +class AdvancedSettingsFragment : Fragment() { + private lateinit var binding: SettingsAdvancedFragmentBinding + private lateinit var viewModel: AdvancedSettingsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SettingsAdvancedFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(AdvancedSettingsViewModel::class.java) + binding.viewModel = viewModel + + binding.setBackClickListener { findNavController().popBackStack() } + binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE + + viewModel.setNightModeEvent.observe(viewLifecycleOwner, Observer { + it.consume { value -> + AppCompatDelegate.setDefaultNightMode( + when (value) { + 0 -> AppCompatDelegate.MODE_NIGHT_NO + 1 -> AppCompatDelegate.MODE_NIGHT_YES + else -> AppCompatDelegate.MODE_NIGHT_UNSPECIFIED + } + ) + } + }) + + viewModel.goToAndroidSettingsEvent.observe(viewLifecycleOwner, Observer { it.consume { + val intent = Intent() + intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + intent.addCategory(Intent.CATEGORY_DEFAULT) + intent.data = Uri.parse("package:${requireContext().packageName}") + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + ContextCompat.startActivity(requireContext(), intent, null) + } }) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/AudioSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/AudioSettingsFragment.kt new file mode 100644 index 000000000..8e1259fd7 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/AudioSettingsFragment.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.fragments + +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.BR +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.settings.SettingListenerStub +import org.linphone.activities.main.settings.viewmodels.AudioSettingsViewModel +import org.linphone.core.tools.Log +import org.linphone.databinding.SettingsAudioFragmentBinding +import org.linphone.utils.PermissionHelper + +class AudioSettingsFragment : Fragment() { + private lateinit var binding: SettingsAudioFragmentBinding + private lateinit var viewModel: AudioSettingsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SettingsAudioFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(AudioSettingsViewModel::class.java) + binding.viewModel = viewModel + + binding.setBackClickListener { findNavController().popBackStack() } + binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE + + viewModel.askAudioRecordPermissionForEchoCancellerCalibrationEvent.observe(viewLifecycleOwner, Observer { + it.consume { + Log.i("[Audio Settings] Asking for RECORD_AUDIO permission for echo canceller calibration") + requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 1) + } + }) + + viewModel.askAudioRecordPermissionForEchoTesterEvent.observe(viewLifecycleOwner, Observer { + it.consume { + Log.i("[Audio Settings] Asking for RECORD_AUDIO permission for echo tester") + requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 2) + } + }) + + initAudioCodecsList() + + if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) { + Log.i("[Audio Settings] Asking for RECORD_AUDIO permission") + requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 0) + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + val granted = grantResults[0] == PackageManager.PERMISSION_GRANTED + if (granted) { + Log.i("[Audio Settings] RECORD_AUDIO permission granted") + if (requestCode == 1) { + viewModel.startEchoCancellerCalibration() + } else if (requestCode == 2) { + viewModel.startEchoTester() + } + } else { + Log.w("[Audio Settings] RECORD_AUDIO permission denied") + } + } + + private fun initAudioCodecsList() { + val list = arrayListOf() + for (payload in coreContext.core.audioPayloadTypes) { + val binding = DataBindingUtil.inflate(LayoutInflater.from(requireContext()), R.layout.settings_widget_switch, null, false) + binding.setVariable(BR.title, payload.mimeType) + binding.setVariable(BR.subtitle, "${payload.clockRate} Hz") + binding.setVariable(BR.checked, payload.enabled()) + binding.setVariable(BR.listener, object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + payload.enable(newValue) + } + }) + binding.lifecycleOwner = this + list.add(binding) + } + viewModel.audioCodecs.value = list + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/CallSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/CallSettingsFragment.kt new file mode 100644 index 000000000..b67b3331a --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/CallSettingsFragment.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.fragments + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.main.settings.viewmodels.CallSettingsViewModel +import org.linphone.compatibility.Compatibility +import org.linphone.databinding.SettingsCallFragmentBinding + +class CallSettingsFragment : Fragment() { + private lateinit var binding: SettingsCallFragmentBinding + private lateinit var viewModel: CallSettingsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SettingsCallFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(CallSettingsViewModel::class.java) + binding.viewModel = viewModel + + binding.setBackClickListener { findNavController().popBackStack() } + binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE + + viewModel.overlayEnabledEvent.observe(viewLifecycleOwner, Observer { + it.consume { + if (!Compatibility.canDrawOverlay(requireContext())) { + val intent = Intent("android.settings.action.MANAGE_OVERLAY_PERMISSION", Uri.parse("package:${requireContext().packageName}")) + startActivityForResult(intent, 0) + } + } + }) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (!Compatibility.canDrawOverlay(requireContext())) { + viewModel.overlayListener.onBoolValueChanged(false) + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/ChatSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/ChatSettingsFragment.kt new file mode 100644 index 000000000..eed6cd9ff --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/ChatSettingsFragment.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.main.settings.viewmodels.ChatSettingsViewModel +import org.linphone.databinding.SettingsChatFragmentBinding + +class ChatSettingsFragment : Fragment() { + private lateinit var binding: SettingsChatFragmentBinding + private lateinit var viewModel: ChatSettingsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SettingsChatFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(ChatSettingsViewModel::class.java) + binding.viewModel = viewModel + + binding.setBackClickListener { findNavController().popBackStack() } + binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt new file mode 100644 index 000000000..38edbe1a0 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.main.settings.viewmodels.ContactsSettingsViewModel +import org.linphone.compatibility.Compatibility +import org.linphone.databinding.SettingsContactsFragmentBinding + +class ContactsSettingsFragment : Fragment() { + private lateinit var binding: SettingsContactsFragmentBinding + private lateinit var viewModel: ContactsSettingsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SettingsContactsFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(ContactsSettingsViewModel::class.java) + binding.viewModel = viewModel + + binding.setBackClickListener { findNavController().popBackStack() } + binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE + + viewModel.launcherShortcutsEvent.observe(viewLifecycleOwner, Observer { + it.consume { newValue -> + if (newValue) { + Compatibility.createShortcutsToContacts(requireContext()) + } else { + Compatibility.removeShortcutsToContacts(requireContext()) + } + } + }) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/NetworkSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/NetworkSettingsFragment.kt new file mode 100644 index 000000000..169dc04b9 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/NetworkSettingsFragment.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.main.settings.viewmodels.NetworkSettingsViewModel +import org.linphone.databinding.SettingsNetworkFragmentBinding + +class NetworkSettingsFragment : Fragment() { + private lateinit var binding: SettingsNetworkFragmentBinding + private lateinit var viewModel: NetworkSettingsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SettingsNetworkFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(NetworkSettingsViewModel::class.java) + binding.viewModel = viewModel + + binding.setBackClickListener { findNavController().popBackStack() } + binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt new file mode 100644 index 000000000..4388aa9ca --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.main.settings.SettingListenerStub +import org.linphone.activities.main.settings.viewmodels.SettingsViewModel +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.core.tools.Log +import org.linphone.databinding.SettingsFragmentBinding + +class SettingsFragment : Fragment() { + private lateinit var binding: SettingsFragmentBinding + private lateinit var sharedViewModel: SharedMainViewModel + private lateinit var viewModel: SettingsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SettingsFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + viewModel = ViewModelProvider(this).get(SettingsViewModel::class.java) + binding.viewModel = viewModel + + binding.setBackClickListener { findNavController().popBackStack() } + + sharedViewModel.proxyConfigRemoved.observe(viewLifecycleOwner, Observer { + Log.i("[Settings] Proxy config removed, update accounts list") + viewModel.updateAccountsList() + }) + + val identity = arguments?.getString("identity") + if (identity != null) { + val args = Bundle() + args.putString("Identity", identity) + Log.i("[Settings] Found identity parameter in arguments: $identity") + arguments?.clear() + + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.settingsFragment) { + findNavController().navigate( + R.id.action_settingsFragment_to_accountSettingsFragment, + args + ) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_accountSettingsFragment, args) + } + } + + viewModel.accountsSettingsListener = object : SettingListenerStub() { + override fun onAccountClicked(identity: String) { + val args = Bundle() + args.putString("Identity", identity) + Log.i("[Settings] Navigation to settings for proxy with identity: $identity") + + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.settingsFragment) { + findNavController().navigate( + R.id.action_settingsFragment_to_accountSettingsFragment, + args + ) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_accountSettingsFragment, args) + } + } + } + + viewModel.tunnelSettingsListener = object : SettingListenerStub() { + override fun onClicked() { + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.settingsFragment) { + findNavController().navigate(R.id.action_settingsFragment_to_tunnelSettingsFragment) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_tunnelSettingsFragment) + } + } + } + + viewModel.audioSettingsListener = object : SettingListenerStub() { + override fun onClicked() { + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.settingsFragment) { + findNavController().navigate(R.id.action_settingsFragment_to_audioSettingsFragment) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_audioSettingsFragment) + } + } + } + + viewModel.videoSettingsListener = object : SettingListenerStub() { + override fun onClicked() { + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.settingsFragment) { + findNavController().navigate(R.id.action_settingsFragment_to_videoSettingsFragment) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_videoSettingsFragment) + } + } + } + + viewModel.callSettingsListener = object : SettingListenerStub() { + override fun onClicked() { + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.settingsFragment) { + findNavController().navigate(R.id.action_settingsFragment_to_callSettingsFragment) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_callSettingsFragment) + } + } + } + + viewModel.chatSettingsListener = object : SettingListenerStub() { + override fun onClicked() { + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.settingsFragment) { + findNavController().navigate(R.id.action_settingsFragment_to_chatSettingsFragment) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_chatSettingsFragment) + } + } + } + + viewModel.networkSettingsListener = object : SettingListenerStub() { + override fun onClicked() { + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.settingsFragment) { + findNavController().navigate(R.id.action_settingsFragment_to_networkSettingsFragment) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_networkSettingsFragment) + } + } + } + + viewModel.contactsSettingsListener = object : SettingListenerStub() { + override fun onClicked() { + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.settingsFragment) { + findNavController().navigate(R.id.action_settingsFragment_to_contactsSettingsFragment) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_contactsSettingsFragment) + } + } + } + + viewModel.advancedSettingsListener = object : SettingListenerStub() { + override fun onClicked() { + if (!resources.getBoolean(R.bool.isTablet)) { + if (findNavController().currentDestination?.id == R.id.settingsFragment) { + findNavController().navigate(R.id.action_settingsFragment_to_advancedSettingsFragment) + } + } else { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment + navHostFragment.navController.navigate(R.id.action_global_advancedSettingsFragment) + } + } + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/TunnelSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/TunnelSettingsFragment.kt new file mode 100644 index 000000000..163f7bf9d --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/TunnelSettingsFragment.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.main.settings.viewmodels.TunnelSettingsViewModel +import org.linphone.databinding.SettingsTunnelFragmentBinding + +class TunnelSettingsFragment : Fragment() { + private lateinit var binding: SettingsTunnelFragmentBinding + private lateinit var viewModel: TunnelSettingsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SettingsTunnelFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(TunnelSettingsViewModel::class.java) + binding.viewModel = viewModel + + binding.setBackClickListener { findNavController().popBackStack() } + binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/VideoSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/VideoSettingsFragment.kt new file mode 100644 index 000000000..c57dc8b87 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/VideoSettingsFragment.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.fragments + +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.BR +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.settings.SettingListenerStub +import org.linphone.activities.main.settings.viewmodels.VideoSettingsViewModel +import org.linphone.core.tools.Log +import org.linphone.databinding.SettingsVideoFragmentBinding +import org.linphone.utils.PermissionHelper + +class VideoSettingsFragment : Fragment() { + private lateinit var binding: SettingsVideoFragmentBinding + private lateinit var viewModel: VideoSettingsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SettingsVideoFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(VideoSettingsViewModel::class.java) + binding.viewModel = viewModel + + binding.setBackClickListener { findNavController().popBackStack() } + binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE + + initVideoCodecsList() + + if (!PermissionHelper.required(requireContext()).hasCameraPermission()) { + Log.i("[Video Settings] Asking for CAMERA permission") + requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 0) + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + val granted = grantResults[0] == PackageManager.PERMISSION_GRANTED + if (granted) { + Log.i("[Video Settings] CAMERA permission granted") + } else { + Log.w("[Video Settings] CAMERA permission denied") + } + } + + private fun initVideoCodecsList() { + val list = arrayListOf() + for (payload in coreContext.core.videoPayloadTypes) { + val binding = DataBindingUtil.inflate(LayoutInflater.from(requireContext()), R.layout.settings_widget_switch, null, false) + binding.setVariable(BR.title, payload.mimeType) + binding.setVariable(BR.subtitle, "") + binding.setVariable(BR.checked, payload.enabled()) + binding.setVariable(BR.listener, object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + payload.enable(newValue) + } + }) + binding.lifecycleOwner = this + list.add(binding) + } + viewModel.videoCodecs.value = list + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt new file mode 100644 index 000000000..7f9f9836d --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import java.util.* +import kotlin.collections.ArrayList +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.settings.SettingListenerStub +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.Event +import org.linphone.utils.LinphoneUtils + +class AccountSettingsViewModelFactory(private val identity: String) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + for (proxy in coreContext.core.proxyConfigList) { + if (proxy.identityAddress.asStringUriOnly() == identity) { + return AccountSettingsViewModel(proxy) as T + } + } + return AccountSettingsViewModel(coreContext.core.defaultProxyConfig) as T + } +} + +class AccountSettingsViewModel(val proxyConfig: ProxyConfig) : GenericSettingsViewModel() { + val isDefault = MutableLiveData() + + val displayName = MutableLiveData() + + val identity = MutableLiveData() + + val iconResource = MutableLiveData() + val iconContentDescription = MutableLiveData() + + lateinit var accountsSettingsListener: SettingListenerStub + + val waitForUnregister = MutableLiveData() + + val proxyConfigRemovedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private var proxyConfigToDelete: ProxyConfig? = null + + val listener: CoreListenerStub = object : CoreListenerStub() { + override fun onRegistrationStateChanged( + core: Core, + cfg: ProxyConfig, + state: RegistrationState, + message: String + ) { + if (state == RegistrationState.Cleared && cfg == proxyConfigToDelete) { + Log.i("[Account Settings] Proxy config to remove registration is now cleared") + val authInfo = cfg.findAuthInfo() + core.removeProxyConfig(cfg) + core.removeAuthInfo(authInfo) + + waitForUnregister.value = false + proxyConfigRemovedEvent.value = Event(true) + } else { + update() + } + } + } + + /* Settings part */ + + val userNameListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + // TODO + } + } + val userName = MutableLiveData() + + val userIdListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + // TODO + } + } + val userId = MutableLiveData() + + val passwordListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + // TODO + } + } + val password = MutableLiveData() + + val domainListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + // TODO + } + } + val domain = MutableLiveData() + + val displayNameListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + proxyConfig.identityAddress.displayName = newValue + } + } + // displayName mutable is above + + val disableListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + proxyConfig.enableRegister(!newValue) + } + } + val disable = MutableLiveData() + + val isDefaultListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + if (newValue) { + core.defaultProxyConfig = proxyConfig + } + } + } + // isDefault mutable is above + + val deleteListener = object : SettingListenerStub() { + override fun onClicked() { + proxyConfigToDelete = proxyConfig + waitForUnregister.value = true + + if (core.defaultProxyConfig == proxyConfig) { + Log.i("[Account Settings] Proxy config was default, let's look for a replacement") + for (proxyConfigIterator in core.proxyConfigList) { + if (proxyConfig != proxyConfigIterator) { + core.defaultProxyConfig = proxyConfigIterator + Log.i("[Account Settings] New default proxy config is $proxyConfigIterator") + break + } + } + } + + proxyConfig.edit() + proxyConfig.enableRegister(false) + proxyConfig.done() + } + } + + val pushNotificationListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + proxyConfig.isPushNotificationAllowed = newValue + } + } + val pushNotification = MutableLiveData() + + val transportListener = object : SettingListenerStub() { + override fun onListValueChanged(position: Int) { + // TODO + } + } + val transportIndex = MutableLiveData() + val transportLabels = MutableLiveData>() + + val proxyListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + proxyConfig.serverAddr = newValue + if (outboundProxy.value == true) { + // TODO + } + } + } + val proxy = MutableLiveData() + + val outboundProxyListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + // TODO + } + } + val outboundProxy = MutableLiveData() + + val stunServerListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + proxyConfig.natPolicy.stunServer = newValue + if (newValue.isEmpty()) ice.value = false + stunServer.value = newValue + } + } + val stunServer = MutableLiveData() + + val iceListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + proxyConfig.natPolicy.enableIce(newValue) + } + } + val ice = MutableLiveData() + + val avpfListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + proxyConfig.avpfMode = if (newValue) AVPFMode.Enabled else AVPFMode.Disabled + } + } + val avpf = MutableLiveData() + + val avpfRrIntervalListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + proxyConfig.avpfRrInterval = newValue.toInt() + } + } + val avpfRrInterval = MutableLiveData() + + val expiresListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + proxyConfig.expires = newValue.toInt() + } + } + val expires = MutableLiveData() + + val dialPrefixListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + proxyConfig.dialPrefix = newValue + } + } + val dialPrefix = MutableLiveData() + + val escapePlusListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + proxyConfig.dialEscapePlus = newValue + } + } + val escapePlus = MutableLiveData() + + val linkPhoneNumberListener = object : SettingListenerStub() { + override fun onClicked() { + linkPhoneNumberEvent.value = Event(true) + } + } + val linkPhoneNumberEvent = MutableLiveData>() + + init { + update() + core.addListener(listener) + initTransportList() + } + + override fun onCleared() { + core.removeListener(listener) + super.onCleared() + } + + private fun update() { + isDefault.value = core.defaultProxyConfig == proxyConfig + displayName.value = LinphoneUtils.getDisplayName(proxyConfig.identityAddress) + identity.value = proxyConfig.identityAddress.asStringUriOnly() + + iconResource.value = when (proxyConfig.state) { + RegistrationState.Ok -> R.drawable.led_connected + RegistrationState.Failed -> R.drawable.led_error + RegistrationState.Progress -> R.drawable.led_inprogress + else -> R.drawable.led_disconnected + } + iconContentDescription.value = when (proxyConfig.state) { + RegistrationState.Ok -> R.string.status_connected + RegistrationState.Progress -> R.string.status_in_progress + RegistrationState.Failed -> R.string.status_error + else -> R.string.status_not_connected + } + + userName.value = proxyConfig.identityAddress.username + userId.value = proxyConfig.findAuthInfo()?.userid + domain.value = proxyConfig.identityAddress.domain + disable.value = !proxyConfig.registerEnabled() + pushNotification.value = proxyConfig.isPushNotificationAllowed + proxy.value = proxyConfig.serverAddr + outboundProxy.value = proxyConfig.serverAddr == proxyConfig.route + stunServer.value = proxyConfig.natPolicy?.stunServer + ice.value = proxyConfig.natPolicy?.iceEnabled() + avpf.value = proxyConfig.avpfEnabled() + avpfRrInterval.value = proxyConfig.avpfRrInterval + expires.value = proxyConfig.expires + dialPrefix.value = proxyConfig.dialPrefix + escapePlus.value = proxyConfig.dialEscapePlus + } + + private fun initTransportList() { + val labels = arrayListOf() + + labels.add(prefs.getString(R.string.account_settings_transport_udp)) + labels.add(prefs.getString(R.string.account_settings_transport_tcp)) + labels.add(prefs.getString(R.string.account_settings_transport_tls)) + + transportLabels.value = labels + transportIndex.value = labels.indexOf(proxyConfig.transport.toUpperCase(Locale.getDefault())) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AdvancedSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AdvancedSettingsViewModel.kt new file mode 100644 index 000000000..add780572 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AdvancedSettingsViewModel.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.viewmodels + +import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.settings.SettingListenerStub +import org.linphone.utils.Event + +class AdvancedSettingsViewModel : GenericSettingsViewModel() { + val debugModeListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + prefs.debugLogs = newValue + } + } + val debugMode = MutableLiveData() + + val backgroundModeListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + prefs.keepServiceAlive = newValue + + if (newValue) { + coreContext.notificationsManager.startForeground() + } else { + coreContext.notificationsManager.stopForegroundNotificationIfPossible() + } + } + } + val backgroundMode = MutableLiveData() + + val autoStartListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + prefs.autoStart = newValue + } + } + val autoStart = MutableLiveData() + + val darkModeListener = object : SettingListenerStub() { + override fun onListValueChanged(position: Int) { + darkModeIndex.value = position + val value = darkModeValues[position] + prefs.darkMode = value + setNightModeEvent.value = Event(value) + } + } + val darkModeIndex = MutableLiveData() + val darkModeLabels = MutableLiveData>() + private val darkModeValues = arrayListOf(-1, 0, 1) + val setNightModeEvent = MutableLiveData>() + + val deviceNameListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + prefs.deviceName = newValue + } + } + val deviceName = MutableLiveData() + + val remoteProvisioningUrlListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + if (newValue.isEmpty()) { + core.provisioningUri = null + } else { + core.provisioningUri = newValue + } + } + } + val remoteProvisioningUrl = MutableLiveData() + + val logsServerUrlListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + core.logCollectionUploadServerUrl = newValue + } + } + val logsServerUrl = MutableLiveData() + + val goToAndroidSettingsListener = object : SettingListenerStub() { + override fun onClicked() { + goToAndroidSettingsEvent.value = Event(true) + } + } + val goToAndroidSettingsEvent = MutableLiveData>() + + init { + debugMode.value = prefs.debugLogs + backgroundMode.value = prefs.keepServiceAlive + autoStart.value = prefs.autoStart + + val labels = arrayListOf() + labels.add(prefs.getString(R.string.advanced_settings_dark_mode_label_auto)) + labels.add(prefs.getString(R.string.advanced_settings_dark_mode_label_no)) + labels.add(prefs.getString(R.string.advanced_settings_dark_mode_label_yes)) + darkModeLabels.value = labels + darkModeIndex.value = darkModeValues.indexOf(prefs.darkMode) + + deviceName.value = prefs.deviceName + remoteProvisioningUrl.value = core.provisioningUri + logsServerUrl.value = core.logCollectionUploadServerUrl + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt new file mode 100644 index 000000000..432106d3a --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.viewmodels + +import androidx.databinding.ViewDataBinding +import androidx.lifecycle.MutableLiveData +import org.linphone.R +import org.linphone.activities.main.settings.SettingListenerStub +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub +import org.linphone.core.EcCalibratorStatus +import org.linphone.utils.Event +import org.linphone.utils.PermissionHelper + +class AudioSettingsViewModel : GenericSettingsViewModel() { + val askAudioRecordPermissionForEchoCancellerCalibrationEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val askAudioRecordPermissionForEchoTesterEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val echoCancellationListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + core.enableEchoCancellation(newValue) + } + } + val echoCancellation = MutableLiveData() + val listener = object : CoreListenerStub() { + override fun onEcCalibrationResult(core: Core, status: EcCalibratorStatus, delayMs: Int) { + if (status == EcCalibratorStatus.InProgress) return + echoCancellerCalibrationFinished(status, delayMs) + } + } + + val echoCancellerCalibrationListener = object : SettingListenerStub() { + override fun onClicked() { + if (PermissionHelper.get().hasRecordAudioPermission()) { + startEchoCancellerCalibration() + } else { + askAudioRecordPermissionForEchoCancellerCalibrationEvent.value = Event(true) + } + } + } + val echoCalibration = MutableLiveData() + + val echoTesterListener = object : SettingListenerStub() { + override fun onClicked() { + if (PermissionHelper.get().hasRecordAudioPermission()) { + if (echoTesterIsRunning) { + stopEchoTester() + } else { + startEchoTester() + } + } else { + askAudioRecordPermissionForEchoTesterEvent.value = Event(true) + } + } + } + private var echoTesterIsRunning = false + val echoTesterStatus = MutableLiveData() + + val adaptiveRateControlListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + core.enableAdaptiveRateControl(newValue) + } + } + val adaptiveRateControl = MutableLiveData() + + val codecBitrateListener = object : SettingListenerStub() { + override fun onListValueChanged(position: Int) { + for (payloadType in core.audioPayloadTypes) { + if (payloadType.isVbr) { + payloadType.normalBitrate = codecBitrateValues[position] + } + } + } + } + val codecBitrateIndex = MutableLiveData() + val codecBitrateLabels = MutableLiveData>() + private val codecBitrateValues = arrayListOf(10, 15, 20, 36, 64, 128) + + val microphoneGainListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + core.micGainDb = newValue.toFloat() + } + } + val microphoneGain = MutableLiveData() + + val playbackGainListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + core.playbackGainDb = newValue.toFloat() + } + } + val playbackGain = MutableLiveData() + + val audioCodecs = MutableLiveData>() + + init { + echoCancellation.value = core.echoCancellationEnabled() + adaptiveRateControl.value = core.adaptiveRateControlEnabled() + echoCalibration.value = if (core.echoCancellationEnabled()) { + prefs.getString(R.string.audio_settings_echo_cancellation_calibration_value).format(prefs.echoCancellerCalibration) + } else { + prefs.getString(R.string.audio_settings_echo_canceller_calibration_summary) + } + echoTesterStatus.value = prefs.getString(R.string.audio_settings_echo_tester_summary) + initCodecBitrateList() + microphoneGain.value = core.micGainDb + playbackGain.value = core.playbackGainDb + } + + fun startEchoCancellerCalibration() { + if (echoTesterIsRunning) { + stopEchoTester() + } + + core.addListener(listener) + core.startEchoCancellerCalibration() + echoCalibration.value = prefs.getString(R.string.audio_settings_echo_cancellation_calibration_started) + } + + fun echoCancellerCalibrationFinished(status: EcCalibratorStatus, delay: Int) { + core.removeListener(listener) + + when (status) { + EcCalibratorStatus.DoneNoEcho -> { + echoCalibration.value = prefs.getString(R.string.audio_settings_echo_cancellation_calibration_no_echo) + } + EcCalibratorStatus.Done -> { + echoCalibration.value = prefs.getString(R.string.audio_settings_echo_cancellation_calibration_value).format(delay) + } + EcCalibratorStatus.Failed -> { + echoCalibration.value = prefs.getString(R.string.audio_settings_echo_cancellation_calibration_failed) + } + } + + echoCancellation.value = status != EcCalibratorStatus.DoneNoEcho + } + + fun startEchoTester() { + echoTesterIsRunning = true + echoTesterStatus.value = prefs.getString(R.string.audio_settings_echo_tester_summary_is_running) + core.startEchoTester(0) + } + + fun stopEchoTester() { + echoTesterIsRunning = false + echoTesterStatus.value = prefs.getString(R.string.audio_settings_echo_tester_summary_is_stopped) + core.stopEchoTester() + } + + private fun initCodecBitrateList() { + val labels = arrayListOf() + for (value in codecBitrateValues) { + labels.add("$value kbits/s") + } + codecBitrateLabels.value = labels + + var currentValue = 36 + for (payloadType in core.audioPayloadTypes) { + if (payloadType.isVbr && payloadType.normalBitrate in codecBitrateValues) { + currentValue = payloadType.normalBitrate + break + } + } + codecBitrateIndex.value = codecBitrateValues.indexOf(currentValue) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt new file mode 100644 index 000000000..da681179f --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.viewmodels + +import androidx.lifecycle.MutableLiveData +import org.linphone.R +import org.linphone.activities.main.settings.SettingListenerStub +import org.linphone.core.MediaEncryption +import org.linphone.mediastream.Version +import org.linphone.utils.Event + +class CallSettingsViewModel : GenericSettingsViewModel() { + val deviceRingtoneListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + core.ring = if (newValue) null else prefs.ringtonePath + } + } + val deviceRingtone = MutableLiveData() + + val vibrateOnIncomingCallListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + prefs.vibrateWhileIncomingCall = newValue + } + } + val vibrateOnIncomingCall = MutableLiveData() + + val encryptionListener = object : SettingListenerStub() { + override fun onListValueChanged(position: Int) { + core.mediaEncryption = MediaEncryption.fromInt(encryptionValues[position]) + encryptionIndex.value = position + } + } + val encryptionIndex = MutableLiveData() + val encryptionLabels = MutableLiveData>() + private val encryptionValues = arrayListOf() + + val encryptionMandatoryListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + core.isMediaEncryptionMandatory = newValue + } + } + val encryptionMandatory = MutableLiveData() + + val overlayListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { + if (newValue) overlayEnabledEvent.value = Event(true) + } + prefs.showCallOverlay = newValue + } + } + val overlay = MutableLiveData() + val overlayEnabledEvent = MutableLiveData>() + + val sipInfoDtmfListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + core.useInfoForDtmf = newValue + } + } + val sipInfoDtmf = MutableLiveData() + + val rfc2833DtmfListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + core.useRfc2833ForDtmf = newValue + } + } + val rfc2833Dtmf = MutableLiveData() + + val autoAnswerListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + prefs.autoAnswerEnabled = newValue + } + } + val autoAnswer = MutableLiveData() + + val autoAnswerDelayListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + prefs.autoAnswerDelay = newValue.toInt() + } + } + val autoAnswerDelay = MutableLiveData() + + val incomingTimeoutListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + core.incTimeout = newValue.toInt() + } + } + val incomingTimeout = MutableLiveData() + + val voiceMailUriListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + prefs.voiceMailUri = newValue + } + } + val voiceMailUri = MutableLiveData() + + init { + deviceRingtone.value = core.ring == null + vibrateOnIncomingCall.value = prefs.vibrateWhileIncomingCall + + initEncryptionList() + encryptionMandatory.value = core.isMediaEncryptionMandatory + + overlay.value = prefs.showCallOverlay + sipInfoDtmf.value = core.useInfoForDtmf + rfc2833Dtmf.value = core.useRfc2833ForDtmf + autoAnswer.value = prefs.autoAnswerEnabled + autoAnswerDelay.value = prefs.autoAnswerDelay + incomingTimeout.value = core.incTimeout + voiceMailUri.value = prefs.voiceMailUri + } + + private fun initEncryptionList() { + val labels = arrayListOf() + + labels.add(prefs.getString(R.string.call_settings_media_encryption_none)) + encryptionValues.add(MediaEncryption.None.toInt()) + + if (core.mediaEncryptionSupported(MediaEncryption.SRTP)) { + labels.add(prefs.getString(R.string.call_settings_media_encryption_srtp)) + encryptionValues.add(MediaEncryption.SRTP.toInt()) + } + if (core.mediaEncryptionSupported(MediaEncryption.ZRTP)) { + labels.add(prefs.getString(R.string.call_settings_media_encryption_zrtp)) + encryptionValues.add(MediaEncryption.ZRTP.toInt()) + } + if (core.mediaEncryptionSupported(MediaEncryption.DTLS)) { + labels.add(prefs.getString(R.string.call_settings_media_encryption_dtls)) + encryptionValues.add(MediaEncryption.DTLS.toInt()) + } + + encryptionLabels.value = labels + encryptionIndex.value = encryptionValues.indexOf(core.mediaEncryption.toInt()) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt new file mode 100644 index 000000000..6dcb5ef42 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.viewmodels + +import androidx.lifecycle.MutableLiveData +import org.linphone.activities.main.settings.SettingListenerStub + +class ChatSettingsViewModel : GenericSettingsViewModel() { + val fileSharingUrlListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + core.logCollectionUploadServerUrl = newValue + } + } + val fileSharingUrl = MutableLiveData() + + val downloadedImagesPublicListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + prefs.makePublicDownloadedImages = newValue + } + } + val downloadedImagesPublic = MutableLiveData() + + val hideEmptyRoomsListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + prefs.hideEmptyRooms = newValue + } + } + val hideEmptyRooms = MutableLiveData() + + val hideRoomsRemovedProxiesListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + prefs.hideRoomsFromRemovedProxies = newValue + } + } + val hideRoomsRemovedProxies = MutableLiveData() + + init { + downloadedImagesPublic.value = prefs.makePublicDownloadedImages + hideEmptyRooms.value = prefs.hideEmptyRooms + hideRoomsRemovedProxies.value = prefs.hideRoomsFromRemovedProxies + fileSharingUrl.value = core.fileTransferServer + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt new file mode 100644 index 000000000..38c271c6c --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.viewmodels + +import androidx.lifecycle.MutableLiveData +import org.linphone.activities.main.settings.SettingListenerStub +import org.linphone.utils.Event + +class ContactsSettingsViewModel : GenericSettingsViewModel() { + val friendListSubscribeListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + core.enableFriendListSubscription(newValue) + } + } + val friendListSubscribe = MutableLiveData() + + val nativePresenceListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + prefs.storePresenceInNativeContact = newValue + } + } + val nativePresence = MutableLiveData() + + val showOrganizationListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + prefs.displayOrganization = newValue + } + } + val showOrganization = MutableLiveData() + + val launcherShortcutsListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + prefs.contactsShortcuts = newValue + launcherShortcutsEvent.value = Event(newValue) + } + } + val launcherShortcuts = MutableLiveData() + val launcherShortcutsEvent = MutableLiveData>() + + init { + friendListSubscribe.value = core.isFriendListSubscriptionEnabled + nativePresence.value = prefs.storePresenceInNativeContact + showOrganization.value = prefs.displayOrganization + launcherShortcuts.value = prefs.contactsShortcuts + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/GenericSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/GenericSettingsViewModel.kt new file mode 100644 index 000000000..1313bbd40 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/GenericSettingsViewModel.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.viewmodels + +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences + +abstract class GenericSettingsViewModel : ViewModel() { + protected val prefs = corePreferences + protected val core = coreContext.core +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt new file mode 100644 index 000000000..24f8c191d --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.viewmodels + +import androidx.lifecycle.MutableLiveData +import org.linphone.activities.main.settings.SettingListenerStub + +class NetworkSettingsViewModel : GenericSettingsViewModel() { + val wifiOnlyListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + core.enableWifiOnly(newValue) + } + } + val wifiOnly = MutableLiveData() + + val allowIpv6Listener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + core.enableIpv6(newValue) + } + } + val allowIpv6 = MutableLiveData() + + val pushNotificationsListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + core.isPushNotificationEnabled = newValue + } + } + val pushNotifications = MutableLiveData() + + val randomPortsListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + val port = if (newValue) -1 else 5060 + setSipPort(port) + sipPort.value = port + } + } + val randomPorts = MutableLiveData() + + val sipPortListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + val port = newValue.toInt() + setSipPort(port) + } + } + val sipPort = MutableLiveData() + + init { + wifiOnly.value = core.wifiOnlyEnabled() + allowIpv6.value = core.ipv6Enabled() + pushNotifications.value = core.isPushNotificationEnabled + randomPorts.value = getSipPort() == -1 + sipPort.value = getSipPort() + } + + private fun setSipPort(port: Int) { + val transports = core.transports + transports.udpPort = port + transports.tcpPort = port + transports.tlsPort = -1 + core.transports = transports + } + + private fun getSipPort(): Int { + val transports = core.transports + if (transports.udpPort > 0) return transports.udpPort + return transports.tcpPort + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/SettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/SettingsViewModel.kt new file mode 100644 index 000000000..7a92fd673 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/SettingsViewModel.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.activities.main.settings.SettingListenerStub + +class SettingsViewModel : ViewModel() { + val tunnelAvailable: Boolean = coreContext.core.tunnelAvailable() + + val accounts = MutableLiveData>() + + private var accountClickListener = object : SettingListenerStub() { + override fun onAccountClicked(identity: String) { + accountsSettingsListener.onAccountClicked(identity) + } + } + + lateinit var accountsSettingsListener: SettingListenerStub + + lateinit var tunnelSettingsListener: SettingListenerStub + + lateinit var audioSettingsListener: SettingListenerStub + + lateinit var videoSettingsListener: SettingListenerStub + + lateinit var callSettingsListener: SettingListenerStub + + lateinit var chatSettingsListener: SettingListenerStub + + lateinit var networkSettingsListener: SettingListenerStub + + lateinit var contactsSettingsListener: SettingListenerStub + + lateinit var advancedSettingsListener: SettingListenerStub + + init { + updateAccountsList() + } + + fun updateAccountsList() { + val list = arrayListOf() + if (coreContext.core.proxyConfigList.isNotEmpty()) { + for (proxy in coreContext.core.proxyConfigList) { + val viewModel = AccountSettingsViewModel(proxy) + viewModel.accountsSettingsListener = accountClickListener + list.add(viewModel) + } + } + accounts.value = list + } +} diff --git a/app/src/main/java/org/linphone/dialer/views/AddressAware.java b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/TunnelSettingsViewModel.kt similarity index 79% rename from app/src/main/java/org/linphone/dialer/views/AddressAware.java rename to app/src/main/java/org/linphone/activities/main/settings/viewmodels/TunnelSettingsViewModel.kt index 61aa2a63e..ef4688fb1 100644 --- a/app/src/main/java/org/linphone/dialer/views/AddressAware.java +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/TunnelSettingsViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2019 Belledonne Communications SARL. + * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,8 +17,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.dialer.views; +package org.linphone.activities.main.settings.viewmodels -public interface AddressAware { - void setAddressWidget(AddressText address); +class TunnelSettingsViewModel : GenericSettingsViewModel() { + // TODO } diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt new file mode 100644 index 000000000..3ef60ef6a --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.settings.viewmodels + +import androidx.databinding.ViewDataBinding +import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.settings.SettingListenerStub +import org.linphone.core.Factory + +class VideoSettingsViewModel : GenericSettingsViewModel() { + val enableVideoListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + core.enableVideoCapture(newValue) + core.enableVideoDisplay(newValue) + } + } + val enableVideo = MutableLiveData() + + val tabletPreviewListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + prefs.videoPreview = newValue + } + } + val tabletPreview = MutableLiveData() + val isTablet = MutableLiveData() + + val initiateCallListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + val policy = core.videoActivationPolicy + policy.automaticallyInitiate = newValue + core.videoActivationPolicy = policy + } + } + val initiateCall = MutableLiveData() + + val autoAcceptListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + val policy = core.videoActivationPolicy + policy.automaticallyAccept = newValue + core.videoActivationPolicy = policy + } + } + val autoAccept = MutableLiveData() + + val cameraDeviceListener = object : SettingListenerStub() { + override fun onListValueChanged(position: Int) { + core.videoDevice = cameraDeviceLabels.value.orEmpty()[position] + } + } + val cameraDeviceIndex = MutableLiveData() + val cameraDeviceLabels = MutableLiveData>() + + val videoSizeListener = object : SettingListenerStub() { + override fun onListValueChanged(position: Int) { + core.preferredVideoDefinition = Factory.instance().createVideoDefinitionFromName(videoSizeLabels.value.orEmpty()[position]) + } + } + val videoSizeIndex = MutableLiveData() + val videoSizeLabels = MutableLiveData>() + + val videoPresetListener = object : SettingListenerStub() { + override fun onListValueChanged(position: Int) { + videoPresetIndex.value = position // Needed to display/hide two below settings + core.videoPreset = videoPresetLabels.value.orEmpty()[position] + } + } + val videoPresetIndex = MutableLiveData() + val videoPresetLabels = MutableLiveData>() + + val preferredFpsListener = object : SettingListenerStub() { + override fun onListValueChanged(position: Int) { + core.preferredFramerate = preferredFpsLabels.value.orEmpty()[position].toFloat() + } + } + val preferredFpsIndex = MutableLiveData() + val preferredFpsLabels = MutableLiveData>() + + val bandwidthLimitListener = object : SettingListenerStub() { + override fun onTextValueChanged(newValue: String) { + core.downloadBandwidth = newValue.toInt() + core.uploadBandwidth = newValue.toInt() + } + } + val bandwidthLimit = MutableLiveData() + + val videoCodecs = MutableLiveData>() + + init { + enableVideo.value = core.videoEnabled() && core.videoSupported() + tabletPreview.value = prefs.videoPreview + isTablet.value = coreContext.context.resources.getBoolean(R.bool.isTablet) + initiateCall.value = core.videoActivationPolicy.automaticallyInitiate + autoAccept.value = core.videoActivationPolicy.automaticallyAccept + + initCameraDevicesList() + initVideoSizeList() + initVideoPresetList() + initFpsList() + + bandwidthLimit.value = core.downloadBandwidth + } + + private fun initCameraDevicesList() { + val labels = arrayListOf() + for (camera in core.videoDevicesList) { + labels.add(camera) + } + + cameraDeviceLabels.value = labels + cameraDeviceIndex.value = labels.indexOf(core.videoDevice) + } + + private fun initVideoSizeList() { + val labels = arrayListOf() + + for (size in Factory.instance().supportedVideoDefinitions) { + labels.add(size.name) + } + + videoSizeLabels.value = labels + videoSizeIndex.value = labels.indexOf(core.preferredVideoDefinition.name) + } + + private fun initVideoPresetList() { + val labels = arrayListOf() + + labels.add("default") + labels.add("high-fps") + labels.add("custom") + + videoPresetLabels.value = labels + videoPresetIndex.value = labels.indexOf(core.videoPreset) + } + + private fun initFpsList() { + val labels = arrayListOf("5", "10", "15", "20", "25", "30") + preferredFpsLabels.value = labels + preferredFpsIndex.value = labels.indexOf(core.preferredFramerate.toInt().toString()) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt b/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt new file mode 100644 index 000000000..c8d7a8254 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.sidemenu.fragments + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.assistant.AssistantActivity +import org.linphone.activities.main.settings.SettingListenerStub +import org.linphone.activities.main.sidemenu.viewmodels.SideMenuViewModel +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.core.tools.Log +import org.linphone.databinding.SideMenuFragmentBinding +import org.linphone.utils.Event + +class SideMenuFragment : Fragment() { + private lateinit var binding: SideMenuFragmentBinding + private lateinit var viewModel: SideMenuViewModel + private lateinit var sharedViewModel: SharedMainViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SideMenuFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.lifecycleOwner = this + + viewModel = ViewModelProvider(this).get(SideMenuViewModel::class.java) + binding.viewModel = viewModel + + sharedViewModel = activity?.run { + ViewModelProvider(this).get(SharedMainViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + sharedViewModel.proxyConfigRemoved.observe(viewLifecycleOwner, Observer { + Log.i("[Side Menu] Proxy config removed, update accounts list") + viewModel.updateAccountsList() + }) + + viewModel.accountsSettingsListener = object : SettingListenerStub() { + override fun onAccountClicked(identity: String) { + val args = Bundle() + args.putString("Identity", identity) + Log.i("[Side Menu] Navigation to settings for proxy with identity: $identity") + + sharedViewModel.toggleDrawerEvent.value = Event(true) + val deepLink = "linphone-android://account-settings/$identity" + findNavController().navigate(Uri.parse(deepLink)) + } + } + + binding.setAssistantClickListener { + sharedViewModel.toggleDrawerEvent.value = Event(true) + startActivity(Intent(context, AssistantActivity::class.java)) + } + + binding.setSettingsClickListener { + sharedViewModel.toggleDrawerEvent.value = Event(true) + findNavController().navigate(R.id.action_global_settingsFragment) + } + + binding.setRecordingsClickListener { + sharedViewModel.toggleDrawerEvent.value = Event(true) + findNavController().navigate(R.id.action_global_recordingsFragment) + } + + binding.setAboutClickListener { + sharedViewModel.toggleDrawerEvent.value = Event(true) + findNavController().navigate(R.id.action_global_aboutFragment) + } + + binding.setQuitClickListener { + val intent = Intent() + intent.setAction(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + try { + startActivity(intent) + } catch (ise: IllegalStateException) { + Log.e("[Side Menu] Can't start home activity: ", ise) + } + viewModel.quit() + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/sidemenu/viewmodels/SideMenuViewModel.kt b/app/src/main/java/org/linphone/activities/main/sidemenu/viewmodels/SideMenuViewModel.kt new file mode 100644 index 000000000..0913eddaa --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/sidemenu/viewmodels/SideMenuViewModel.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.sidemenu.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.activities.main.settings.SettingListenerStub +import org.linphone.activities.main.settings.viewmodels.AccountSettingsViewModel +import org.linphone.core.* +import org.linphone.core.tools.Log + +class SideMenuViewModel : ViewModel() { + val showAssistant: Boolean = true + val showSettings: Boolean = true + val showRecordings: Boolean = true + val showAbout: Boolean = true + + val defaultAccount = MutableLiveData() + val defaultAccountFound = MutableLiveData() + + val accounts = MutableLiveData>() + + lateinit var accountsSettingsListener: SettingListenerStub + + private var accountClickListener = object : SettingListenerStub() { + override fun onAccountClicked(identity: String) { + accountsSettingsListener.onAccountClicked(identity) + } + } + + private val listener: CoreListenerStub = object : CoreListenerStub() { + override fun onRegistrationStateChanged( + core: Core, + cfg: ProxyConfig, + state: RegistrationState, + message: String? + ) { + if (coreContext.core.proxyConfigList.size != accounts.value?.size) { + // Only refresh the list if a proxy has been added or removed + updateAccountsList() + } + } + } + + private val quitListener: CoreListenerStub = object : CoreListenerStub() { + override fun onGlobalStateChanged(core: Core, state: GlobalState, message: String?) { + if (state == GlobalState.Off) { + Log.w("[Side Menu] Core properly terminated, killing process") + android.os.Process.killProcess(android.os.Process.myPid()) + } + } + } + + init { + defaultAccountFound.value = false + coreContext.core.addListener(listener) + updateAccountsList() + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + super.onCleared() + } + + fun quit() { + coreContext.core.addListener(quitListener) + coreContext.stop() + } + + fun updateAccountsList() { + val list = arrayListOf() + if (coreContext.core.proxyConfigList.isNotEmpty()) { + val defaultProxyConfig = coreContext.core.defaultProxyConfig + if (defaultProxyConfig != null) { + val defaultViewModel = AccountSettingsViewModel(defaultProxyConfig) + defaultViewModel.accountsSettingsListener = accountClickListener + defaultAccount.value = defaultViewModel + defaultAccountFound.value = true + } + + for (proxy in coreContext.core.proxyConfigList) { + if (proxy != coreContext.core.defaultProxyConfig) { + val viewModel = AccountSettingsViewModel(proxy) + viewModel.accountsSettingsListener = accountClickListener + list.add(viewModel) + } + } + } + accounts.value = list + } +} diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/DialogViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/DialogViewModel.kt new file mode 100644 index 000000000..ded7e54f8 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/viewmodels/DialogViewModel.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class DialogViewModel(val message: String, val title: String = "") : ViewModel() { + var showDoNotAskAgain: Boolean = false + + var showZrtp: Boolean = false + + var zrtpReadSas: String = "" + + var zrtpListenSas: String = "" + + var showTitle: Boolean = false + + var showIcon: Boolean = false + + var iconResource: Int = 0 + + val doNotAskAgain = MutableLiveData() + + init { + doNotAskAgain.value = false + } + + var showCancel: Boolean = false + private var onCancel: (Boolean) -> Unit = {} + + fun showCancelButton(cancel: (Boolean) -> Unit) { + showCancel = true + onCancel = cancel + } + + fun onCancelClicked() { + onCancel(doNotAskAgain.value == true) + } + + var showDelete: Boolean = false + var deleteLabel: String = "Delete" + private var onDelete: (Boolean) -> Unit = {} + + fun showDeleteButton(delete: (Boolean) -> Unit, label: String) { + showDelete = true + onDelete = delete + deleteLabel = label + } + + fun onDeleteClicked() { + onDelete(doNotAskAgain.value == true) + } + + var showOk: Boolean = false + var okLabel: String = "OK" + private var onOk: (Boolean) -> Unit = {} + + fun showOkButton(ok: (Boolean) -> Unit, label: String) { + showOk = true + onOk = ok + okLabel = label + } + + fun onOkClicked() { + onOk(doNotAskAgain.value == true) + } +} diff --git a/app/src/main/java/org/linphone/dialer/views/AddressType.java b/app/src/main/java/org/linphone/activities/main/viewmodels/ErrorReportingViewModel.kt similarity index 64% rename from app/src/main/java/org/linphone/dialer/views/AddressType.java rename to app/src/main/java/org/linphone/activities/main/viewmodels/ErrorReportingViewModel.kt index fd3088a4a..24c7a3924 100644 --- a/app/src/main/java/org/linphone/dialer/views/AddressType.java +++ b/app/src/main/java/org/linphone/activities/main/viewmodels/ErrorReportingViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2019 Belledonne Communications SARL. + * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,10 +17,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.dialer.views; +package org.linphone.activities.main.viewmodels -public interface AddressType { - CharSequence getText(); +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.utils.Event - String getDisplayedName(); +/* Helper for view models to notify user of an error through a Snackbar */ +abstract class ErrorReportingViewModel : ViewModel() { + val onErrorEvent = MutableLiveData>() } diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/ListTopBarViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/ListTopBarViewModel.kt new file mode 100644 index 000000000..c8980f75d --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/viewmodels/ListTopBarViewModel.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.utils.Event + +/** + * This view model is dedicated to the top bar while in edition mode for item(s) selection in list + */ +class ListTopBarViewModel : ViewModel() { + val isEditionEnabled = MutableLiveData() + + val isSelectionNotEmpty = MutableLiveData() + + val selectAllEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val unSelectAllEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val deleteSelectionEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val selectedItems = MutableLiveData>() + + init { + isEditionEnabled.value = false + isSelectionNotEmpty.value = false + selectedItems.value = arrayListOf() + } + + fun onSelectAll(lastIndex: Int) { + val list = arrayListOf() + list.addAll(0.rangeTo(lastIndex)) + + selectedItems.value = list + isSelectionNotEmpty.value = list.isNotEmpty() + } + + fun onUnSelectAll() { + val list = arrayListOf() + + selectedItems.value = list + isSelectionNotEmpty.value = list.isNotEmpty() + } + + fun onToggleSelect(position: Int) { + val list = arrayListOf() + list.addAll(selectedItems.value.orEmpty()) + + if (list.contains(position)) { + list.remove(position) + } else { + list.add(position) + } + + isSelectionNotEmpty.value = list.isNotEmpty() + selectedItems.value = list + } +} diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/SharedMainViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/SharedMainViewModel.kt new file mode 100644 index 000000000..12bbc6cf3 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/viewmodels/SharedMainViewModel.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.contact.Contact +import org.linphone.core.* +import org.linphone.utils.Event + +class SharedMainViewModel : ViewModel() { + val toggleDrawerEvent = MutableLiveData>() + + /* Call history */ + + val selectedCallLog = MutableLiveData() + + /* Chat */ + + val selectedChatRoom = MutableLiveData() + + val selectedGroupChatRoom = MutableLiveData() + + val filesToShare = MutableLiveData>() + + val messageToForwardEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + var createEncryptedChatRoom: Boolean = false + + /* Contacts */ + + val selectedContact = MutableLiveData() + + /* Call */ + + var pendingCallTransfer: Boolean = false + + /* Accounts */ + + val proxyConfigRemoved = MutableLiveData() +} diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/StatusViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/StatusViewModel.kt new file mode 100644 index 000000000..35eec6b38 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/viewmodels/StatusViewModel.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import java.util.* +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.core.* +import org.linphone.core.tools.Log + +open class StatusViewModel : ViewModel() { + val registrationStatusText = MutableLiveData() + + val registrationStatusDrawable = MutableLiveData() + + val voiceMailCount = MutableLiveData() + + private val listener: CoreListenerStub = object : CoreListenerStub() { + override fun onRegistrationStateChanged( + core: Core, + proxyConfig: ProxyConfig, + state: RegistrationState, + message: String + ) { + updateDefaultProxyConfigRegistrationStatus(state) + } + + override fun onNotifyReceived( + core: Core, + event: Event, + notifiedEvent: String, + body: Content + ) { + if (body.type == "application" && body.subtype == "simple-message-summary" && body.size > 0) { + val data = body.stringBuffer.toLowerCase(Locale.getDefault()) + val voiceMail = data.split("voice-message: ") + if (voiceMail.size >= 2) { + val toParse = voiceMail[1].split("/", limit = 0) + try { + val unreadCount: Int = toParse[0].toInt() + voiceMailCount.value = unreadCount + } catch (nfe: NumberFormatException) { + Log.e("[Status Fragment] $nfe") + } + } + } + } + } + + init { + val core = coreContext.core + core.addListener(listener) + + var state: RegistrationState = RegistrationState.None + if (core.defaultProxyConfig != null) { + state = core.defaultProxyConfig.state + } + updateDefaultProxyConfigRegistrationStatus(state) + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + super.onCleared() + } + + fun refreshRegister() { + coreContext.core.refreshRegisters() + } + + fun updateDefaultProxyConfigRegistrationStatus(state: RegistrationState) { + registrationStatusText.value = getStatusIconText(state) + registrationStatusDrawable.value = getStatusIconResource(state) + } + + private fun getStatusIconText(state: RegistrationState): Int { + return when (state) { + RegistrationState.Ok -> R.string.status_connected + RegistrationState.Progress -> R.string.status_in_progress + RegistrationState.Failed -> R.string.status_error + else -> R.string.status_not_connected + } + } + + private fun getStatusIconResource(state: RegistrationState): Int { + return when (state) { + RegistrationState.Ok -> R.drawable.led_connected + RegistrationState.Progress -> R.drawable.led_inprogress + RegistrationState.Failed -> R.drawable.led_error + else -> R.drawable.led_disconnected + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/TabsViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/TabsViewModel.kt new file mode 100644 index 000000000..30d567687 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/viewmodels/TabsViewModel.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.activities.main.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.* + +class TabsViewModel : ViewModel() { + val unreadMessagesCount = MutableLiveData() + val missedCallsCount = MutableLiveData() + + private val listener: CoreListenerStub = object : CoreListenerStub() { + override fun onCallStateChanged( + core: Core, + call: Call, + state: Call.State, + message: String + ) { + if (state == Call.State.End || state == Call.State.Error) { + updateMissedCallCount() + } + } + + override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) { + updateUnreadChatCount() + } + + override fun onMessageReceived(core: Core, chatRoom: ChatRoom, message: ChatMessage) { + updateUnreadChatCount() + } + + override fun onChatRoomStateChanged(core: Core, chatRoom: ChatRoom, state: ChatRoom.State) { + if (state == ChatRoom.State.Deleted) { + updateUnreadChatCount() + } + } + } + + init { + coreContext.core.addListener(listener) + + updateUnreadChatCount() + updateMissedCallCount() + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + super.onCleared() + } + + fun updateMissedCallCount() { + missedCallsCount.value = coreContext.core.missedCallsCount + } + + fun updateUnreadChatCount() { + unreadMessagesCount.value = coreContext.core.unreadChatMessageCountFromActiveLocals + } +} diff --git a/app/src/main/java/org/linphone/assistant/AccountConnectionAssistantActivity.java b/app/src/main/java/org/linphone/assistant/AccountConnectionAssistantActivity.java deleted file mode 100644 index 868357231..000000000 --- a/app/src/main/java/org/linphone/assistant/AccountConnectionAssistantActivity.java +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.assistant; - -import android.content.Intent; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.View; -import android.widget.CompoundButton; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.Switch; -import android.widget.TextView; -import androidx.annotation.Nullable; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.AccountCreator; -import org.linphone.core.AccountCreatorListenerStub; -import org.linphone.core.Core; -import org.linphone.core.DialPlan; -import org.linphone.core.tools.Log; - -public class AccountConnectionAssistantActivity extends AssistantActivity { - private RelativeLayout mPhoneNumberConnection, mUsernameConnection; - private Switch mUsernameConnectionSwitch; - private EditText mPrefix, mPhoneNumber, mUsername, mPassword; - private TextView mCountryPicker, mError, mConnect; - - private AccountCreatorListenerStub mListener; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.assistant_account_connection); - - mPhoneNumberConnection = findViewById(R.id.phone_number_form); - - mUsernameConnection = findViewById(R.id.username_form); - - mUsernameConnectionSwitch = findViewById(R.id.username_login); - mUsernameConnectionSwitch.setOnCheckedChangeListener( - new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - mPhoneNumberConnection.setVisibility(isChecked ? View.GONE : View.VISIBLE); - mUsernameConnection.setVisibility(isChecked ? View.VISIBLE : View.GONE); - } - }); - - mConnect = findViewById(R.id.assistant_login); - mConnect.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - AccountCreator accountCreator = getAccountCreator(); - accountCreator.reset(); - mConnect.setEnabled(false); - - if (mUsernameConnectionSwitch.isChecked()) { - accountCreator.setUsername(mUsername.getText().toString()); - accountCreator.setPassword(mPassword.getText().toString()); - - createProxyConfigAndLeaveAssistant(); - } else { - accountCreator.setPhoneNumber( - mPhoneNumber.getText().toString(), - mPrefix.getText().toString()); - accountCreator.setUsername(accountCreator.getPhoneNumber()); - - AccountCreator.Status status = accountCreator.recoverAccount(); - if (status != AccountCreator.Status.RequestOk) { - Log.e( - "[Account Connection Assistant] recoverAccount returned " - + status); - mConnect.setEnabled(true); - showGenericErrorDialog(status); - } - } - } - }); - mConnect.setEnabled(false); - - if (getResources().getBoolean(R.bool.use_phone_number_validation)) { - if (getResources().getBoolean(R.bool.isTablet)) { - mUsernameConnectionSwitch.setChecked(true); - } else { - mUsernameConnection.setVisibility(View.GONE); - } - } else { - mUsernameConnectionSwitch.setChecked(true); - mPhoneNumberConnection.setVisibility(View.GONE); - findViewById(R.id.username_switch_layout).setVisibility(View.GONE); - } - - mCountryPicker = findViewById(R.id.select_country); - mCountryPicker.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - showCountryPickerDialog(); - } - }); - - mError = findViewById(R.id.phone_number_error); - - mPrefix = findViewById(R.id.dial_code); - mPrefix.setText("+"); - mPrefix.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - String prefix = s.toString(); - if (prefix.startsWith("+")) { - prefix = prefix.substring(1); - } - DialPlan dp = getDialPlanFromPrefix(prefix); - if (dp != null) { - mCountryPicker.setText(dp.getCountry()); - } - - updateConnectButtonAndDisplayError(); - } - }); - - mPhoneNumber = findViewById(R.id.phone_number); - mPhoneNumber.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - updateConnectButtonAndDisplayError(); - } - }); - - ImageView phoneNumberInfos = findViewById(R.id.info_phone_number); - phoneNumberInfos.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - showPhoneNumberDialog(); - } - }); - - mUsername = findViewById(R.id.assistant_username); - mUsername.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - mConnect.setEnabled(s.length() > 0 && mPassword.getText().length() > 0); - } - }); - - mPassword = findViewById(R.id.assistant_password); - mPassword.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - mConnect.setEnabled(s.length() > 0 && mUsername.getText().length() > 0); - } - }); - - mListener = - new AccountCreatorListenerStub() { - @Override - public void onRecoverAccount( - AccountCreator creator, AccountCreator.Status status, String resp) { - Log.i( - "[Account Connection Assistant] onRecoverAccount status is " - + status); - if (status.equals(AccountCreator.Status.RequestOk)) { - Intent intent = - new Intent( - AccountConnectionAssistantActivity.this, - PhoneAccountValidationAssistantActivity.class); - intent.putExtra("isLoginVerification", true); - startActivity(intent); - } else { - mConnect.setEnabled(true); - showGenericErrorDialog(status); - } - } - }; - } - - @Override - protected void onResume() { - super.onResume(); - - Core core = LinphoneManager.getCore(); - if (core != null) { - reloadLinphoneAccountCreatorConfig(); - } - - getAccountCreator().addListener(mListener); - - DialPlan dp = getDialPlanForCurrentCountry(); - displayDialPlan(dp); - - String phoneNumber = getDevicePhoneNumber(); - if (phoneNumber != null) { - mPhoneNumber.setText(phoneNumber); - } - } - - @Override - protected void onPause() { - super.onPause(); - getAccountCreator().removeListener(mListener); - } - - @Override - public void onCountryClicked(DialPlan dialPlan) { - super.onCountryClicked(dialPlan); - displayDialPlan(dialPlan); - } - - private void updateConnectButtonAndDisplayError() { - if (mPrefix.getText().toString().isEmpty() || mPhoneNumber.getText().toString().isEmpty()) - return; - - int status = arePhoneNumberAndPrefixOk(mPrefix, mPhoneNumber); - if (status == AccountCreator.PhoneNumberStatus.Ok.toInt()) { - mConnect.setEnabled(true); - mError.setText(""); - mError.setVisibility(View.INVISIBLE); - } else { - mConnect.setEnabled(false); - mError.setText(getErrorFromPhoneNumberStatus(status)); - mError.setVisibility(View.VISIBLE); - } - } - - private void displayDialPlan(DialPlan dp) { - if (dp != null) { - mPrefix.setText("+" + dp.getCountryCallingCode()); - mCountryPicker.setText(dp.getCountry()); - } - } -} diff --git a/app/src/main/java/org/linphone/assistant/AssistantActivity.java b/app/src/main/java/org/linphone/assistant/AssistantActivity.java deleted file mode 100644 index 2f1354963..000000000 --- a/app/src/main/java/org/linphone/assistant/AssistantActivity.java +++ /dev/null @@ -1,351 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.assistant; - -import android.app.AlertDialog; -import android.content.Intent; -import android.telephony.TelephonyManager; -import android.view.KeyEvent; -import android.view.View; -import android.view.WindowManager; -import android.widget.EditText; -import android.widget.ImageView; -import java.util.Locale; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.activities.LinphoneGenericActivity; -import org.linphone.core.AccountCreator; -import org.linphone.core.Core; -import org.linphone.core.DialPlan; -import org.linphone.core.Factory; -import org.linphone.core.ProxyConfig; -import org.linphone.core.tools.Log; -import org.linphone.dialer.DialerActivity; -import org.linphone.settings.LinphonePreferences; - -public abstract class AssistantActivity extends LinphoneGenericActivity - implements CountryPicker.CountryPickedListener { - protected ImageView mBack; - private AlertDialog mCountryPickerDialog; - - private CountryPicker mCountryPicker; - - @Override - protected void onResume() { - super.onResume(); - - getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); - - View statusBar = findViewById(R.id.status); - if (getResources().getBoolean(R.bool.assistant_hide_status_bar)) { - statusBar.setVisibility(View.GONE); - } - - View topBar = findViewById(R.id.top_bar); - if (getResources().getBoolean(R.bool.assistant_hide_top_bar)) { - topBar.setVisibility(View.GONE); - } - - mBack = findViewById(R.id.back); - mBack.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - finish(); - } - }); - } - - @Override - protected void onDestroy() { - mBack = null; - mCountryPickerDialog = null; - mCountryPicker = null; - - super.onDestroy(); - } - - @Override - public void onCountryClicked(DialPlan dialPlan) { - if (mCountryPickerDialog != null) { - mCountryPickerDialog.dismiss(); - mCountryPickerDialog = null; - } - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - if (!mBack.isEnabled()) return true; - } - return super.onKeyDown(keyCode, event); - } - - public AccountCreator getAccountCreator() { - return LinphoneManager.getInstance().getAccountCreator(); - } - - private void reloadAccountCreatorConfig(String path) { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.loadConfigFromXml(path); - AccountCreator accountCreator = getAccountCreator(); - accountCreator.reset(); - accountCreator.setLanguage(Locale.getDefault().getLanguage()); - } - } - - void reloadDefaultAccountCreatorConfig() { - Log.i("[Assistant] Reloading configuration with default"); - reloadAccountCreatorConfig(LinphonePreferences.instance().getDefaultDynamicConfigFile()); - } - - void reloadLinphoneAccountCreatorConfig() { - Log.i("[Assistant] Reloading configuration with specifics"); - reloadAccountCreatorConfig(LinphonePreferences.instance().getLinphoneDynamicConfigFile()); - } - - void createProxyConfigAndLeaveAssistant() { - createProxyConfigAndLeaveAssistant(false); - } - - void createProxyConfigAndLeaveAssistant(boolean isGenericAccount) { - Core core = LinphoneManager.getCore(); - boolean useLinphoneDefaultValues = - getString(R.string.default_domain).equals(getAccountCreator().getDomain()); - - if (isGenericAccount) { - if (useLinphoneDefaultValues) { - Log.i( - "[Assistant] Default domain found for generic connection, reloading configuration"); - core.loadConfigFromXml( - LinphonePreferences.instance().getLinphoneDynamicConfigFile()); - } else { - Log.i("[Assistant] Third party domain found, keeping default values"); - } - } - - ProxyConfig proxyConfig = getAccountCreator().createProxyConfig(); - - if (isGenericAccount) { - if (useLinphoneDefaultValues) { - // Restore default values - Log.i("[Assistant] Restoring default assistant configuration"); - core.loadConfigFromXml( - LinphonePreferences.instance().getDefaultDynamicConfigFile()); - } else { - // If this isn't a sip.linphone.org account, disable push notifications and enable - // service notification, otherwise incoming calls won't work (most probably) - if (proxyConfig != null) { - proxyConfig.setPushNotificationAllowed(false); - } - Log.w( - "[Assistant] Unknown domain used, push probably won't work, enable service mode"); - LinphonePreferences.instance().setServiceNotificationVisibility(true); - LinphoneContext.instance().getNotificationManager().startForeground(); - } - } - LinphonePreferences.instance() - .setPushNotificationEnabled(!isGenericAccount || useLinphoneDefaultValues); - - if (proxyConfig == null) { - Log.e("[Assistant] Account creator couldn't create proxy config"); - // TODO: display error message - } else { - if (proxyConfig.getDialPrefix() == null) { - DialPlan dialPlan = getDialPlanForCurrentCountry(); - if (dialPlan != null) { - proxyConfig.setDialPrefix(dialPlan.getCountryCallingCode()); - } - } - - LinphonePreferences.instance().firstLaunchSuccessful(); - goToLinphoneActivity(); - } - } - - void goToLinphoneActivity() { - boolean needsEchoCalibration = - LinphoneManager.getCore().isEchoCancellerCalibrationRequired(); - boolean echoCalibrationDone = - LinphonePreferences.instance().isEchoCancellationCalibrationDone(); - Log.i( - "[Assistant] Echo cancellation calibration required ? " - + needsEchoCalibration - + ", already done ? " - + echoCalibrationDone); - - Intent intent; - if (needsEchoCalibration && !echoCalibrationDone) { - intent = new Intent(this, EchoCancellerCalibrationAssistantActivity.class); - } else { - /*boolean openH264 = LinphonePreferences.instance().isOpenH264CodecDownloadEnabled(); - boolean codecFound = - LinphoneManager.getInstance().getOpenH264DownloadHelper().isCodecFound(); - boolean abiSupported = - Version.getCpuAbis().contains("armeabi-v7a") - && !Version.getCpuAbis().contains("x86"); - boolean androidVersionOk = Version.sdkStrictlyBelow(Build.VERSION_CODES.M); - - if (openH264 && abiSupported && androidVersionOk && !codecFound) { - intent = new Intent(this, OpenH264DownloadAssistantActivity.class); - } else {*/ - intent = new Intent(this, DialerActivity.class); - intent.addFlags( - Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - // } - } - startActivity(intent); - } - - void showPhoneNumberDialog() { - new AlertDialog.Builder(this) - .setTitle(getString(R.string.phone_number_info_title)) - .setMessage( - getString(R.string.phone_number_link_info_content) - + "\n" - + getString( - R.string.phone_number_link_info_content_already_account)) - .show(); - } - - void showAccountAlreadyExistsDialog() { - new AlertDialog.Builder(this) - .setTitle(getString(R.string.account_already_exist)) - .setMessage(getString(R.string.assistant_phone_number_unavailable)) - .show(); - } - - void showGenericErrorDialog(AccountCreator.Status status) { - String message; - - switch (status) { - // TODO handle other possible status - case PhoneNumberInvalid: - message = getString(R.string.phone_number_invalid); - break; - case WrongActivationCode: - message = getString(R.string.activation_code_invalid); - break; - case PhoneNumberOverused: - message = getString(R.string.phone_number_overuse); - break; - case AccountNotExist: - message = getString(R.string.account_doesnt_exist); - break; - default: - message = getString(R.string.error_unknown); - break; - } - - new AlertDialog.Builder(this) - .setTitle(getString(R.string.error)) - .setMessage(message) - .show(); - } - - void showCountryPickerDialog() { - if (mCountryPicker == null) { - mCountryPicker = new CountryPicker(this, this); - } - mCountryPickerDialog = - new AlertDialog.Builder(this).setView(mCountryPicker.getView()).show(); - } - - DialPlan getDialPlanForCurrentCountry() { - try { - TelephonyManager tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); - String countryIso = tm.getNetworkCountryIso(); - return getDialPlanFromCountryCode(countryIso); - } catch (Exception e) { - Log.e("[Assistant] " + e); - } - return null; - } - - String getDevicePhoneNumber() { - try { - TelephonyManager tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); - return tm.getLine1Number(); - } catch (Exception e) { - Log.e("[Assistant] " + e); - } - return null; - } - - DialPlan getDialPlanFromPrefix(String prefix) { - if (prefix == null || prefix.isEmpty()) return null; - - for (DialPlan c : Factory.instance().getDialPlans()) { - if (prefix.equalsIgnoreCase(c.getCountryCallingCode())) return c; - } - return null; - } - - private DialPlan getDialPlanFromCountryCode(String countryCode) { - if (countryCode == null || countryCode.isEmpty()) return null; - - for (DialPlan c : Factory.instance().getDialPlans()) { - if (countryCode.equalsIgnoreCase(c.getIsoCountryCode())) return c; - } - return null; - } - - int arePhoneNumberAndPrefixOk(EditText prefixEditText, EditText phoneNumberEditText) { - String prefix = prefixEditText.getText().toString(); - if (prefix.startsWith("+")) { - prefix = prefix.substring(1); - } - - String phoneNumber = phoneNumberEditText.getText().toString(); - return getAccountCreator().setPhoneNumber(phoneNumber, prefix); - } - - String getErrorFromPhoneNumberStatus(int status) { - AccountCreator.PhoneNumberStatus phoneNumberStatus = - AccountCreator.PhoneNumberStatus.fromInt(status); - switch (phoneNumberStatus) { - case InvalidCountryCode: - return getString(R.string.country_code_invalid); - case TooShort: - return getString(R.string.phone_number_too_short); - case TooLong: - return getString(R.string.phone_number_too_long); - case Invalid: - return getString(R.string.phone_number_invalid); - } - return null; - } - - String getErrorFromUsernameStatus(AccountCreator.UsernameStatus status) { - switch (status) { - case Invalid: - return getString(R.string.username_invalid_size); - case InvalidCharacters: - return getString(R.string.invalid_characters); - case TooLong: - return getString(R.string.username_too_long); - case TooShort: - return getString(R.string.username_too_short); - } - return null; - } -} diff --git a/app/src/main/java/org/linphone/assistant/CountryAdapter.java b/app/src/main/java/org/linphone/assistant/CountryAdapter.java deleted file mode 100644 index 143ce94ae..000000000 --- a/app/src/main/java/org/linphone/assistant/CountryAdapter.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.assistant; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.Filter; -import android.widget.Filterable; -import android.widget.TextView; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.linphone.R; -import org.linphone.core.DialPlan; -import org.linphone.core.Factory; - -class CountryAdapter extends BaseAdapter implements Filterable { - private final Context mContext; - private final LayoutInflater mInflater; - private final DialPlan[] mAllCountries; - private List mFilteredCountries; - - public CountryAdapter(Context context, LayoutInflater inflater) { - mContext = context; - mInflater = inflater; - mAllCountries = Factory.instance().getDialPlans(); - mFilteredCountries = new ArrayList<>(Arrays.asList(mAllCountries)); - } - - @Override - public int getCount() { - return mFilteredCountries.size(); - } - - @Override - public DialPlan getItem(int position) { - return mFilteredCountries.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view; - - if (convertView != null) { - view = convertView; - } else { - view = mInflater.inflate(R.layout.assistant_country_cell, parent, false); - } - - DialPlan c = mFilteredCountries.get(position); - - TextView name = view.findViewById(R.id.country_name); - name.setText(c.getCountry()); - - TextView dial_code = view.findViewById(R.id.country_prefix); - dial_code.setText( - String.format( - mContext.getString(R.string.country_code), c.getCountryCallingCode())); - - view.setTag(c); - return view; - } - - @Override - public Filter getFilter() { - return new Filter() { - @Override - protected FilterResults performFiltering(CharSequence constraint) { - List filteredCountries = new ArrayList<>(); - for (DialPlan c : mAllCountries) { - if (c.getCountry().toLowerCase().contains(constraint) - || c.getCountryCallingCode().contains(constraint)) { - filteredCountries.add(c); - } - } - FilterResults filterResults = new FilterResults(); - filterResults.values = filteredCountries; - return filterResults; - } - - @Override - @SuppressWarnings("unchecked") - protected void publishResults(CharSequence constraint, FilterResults results) { - mFilteredCountries = (List) results.values; - notifyDataSetChanged(); - } - }; - } -} diff --git a/app/src/main/java/org/linphone/assistant/CountryPicker.java b/app/src/main/java/org/linphone/assistant/CountryPicker.java deleted file mode 100644 index 0e2cc8f81..000000000 --- a/app/src/main/java/org/linphone/assistant/CountryPicker.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.assistant; - -import android.content.Context; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.AdapterView; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.ListView; -import org.linphone.R; -import org.linphone.core.DialPlan; - -class CountryPicker { - private final LayoutInflater mInflater; - private final CountryAdapter mAdapter; - private EditText mSearch; - private final CountryPickedListener mListener; - - public CountryPicker(Context context, CountryPickedListener listener) { - mListener = listener; - mInflater = LayoutInflater.from(context); - mAdapter = new CountryAdapter(context, mInflater); - } - - private View createView() { - View view = mInflater.inflate(R.layout.assistant_country_list, null, false); - - ListView list = view.findViewById(R.id.countryList); - list.setAdapter(mAdapter); - list.setOnItemClickListener( - new AdapterView.OnItemClickListener() { - @Override - public void onItemClick( - AdapterView parent, View view, int position, long id) { - DialPlan dp = null; - if (position > 0 && position < mAdapter.getCount()) { - dp = mAdapter.getItem(position); - } - - if (mListener != null) { - mListener.onCountryClicked(dp); - } - } - }); - - mSearch = view.findViewById(R.id.search_country); - mSearch.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - mAdapter.getFilter().filter(s); - } - }); - mSearch.setText(""); - - ImageView clear = view.findViewById(R.id.clear_field); - clear.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - mSearch.setText(""); - } - }); - - return view; - } - - public View getView() { - return createView(); - } - - public interface CountryPickedListener { - void onCountryClicked(DialPlan dialPlan); - } -} diff --git a/app/src/main/java/org/linphone/assistant/EchoCancellerCalibrationAssistantActivity.java b/app/src/main/java/org/linphone/assistant/EchoCancellerCalibrationAssistantActivity.java deleted file mode 100644 index a39dddcb0..000000000 --- a/app/src/main/java/org/linphone/assistant/EchoCancellerCalibrationAssistantActivity.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.assistant; - -import android.Manifest; -import android.content.pm.PackageManager; -import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.core.app.ActivityCompat; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.EcCalibratorStatus; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; - -public class EchoCancellerCalibrationAssistantActivity extends AssistantActivity { - private static final int RECORD_AUDIO_PERMISSION_RESULT = 1; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.assistant_echo_canceller_calibration); - } - - @Override - protected void onStart() { - super.onStart(); - - checkRecordAudioPermissionForEchoCancellerCalibration(); - } - - @Override - protected void onResume() { - super.onResume(); - - LinphonePreferences.instance().setEchoCancellationCalibrationDone(true); - if (isRecordAudioPermissionGranted()) { - startEchoCancellerCalibration(); - } else { - goToLinphoneActivity(); - } - } - - @Override - public void onRequestPermissionsResult( - int requestCode, String[] permissions, final int[] grantResults) { - for (int i = 0; i < permissions.length; i++) { - Log.i( - "[Permission] " - + permissions[i] - + " has been " - + (grantResults[i] == PackageManager.PERMISSION_GRANTED - ? "granted" - : "denied")); - } - - if (requestCode == RECORD_AUDIO_PERMISSION_RESULT) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - startEchoCancellerCalibration(); - } else { - // TODO: permission denied, display something to the user - } - } - } - - private boolean isRecordAudioPermissionGranted() { - int permissionGranted = - getPackageManager() - .checkPermission(Manifest.permission.RECORD_AUDIO, getPackageName()); - Log.i( - "[Permission] Manifest.permission.RECORD_AUDIO is " - + (permissionGranted == PackageManager.PERMISSION_GRANTED - ? "granted" - : "denied")); - - return permissionGranted == PackageManager.PERMISSION_GRANTED; - } - - private void checkRecordAudioPermissionForEchoCancellerCalibration() { - if (!isRecordAudioPermissionGranted()) { - Log.i("[Permission] Asking for " + Manifest.permission.RECORD_AUDIO); - ActivityCompat.requestPermissions( - this, - new String[] {Manifest.permission.RECORD_AUDIO}, - RECORD_AUDIO_PERMISSION_RESULT); - } - } - - private void startEchoCancellerCalibration() { - LinphoneManager.getCore() - .addListener( - new CoreListenerStub() { - @Override - public void onEcCalibrationResult( - Core core, EcCalibratorStatus status, int delayMs) { - if (status == EcCalibratorStatus.InProgress) return; - core.removeListener(this); - goToLinphoneActivity(); - } - }); - LinphoneManager.getCore().startEchoCancellerCalibration(); - } -} diff --git a/app/src/main/java/org/linphone/assistant/EmailAccountCreationAssistantActivity.java b/app/src/main/java/org/linphone/assistant/EmailAccountCreationAssistantActivity.java deleted file mode 100644 index 91f5b4649..000000000 --- a/app/src/main/java/org/linphone/assistant/EmailAccountCreationAssistantActivity.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.assistant; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Intent; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.Patterns; -import android.view.View; -import android.widget.EditText; -import android.widget.TextView; -import androidx.annotation.Nullable; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.AccountCreator; -import org.linphone.core.AccountCreatorListenerStub; -import org.linphone.core.Core; -import org.linphone.core.tools.Log; - -public class EmailAccountCreationAssistantActivity extends AssistantActivity { - private EditText mUsername, mPassword, mPasswordConfirm, mEmail; - private TextView mCreate, mUsernameError, mPasswordError, mPasswordConfirmError, mEmailError; - - private AccountCreatorListenerStub mListener; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.assistant_email_account_creation); - - mUsernameError = findViewById(R.id.username_error); - mPasswordError = findViewById(R.id.password_error); - mPasswordConfirmError = findViewById(R.id.confirm_password_error); - mEmailError = findViewById(R.id.email_error); - - mUsername = findViewById(R.id.assistant_username); - mUsername.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - AccountCreator.UsernameStatus status = - getAccountCreator().setUsername(s.toString()); - mUsernameError.setVisibility( - status == AccountCreator.UsernameStatus.Ok - ? View.INVISIBLE - : View.VISIBLE); - mUsernameError.setText(getErrorFromUsernameStatus(status)); - updateCreateButton(); - } - }); - - mPassword = findViewById(R.id.assistant_password); - mPassword.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - AccountCreator.PasswordStatus status = - getAccountCreator().setPassword(s.toString()); - mPasswordError.setVisibility( - status == AccountCreator.PasswordStatus.Ok - ? View.INVISIBLE - : View.VISIBLE); - - mPasswordConfirmError.setVisibility( - s.toString().equals(mPasswordConfirm.getText().toString()) - ? View.INVISIBLE - : View.VISIBLE); - - switch (status) { - case InvalidCharacters: - mPasswordError.setText(getString(R.string.invalid_characters)); - break; - case TooLong: - mPasswordError.setText(getString(R.string.password_too_long)); - break; - case TooShort: - mPasswordError.setText(getString(R.string.password_too_short)); - break; - } - updateCreateButton(); - } - }); - - mPasswordConfirm = findViewById(R.id.assistant_password_confirmation); - mPasswordConfirm.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - mPasswordConfirmError.setVisibility( - s.toString().equals(mPassword.getText().toString()) - ? View.INVISIBLE - : View.VISIBLE); - updateCreateButton(); - } - }); - - mEmail = findViewById(R.id.assistant_email); - mEmail.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - AccountCreator.EmailStatus status = - getAccountCreator().setEmail(s.toString()); - mEmailError.setVisibility( - status == AccountCreator.EmailStatus.Ok - ? View.INVISIBLE - : View.VISIBLE); - updateCreateButton(); - } - }); - - mCreate = findViewById(R.id.assistant_create); - mCreate.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - enableButtonsAndFields(false); - - AccountCreator.Status status = getAccountCreator().isAccountExist(); - if (status != AccountCreator.Status.RequestOk) { - enableButtonsAndFields(true); - Log.e( - "[Email Account Creation Assistant] isAccountExists returned " - + status); - showGenericErrorDialog(status); - } - } - }); - mCreate.setEnabled(false); - - mListener = - new AccountCreatorListenerStub() { - public void onIsAccountExist( - AccountCreator creator, AccountCreator.Status status, String resp) { - Log.i( - "[Email Account Creation Assistant] onIsAccountExist status is " - + status); - if (status.equals(AccountCreator.Status.AccountExist) - || status.equals(AccountCreator.Status.AccountExistWithAlias)) { - showAccountAlreadyExistsDialog(); - enableButtonsAndFields(true); - } else if (status.equals(AccountCreator.Status.AccountNotExist)) { - status = getAccountCreator().createAccount(); - if (status != AccountCreator.Status.RequestOk) { - Log.e( - "[Email Account Creation Assistant] createAccount returned " - + status); - enableButtonsAndFields(true); - showGenericErrorDialog(status); - } - } else { - enableButtonsAndFields(true); - showGenericErrorDialog(status); - } - } - - @Override - public void onCreateAccount( - AccountCreator creator, AccountCreator.Status status, String resp) { - Log.i( - "[Email Account Creation Assistant] onCreateAccount status is " - + status); - if (status.equals(AccountCreator.Status.AccountCreated)) { - startActivity( - new Intent( - EmailAccountCreationAssistantActivity.this, - EmailAccountValidationAssistantActivity.class)); - } else { - enableButtonsAndFields(true); - showGenericErrorDialog(status); - } - } - }; - } - - private void enableButtonsAndFields(boolean enable) { - mUsername.setEnabled(enable); - mPassword.setEnabled(enable); - mPasswordConfirm.setEnabled(enable); - mEmail.setEnabled(enable); - mCreate.setEnabled(enable); - } - - private void updateCreateButton() { - mCreate.setEnabled( - mUsername.getText().length() > 0 - && mPassword.getText().toString().length() > 0 - && mEmail.getText().toString().length() > 0 - && mEmailError.getVisibility() == View.INVISIBLE - && mUsernameError.getVisibility() == View.INVISIBLE - && mPasswordError.getVisibility() == View.INVISIBLE - && mPasswordConfirmError.getVisibility() == View.INVISIBLE); - } - - @Override - protected void onResume() { - super.onResume(); - - Core core = LinphoneManager.getCore(); - if (core != null) { - reloadLinphoneAccountCreatorConfig(); - } - - getAccountCreator().addListener(mListener); - - if (getResources().getBoolean(R.bool.pre_fill_email_in_assistant)) { - Account[] accounts = AccountManager.get(this).getAccountsByType("com.google"); - for (Account account : accounts) { - if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { - String possibleEmail = account.name; - mEmail.setText(possibleEmail); - break; - } - } - } - } - - @Override - protected void onPause() { - super.onPause(); - getAccountCreator().removeListener(mListener); - } -} diff --git a/app/src/main/java/org/linphone/assistant/EmailAccountValidationAssistantActivity.java b/app/src/main/java/org/linphone/assistant/EmailAccountValidationAssistantActivity.java deleted file mode 100644 index 7d38e85d7..000000000 --- a/app/src/main/java/org/linphone/assistant/EmailAccountValidationAssistantActivity.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.assistant; - -import android.os.Bundle; -import android.view.View; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.Nullable; -import org.linphone.R; -import org.linphone.core.AccountCreator; -import org.linphone.core.AccountCreatorListenerStub; -import org.linphone.core.tools.Log; - -public class EmailAccountValidationAssistantActivity extends AssistantActivity { - private TextView mFinishCreation; - - private AccountCreatorListenerStub mListener; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.assistant_email_account_validation); - - TextView email = findViewById(R.id.send_email); - email.setText(getAccountCreator().getEmail()); - - mFinishCreation = findViewById(R.id.assistant_check); - mFinishCreation.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - mFinishCreation.setEnabled(false); - - AccountCreator.Status status = getAccountCreator().isAccountActivated(); - if (status != AccountCreator.Status.RequestOk) { - Log.e( - "[Email Account Validation Assistant] activateAccount returned " - + status); - mFinishCreation.setEnabled(true); - showGenericErrorDialog(status); - } - } - }); - - mListener = - new AccountCreatorListenerStub() { - @Override - public void onIsAccountActivated( - AccountCreator creator, AccountCreator.Status status, String resp) { - Log.i( - "[Email Account Validation Assistant] onIsAccountActivated status is " - + status); - if (status.equals(AccountCreator.Status.AccountActivated)) { - createProxyConfigAndLeaveAssistant(); - } else if (status.equals(AccountCreator.Status.AccountNotActivated)) { - Toast.makeText( - EmailAccountValidationAssistantActivity.this, - getString(R.string.assistant_account_not_validated), - Toast.LENGTH_LONG) - .show(); - mFinishCreation.setEnabled(true); - } else { - showGenericErrorDialog(status); - mFinishCreation.setEnabled(true); - } - } - }; - } - - @Override - protected void onResume() { - super.onResume(); - getAccountCreator().addListener(mListener); - - // Prevent user to go back, it won't be able to come back here after... - mBack.setEnabled(false); - } - - @Override - protected void onPause() { - super.onPause(); - getAccountCreator().removeListener(mListener); - } -} diff --git a/app/src/main/java/org/linphone/assistant/GenericConnectionAssistantActivity.java b/app/src/main/java/org/linphone/assistant/GenericConnectionAssistantActivity.java deleted file mode 100644 index 9ecc21b68..000000000 --- a/app/src/main/java/org/linphone/assistant/GenericConnectionAssistantActivity.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.assistant; - -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.View; -import android.widget.EditText; -import android.widget.RadioGroup; -import android.widget.TextView; -import androidx.annotation.Nullable; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.AccountCreator; -import org.linphone.core.Core; -import org.linphone.core.TransportType; -import org.linphone.core.tools.Log; - -public class GenericConnectionAssistantActivity extends AssistantActivity implements TextWatcher { - private TextView mLogin; - private EditText mUsername, mPassword, mDomain, mDisplayName; - private RadioGroup mTransport; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.assistant_generic_connection); - - mLogin = findViewById(R.id.assistant_login); - mLogin.setEnabled(false); - mLogin.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - configureAccount(); - } - }); - - mUsername = findViewById(R.id.assistant_username); - mUsername.addTextChangedListener(this); - mDisplayName = findViewById(R.id.assistant_display_name); - mDisplayName.addTextChangedListener(this); - mPassword = findViewById(R.id.assistant_password); - mPassword.addTextChangedListener(this); - mDomain = findViewById(R.id.assistant_domain); - mDomain.addTextChangedListener(this); - mTransport = findViewById(R.id.assistant_transports); - } - - private void configureAccount() { - Core core = LinphoneManager.getCore(); - if (core != null) { - Log.i("[Generic Connection Assistant] Reloading configuration with default"); - reloadDefaultAccountCreatorConfig(); - } - - AccountCreator accountCreator = getAccountCreator(); - accountCreator.setUsername(mUsername.getText().toString()); - accountCreator.setDomain(mDomain.getText().toString()); - accountCreator.setPassword(mPassword.getText().toString()); - accountCreator.setDisplayName(mDisplayName.getText().toString()); - - switch (mTransport.getCheckedRadioButtonId()) { - case R.id.transport_udp: - accountCreator.setTransport(TransportType.Udp); - break; - case R.id.transport_tcp: - accountCreator.setTransport(TransportType.Tcp); - break; - case R.id.transport_tls: - accountCreator.setTransport(TransportType.Tls); - break; - } - - createProxyConfigAndLeaveAssistant(true); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - mLogin.setEnabled( - !mUsername.getText().toString().isEmpty() - && !mDomain.getText().toString().isEmpty()); - } - - @Override - public void afterTextChanged(Editable s) {} -} diff --git a/app/src/main/java/org/linphone/assistant/MenuAssistantActivity.java b/app/src/main/java/org/linphone/assistant/MenuAssistantActivity.java deleted file mode 100644 index 92b099a4e..000000000 --- a/app/src/main/java/org/linphone/assistant/MenuAssistantActivity.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.assistant; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.Spanned; -import android.text.method.LinkMovementMethod; -import android.text.style.ClickableSpan; -import android.view.KeyEvent; -import android.view.View; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.linphone.R; -import org.linphone.settings.LinphonePreferences; - -public class MenuAssistantActivity extends AssistantActivity { - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.assistant_menu); - - TextView accountCreation = findViewById(R.id.account_creation); - accountCreation.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent; - if (getResources().getBoolean(R.bool.isTablet) - || !getResources().getBoolean(R.bool.use_phone_number_validation)) { - intent = - new Intent( - MenuAssistantActivity.this, - EmailAccountCreationAssistantActivity.class); - } else { - intent = - new Intent( - MenuAssistantActivity.this, - PhoneAccountCreationAssistantActivity.class); - } - startActivity(intent); - } - }); - - TextView accountConnection = findViewById(R.id.account_connection); - accountConnection.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - startActivity( - new Intent( - MenuAssistantActivity.this, - AccountConnectionAssistantActivity.class)); - } - }); - if (getResources().getBoolean(R.bool.hide_linphone_accounts_in_assistant)) { - accountConnection.setVisibility(View.GONE); - } - - TextView genericConnection = findViewById(R.id.generic_connection); - genericConnection.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - startActivity( - new Intent( - MenuAssistantActivity.this, - GenericConnectionAssistantActivity.class)); - } - }); - if (getResources().getBoolean(R.bool.hide_generic_accounts_in_assistant)) { - genericConnection.setVisibility(View.GONE); - } - - TextView remoteConfiguration = findViewById(R.id.remote_configuration); - remoteConfiguration.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - startActivity( - new Intent( - MenuAssistantActivity.this, - RemoteConfigurationAssistantActivity.class)); - } - }); - if (getResources().getBoolean(R.bool.hide_remote_provisioning_in_assistant)) { - remoteConfiguration.setVisibility(View.GONE); - } - - if (getResources().getBoolean(R.bool.assistant_use_linphone_login_as_first_fragment)) { - startActivity( - new Intent( - MenuAssistantActivity.this, AccountConnectionAssistantActivity.class)); - finish(); - } else if (getResources() - .getBoolean(R.bool.assistant_use_generic_login_as_first_fragment)) { - startActivity( - new Intent( - MenuAssistantActivity.this, GenericConnectionAssistantActivity.class)); - finish(); - } else if (getResources() - .getBoolean(R.bool.assistant_use_create_linphone_account_as_first_fragment)) { - startActivity( - new Intent( - MenuAssistantActivity.this, - PhoneAccountCreationAssistantActivity.class)); - finish(); - } - - setUpTermsAndPrivacyLinks(); - } - - @Override - protected void onResume() { - super.onResume(); - - if (getResources() - .getBoolean(R.bool.forbid_to_leave_assistant_before_account_configuration)) { - mBack.setEnabled(false); - } - - mBack.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - LinphonePreferences.instance().firstLaunchSuccessful(); - goToLinphoneActivity(); - } - }); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - if (getResources() - .getBoolean(R.bool.forbid_to_leave_assistant_before_account_configuration)) { - // Do nothing - return true; - } else { - LinphonePreferences.instance().firstLaunchSuccessful(); - goToLinphoneActivity(); - return true; - } - } - return super.onKeyDown(keyCode, event); - } - - private void setUpTermsAndPrivacyLinks() { - String terms = getString(R.string.assistant_general_terms); - String privacy = getString(R.string.assistant_privacy_policy); - - String label = getString(R.string.assistant_read_and_agree_terms, terms, privacy); - Spannable spannable = new SpannableString(label); - - Matcher termsMatcher = Pattern.compile(terms).matcher(label); - if (termsMatcher.find()) { - ClickableSpan clickableSpan = - new ClickableSpan() { - @Override - public void onClick(@NonNull View widget) { - Intent browserIntent = - new Intent( - Intent.ACTION_VIEW, - Uri.parse( - getString( - R.string - .assistant_general_terms_link))); - startActivity(browserIntent); - } - }; - spannable.setSpan( - clickableSpan, - termsMatcher.start(0), - termsMatcher.end(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - Matcher privacyMatcher = Pattern.compile(privacy).matcher(label); - if (privacyMatcher.find()) { - ClickableSpan clickableSpan = - new ClickableSpan() { - @Override - public void onClick(@NonNull View widget) { - Intent browserIntent = - new Intent( - Intent.ACTION_VIEW, - Uri.parse( - getString( - R.string - .assistant_privacy_policy_link))); - startActivity(browserIntent); - } - }; - spannable.setSpan( - clickableSpan, - privacyMatcher.start(0), - privacyMatcher.end(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - TextView termsAndPrivacy = findViewById(R.id.terms_and_privacy); - final CheckBox termsAndPrivacyCheckBox = findViewById(R.id.terms_and_privacy_checkbox); - - termsAndPrivacy.setText(spannable); - termsAndPrivacy.setMovementMethod(new LinkMovementMethod()); - if (LinphonePreferences.instance().getReadAndAgreeTermsAndPrivacy()) { - termsAndPrivacyCheckBox.setEnabled(false); - termsAndPrivacyCheckBox.setChecked(true); - } else { - final TextView accountCreation = findViewById(R.id.account_creation); - final TextView accountConnection = findViewById(R.id.account_connection); - final TextView genericConnection = findViewById(R.id.generic_connection); - final TextView remoteConfiguration = findViewById(R.id.remote_configuration); - accountCreation.setEnabled(false); - accountConnection.setEnabled(false); - genericConnection.setEnabled(false); - remoteConfiguration.setEnabled(false); - - termsAndPrivacyCheckBox.setOnCheckedChangeListener( - new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - LinphonePreferences.instance().setReadAndAgreeTermsAndPrivacy(true); - termsAndPrivacyCheckBox.setEnabled(false); - accountCreation.setEnabled(true); - accountConnection.setEnabled(true); - genericConnection.setEnabled(true); - remoteConfiguration.setEnabled(true); - } - } - }); - } - } -} diff --git a/app/src/main/java/org/linphone/assistant/PhoneAccountCreationAssistantActivity.java b/app/src/main/java/org/linphone/assistant/PhoneAccountCreationAssistantActivity.java deleted file mode 100644 index 90b600672..000000000 --- a/app/src/main/java/org/linphone/assistant/PhoneAccountCreationAssistantActivity.java +++ /dev/null @@ -1,303 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.assistant; - -import android.content.Intent; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.View; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.TextView; -import androidx.annotation.Nullable; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.AccountCreator; -import org.linphone.core.AccountCreatorListenerStub; -import org.linphone.core.Core; -import org.linphone.core.DialPlan; -import org.linphone.core.tools.Log; - -public class PhoneAccountCreationAssistantActivity extends AssistantActivity { - private TextView mCountryPicker, mError, mSipUri, mCreate; - private EditText mPrefix, mPhoneNumber, mUsername; - private CheckBox mUseUsernameInsteadOfPhoneNumber; - - private AccountCreatorListenerStub mListener; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.assistant_phone_account_creation); - - mCountryPicker = findViewById(R.id.select_country); - mCountryPicker.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - showCountryPickerDialog(); - } - }); - - mError = findViewById(R.id.phone_number_error); - - mSipUri = findViewById(R.id.sip_uri); - - mCreate = findViewById(R.id.assistant_create); - mCreate.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - AccountCreator accountCreator = getAccountCreator(); - enableButtonsAndFields(false); - - if (mUseUsernameInsteadOfPhoneNumber.isChecked()) { - accountCreator.setUsername(mUsername.getText().toString()); - } else { - accountCreator.setUsername(accountCreator.getPhoneNumber()); - } - - AccountCreator.Status status = accountCreator.isAccountExist(); - if (status != AccountCreator.Status.RequestOk) { - Log.e( - "[Phone Account Creation Assistant] isAccountExists returned " - + status); - enableButtonsAndFields(true); - showGenericErrorDialog(status); - } - } - }); - mCreate.setEnabled(false); - - mPrefix = findViewById(R.id.dial_code); - mPrefix.setText("+"); - mPrefix.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - String prefix = s.toString(); - if (prefix.startsWith("+")) { - prefix = prefix.substring(1); - } - DialPlan dp = getDialPlanFromPrefix(prefix); - if (dp != null) { - mCountryPicker.setText(dp.getCountry()); - } - - updateCreateButtonAndDisplayError(); - } - }); - - mPhoneNumber = findViewById(R.id.phone_number); - mPhoneNumber.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - updateCreateButtonAndDisplayError(); - } - }); - - ImageView phoneNumberInfos = findViewById(R.id.info_phone_number); - phoneNumberInfos.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - showPhoneNumberDialog(); - } - }); - - mUseUsernameInsteadOfPhoneNumber = findViewById(R.id.use_username); - mUseUsernameInsteadOfPhoneNumber.setOnCheckedChangeListener( - new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - mUsername.setVisibility(isChecked ? View.VISIBLE : View.GONE); - updateCreateButtonAndDisplayError(); - } - }); - - mUsername = findViewById(R.id.username); - mUsername.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - updateCreateButtonAndDisplayError(); - } - }); - - mListener = - new AccountCreatorListenerStub() { - public void onIsAccountExist( - AccountCreator creator, AccountCreator.Status status, String resp) { - Log.i( - "[Phone Account Creation Assistant] onIsAccountExist status is " - + status); - if (status.equals(AccountCreator.Status.AccountExist) - || status.equals(AccountCreator.Status.AccountExistWithAlias)) { - showAccountAlreadyExistsDialog(); - enableButtonsAndFields(true); - } else if (status.equals(AccountCreator.Status.AccountNotExist)) { - status = getAccountCreator().createAccount(); - if (status != AccountCreator.Status.RequestOk) { - Log.e( - "[Phone Account Creation Assistant] createAccount returned " - + status); - enableButtonsAndFields(true); - showGenericErrorDialog(status); - } - } else { - enableButtonsAndFields(true); - showGenericErrorDialog(status); - } - } - - @Override - public void onCreateAccount( - AccountCreator creator, AccountCreator.Status status, String resp) { - Log.i( - "[Phone Account Creation Assistant] onCreateAccount status is " - + status); - if (status.equals(AccountCreator.Status.AccountCreated)) { - startActivity( - new Intent( - PhoneAccountCreationAssistantActivity.this, - PhoneAccountValidationAssistantActivity.class)); - } else { - enableButtonsAndFields(true); - showGenericErrorDialog(status); - } - } - }; - } - - @Override - protected void onResume() { - super.onResume(); - - Core core = LinphoneManager.getCore(); - if (core != null) { - reloadLinphoneAccountCreatorConfig(); - } - - getAccountCreator().addListener(mListener); - - DialPlan dp = getDialPlanForCurrentCountry(); - displayDialPlan(dp); - - String phoneNumber = getDevicePhoneNumber(); - if (phoneNumber != null) { - mPhoneNumber.setText(phoneNumber); - } - } - - @Override - protected void onPause() { - super.onPause(); - getAccountCreator().removeListener(mListener); - } - - @Override - public void onCountryClicked(DialPlan dialPlan) { - super.onCountryClicked(dialPlan); - displayDialPlan(dialPlan); - } - - private void enableButtonsAndFields(boolean enable) { - mPrefix.setEnabled(enable); - mPhoneNumber.setEnabled(enable); - mCreate.setEnabled(enable); - } - - private void updateCreateButtonAndDisplayError() { - if (mPrefix.getText().toString().isEmpty() || mPhoneNumber.getText().toString().isEmpty()) - return; - - mCreate.setEnabled(true); - mError.setText(""); - mError.setVisibility(View.INVISIBLE); - - int status = arePhoneNumberAndPrefixOk(mPrefix, mPhoneNumber); - if (status == AccountCreator.PhoneNumberStatus.Ok.toInt()) { - if (mUseUsernameInsteadOfPhoneNumber.isChecked()) { - AccountCreator.UsernameStatus usernameStatus = - getAccountCreator().setUsername(mUsername.getText().toString()); - if (usernameStatus != AccountCreator.UsernameStatus.Ok) { - mCreate.setEnabled(false); - mError.setText(getErrorFromUsernameStatus(usernameStatus)); - mError.setVisibility(View.VISIBLE); - } - } - } else { - mCreate.setEnabled(false); - mError.setText(getErrorFromPhoneNumberStatus(status)); - mError.setVisibility(View.VISIBLE); - } - - String username; - if (mUseUsernameInsteadOfPhoneNumber.isChecked()) { - username = mUsername.getText().toString(); - } else { - username = getAccountCreator().getPhoneNumber(); - } - - if (username != null) { - String sip = - getString(R.string.assistant_create_account_phone_number_address) - + " "; - mSipUri.setText(sip); - } - } - - private void displayDialPlan(DialPlan dp) { - if (dp != null) { - mPrefix.setText("+" + dp.getCountryCallingCode()); - mCountryPicker.setText(dp.getCountry()); - } - } -} diff --git a/app/src/main/java/org/linphone/assistant/PhoneAccountLinkingAssistantActivity.java b/app/src/main/java/org/linphone/assistant/PhoneAccountLinkingAssistantActivity.java deleted file mode 100644 index 88ac130ab..000000000 --- a/app/src/main/java/org/linphone/assistant/PhoneAccountLinkingAssistantActivity.java +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.assistant; - -import android.content.Intent; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.View; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.Nullable; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.AccountCreator; -import org.linphone.core.AccountCreatorListenerStub; -import org.linphone.core.Address; -import org.linphone.core.AuthInfo; -import org.linphone.core.Core; -import org.linphone.core.DialPlan; -import org.linphone.core.ProxyConfig; -import org.linphone.core.tools.Log; - -public class PhoneAccountLinkingAssistantActivity extends AssistantActivity { - private TextView mCountryPicker, mError, mLink; - private EditText mPrefix, mPhoneNumber; - - private AccountCreatorListenerStub mListener; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.assistant_phone_account_linking); - - if (getIntent() != null) { - int proxyConfigIndex = 0; - if (getIntent().hasExtra("AccountNumber")) - proxyConfigIndex = getIntent().getExtras().getInt("AccountNumber"); - - Core core = LinphoneManager.getCore(); - if (core == null) { - Log.e("[Account Linking Assistant] Core not available"); - unexpectedError(); - return; - } - - ProxyConfig[] proxyConfigs = core.getProxyConfigList(); - if (proxyConfigIndex >= 0 && proxyConfigIndex < proxyConfigs.length) { - ProxyConfig mProxyConfig = proxyConfigs[proxyConfigIndex]; - AccountCreator accountCreator = getAccountCreator(); - - Address identity = mProxyConfig.getIdentityAddress(); - if (identity == null) { - Log.e("[Account Linking Assistant] Proxy doesn't have an identity address"); - unexpectedError(); - return; - } - if (!mProxyConfig.getDomain().equals(getString(R.string.default_domain))) { - Log.e( - "[Account Linking Assistant] Can't link account on domain " - + mProxyConfig.getDomain()); - unexpectedError(); - return; - } - accountCreator.setUsername(identity.getUsername()); - - AuthInfo authInfo = mProxyConfig.findAuthInfo(); - if (authInfo == null) { - Log.e("[Account Linking Assistant] Auth info not found"); - unexpectedError(); - return; - } - accountCreator.setHa1(authInfo.getHa1()); - accountCreator.setAlgorithm((authInfo.getAlgorithm())); - } else { - Log.e( - "[Account Linking Assistant] Proxy config index out of bounds: " - + proxyConfigIndex); - unexpectedError(); - return; - } - } - - mCountryPicker = findViewById(R.id.select_country); - mCountryPicker.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - showCountryPickerDialog(); - } - }); - - mError = findViewById(R.id.phone_number_error); - - mLink = findViewById(R.id.assistant_link); - mLink.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - enableButtonsAndFields(false); - - AccountCreator.Status status = getAccountCreator().isAliasUsed(); - if (status != AccountCreator.Status.RequestOk) { - Log.e( - "[Phone Account Linking Assistant] isAliasUsed returned " - + status); - enableButtonsAndFields(true); - showGenericErrorDialog(status); - } - } - }); - mLink.setEnabled(false); - - mPrefix = findViewById(R.id.dial_code); - mPrefix.setText("+"); - mPrefix.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - String prefix = s.toString(); - if (prefix.startsWith("+")) { - prefix = prefix.substring(1); - } - DialPlan dp = getDialPlanFromPrefix(prefix); - if (dp != null) { - mCountryPicker.setText(dp.getCountry()); - } - - updateCreateButtonAndDisplayError(); - } - }); - - mPhoneNumber = findViewById(R.id.phone_number); - mPhoneNumber.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - updateCreateButtonAndDisplayError(); - } - }); - - ImageView phoneNumberInfos = findViewById(R.id.info_phone_number); - phoneNumberInfos.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - showPhoneNumberDialog(); - } - }); - - mListener = - new AccountCreatorListenerStub() { - @Override - public void onIsAliasUsed( - AccountCreator creator, AccountCreator.Status status, String resp) { - Log.i( - "[Phone Account Linking Assistant] onIsAliasUsed status is " - + status); - if (status.equals(AccountCreator.Status.AliasNotExist)) { - status = getAccountCreator().linkAccount(); - if (status != AccountCreator.Status.RequestOk) { - Log.e( - "[Phone Account Linking Assistant] linkAccount returned " - + status); - enableButtonsAndFields(true); - showGenericErrorDialog(status); - } - } else { - if (status.equals(AccountCreator.Status.AliasIsAccount) - || status.equals(AccountCreator.Status.AliasExist)) { - showAccountAlreadyExistsDialog(); - } else { - showGenericErrorDialog(status); - } - enableButtonsAndFields(true); - } - } - - @Override - public void onLinkAccount( - AccountCreator creator, AccountCreator.Status status, String resp) { - Log.i( - "[Phone Account Linking Assistant] onLinkAccount status is " - + status); - if (status.equals(AccountCreator.Status.RequestOk)) { - Intent intent = - new Intent( - PhoneAccountLinkingAssistantActivity.this, - PhoneAccountValidationAssistantActivity.class); - intent.putExtra("isLinkingVerification", true); - startActivity(intent); - } else { - enableButtonsAndFields(true); - showGenericErrorDialog(status); - } - } - }; - } - - @Override - protected void onResume() { - super.onResume(); - - Core core = LinphoneManager.getCore(); - - // Save & restore username & password informations, reload will reset - String username = getAccountCreator().getUsername(); - String ha1 = getAccountCreator().getHa1(); - String algo = getAccountCreator().getAlgorithm(); - if (core != null) { - reloadLinphoneAccountCreatorConfig(); - } - AccountCreator creator = getAccountCreator(); - creator.setUsername(username); - creator.setHa1(ha1); - creator.setAlgorithm(algo); - - getAccountCreator().addListener(mListener); - - DialPlan dp = getDialPlanForCurrentCountry(); - displayDialPlan(dp); - - String phoneNumber = getDevicePhoneNumber(); - if (phoneNumber != null) { - mPhoneNumber.setText(phoneNumber); - } - } - - @Override - protected void onPause() { - super.onPause(); - getAccountCreator().removeListener(mListener); - } - - @Override - public void onCountryClicked(DialPlan dialPlan) { - super.onCountryClicked(dialPlan); - displayDialPlan(dialPlan); - } - - private void enableButtonsAndFields(boolean enable) { - mPrefix.setEnabled(enable); - mPhoneNumber.setEnabled(enable); - mLink.setEnabled(enable); - } - - private void updateCreateButtonAndDisplayError() { - if (mPrefix.getText().toString().isEmpty() || mPhoneNumber.getText().toString().isEmpty()) - return; - - int status = arePhoneNumberAndPrefixOk(mPrefix, mPhoneNumber); - if (status == AccountCreator.PhoneNumberStatus.Ok.toInt()) { - mLink.setEnabled(true); - mError.setText(""); - mError.setVisibility(View.INVISIBLE); - } else { - mLink.setEnabled(false); - mError.setText(getErrorFromPhoneNumberStatus(status)); - mError.setVisibility(View.VISIBLE); - } - } - - private void displayDialPlan(DialPlan dp) { - if (dp != null) { - mPrefix.setText("+" + dp.getCountryCallingCode()); - mCountryPicker.setText(dp.getCountry()); - } - } - - private void unexpectedError() { - Toast.makeText(this, R.string.error_unknown, Toast.LENGTH_SHORT).show(); - finish(); - } -} diff --git a/app/src/main/java/org/linphone/assistant/PhoneAccountValidationAssistantActivity.java b/app/src/main/java/org/linphone/assistant/PhoneAccountValidationAssistantActivity.java deleted file mode 100644 index 857efd36c..000000000 --- a/app/src/main/java/org/linphone/assistant/PhoneAccountValidationAssistantActivity.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.assistant; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.View; -import android.widget.EditText; -import android.widget.TextView; -import androidx.annotation.Nullable; -import org.linphone.R; -import org.linphone.core.AccountCreator; -import org.linphone.core.AccountCreatorListenerStub; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; - -public class PhoneAccountValidationAssistantActivity extends AssistantActivity { - private TextView mFinishCreation; - private EditText mSmsCode; - private ClipboardManager mClipboard; - - private int mActivationCodeLength; - private boolean mIsLinking = false, mIsLogin = false; - private AccountCreatorListenerStub mListener; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.assistant_phone_account_validation); - - if (getIntent() != null && getIntent().getBooleanExtra("isLoginVerification", false)) { - findViewById(R.id.title_account_login).setVisibility(View.VISIBLE); - mIsLogin = true; - } else if (getIntent() != null - && getIntent().getBooleanExtra("isLinkingVerification", false)) { - mIsLinking = true; - findViewById(R.id.title_account_linking).setVisibility(View.VISIBLE); - } else { - findViewById(R.id.title_account_creation).setVisibility(View.VISIBLE); - } - - mActivationCodeLength = - getResources().getInteger(R.integer.phone_number_validation_code_length); - - TextView phoneNumber = findViewById(R.id.phone_number); - phoneNumber.setText(getAccountCreator().getPhoneNumber()); - - mSmsCode = findViewById(R.id.sms_code); - mSmsCode.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - mFinishCreation.setEnabled(s.length() == mActivationCodeLength); - } - }); - - mFinishCreation = findViewById(R.id.finish_account_creation); - mFinishCreation.setEnabled(false); - mFinishCreation.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - AccountCreator accountCreator = getAccountCreator(); - mFinishCreation.setEnabled(false); - accountCreator.setActivationCode(mSmsCode.getText().toString()); - - AccountCreator.Status status; - if (mIsLinking) { - status = accountCreator.activateAlias(); - } else if (mIsLogin) { - status = accountCreator.loginLinphoneAccount(); - } else { - status = accountCreator.activateAccount(); - } - if (status != AccountCreator.Status.RequestOk) { - Log.e( - "[Phone Account Validation] " - + (mIsLinking - ? "linkAccount" - : (mIsLogin - ? "loginLinphoneAccount" - : "activateAccount") - + " returned ") - + status); - mFinishCreation.setEnabled(true); - showGenericErrorDialog(status); - } - } - }); - - mListener = - new AccountCreatorListenerStub() { - @Override - public void onActivateAccount( - AccountCreator creator, AccountCreator.Status status, String resp) { - Log.i("[Phone Account Validation] onActivateAccount status is " + status); - if (status.equals(AccountCreator.Status.AccountActivated)) { - createProxyConfigAndLeaveAssistant(); - } else { - onError(status); - } - } - - @Override - public void onActivateAlias( - AccountCreator creator, AccountCreator.Status status, String resp) { - Log.i("[Phone Account Validation] onActivateAlias status is " + status); - if (status.equals(AccountCreator.Status.AccountActivated)) { - LinphonePreferences.instance().setLinkPopupTime(""); - goToLinphoneActivity(); - } else { - onError(status); - } - } - - @Override - public void onLoginLinphoneAccount( - AccountCreator creator, AccountCreator.Status status, String resp) { - Log.i( - "[Phone Account Validation] onLoginLinphoneAccount status is " - + status); - if (status.equals(AccountCreator.Status.RequestOk)) { - createProxyConfigAndLeaveAssistant(); - } else { - onError(status); - } - } - }; - - mClipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - mClipboard.addPrimaryClipChangedListener( - new ClipboardManager.OnPrimaryClipChangedListener() { - @Override - public void onPrimaryClipChanged() { - ClipData data = mClipboard.getPrimaryClip(); - if (data != null && data.getItemCount() > 0) { - String clip = data.getItemAt(0).getText().toString(); - if (clip.length() == mActivationCodeLength) { - mSmsCode.setText(clip); - } - } - } - }); - } - - @Override - protected void onResume() { - super.onResume(); - getAccountCreator().addListener(mListener); - - // Prevent user to go back, it won't be able to come back here after... - mBack.setEnabled(false); - } - - @Override - protected void onPause() { - super.onPause(); - getAccountCreator().removeListener(mListener); - } - - private void onError(AccountCreator.Status status) { - mFinishCreation.setEnabled(true); - showGenericErrorDialog(status); - - if (status.equals(AccountCreator.Status.WrongActivationCode)) { - // TODO do something so the server re-send a SMS - } - } -} diff --git a/app/src/main/java/org/linphone/assistant/QrCodeConfigurationAssistantActivity.java b/app/src/main/java/org/linphone/assistant/QrCodeConfigurationAssistantActivity.java deleted file mode 100644 index e22434680..000000000 --- a/app/src/main/java/org/linphone/assistant/QrCodeConfigurationAssistantActivity.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.assistant; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.view.TextureView; -import android.view.View; -import android.widget.ImageView; -import androidx.annotation.Nullable; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.tools.Log; - -public class QrCodeConfigurationAssistantActivity extends AssistantActivity { - private TextureView mQrcodeView; - - private CoreListenerStub mListener; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.assistant_qr_code_remote_configuration); - - mQrcodeView = findViewById(R.id.qr_code_capture_texture); - - mListener = - new CoreListenerStub() { - @Override - public void onQrcodeFound(Core core, String result) { - Intent resultIntent = new Intent(); - resultIntent.putExtra("URL", result); - setResult(Activity.RESULT_OK, resultIntent); - finish(); - } - }; - - ImageView changeCamera = findViewById(R.id.qr_code_capture_change_camera); - changeCamera.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - LinphoneManager.getCallManager().switchCamera(); - } - }); - Core core = LinphoneManager.getCore(); - if (core != null && core.getVideoDevicesList().length > 1) { - changeCamera.setVisibility(View.VISIBLE); - } - - setBackCamera(); - } - - @Override - protected void onResume() { - super.onResume(); - - enableQrcodeReader(true); - } - - @Override - public void onPause() { - enableQrcodeReader(false); - - super.onPause(); - } - - private void enableQrcodeReader(boolean enable) { - Core core = LinphoneManager.getCore(); - if (core == null) return; - - core.setNativePreviewWindowId(enable ? mQrcodeView : null); - core.enableQrcodeVideoPreview(enable); - core.enableVideoPreview(enable); - - if (enable) { - core.addListener(mListener); - } else { - core.removeListener(mListener); - } - } - - private void setBackCamera() { - Core core = LinphoneManager.getCore(); - if (core == null) return; - - String firstDevice = null; - for (String camera : core.getVideoDevicesList()) { - if (firstDevice == null) { - firstDevice = camera; - } - - if (camera.contains("Back")) { - Log.i("[QR Code] Found back facing camera: " + camera); - core.setVideoDevice(camera); - return; - } - } - - Log.i("[QR Code] Using first camera available: " + firstDevice); - core.setVideoDevice(firstDevice); - } -} diff --git a/app/src/main/java/org/linphone/assistant/RemoteConfigurationAssistantActivity.java b/app/src/main/java/org/linphone/assistant/RemoteConfigurationAssistantActivity.java deleted file mode 100644 index 3157d474c..000000000 --- a/app/src/main/java/org/linphone/assistant/RemoteConfigurationAssistantActivity.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.assistant; - -import android.Manifest; -import android.app.Activity; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.Patterns; -import android.view.View; -import android.widget.EditText; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.Nullable; -import androidx.core.app.ActivityCompat; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.ConfiguringState; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.LinphoneUtils; - -public class RemoteConfigurationAssistantActivity extends AssistantActivity { - private static final int QR_CODE_ACTIVITY_RESULT = 1; - private static final int CAMERA_PERMISSION_RESULT = 2; - - private TextView mFetchAndApply; - private EditText mRemoteConfigurationUrl; - private RelativeLayout mWaitLayout; - - private CoreListenerStub mListener; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.assistant_remote_configuration); - - mWaitLayout = findViewById(R.id.waitScreen); - mWaitLayout.setVisibility(View.GONE); - - mFetchAndApply = findViewById(R.id.fetch_and_apply_remote_configuration); - mFetchAndApply.setEnabled(false); - mFetchAndApply.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - String url = mRemoteConfigurationUrl.getText().toString(); - if (Patterns.WEB_URL.matcher(url).matches()) { - mWaitLayout.setVisibility(View.VISIBLE); - mFetchAndApply.setEnabled(false); - LinphonePreferences.instance().setRemoteProvisioningUrl(url); - Core core = LinphoneManager.getCore(); - if (core != null) { - core.getConfig().sync(); - core.addListener(mListener); - } - LinphoneManager.getInstance().restartCore(); - } else { - // TODO improve error text - Toast.makeText( - RemoteConfigurationAssistantActivity.this, - getString(R.string.remote_provisioning_failure), - Toast.LENGTH_LONG) - .show(); - } - } - }); - - mRemoteConfigurationUrl = findViewById(R.id.remote_configuration_url); - mRemoteConfigurationUrl.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - mFetchAndApply.setEnabled(!s.toString().isEmpty()); - } - - @Override - public void afterTextChanged(Editable s) {} - }); - mRemoteConfigurationUrl.setText(LinphonePreferences.instance().getRemoteProvisioningUrl()); - - TextView qrCode = findViewById(R.id.qr_code); - qrCode.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - if (checkCameraPermissionForQrCode()) { - startActivityForResult( - new Intent( - RemoteConfigurationAssistantActivity.this, - QrCodeConfigurationAssistantActivity.class), - QR_CODE_ACTIVITY_RESULT); - } - } - }); - - mListener = - new CoreListenerStub() { - @Override - public void onConfiguringStatus( - Core core, ConfiguringState status, String message) { - core.removeListener(mListener); - mWaitLayout.setVisibility(View.GONE); - mFetchAndApply.setEnabled(true); - - if (status == ConfiguringState.Successful) { - LinphonePreferences.instance().firstLaunchSuccessful(); - goToLinphoneActivity(); - } else if (status == ConfiguringState.Failed) { - Toast.makeText( - RemoteConfigurationAssistantActivity.this, - getString(R.string.remote_provisioning_failure), - Toast.LENGTH_LONG) - .show(); - } - } - }; - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - if (requestCode == QR_CODE_ACTIVITY_RESULT - && resultCode == Activity.RESULT_OK - && data != null) { - String url = data.getStringExtra("URL"); - mRemoteConfigurationUrl.setText(url); - } - super.onActivityResult(requestCode, resultCode, data); - } - - private boolean checkCameraPermissionForQrCode() { - int permissionGranted = - getPackageManager().checkPermission(Manifest.permission.CAMERA, getPackageName()); - Log.i( - "[Permission] Manifest.permission.CAMERA is " - + (permissionGranted == PackageManager.PERMISSION_GRANTED - ? "granted" - : "denied")); - - if (permissionGranted != PackageManager.PERMISSION_GRANTED) { - Log.i("[Permission] Asking for " + Manifest.permission.CAMERA); - ActivityCompat.requestPermissions( - this, new String[] {Manifest.permission.CAMERA}, CAMERA_PERMISSION_RESULT); - return false; - } - return true; - } - - @Override - public void onRequestPermissionsResult( - int requestCode, String[] permissions, final int[] grantResults) { - for (int i = 0; i < permissions.length; i++) { - Log.i( - "[Permission] " - + permissions[i] - + " has been " - + (grantResults[i] == PackageManager.PERMISSION_GRANTED - ? "granted" - : "denied")); - } - - if (requestCode == CAMERA_PERMISSION_RESULT) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - LinphoneUtils.reloadVideoDevices(); - startActivityForResult( - new Intent( - RemoteConfigurationAssistantActivity.this, - QrCodeConfigurationAssistantActivity.class), - QR_CODE_ACTIVITY_RESULT); - } else { - // TODO: permission denied, display something to the user - } - } - } -} diff --git a/app/src/main/java/org/linphone/call/AndroidAudioManager.java b/app/src/main/java/org/linphone/call/AndroidAudioManager.java deleted file mode 100644 index 4e296a5ab..000000000 --- a/app/src/main/java/org/linphone/call/AndroidAudioManager.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call; - -import static android.media.AudioManager.STREAM_VOICE_CALL; - -import android.content.Context; -import android.media.AudioManager; -import android.view.KeyEvent; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.AudioDevice; -import org.linphone.core.Call; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.tools.Log; - -public class AndroidAudioManager { - private Context mContext; - private AudioManager mAudioManager; - private boolean mEchoTesterIsRunning = false; - private boolean mPreviousStateIsConnected = false; - - private CoreListenerStub mListener; - - public AndroidAudioManager(Context context) { - mContext = context; - mAudioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); - mPreviousStateIsConnected = false; - - mListener = - new CoreListenerStub() { - @Override - public void onCallStateChanged( - final Core core, - final Call call, - final Call.State state, - final String message) { - if (state == Call.State.Connected) { - if (core.getCallsNb() == 1) { - if (!isBluetoothHeadsetConnected()) { - if (mContext.getResources().getBoolean(R.bool.isTablet)) { - routeAudioToSpeaker(); - } else { - // Only force earpiece audio route for incoming audio calls, - // outgoing calls may have manually enabled speaker - if (call.getDir() == Call.Dir.Incoming) { - routeAudioToEarPiece(); - } - } - } - } - } else if (state == Call.State.StreamsRunning - && mPreviousStateIsConnected) { - if (isBluetoothHeadsetConnected()) { - routeAudioToBluetooth(); - } - } - mPreviousStateIsConnected = state == Call.State.Connected; - } - }; - - Core core = LinphoneManager.getCore(); - if (core != null) { - core.addListener(mListener); - } - } - - public void destroy() { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.removeListener(mListener); - } - } - - /* Audio routing */ - - public void routeAudioToEarPiece() { - routeAudioToSpeakerHelper(false); - } - - public void routeAudioToSpeaker() { - routeAudioToSpeakerHelper(true); - } - - public boolean isAudioRoutedToSpeaker() { - return isUsingSpeakerAudioRoute() && !isUsingBluetoothAudioRoute(); - } - - public boolean isAudioRoutedToEarpiece() { - return !isUsingSpeakerAudioRoute() && !isUsingBluetoothAudioRoute(); - } - - /* Echo cancellation */ - - public void startEchoTester() { - Core core = LinphoneManager.getCore(); - if (core == null) { - return; - } - int sampleRate; - String sampleRateProperty = - mAudioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); - sampleRate = Integer.parseInt(sampleRateProperty); - mEchoTesterIsRunning = true; - core.startEchoTester(sampleRate); - } - - public void stopEchoTester() { - Core core = LinphoneManager.getCore(); - if (core == null) { - return; - } - - core.stopEchoTester(); - mEchoTesterIsRunning = false; - Log.i("[Audio Manager] Set audio mode on 'Normal'"); - } - - public boolean getEchoTesterStatus() { - return mEchoTesterIsRunning; - } - - public boolean onKeyVolumeAdjust(int keyCode) { - if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { - adjustVolume(1); - return true; - } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { - adjustVolume(-1); - return true; - } - return false; - } - - public synchronized boolean isUsingSpeakerAudioRoute() { - if (LinphoneManager.getCore().getCallsNb() == 0) return false; - Call currentCall = LinphoneManager.getCore().getCurrentCall(); - if (currentCall == null) currentCall = LinphoneManager.getCore().getCalls()[0]; - if (currentCall == null) return false; - AudioDevice audioDevice = currentCall.getOutputAudioDevice(); - if (audioDevice == null) return false; - Log.i( - "[Audio Manager] Currently used audio device: ", - audioDevice.getDeviceName(), - "/", - audioDevice.getType().name()); - return audioDevice.getType() == AudioDevice.Type.Speaker; - } - - private void routeAudioToSpeakerHelper(boolean speakerOn) { - Log.w("[Audio Manager] Routing audio to " + (speakerOn ? "speaker" : "earpiece")); - - if (LinphoneManager.getCore().getCallsNb() == 0) return; - Call currentCall = LinphoneManager.getCore().getCurrentCall(); - if (currentCall == null) currentCall = LinphoneManager.getCore().getCalls()[0]; - if (currentCall == null) return; - - for (AudioDevice audioDevice : LinphoneManager.getCore().getAudioDevices()) { - if (speakerOn && audioDevice.getType() == AudioDevice.Type.Speaker) { - currentCall.setOutputAudioDevice(audioDevice); - return; - } else if (!speakerOn && audioDevice.getType() == AudioDevice.Type.Earpiece) { - currentCall.setOutputAudioDevice(audioDevice); - return; - } - } - } - - private void adjustVolume(int i) { - if (mAudioManager.isVolumeFixed()) { - Log.e("[Audio Manager] Can't adjust volume, device has it fixed..."); - // Keep going just in case... - } - - int stream = STREAM_VOICE_CALL; - if (isUsingBluetoothAudioRoute()) { - Log.i( - "[Audio Manager] Bluetooth is connected, try to change the volume on STREAM_BLUETOOTH_SCO"); - stream = 6; // STREAM_BLUETOOTH_SCO, it's hidden... - } - - // starting from ICS, volume must be adjusted by the application, - // at least for STREAM_VOICE_CALL volume stream - mAudioManager.adjustStreamVolume( - stream, - i < 0 ? AudioManager.ADJUST_LOWER : AudioManager.ADJUST_RAISE, - AudioManager.FLAG_SHOW_UI); - } - - public synchronized boolean isUsingBluetoothAudioRoute() { - if (LinphoneManager.getCore().getCallsNb() == 0) return false; - Call currentCall = LinphoneManager.getCore().getCurrentCall(); - if (currentCall == null) currentCall = LinphoneManager.getCore().getCalls()[0]; - if (currentCall == null) return false; - AudioDevice audioDevice = currentCall.getOutputAudioDevice(); - if (audioDevice == null) return false; - Log.i( - "[Audio Manager] Currently used audio device: ", - audioDevice.getDeviceName(), - "/", - audioDevice.getType().name()); - return audioDevice.getType() == AudioDevice.Type.Bluetooth; - } - - public synchronized boolean isBluetoothHeadsetConnected() { - for (AudioDevice audioDevice : LinphoneManager.getCore().getAudioDevices()) { - if (audioDevice.getType() == AudioDevice.Type.Bluetooth - && audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) { - Log.i( - "[Audio Manager] Found bluetooth device: ", - audioDevice.getDeviceName(), - "/", - audioDevice.getType().name()); - return true; - } - } - return false; - } - - public synchronized boolean isWiredHeadsetAvailable() { - for (AudioDevice audioDevice : LinphoneManager.getCore().getExtendedAudioDevices()) { - if (audioDevice.getType() == AudioDevice.Type.Headphones - || audioDevice.getType() == AudioDevice.Type.Headset) { - Log.i( - "[Audio Manager] Found headset/headphone device: ", - audioDevice.getDeviceName(), - "/", - audioDevice.getType().name()); - return true; - } - } - return false; - } - - public synchronized void routeAudioToBluetooth() { - if (LinphoneManager.getCore().getCallsNb() == 0) return; - Call currentCall = LinphoneManager.getCore().getCurrentCall(); - if (currentCall == null) currentCall = LinphoneManager.getCore().getCalls()[0]; - if (currentCall == null) return; - - for (AudioDevice audioDevice : LinphoneManager.getCore().getAudioDevices()) { - if (audioDevice.getType() == AudioDevice.Type.Bluetooth - && audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) { - Log.i( - "[Audio Manager] Found bluetooth audio device", - audioDevice.getDeviceName(), - "/", - audioDevice.getType().name(), - ", routing audio to it"); - currentCall.setOutputAudioDevice(audioDevice); - return; - } - } - Log.w( - "[Audio Manager] Didn't find any bluetooth audio device, keeping default audio route"); - } -} diff --git a/app/src/main/java/org/linphone/call/BandwidthManager.java b/app/src/main/java/org/linphone/call/BandwidthManager.java deleted file mode 100644 index 085f147e3..000000000 --- a/app/src/main/java/org/linphone/call/BandwidthManager.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call; - -import org.linphone.core.CallParams; - -public class BandwidthManager { - private static final int HIGH_RESOLUTION = 0; - private static final int LOW_RESOLUTION = 1; - private static final int LOW_BANDWIDTH = 2; - - private static final int currentProfile = HIGH_RESOLUTION; - - public BandwidthManager() { - // FIXME register a listener on NetworkManager to get notified of network state - // FIXME register a listener on Preference to get notified of change in video enable value - - // FIXME initially get those values - } - - public void destroy() {} - - public void updateWithProfileSettings(CallParams callParams) { - if (callParams != null) { // in call - // Update video parm if - if (!isVideoPossible()) { // NO VIDEO - callParams.enableVideo(false); - callParams.setAudioBandwidthLimit(40); - } else { - callParams.enableVideo(true); - callParams.setAudioBandwidthLimit(0); // disable limitation - } - } - } - - private boolean isVideoPossible() { - return currentProfile != LOW_BANDWIDTH; - } -} diff --git a/app/src/main/java/org/linphone/call/CallActivity.java b/app/src/main/java/org/linphone/call/CallActivity.java deleted file mode 100644 index 20def9233..000000000 --- a/app/src/main/java/org/linphone/call/CallActivity.java +++ /dev/null @@ -1,1211 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call; - -import android.Manifest; -import android.app.Dialog; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.os.CountDownTimer; -import android.os.SystemClock; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.TextureView; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.Chronometer; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import androidx.drawerlayout.widget.DrawerLayout; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.activities.LinphoneGenericActivity; -import org.linphone.chat.ChatActivity; -import org.linphone.compatibility.Compatibility; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.ContactsUpdatedListener; -import org.linphone.contacts.LinphoneContact; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.core.Address; -import org.linphone.core.Call; -import org.linphone.core.ChatMessage; -import org.linphone.core.ChatRoom; -import org.linphone.core.Core; -import org.linphone.core.CoreListener; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.tools.Log; -import org.linphone.dialer.DialerActivity; -import org.linphone.service.LinphoneService; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.LinphoneUtils; - -public class CallActivity extends LinphoneGenericActivity - implements CallStatusBarFragment.StatsClikedListener, - ContactsUpdatedListener, - CallActivityInterface { - private static final int SECONDS_BEFORE_HIDING_CONTROLS = 4000; - private static final int SECONDS_BEFORE_DENYING_CALL_UPDATE = 30000; - - private static final int CAMERA_TO_TOGGLE_VIDEO = 0; - private static final int MIC_TO_DISABLE_MUTE = 1; - private static final int WRITE_EXTERNAL_STORAGE_FOR_RECORDING = 2; - private static final int CAMERA_TO_ACCEPT_UPDATE = 3; - private static final int ALL_PERMISSIONS = 4; - - private static class HideControlsRunnable implements Runnable { - private WeakReference mWeakCallActivity; - - public HideControlsRunnable(CallActivity activity) { - mWeakCallActivity = new WeakReference<>(activity); - } - - @Override - public void run() { - // Make sure that at the time this is executed this is still required - if (!LinphoneContext.isReady()) return; - - Call call = LinphoneManager.getCore().getCurrentCall(); - if (call != null && call.getCurrentParams().videoEnabled()) { - CallActivity activity = mWeakCallActivity.get(); - if (activity != null) activity.updateButtonsVisibility(false); - } - } - } - - private final HideControlsRunnable mHideControlsRunnable = new HideControlsRunnable(this); - - private float mPreviewX, mPreviewY; - private TextureView mLocalPreview, mRemoteVideo; - private RelativeLayout mButtons, - mActiveCalls, - mContactAvatar, - mActiveCallHeader, - mConferenceHeader; - private LinearLayout mCallsList, mCallPausedByRemote, mConferenceList; - private ImageView mMicro, mSpeaker, mVideo; - private ImageView mPause, mSwitchCamera, mRecordingInProgress; - private ImageView mExtrasButtons, mAddCall, mTransferCall, mRecordCall, mConference; - private ImageView mAudioRoute, mRouteEarpiece, mRouteSpeaker, mRouteBluetooth; - private TextView mContactName, mMissedMessages; - private ProgressBar mVideoInviteInProgress; - private Chronometer mCallTimer; - private CountDownTimer mCallUpdateCountDownTimer; - private Dialog mCallUpdateDialog; - - private CallStatsFragment mStatsFragment; - private Core mCore; - private CoreListener mListener; - private AndroidAudioManager mAudioManager; - private VideoZoomHelper mZoomHelper; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - Compatibility.setShowWhenLocked(this, true); - - setContentView(R.layout.call); - - mLocalPreview = findViewById(R.id.local_preview_texture); - mLocalPreview.setOnTouchListener( - new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - return moveLocalPreview(v, event); - } - }); - - mRemoteVideo = findViewById(R.id.remote_video_texture); - mRemoteVideo.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - makeButtonsVisibleTemporary(); - } - }); - - mActiveCalls = findViewById(R.id.active_calls); - mActiveCallHeader = findViewById(R.id.active_call); - mCallPausedByRemote = findViewById(R.id.remote_pause); - mCallsList = findViewById(R.id.calls_list); - mConferenceList = findViewById(R.id.conference_list); - mConferenceHeader = findViewById(R.id.conference_header); - mButtons = findViewById(R.id.buttons); - - ImageView conferencePause = findViewById(R.id.conference_pause); - conferencePause.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - LinphoneManager.getCallManager().pauseConference(); - updateCallsList(); - } - }); - - mContactName = findViewById(R.id.current_contact_name); - mContactAvatar = findViewById(R.id.avatar_layout); - mCallTimer = findViewById(R.id.current_call_timer); - - mVideoInviteInProgress = findViewById(R.id.video_in_progress); - mVideo = findViewById(R.id.video); - mVideo.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - if (checkAndRequestPermission( - Manifest.permission.CAMERA, CAMERA_TO_TOGGLE_VIDEO)) { - toggleVideo(); - } - } - }); - - mMicro = findViewById(R.id.micro); - mMicro.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - if (checkAndRequestPermission( - Manifest.permission.RECORD_AUDIO, MIC_TO_DISABLE_MUTE)) { - toggleMic(); - } - } - }); - - mSpeaker = findViewById(R.id.speaker); - mSpeaker.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - toggleSpeaker(); - } - }); - - mAudioRoute = findViewById(R.id.audio_route); - mAudioRoute.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - toggleAudioRouteButtons(); - } - }); - - mRouteEarpiece = findViewById(R.id.route_earpiece); - mRouteEarpiece.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - mAudioManager.routeAudioToEarPiece(); - updateAudioRouteButtons(); - } - }); - - mRouteSpeaker = findViewById(R.id.route_speaker); - mRouteSpeaker.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - mAudioManager.routeAudioToSpeaker(); - updateAudioRouteButtons(); - } - }); - - mRouteBluetooth = findViewById(R.id.route_bluetooth); - mRouteBluetooth.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - mAudioManager.routeAudioToBluetooth(); - updateAudioRouteButtons(); - } - }); - - mExtrasButtons = findViewById(R.id.options); - mExtrasButtons.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - toggleExtrasButtons(); - } - }); - mExtrasButtons.setSelected(false); - mExtrasButtons.setEnabled(!getResources().getBoolean(R.bool.disable_options_in_call)); - - mAddCall = findViewById(R.id.add_call); - mAddCall.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - goBackToDialer(); - } - }); - - mTransferCall = findViewById(R.id.transfer); - mTransferCall.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - goBackToDialerAndDisplayTransferButton(); - } - }); - mTransferCall.setEnabled(getResources().getBoolean(R.bool.allow_transfers)); - - mConference = findViewById(R.id.conference); - mConference.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - mCore.addAllToConference(); - } - }); - - mRecordCall = findViewById(R.id.record_call); - mRecordCall.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - if (checkAndRequestPermission( - Manifest.permission.WRITE_EXTERNAL_STORAGE, - WRITE_EXTERNAL_STORAGE_FOR_RECORDING)) { - toggleRecording(); - } - } - }); - - ImageView numpadButton = findViewById(R.id.dialer); - numpadButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - View numpad = findViewById(R.id.numpad); - boolean isNumpadVisible = numpad.getVisibility() == View.VISIBLE; - numpad.setVisibility(isNumpadVisible ? View.GONE : View.VISIBLE); - v.setSelected(!isNumpadVisible); - } - }); - numpadButton.setSelected(false); - - ImageView hangUp = findViewById(R.id.hang_up); - hangUp.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - LinphoneManager.getCallManager().terminateCurrentCallOrConferenceOrAll(); - } - }); - - ImageView chat = findViewById(R.id.chat); - chat.setEnabled(!getResources().getBoolean(R.bool.disable_chat)); - chat.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - goToChatList(); - } - }); - - mPause = findViewById(R.id.pause); - mPause.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - togglePause(mCore.getCurrentCall()); - } - }); - - mSwitchCamera = findViewById(R.id.switchCamera); - mSwitchCamera.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - LinphoneManager.getCallManager().switchCamera(); - } - }); - - mRecordingInProgress = findViewById(R.id.recording); - mRecordingInProgress.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - toggleRecording(); - } - }); - - mMissedMessages = findViewById(R.id.missed_chats); - - DrawerLayout sideMenu = findViewById(R.id.side_menu); - RelativeLayout sideMenuContent = findViewById(R.id.side_menu_content); - mStatsFragment = - (CallStatsFragment) getFragmentManager().findFragmentById(R.id.call_stats_fragment); - mStatsFragment.setDrawer(sideMenu, sideMenuContent); - - CallStatusBarFragment statusBarFragment = - (CallStatusBarFragment) - getFragmentManager().findFragmentById(R.id.status_bar_fragment); - statusBarFragment.setStatsListener(this); - - mZoomHelper = new VideoZoomHelper(this, mRemoteVideo); - - mListener = - new CoreListenerStub() { - @Override - public void onMessageReceived(Core core, ChatRoom cr, ChatMessage message) { - updateMissedChatCount(); - } - - @Override - public void onCallStateChanged( - Core core, Call call, Call.State state, String message) { - if (state == Call.State.End || state == Call.State.Released) { - if (core.getCallsNb() == 0) { - finish(); - } else { - showVideoControls(false); - } - } else if (state == Call.State.PausedByRemote) { - if (core.getCurrentCall() != null) { - showVideoControls(false); - mCallPausedByRemote.setVisibility(View.VISIBLE); - } - } else if (state == Call.State.Pausing || state == Call.State.Paused) { - if (core.getCurrentCall() != null) { - showVideoControls(false); - } - } else if (state == Call.State.StreamsRunning) { - mCallPausedByRemote.setVisibility(View.GONE); - - if (call.getCurrentParams().videoEnabled()) { - AndroidAudioManager manager = LinphoneManager.getAudioManager(); - if (!manager.isUsingBluetoothAudioRoute() - && !manager.isWiredHeadsetAvailable()) { - LinphoneManager.getAudioManager().routeAudioToSpeaker(); - } - } - - setCurrentCallContactInformation(); - updateInterfaceDependingOnVideo(); - } else if (state == Call.State.UpdatedByRemote) { - // If the correspondent asks for video while in audio call - boolean videoEnabled = LinphonePreferences.instance().isVideoEnabled(); - if (!videoEnabled) { - // Video is disabled globally, don't even ask user - acceptCallUpdate(false); - return; - } - - boolean showAcceptUpdateDialog = - LinphoneManager.getCallManager() - .shouldShowAcceptCallUpdateDialog(call); - if (showAcceptUpdateDialog) { - showAcceptCallUpdateDialog(); - createTimerForDialog(SECONDS_BEFORE_DENYING_CALL_UPDATE); - } - } - - updateButtons(); - updateCallsList(); - } - - @Override - public void onAudioDevicesListUpdated(@NonNull Core core) { - if (mAudioManager.isBluetoothHeadsetConnected()) { - mAudioManager.routeAudioToBluetooth(); - } - updateButtons(); - } - }; - - mCore = LinphoneManager.getCore(); - if (mCore != null) { - boolean recordAudioPermissionGranted = - checkPermission(Manifest.permission.RECORD_AUDIO); - if (!recordAudioPermissionGranted) { - Log.w("[Call Activity] RECORD_AUDIO permission denied, muting microphone"); - mCore.enableMic(false); - } - - Call call = mCore.getCurrentCall(); - boolean videoEnabled = - LinphonePreferences.instance().isVideoEnabled() - && call != null - && call.getCurrentParams().videoEnabled(); - - if (videoEnabled) { - mAudioManager = LinphoneManager.getAudioManager(); - if (!mAudioManager.isWiredHeadsetAvailable() - && !mAudioManager.isUsingBluetoothAudioRoute()) { - mAudioManager.routeAudioToSpeaker(); - mSpeaker.setSelected(true); - } - } - } - } - - @Override - protected void onStart() { - super.onStart(); - - // This also must be done here in case of an outgoing call accepted - // before user granted or denied permissions - // or if an incoming call was answer from the notification - checkAndRequestCallPermissions(); - - mCore = LinphoneManager.getCore(); - if (mCore != null) { - mCore.setNativeVideoWindowId(mRemoteVideo); - mCore.setNativePreviewWindowId(mLocalPreview); - mCore.addListener(mListener); - } - } - - @Override - protected void onResume() { - super.onResume(); - - mAudioManager = LinphoneManager.getAudioManager(); - - updateButtons(); - updateMissedChatCount(); - updateInterfaceDependingOnVideo(); - - updateCallsList(); - ContactsManager.getInstance().addContactsListener(this); - LinphoneManager.getCallManager().setCallInterface(this); - - if (mCore.getCallsNb() == 0) { - Log.w("[Call Activity] Resuming but no call found..."); - finish(); - } - - if (LinphoneService.isReady()) LinphoneService.instance().destroyOverlay(); - } - - @Override - protected void onPause() { - ContactsManager.getInstance().removeContactsListener(this); - LinphoneManager.getCallManager().setCallInterface(null); - - Core core = LinphoneManager.getCore(); - if (LinphonePreferences.instance().isOverlayEnabled() - && core != null - && core.getCurrentCall() != null) { - Call call = core.getCurrentCall(); - if (call.getState() == Call.State.StreamsRunning) { - // Prevent overlay creation if video call is paused by remote - if (LinphoneService.isReady()) LinphoneService.instance().createOverlay(); - } - } - - super.onPause(); - } - - @Override - protected void onDestroy() { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.removeListener(mListener); - core.setNativeVideoWindowId(null); - core.setNativePreviewWindowId(null); - } - - if (mZoomHelper != null) { - mZoomHelper.destroy(); - mZoomHelper = null; - } - if (mCallUpdateCountDownTimer != null) { - mCallUpdateCountDownTimer.cancel(); - mCallUpdateCountDownTimer = null; - } - - mCallTimer.stop(); - mCallTimer = null; - - mListener = null; - mLocalPreview = null; - mRemoteVideo = null; - mStatsFragment = null; - - mButtons = null; - mActiveCalls = null; - mContactAvatar = null; - mActiveCallHeader = null; - mConferenceHeader = null; - mCallsList = null; - mCallPausedByRemote = null; - mConferenceList = null; - mMicro = null; - mSpeaker = null; - mVideo = null; - mPause = null; - mSwitchCamera = null; - mRecordingInProgress = null; - mExtrasButtons = null; - mAddCall = null; - mTransferCall = null; - mRecordCall = null; - mConference = null; - mAudioRoute = null; - mRouteEarpiece = null; - mRouteSpeaker = null; - mRouteBluetooth = null; - mContactName = null; - mMissedMessages = null; - mVideoInviteInProgress = null; - mCallUpdateDialog = null; - - super.onDestroy(); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (mAudioManager.onKeyVolumeAdjust(keyCode)) return true; - return super.onKeyDown(keyCode, event); - } - - @Override - public void onStatsClicked() { - if (mStatsFragment.isOpened()) { - mStatsFragment.openOrCloseSideMenu(false, true); - } else { - mStatsFragment.openOrCloseSideMenu(true, true); - } - } - - @Override - public void onRequestPermissionsResult( - int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - // Permission not granted, won't change anything - - if (requestCode == ALL_PERMISSIONS) { - for (int index = 0; index < permissions.length; index++) { - int granted = grantResults[index]; - if (granted == PackageManager.PERMISSION_GRANTED) { - String permission = permissions[index]; - if (Manifest.permission.RECORD_AUDIO.equals(permission)) { - toggleMic(); - } else if (Manifest.permission.CAMERA.equals(permission)) { - LinphoneUtils.reloadVideoDevices(); - } - } - } - } else { - if (grantResults[0] != PackageManager.PERMISSION_GRANTED) return; - switch (requestCode) { - case CAMERA_TO_TOGGLE_VIDEO: - LinphoneUtils.reloadVideoDevices(); - toggleVideo(); - break; - case MIC_TO_DISABLE_MUTE: - toggleMic(); - break; - case WRITE_EXTERNAL_STORAGE_FOR_RECORDING: - toggleRecording(); - break; - case CAMERA_TO_ACCEPT_UPDATE: - LinphoneUtils.reloadVideoDevices(); - acceptCallUpdate(true); - break; - } - } - } - - private boolean checkPermission(String permission) { - int granted = getPackageManager().checkPermission(permission, getPackageName()); - Log.i( - "[Permission] " - + permission - + " permission is " - + (granted == PackageManager.PERMISSION_GRANTED ? "granted" : "denied")); - return granted == PackageManager.PERMISSION_GRANTED; - } - - private boolean checkAndRequestPermission(String permission, int result) { - if (!checkPermission(permission)) { - Log.i("[Permission] Asking for " + permission); - ActivityCompat.requestPermissions(this, new String[] {permission}, result); - return false; - } - return true; - } - - private void checkAndRequestCallPermissions() { - ArrayList permissionsList = new ArrayList<>(); - - int recordAudio = - getPackageManager() - .checkPermission(Manifest.permission.RECORD_AUDIO, getPackageName()); - Log.i( - "[Permission] Record audio permission is " - + (recordAudio == PackageManager.PERMISSION_GRANTED - ? "granted" - : "denied")); - int camera = - getPackageManager().checkPermission(Manifest.permission.CAMERA, getPackageName()); - Log.i( - "[Permission] Camera permission is " - + (camera == PackageManager.PERMISSION_GRANTED ? "granted" : "denied")); - - int readPhoneState = - getPackageManager() - .checkPermission(Manifest.permission.READ_PHONE_STATE, getPackageName()); - Log.i( - "[Permission] Read phone state permission is " - + (camera == PackageManager.PERMISSION_GRANTED ? "granted" : "denied")); - - if (recordAudio != PackageManager.PERMISSION_GRANTED) { - Log.i("[Permission] Asking for record audio"); - permissionsList.add(Manifest.permission.RECORD_AUDIO); - } - if (readPhoneState != PackageManager.PERMISSION_GRANTED) { - Log.i("[Permission] Asking for read phone state"); - permissionsList.add(Manifest.permission.READ_PHONE_STATE); - } - - Call call = mCore.getCurrentCall(); - if (LinphonePreferences.instance().shouldInitiateVideoCall() - || (LinphonePreferences.instance().shouldAutomaticallyAcceptVideoRequests() - && call != null - && call.getRemoteParams().videoEnabled())) { - if (camera != PackageManager.PERMISSION_GRANTED) { - Log.i("[Permission] Asking for camera"); - permissionsList.add(Manifest.permission.CAMERA); - } - } - - if (permissionsList.size() > 0) { - String[] permissions = new String[permissionsList.size()]; - permissions = permissionsList.toArray(permissions); - ActivityCompat.requestPermissions(this, permissions, ALL_PERMISSIONS); - } - } - - @Override - public void onContactsUpdated() { - setCurrentCallContactInformation(); - } - - @Override - public void onUserLeaveHint() { - if (mCore == null) return; - Call call = mCore.getCurrentCall(); - if (call == null) return; - boolean videoEnabled = - LinphonePreferences.instance().isVideoEnabled() - && call.getCurrentParams().videoEnabled(); - if (videoEnabled && getResources().getBoolean(R.bool.allow_pip_while_video_call)) { - Compatibility.enterPipMode(this); - } - } - - @Override - public void onPictureInPictureModeChanged( - boolean isInPictureInPictureMode, Configuration newConfig) { - if (isInPictureInPictureMode) { - updateButtonsVisibility(false); - } - } - - @Override - public void refreshInCallActions() { - updateButtons(); - } - - @Override - public void resetCallControlsHidingTimer() { - LinphoneUtils.removeFromUIThreadDispatcher(mHideControlsRunnable); - LinphoneUtils.dispatchOnUIThreadAfter( - mHideControlsRunnable, SECONDS_BEFORE_HIDING_CONTROLS); - } - - // BUTTONS - - private void updateAudioRouteButtons() { - mRouteSpeaker.setSelected(mAudioManager.isAudioRoutedToSpeaker()); - mRouteBluetooth.setSelected(mAudioManager.isUsingBluetoothAudioRoute()); - mRouteEarpiece.setSelected(mAudioManager.isAudioRoutedToEarpiece()); - } - - private void updateButtons() { - Call call = mCore.getCurrentCall(); - - mMicro.setSelected(!mCore.micEnabled()); - - mSpeaker.setSelected(mAudioManager.isAudioRoutedToSpeaker()); - - updateAudioRouteButtons(); - - boolean isBluetoothAvailable = mAudioManager.isBluetoothHeadsetConnected(); - mSpeaker.setVisibility(isBluetoothAvailable ? View.GONE : View.VISIBLE); - mAudioRoute.setVisibility(isBluetoothAvailable ? View.VISIBLE : View.GONE); - if (!isBluetoothAvailable) { - mRouteBluetooth.setVisibility(View.GONE); - mRouteSpeaker.setVisibility(View.GONE); - mRouteEarpiece.setVisibility(View.GONE); - } - - mVideo.setEnabled( - LinphonePreferences.instance().isVideoEnabled() - && call != null - && !call.mediaInProgress()); - mVideo.setSelected(call != null && call.getCurrentParams().videoEnabled()); - mSwitchCamera.setVisibility( - call != null && call.getCurrentParams().videoEnabled() - ? View.VISIBLE - : View.INVISIBLE); - - mPause.setEnabled(call != null && !call.mediaInProgress()); - - mRecordCall.setSelected(call != null && call.isRecording()); - mRecordingInProgress.setVisibility( - call != null && call.isRecording() ? View.VISIBLE : View.GONE); - - mConference.setEnabled( - mCore.getCallsNb() > 1 - && mCore.getCallsNb() > mCore.getConferenceSize() - && !mCore.soundResourcesLocked()); - } - - private void toggleMic() { - mCore.enableMic(!mCore.micEnabled()); - mMicro.setSelected(!mCore.micEnabled()); - } - - private void toggleSpeaker() { - if (mAudioManager.isAudioRoutedToSpeaker()) { - mAudioManager.routeAudioToEarPiece(); - } else { - mAudioManager.routeAudioToSpeaker(); - } - mSpeaker.setSelected(mAudioManager.isAudioRoutedToSpeaker()); - } - - private void toggleVideo() { - Call call = mCore.getCurrentCall(); - if (call == null) return; - - mVideoInviteInProgress.setVisibility(View.VISIBLE); - mVideo.setEnabled(false); - if (call.getCurrentParams().videoEnabled()) { - LinphoneManager.getCallManager().removeVideo(); - } else { - LinphoneManager.getCallManager().addVideo(); - } - } - - private void togglePause(Call call) { - if (call == null) return; - - if (call == mCore.getCurrentCall()) { - call.pause(); - mPause.setSelected(true); - } else if (call.getState() == Call.State.Paused) { - call.resume(); - mPause.setSelected(false); - } - } - - private void toggleAudioRouteButtons() { - mAudioRoute.setSelected(!mAudioRoute.isSelected()); - mRouteEarpiece.setVisibility(mAudioRoute.isSelected() ? View.VISIBLE : View.GONE); - mRouteSpeaker.setVisibility(mAudioRoute.isSelected() ? View.VISIBLE : View.GONE); - mRouteBluetooth.setVisibility(mAudioRoute.isSelected() ? View.VISIBLE : View.GONE); - } - - private void toggleExtrasButtons() { - mExtrasButtons.setSelected(!mExtrasButtons.isSelected()); - mAddCall.setVisibility(mExtrasButtons.isSelected() ? View.VISIBLE : View.GONE); - mTransferCall.setVisibility(mExtrasButtons.isSelected() ? View.VISIBLE : View.GONE); - mRecordCall.setVisibility(mExtrasButtons.isSelected() ? View.VISIBLE : View.GONE); - mConference.setVisibility(mExtrasButtons.isSelected() ? View.VISIBLE : View.GONE); - } - - private void toggleRecording() { - Call call = mCore.getCurrentCall(); - if (call == null) return; - - if (call.isRecording()) { - call.stopRecording(); - } else { - call.startRecording(); - } - mRecordCall.setSelected(call.isRecording()); - mRecordingInProgress.setVisibility(call.isRecording() ? View.VISIBLE : View.INVISIBLE); - } - - private void updateMissedChatCount() { - int count = 0; - if (mCore != null) { - count = mCore.getUnreadChatMessageCountFromActiveLocals(); - } - - if (count > 0) { - mMissedMessages.setText(String.valueOf(count)); - mMissedMessages.setVisibility(View.VISIBLE); - } else { - mMissedMessages.clearAnimation(); - mMissedMessages.setVisibility(View.GONE); - } - } - - private void updateButtonsVisibility(boolean visible) { - findViewById(R.id.status_bar_fragment).setVisibility(visible ? View.VISIBLE : View.GONE); - if (mActiveCalls != null) mActiveCalls.setVisibility(visible ? View.VISIBLE : View.GONE); - if (mButtons != null) mButtons.setVisibility(visible ? View.VISIBLE : View.GONE); - } - - private void makeButtonsVisibleTemporary() { - updateButtonsVisibility(true); - resetCallControlsHidingTimer(); - } - - // VIDEO RELATED - - private void showVideoControls(boolean videoEnabled) { - mContactAvatar.setVisibility(videoEnabled ? View.GONE : View.VISIBLE); - mRemoteVideo.setVisibility(videoEnabled ? View.VISIBLE : View.GONE); - mLocalPreview.setVisibility(videoEnabled ? View.VISIBLE : View.GONE); - mSwitchCamera.setVisibility(videoEnabled ? View.VISIBLE : View.INVISIBLE); - updateButtonsVisibility(!videoEnabled); - mVideo.setSelected(videoEnabled); - LinphoneManager.getInstance().enableProximitySensing(!videoEnabled); - - if (!videoEnabled) { - LinphoneUtils.removeFromUIThreadDispatcher(mHideControlsRunnable); - } - } - - private void updateInterfaceDependingOnVideo() { - Call call = mCore.getCurrentCall(); - if (call == null) { - showVideoControls(false); - return; - } - - mVideoInviteInProgress.setVisibility(View.GONE); - mVideo.setEnabled( - LinphonePreferences.instance().isVideoEnabled() - && call != null - && !call.mediaInProgress()); - - boolean videoEnabled = - LinphonePreferences.instance().isVideoEnabled() - && call != null - && call.getCurrentParams().videoEnabled(); - showVideoControls(videoEnabled); - } - - private boolean moveLocalPreview(View view, MotionEvent motionEvent) { - switch (motionEvent.getAction()) { - case MotionEvent.ACTION_DOWN: - mPreviewX = view.getX() - motionEvent.getRawX(); - mPreviewY = view.getY() - motionEvent.getRawY(); - break; - - case MotionEvent.ACTION_MOVE: - view.animate() - .x(motionEvent.getRawX() + mPreviewX) - .y(motionEvent.getRawY() + mPreviewY) - .setDuration(0) - .start(); - break; - default: - return false; - } - return true; - } - - // NAVIGATION - - private void goBackToDialer() { - Intent intent = new Intent(); - intent.setClass(this, DialerActivity.class); - intent.putExtra("isTransfer", false); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - startActivity(intent); - } - - private void goBackToDialerAndDisplayTransferButton() { - Intent intent = new Intent(); - intent.setClass(this, DialerActivity.class); - intent.putExtra("isTransfer", true); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - startActivity(intent); - } - - private void goToChatList() { - Intent intent = new Intent(); - intent.setClass(this, ChatActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - startActivity(intent); - } - - // CALL UPDATE - - private void createTimerForDialog(long time) { - mCallUpdateCountDownTimer = - new CountDownTimer(time, 1000) { - public void onTick(long millisUntilFinished) {} - - public void onFinish() { - if (mCallUpdateDialog != null) { - mCallUpdateDialog.dismiss(); - mCallUpdateDialog = null; - } - acceptCallUpdate(false); - } - }.start(); - } - - private void acceptCallUpdate(boolean accept) { - if (mCallUpdateCountDownTimer != null) { - mCallUpdateCountDownTimer.cancel(); - } - LinphoneManager.getCallManager().acceptCallUpdate(accept); - } - - private void showAcceptCallUpdateDialog() { - mCallUpdateDialog = new Dialog(this); - mCallUpdateDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); - mCallUpdateDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); - mCallUpdateDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - mCallUpdateDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - Drawable d = new ColorDrawable(ContextCompat.getColor(this, R.color.dark_grey_color)); - d.setAlpha(200); - mCallUpdateDialog.setContentView(R.layout.dialog); - mCallUpdateDialog - .getWindow() - .setLayout( - WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.MATCH_PARENT); - mCallUpdateDialog.getWindow().setBackgroundDrawable(d); - - TextView customText = mCallUpdateDialog.findViewById(R.id.dialog_message); - customText.setText(getResources().getString(R.string.add_video_dialog)); - mCallUpdateDialog.findViewById(R.id.dialog_delete_button).setVisibility(View.GONE); - Button accept = mCallUpdateDialog.findViewById(R.id.dialog_ok_button); - accept.setVisibility(View.VISIBLE); - accept.setText(R.string.accept); - Button cancel = mCallUpdateDialog.findViewById(R.id.dialog_cancel_button); - cancel.setText(R.string.decline); - - accept.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - if (checkPermission(Manifest.permission.CAMERA)) { - acceptCallUpdate(true); - } else { - checkAndRequestPermission( - Manifest.permission.CAMERA, CAMERA_TO_ACCEPT_UPDATE); - } - mCallUpdateDialog.dismiss(); - mCallUpdateDialog = null; - } - }); - - cancel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - acceptCallUpdate(false); - mCallUpdateDialog.dismiss(); - mCallUpdateDialog = null; - } - }); - mCallUpdateDialog.show(); - } - - // CONFERENCE - - private void displayConferenceCall(final Call call) { - LinearLayout conferenceCallView = - (LinearLayout) - LayoutInflater.from(this) - .inflate(R.layout.call_conference_cell, null, false); - - TextView contactNameView = conferenceCallView.findViewById(R.id.contact_name); - LinphoneContact contact = - ContactsManager.getInstance().findContactFromAddress(call.getRemoteAddress()); - if (contact != null) { - ContactAvatar.displayAvatar( - contact, conferenceCallView.findViewById(R.id.avatar_layout), true); - contactNameView.setText(contact.getFullName()); - } else { - String displayName = LinphoneUtils.getAddressDisplayName(call.getRemoteAddress()); - ContactAvatar.displayAvatar( - displayName, conferenceCallView.findViewById(R.id.avatar_layout), true); - contactNameView.setText(displayName); - } - - Chronometer timer = conferenceCallView.findViewById(R.id.call_timer); - timer.setBase(SystemClock.elapsedRealtime() - 1000 * call.getDuration()); - timer.start(); - - ImageView removeFromConference = - conferenceCallView.findViewById(R.id.remove_from_conference); - removeFromConference.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - LinphoneManager.getCallManager().removeCallFromConference(call); - } - }); - - mConferenceList.addView(conferenceCallView); - } - - private void displayPausedConference() { - LinearLayout pausedConferenceView = - (LinearLayout) - LayoutInflater.from(this) - .inflate(R.layout.call_conference_paused_cell, null, false); - - ImageView conferenceResume = pausedConferenceView.findViewById(R.id.conference_resume); - conferenceResume.setSelected(true); - conferenceResume.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - LinphoneManager.getCallManager().resumeConference(); - updateCallsList(); - } - }); - - mCallsList.addView(pausedConferenceView); - } - - // OTHER - - private void updateCallsList() { - Call currentCall = mCore.getCurrentCall(); - if (currentCall != null) { - setCurrentCallContactInformation(); - } - - boolean callThatIsNotCurrentFound = false; - boolean pausedConferenceDisplayed = false; - boolean conferenceDisplayed = false; - mCallsList.removeAllViews(); - mConferenceList.removeAllViews(); - - for (Call call : mCore.getCalls()) { - if (call != null && call.getConference() != null) { - if (mCore.isInConference()) { - displayConferenceCall(call); - conferenceDisplayed = true; - } else if (!pausedConferenceDisplayed - && mCore.getCallsNb() > 1) { // Workaround for temporary SDK issue - displayPausedConference(); - pausedConferenceDisplayed = true; - } - } else if (call != null && call != currentCall) { - Call.State state = call.getState(); - if (state == Call.State.Paused - || state == Call.State.PausedByRemote - || state == Call.State.Pausing) { - displayPausedCall(call); - callThatIsNotCurrentFound = true; - } - } - } - - mCallsList.setVisibility( - pausedConferenceDisplayed || callThatIsNotCurrentFound ? View.VISIBLE : View.GONE); - mActiveCallHeader.setVisibility( - currentCall != null && !conferenceDisplayed ? View.VISIBLE : View.GONE); - mConferenceHeader.setVisibility(conferenceDisplayed ? View.VISIBLE : View.GONE); - mConferenceList.setVisibility(mConferenceHeader.getVisibility()); - } - - private void displayPausedCall(final Call call) { - LinearLayout callView = - (LinearLayout) - LayoutInflater.from(this).inflate(R.layout.call_inactive_row, null, false); - - TextView contactName = callView.findViewById(R.id.contact_name); - Address address = call.getRemoteAddress(); - LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(address); - if (contact == null) { - String displayName = LinphoneUtils.getAddressDisplayName(address); - contactName.setText(displayName); - ContactAvatar.displayAvatar(displayName, callView.findViewById(R.id.avatar_layout)); - } else { - contactName.setText(contact.getFullName()); - ContactAvatar.displayAvatar(contact, callView.findViewById(R.id.avatar_layout)); - } - - Chronometer timer = callView.findViewById(R.id.call_timer); - timer.setBase(SystemClock.elapsedRealtime() - 1000 * call.getDuration()); - timer.start(); - - ImageView resumeCall = callView.findViewById(R.id.call_pause); - resumeCall.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - togglePause(call); - } - }); - - mCallsList.addView(callView); - } - - private void updateCurrentCallTimer() { - Call call = mCore.getCurrentCall(); - if (call == null) return; - - mCallTimer.setBase(SystemClock.elapsedRealtime() - 1000 * call.getDuration()); - mCallTimer.start(); - } - - private void setCurrentCallContactInformation() { - updateCurrentCallTimer(); - - Call call = mCore.getCurrentCall(); - if (call == null) return; - - LinphoneContact contact = - ContactsManager.getInstance().findContactFromAddress(call.getRemoteAddress()); - if (contact != null) { - ContactAvatar.displayAvatar(contact, mContactAvatar, true); - mContactName.setText(contact.getFullName()); - } else { - String displayName = LinphoneUtils.getAddressDisplayName(call.getRemoteAddress()); - ContactAvatar.displayAvatar(displayName, mContactAvatar, true); - mContactName.setText(displayName); - } - } -} diff --git a/app/src/main/java/org/linphone/call/CallIncomingActivity.java b/app/src/main/java/org/linphone/call/CallIncomingActivity.java deleted file mode 100644 index c3696ce38..000000000 --- a/app/src/main/java/org/linphone/call/CallIncomingActivity.java +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call; - -import android.Manifest; -import android.app.KeyguardManager; -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.view.KeyEvent; -import android.view.TextureView; -import android.view.View; -import android.view.WindowManager; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; -import androidx.core.app.ActivityCompat; -import java.util.ArrayList; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.activities.LinphoneGenericActivity; -import org.linphone.call.views.CallIncomingAnswerButton; -import org.linphone.call.views.CallIncomingButtonListener; -import org.linphone.call.views.CallIncomingDeclineButton; -import org.linphone.compatibility.Compatibility; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.core.Address; -import org.linphone.core.Call; -import org.linphone.core.Call.State; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.LinphoneUtils; - -public class CallIncomingActivity extends LinphoneGenericActivity { - private TextView mName, mNumber; - private Call mCall; - private CoreListenerStub mListener; - private boolean mAlreadyAcceptedOrDeniedCall; - private TextureView mVideoDisplay; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - Compatibility.setShowWhenLocked(this, true); - Compatibility.setTurnScreenOn(this, true); - - setContentView(R.layout.call_incoming); - - mName = findViewById(R.id.contact_name); - mNumber = findViewById(R.id.contact_number); - mVideoDisplay = findViewById(R.id.videoSurface); - - CallIncomingAnswerButton mAccept = findViewById(R.id.answer_button); - CallIncomingDeclineButton mDecline = findViewById(R.id.decline_button); - ImageView mAcceptIcon = findViewById(R.id.acceptIcon); - lookupCurrentCall(); - - if (LinphonePreferences.instance() != null - && mCall != null - && mCall.getRemoteParams() != null - && LinphonePreferences.instance().shouldAutomaticallyAcceptVideoRequests() - && mCall.getRemoteParams().videoEnabled()) { - mAcceptIcon.setImageResource(R.drawable.call_video_start); - } - - KeyguardManager mKeyguardManager = - (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); - boolean doNotUseSliders = - getResources() - .getBoolean( - R.bool.do_not_use_sliders_to_answer_hangup_call_if_phone_unlocked); - if (doNotUseSliders && !mKeyguardManager.inKeyguardRestrictedInputMode()) { - mAccept.setSliderMode(false); - mDecline.setSliderMode(false); - } else { - mAccept.setSliderMode(true); - mDecline.setSliderMode(true); - mAccept.setDeclineButton(mDecline); - mDecline.setAnswerButton(mAccept); - } - mAccept.setListener( - new CallIncomingButtonListener() { - @Override - public void onAction() { - answer(); - } - }); - mDecline.setListener( - new CallIncomingButtonListener() { - @Override - public void onAction() { - decline(); - } - }); - - mListener = - new CoreListenerStub() { - @Override - public void onCallStateChanged( - Core core, Call call, State state, String message) { - if (state == State.End || state == State.Released) { - mCall = null; - finish(); - } - } - }; - } - - @Override - protected void onStart() { - super.onStart(); - checkAndRequestCallPermissions(); - } - - @Override - protected void onResume() { - super.onResume(); - Core core = LinphoneManager.getCore(); - if (core != null) { - core.addListener(mListener); - } - - mAlreadyAcceptedOrDeniedCall = false; - mCall = null; - - // Only one call ringing at a time is allowed - lookupCurrentCall(); - if (mCall == null) { - // The incoming call no longer exists. - Log.d("Couldn't find incoming call"); - finish(); - return; - } - - Address address = mCall.getRemoteAddress(); - LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(address); - if (contact != null) { - ContactAvatar.displayAvatar(contact, findViewById(R.id.avatar_layout), true); - mName.setText(contact.getFullName()); - } else { - String displayName = LinphoneUtils.getAddressDisplayName(address); - ContactAvatar.displayAvatar(displayName, findViewById(R.id.avatar_layout), true); - mName.setText(displayName); - } - mNumber.setText(address.asStringUriOnly()); - - if (LinphonePreferences.instance().acceptIncomingEarlyMedia()) { - if (mCall.getCurrentParams() != null && mCall.getCurrentParams().videoEnabled()) { - findViewById(R.id.avatar_layout).setVisibility(View.GONE); - mCall.getCore().setNativeVideoWindowId(mVideoDisplay); - } - } - } - - @Override - protected void onPause() { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.removeListener(mListener); - } - super.onPause(); - } - - @Override - protected void onDestroy() { - mName = null; - mNumber = null; - mCall = null; - mListener = null; - mVideoDisplay = null; - - super.onDestroy(); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (LinphoneContext.isReady() - && (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_HOME) - && mCall != null) { - mCall.terminate(); - finish(); - } - return super.onKeyDown(keyCode, event); - } - - private void lookupCurrentCall() { - if (LinphoneManager.getCore() != null) { - for (Call call : LinphoneManager.getCore().getCalls()) { - if (State.IncomingReceived == call.getState() - || State.IncomingEarlyMedia == call.getState()) { - mCall = call; - break; - } - } - } - } - - private void decline() { - if (mAlreadyAcceptedOrDeniedCall) { - return; - } - mAlreadyAcceptedOrDeniedCall = true; - - if (mCall != null) mCall.terminate(); - finish(); - } - - private void answer() { - if (mAlreadyAcceptedOrDeniedCall) { - return; - } - mAlreadyAcceptedOrDeniedCall = true; - - if (!LinphoneManager.getCallManager().acceptCall(mCall)) { - // the above method takes care of Samsung Galaxy S - Toast.makeText(this, R.string.couldnt_accept_call, Toast.LENGTH_LONG).show(); - } - } - - private void checkAndRequestCallPermissions() { - ArrayList permissionsList = new ArrayList<>(); - - int recordAudio = - getPackageManager() - .checkPermission(Manifest.permission.RECORD_AUDIO, getPackageName()); - Log.i( - "[Permission] Record audio permission is " - + (recordAudio == PackageManager.PERMISSION_GRANTED - ? "granted" - : "denied")); - int camera = - getPackageManager().checkPermission(Manifest.permission.CAMERA, getPackageName()); - Log.i( - "[Permission] Camera permission is " - + (camera == PackageManager.PERMISSION_GRANTED ? "granted" : "denied")); - - int readPhoneState = - getPackageManager() - .checkPermission(Manifest.permission.READ_PHONE_STATE, getPackageName()); - Log.i( - "[Permission] Read phone state permission is " - + (camera == PackageManager.PERMISSION_GRANTED ? "granted" : "denied")); - - if (recordAudio != PackageManager.PERMISSION_GRANTED) { - Log.i("[Permission] Asking for record audio"); - permissionsList.add(Manifest.permission.RECORD_AUDIO); - } - if (readPhoneState != PackageManager.PERMISSION_GRANTED) { - Log.i("[Permission] Asking for read phone state"); - permissionsList.add(Manifest.permission.READ_PHONE_STATE); - } - if (LinphonePreferences.instance().shouldAutomaticallyAcceptVideoRequests() - && mCall != null - && mCall.getRemoteParams() != null - && mCall.getRemoteParams().videoEnabled()) { - if (camera != PackageManager.PERMISSION_GRANTED) { - Log.i("[Permission] Asking for camera"); - permissionsList.add(Manifest.permission.CAMERA); - } - } - - if (permissionsList.size() > 0) { - String[] permissions = new String[permissionsList.size()]; - permissions = permissionsList.toArray(permissions); - ActivityCompat.requestPermissions(this, permissions, 0); - } - } - - @Override - public void onRequestPermissionsResult( - int requestCode, String[] permissions, int[] grantResults) { - for (int i = 0; i < permissions.length; i++) { - Log.i( - "[Permission] " - + permissions[i] - + " is " - + (grantResults[i] == PackageManager.PERMISSION_GRANTED - ? "granted" - : "denied")); - if (permissions[i].equals(Manifest.permission.CAMERA) - && grantResults[i] == PackageManager.PERMISSION_GRANTED) { - LinphoneUtils.reloadVideoDevices(); - } - } - } -} diff --git a/app/src/main/java/org/linphone/call/CallManager.java b/app/src/main/java/org/linphone/call/CallManager.java deleted file mode 100644 index bf367cce5..000000000 --- a/app/src/main/java/org/linphone/call/CallManager.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call; - -import android.content.ContentResolver; -import android.content.Context; -import android.provider.Settings; -import android.widget.Toast; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.core.Address; -import org.linphone.core.Call; -import org.linphone.core.CallParams; -import org.linphone.core.Core; -import org.linphone.core.MediaEncryption; -import org.linphone.core.ProxyConfig; -import org.linphone.core.tools.Log; -import org.linphone.dialer.views.AddressType; -import org.linphone.mediastream.Version; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.FileUtils; -import org.linphone.utils.LinphoneUtils; - -/** Handle call updating, reinvites. */ -public class CallManager { - private Context mContext; - private CallActivityInterface mCallInterface; - private BandwidthManager mBandwidthManager; - - public CallManager(Context context) { - mContext = context; - mBandwidthManager = new BandwidthManager(); - } - - public void destroy() { - mBandwidthManager.destroy(); - } - - public void terminateCurrentCallOrConferenceOrAll() { - Core core = LinphoneManager.getCore(); - Call call = core.getCurrentCall(); - if (call != null) { - call.terminate(); - } else if (core.isInConference()) { - core.terminateConference(); - } else { - core.terminateAllCalls(); - } - } - - public void addVideo() { - Call call = LinphoneManager.getCore().getCurrentCall(); - if (call.getState() == Call.State.End || call.getState() == Call.State.Released) return; - if (!call.getCurrentParams().videoEnabled()) { - enableCamera(call, true); - reinviteWithVideo(); - } - } - - public void removeVideo() { - Core core = LinphoneManager.getCore(); - Call call = core.getCurrentCall(); - CallParams params = core.createCallParams(call); - params.enableVideo(false); - call.update(params); - } - - public void switchCamera() { - Core core = LinphoneManager.getCore(); - if (core == null) return; - - String currentDevice = core.getVideoDevice(); - Log.i("[Call Manager] Current camera device is " + currentDevice); - - String[] devices = core.getVideoDevicesList(); - for (String d : devices) { - if (!d.equals(currentDevice) && !d.equals("StaticImage: Static picture")) { - Log.i("[Call Manager] New camera device will be " + d); - core.setVideoDevice(d); - break; - } - } - - Call call = core.getCurrentCall(); - if (call == null) { - Log.i("[Call Manager] Switching camera while not in call"); - return; - } - call.update(null); - } - - public boolean acceptCall(Call call) { - if (call == null) return false; - - Core core = LinphoneManager.getCore(); - CallParams params = core.createCallParams(call); - - boolean isLowBandwidthConnection = - !LinphoneUtils.isHighBandwidthConnection( - LinphoneContext.instance().getApplicationContext()); - - if (params != null) { - params.enableLowBandwidth(isLowBandwidthConnection); - params.setRecordFile( - FileUtils.getCallRecordingFilename(mContext, call.getRemoteAddress())); - } else { - Log.e("[Call Manager] Could not create call params for call"); - return false; - } - - call.acceptWithParams(params); - return true; - } - - public void acceptCallUpdate(boolean accept) { - Core core = LinphoneManager.getCore(); - Call call = core.getCurrentCall(); - if (call == null) { - return; - } - - CallParams params = core.createCallParams(call); - if (accept) { - params.enableVideo(true); - core.enableVideoCapture(true); - core.enableVideoDisplay(true); - - if (!LinphoneManager.getAudioManager().isUsingBluetoothAudioRoute() - && !LinphoneManager.getAudioManager().isWiredHeadsetAvailable()) { - LinphoneManager.getAudioManager().routeAudioToSpeaker(); - } - } else { - params.enableVideo(false); - } - - call.acceptUpdate(params); - } - - public void inviteAddress(Address address, boolean forceZRTP) { - boolean isLowBandwidthConnection = - !LinphoneUtils.isHighBandwidthConnection( - LinphoneContext.instance().getApplicationContext()); - - inviteAddress(address, false, isLowBandwidthConnection, forceZRTP); - } - - public void inviteAddress(Address address, boolean videoEnabled, boolean lowBandwidth) { - inviteAddress(address, videoEnabled, lowBandwidth, false); - } - - public void newOutgoingCall(AddressType address) { - String to = address.getText().toString(); - newOutgoingCall(to, address.getDisplayedName()); - } - - public void newOutgoingCall(String to, String displayName) { - if (to == null) return; - - // If to is only a username, try to find the contact to get an alias if existing - if (!to.startsWith("sip:") || !to.contains("@")) { - LinphoneContact contact = ContactsManager.getInstance().findContactFromPhoneNumber(to); - if (contact != null) { - String alias = contact.getContactFromPresenceModelForUriOrTel(to); - if (alias != null) { - to = alias; - } - } - } - - LinphonePreferences preferences = LinphonePreferences.instance(); - Core core = LinphoneManager.getCore(); - Address address; - address = core.interpretUrl(to); // InterpretUrl does normalizePhoneNumber - if (address == null) { - Log.e("[Call Manager] Couldn't convert to String to Address : " + to); - return; - } - - ProxyConfig lpc = core.getDefaultProxyConfig(); - if (mContext.getResources().getBoolean(R.bool.forbid_self_call) - && lpc != null - && address.weakEqual(lpc.getIdentityAddress())) { - return; - } - address.setDisplayName(displayName); - - boolean isLowBandwidthConnection = - !LinphoneUtils.isHighBandwidthConnection( - LinphoneContext.instance().getApplicationContext()); - - if (core.isNetworkReachable()) { - if (Version.isVideoCapable()) { - boolean prefVideoEnable = preferences.isVideoEnabled(); - boolean prefInitiateWithVideo = preferences.shouldInitiateVideoCall(); - inviteAddress( - address, - prefVideoEnable && prefInitiateWithVideo, - isLowBandwidthConnection); - } else { - inviteAddress(address, false, isLowBandwidthConnection); - } - } else { - Toast.makeText( - mContext, - mContext.getString(R.string.error_network_unreachable), - Toast.LENGTH_LONG) - .show(); - Log.e( - "[Call Manager] Error: " - + mContext.getString(R.string.error_network_unreachable)); - } - } - - public void playDtmf(ContentResolver r, char dtmf) { - try { - if (Settings.System.getInt(r, Settings.System.DTMF_TONE_WHEN_DIALING) == 0) { - // audible touch disabled: don't play on speaker, only send in outgoing stream - return; - } - } catch (Settings.SettingNotFoundException e) { - Log.e("[Call Manager] playDtmf exception: " + e); - } - - LinphoneManager.getCore().playDtmf(dtmf, -1); - } - - public boolean shouldShowAcceptCallUpdateDialog(Call call) { - if (call == null) return true; - - boolean remoteVideo = call.getRemoteParams().videoEnabled(); - boolean localVideo = call.getCurrentParams().videoEnabled(); - boolean autoAcceptCameraPolicy = - LinphonePreferences.instance().shouldAutomaticallyAcceptVideoRequests(); - return remoteVideo - && !localVideo - && !autoAcceptCameraPolicy - && !call.getCore().isInConference(); - } - - public void setCallInterface(CallActivityInterface callInterface) { - mCallInterface = callInterface; - } - - public void resetCallControlsHidingTimer() { - if (mCallInterface != null) { - mCallInterface.resetCallControlsHidingTimer(); - } - } - - public void refreshInCallActions() { - if (mCallInterface != null) { - mCallInterface.refreshInCallActions(); - } - } - - public void removeCallFromConference(Call call) { - if (call == null || call.getConference() == null) { - return; - } - call.getConference().removeParticipant(call.getRemoteAddress()); - - if (call.getCore().getConferenceSize() <= 1) { - call.getCore().leaveConference(); - } - } - - public void pauseConference() { - Core core = LinphoneManager.getCore(); - if (core == null) return; - if (core.isInConference()) { - Log.i("[Call Manager] Pausing conference"); - core.leaveConference(); - } else { - Log.w("[Call Manager] Core isn't in a conference, can't pause it"); - } - } - - public void resumeConference() { - Core core = LinphoneManager.getCore(); - if (core == null) return; - if (!core.isInConference()) { - Log.i("[Call Manager] Resuming conference"); - core.enterConference(); - } else { - Log.w("[Call Manager] Core is already in a conference, can't resume it"); - } - } - - private void inviteAddress( - Address address, boolean videoEnabled, boolean lowBandwidth, boolean forceZRTP) { - Core core = LinphoneManager.getCore(); - - CallParams params = core.createCallParams(null); - mBandwidthManager.updateWithProfileSettings(params); - - if (videoEnabled && params.videoEnabled()) { - params.enableVideo(true); - } else { - params.enableVideo(false); - } - - if (lowBandwidth) { - params.enableLowBandwidth(true); - Log.d("[Call Manager] Low bandwidth enabled in call params"); - } - - if (forceZRTP) { - params.setMediaEncryption(MediaEncryption.ZRTP); - } - - String recordFile = - FileUtils.getCallRecordingFilename( - LinphoneContext.instance().getApplicationContext(), address); - params.setRecordFile(recordFile); - - core.inviteAddressWithParams(address, params); - } - - private boolean reinviteWithVideo() { - Core core = LinphoneManager.getCore(); - Call call = core.getCurrentCall(); - if (call == null) { - Log.e("[Call Manager] Trying to add video while not in call"); - return false; - } - if (call.getRemoteParams().lowBandwidthEnabled()) { - Log.e("[Call Manager] Remote has low bandwidth, won't be able to do video"); - return false; - } - if (call.getCurrentParams().videoEnabled()) { - Log.e("[Call Manager] Video is already enabled"); - return false; - } - - CallParams params = core.createCallParams(call); - // Check if video possible regarding bandwidth limitations - mBandwidthManager.updateWithProfileSettings(params); - // Abort if not enough bandwidth... - if (!params.videoEnabled()) { - Log.e("[Call Manager] Video can't be enabled"); - return false; - } - - // Not yet in video call: try to re-invite with video - call.update(params); - return true; - } - - private void enableCamera(Call call, boolean enable) { - if (call != null) { - call.enableCamera(enable); - if (mContext.getResources().getBoolean(R.bool.enable_call_notification)) - LinphoneContext.instance() - .getNotificationManager() - .displayCallNotification(LinphoneManager.getCore().getCurrentCall()); - } - } -} diff --git a/app/src/main/java/org/linphone/call/CallOutgoingActivity.java b/app/src/main/java/org/linphone/call/CallOutgoingActivity.java deleted file mode 100644 index d538de46d..000000000 --- a/app/src/main/java/org/linphone/call/CallOutgoingActivity.java +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call; - -import android.Manifest; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.view.KeyEvent; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.WindowManager; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; -import androidx.core.app.ActivityCompat; -import java.util.ArrayList; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.activities.LinphoneGenericActivity; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.core.Address; -import org.linphone.core.Call; -import org.linphone.core.Call.State; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.Reason; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.LinphoneUtils; - -public class CallOutgoingActivity extends LinphoneGenericActivity implements OnClickListener { - private TextView mName, mNumber; - private ImageView mMicro; - private ImageView mSpeaker; - private Call mCall; - private CoreListenerStub mListener; - private boolean mIsMicMuted, mIsSpeakerEnabled; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - setContentView(R.layout.call_outgoing); - - mName = findViewById(R.id.contact_name); - mNumber = findViewById(R.id.contact_number); - - mIsMicMuted = false; - mIsSpeakerEnabled = false; - - mMicro = findViewById(R.id.micro); - mMicro.setOnClickListener(this); - mSpeaker = findViewById(R.id.speaker); - mSpeaker.setOnClickListener(this); - - ImageView hangUp = findViewById(R.id.outgoing_hang_up); - hangUp.setOnClickListener(this); - - mListener = - new CoreListenerStub() { - @Override - public void onCallStateChanged( - Core core, Call call, Call.State state, String message) { - if (state == State.Error) { - // Convert Core message for internalization - if (call.getErrorInfo().getReason() == Reason.Declined) { - Toast.makeText( - CallOutgoingActivity.this, - getString(R.string.error_call_declined), - Toast.LENGTH_SHORT) - .show(); - } else if (call.getErrorInfo().getReason() == Reason.NotFound) { - Toast.makeText( - CallOutgoingActivity.this, - getString(R.string.error_user_not_found), - Toast.LENGTH_SHORT) - .show(); - } else if (call.getErrorInfo().getReason() == Reason.NotAcceptable) { - Toast.makeText( - CallOutgoingActivity.this, - getString(R.string.error_incompatible_media), - Toast.LENGTH_SHORT) - .show(); - } else if (call.getErrorInfo().getReason() == Reason.Busy) { - Toast.makeText( - CallOutgoingActivity.this, - getString(R.string.error_user_busy), - Toast.LENGTH_SHORT) - .show(); - } else if (message != null) { - Toast.makeText( - CallOutgoingActivity.this, - getString(R.string.error_unknown) + " - " + message, - Toast.LENGTH_SHORT) - .show(); - } - } else if (state == State.End) { - // Convert Core message for internalization - if (call.getErrorInfo().getReason() == Reason.Declined) { - Toast.makeText( - CallOutgoingActivity.this, - getString(R.string.error_call_declined), - Toast.LENGTH_SHORT) - .show(); - } - } else if (state == State.Connected) { - // This is done by the LinphoneContext listener now - // startActivity(new Intent(CallOutgoingActivity.this, - // CallActivity.class)); - } - - if (state == State.End || state == State.Released) { - finish(); - } - } - }; - } - - @Override - protected void onStart() { - super.onStart(); - checkAndRequestCallPermissions(); - } - - @Override - protected void onResume() { - super.onResume(); - Core core = LinphoneManager.getCore(); - if (core != null) { - core.addListener(mListener); - } - - mCall = null; - - // Only one call ringing at a time is allowed - if (LinphoneManager.getCore() != null) { - for (Call call : LinphoneManager.getCore().getCalls()) { - State cstate = call.getState(); - if (State.OutgoingInit == cstate - || State.OutgoingProgress == cstate - || State.OutgoingRinging == cstate - || State.OutgoingEarlyMedia == cstate) { - mCall = call; - break; - } - } - } - if (mCall == null) { - Log.e("[Call Outgoing Activity] Couldn't find outgoing call"); - finish(); - return; - } - - Address address = mCall.getRemoteAddress(); - LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(address); - if (contact != null) { - ContactAvatar.displayAvatar(contact, findViewById(R.id.avatar_layout), true); - mName.setText(contact.getFullName()); - } else { - String displayName = LinphoneUtils.getAddressDisplayName(address); - ContactAvatar.displayAvatar(displayName, findViewById(R.id.avatar_layout), true); - mName.setText(displayName); - } - mNumber.setText(LinphoneUtils.getDisplayableAddress(address)); - - boolean recordAudioPermissionGranted = checkPermission(Manifest.permission.RECORD_AUDIO); - if (!recordAudioPermissionGranted) { - Log.w("[Call Outgoing Activity] RECORD_AUDIO permission denied, muting microphone"); - core.enableMic(false); - mMicro.setSelected(true); - } - } - - @Override - protected void onPause() { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.removeListener(mListener); - } - super.onPause(); - } - - @Override - protected void onDestroy() { - mName = null; - mNumber = null; - mMicro = null; - mSpeaker = null; - mCall = null; - mListener = null; - - super.onDestroy(); - } - - @Override - public void onClick(View v) { - int id = v.getId(); - - if (id == R.id.micro) { - mIsMicMuted = !mIsMicMuted; - mMicro.setSelected(mIsMicMuted); - LinphoneManager.getCore().enableMic(!mIsMicMuted); - } - if (id == R.id.speaker) { - mIsSpeakerEnabled = !mIsSpeakerEnabled; - mSpeaker.setSelected(mIsSpeakerEnabled); - if (mIsSpeakerEnabled) { - LinphoneManager.getAudioManager().routeAudioToSpeaker(); - } else { - LinphoneManager.getAudioManager().routeAudioToEarPiece(); - } - } - if (id == R.id.outgoing_hang_up) { - decline(); - } - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (LinphoneContext.isReady() - && (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_HOME)) { - mCall.terminate(); - finish(); - } - return super.onKeyDown(keyCode, event); - } - - private void decline() { - mCall.terminate(); - finish(); - } - - private void checkAndRequestCallPermissions() { - ArrayList permissionsList = new ArrayList<>(); - - int recordAudio = - getPackageManager() - .checkPermission(Manifest.permission.RECORD_AUDIO, getPackageName()); - Log.i( - "[Permission] Record audio permission is " - + (recordAudio == PackageManager.PERMISSION_GRANTED - ? "granted" - : "denied")); - int camera = - getPackageManager().checkPermission(Manifest.permission.CAMERA, getPackageName()); - Log.i( - "[Permission] Camera permission is " - + (camera == PackageManager.PERMISSION_GRANTED ? "granted" : "denied")); - - int readPhoneState = - getPackageManager() - .checkPermission(Manifest.permission.READ_PHONE_STATE, getPackageName()); - Log.i( - "[Permission] Read phone state permission is " - + (camera == PackageManager.PERMISSION_GRANTED ? "granted" : "denied")); - - if (recordAudio != PackageManager.PERMISSION_GRANTED) { - Log.i("[Permission] Asking for record audio"); - permissionsList.add(Manifest.permission.RECORD_AUDIO); - } - if (readPhoneState != PackageManager.PERMISSION_GRANTED) { - Log.i("[Permission] Asking for read phone state"); - permissionsList.add(Manifest.permission.READ_PHONE_STATE); - } - if (LinphonePreferences.instance().shouldInitiateVideoCall()) { - if (camera != PackageManager.PERMISSION_GRANTED) { - Log.i("[Permission] Asking for camera"); - permissionsList.add(Manifest.permission.CAMERA); - } - } - - if (permissionsList.size() > 0) { - String[] permissions = new String[permissionsList.size()]; - permissions = permissionsList.toArray(permissions); - ActivityCompat.requestPermissions(this, permissions, 0); - } - } - - private boolean checkPermission(String permission) { - int granted = getPackageManager().checkPermission(permission, getPackageName()); - Log.i( - "[Permission] " - + permission - + " permission is " - + (granted == PackageManager.PERMISSION_GRANTED ? "granted" : "denied")); - return granted == PackageManager.PERMISSION_GRANTED; - } - - @Override - public void onRequestPermissionsResult( - int requestCode, String[] permissions, int[] grantResults) { - for (int i = 0; i < permissions.length; i++) { - Log.i( - "[Permission] " - + permissions[i] - + " is " - + (grantResults[i] == PackageManager.PERMISSION_GRANTED - ? "granted" - : "denied")); - if (permissions[i].equals(Manifest.permission.CAMERA) - && grantResults[i] == PackageManager.PERMISSION_GRANTED) { - LinphoneUtils.reloadVideoDevices(); - } else if (permissions[i].equals(Manifest.permission.RECORD_AUDIO) - && grantResults[i] == PackageManager.PERMISSION_GRANTED) { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.enableMic(true); - mMicro.setSelected(!core.micEnabled()); - } - } - } - } -} diff --git a/app/src/main/java/org/linphone/call/CallStatsAdapter.java b/app/src/main/java/org/linphone/call/CallStatsAdapter.java deleted file mode 100644 index f963b0b75..000000000 --- a/app/src/main/java/org/linphone/call/CallStatsAdapter.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseExpandableListAdapter; -import java.util.ArrayList; -import java.util.List; -import org.linphone.R; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.core.Call; -import org.linphone.utils.LinphoneUtils; - -public class CallStatsAdapter extends BaseExpandableListAdapter { - private final Context mContext; - private List mCalls; - - public CallStatsAdapter(Context context) { - mContext = context; - mCalls = new ArrayList<>(); - } - - public void updateListItems(List listCall) { - if (listCall != null) { - mCalls = listCall; - notifyDataSetChanged(); - } - } - - @Override - public View getChildView( - int groupPosition, - int childPosition, - boolean isLastChild, - View view, - ViewGroup viewGroup) { - - CallStatsChildViewHolder holder; - - if (view != null) { - Object possibleHolder = view.getTag(); - if (possibleHolder instanceof CallStatsChildViewHolder) { - holder = (CallStatsChildViewHolder) possibleHolder; - view.setTag(holder); - } - } else { - // opening the statistics view - LayoutInflater inflater = LayoutInflater.from(mContext); - view = inflater.inflate(R.layout.call_stats_child, viewGroup, false); - } - - // filling the view - holder = new CallStatsChildViewHolder(view, mContext); - view.setTag(holder); - holder.setCall(mCalls.get(groupPosition)); - - return view; - } - - @Override - public View getGroupView( - int groupPosition, boolean isExpanded, View view, ViewGroup viewGroup) { - - CallStatsViewHolder holder = null; - if (view != null) { - Object possibleHolder = view.getTag(); - if (possibleHolder instanceof CallStatsViewHolder) { - holder = (CallStatsViewHolder) possibleHolder; - } - } else { - LayoutInflater inflater = LayoutInflater.from(mContext); - view = inflater.inflate(R.layout.call_stats_group, viewGroup, false); - } - if (holder == null) { - holder = new CallStatsViewHolder(view); - view.setTag(holder); - } - - // Recovering the current call - Call call = (Call) getGroup(groupPosition); - // Search for the associated contact - LinphoneContact contact = - ContactsManager.getInstance().findContactFromAddress(call.getRemoteAddress()); - if (contact != null) { - // Setting up the avatar - ContactAvatar.displayAvatar(contact, holder.avatarLayout); - // addition of the participant's name - holder.participantName.setText(contact.getFullName()); - } else { - String displayName = LinphoneUtils.getAddressDisplayName(call.getRemoteAddress()); - ContactAvatar.displayAvatar(displayName, holder.avatarLayout); - holder.participantName.setText(displayName); - } - - // add sip address on group view - holder.sipUri.setText(call.getRemoteAddress().asString()); - - return view; - } - - @Override - public int getGroupCount() { - return mCalls.size(); - } - - @Override - public int getChildrenCount(int groupPosition) { - return 1; - } - - @Override - public Object getGroup(int groupPosition) { - if (mCalls.isEmpty() && groupPosition < mCalls.size()) { - return null; - } - return mCalls.get(groupPosition); - } - - @Override - public Object getChild(int groupPosition, int childPosition) { - if (mCalls.isEmpty() && groupPosition < mCalls.size()) { - return null; - } - return mCalls.get(groupPosition); - } - - @Override - public long getGroupId(int groupPosition) { - return groupPosition; - } - - @Override - public long getChildId(int groupPosition, int childPosition) { - return groupPosition; - } - - @Override - public boolean hasStableIds() { - return false; - } - - @Override - public boolean isChildSelectable(int groupPosition, int childPosition) { - return false; - } -} diff --git a/app/src/main/java/org/linphone/call/CallStatsChildViewHolder.java b/app/src/main/java/org/linphone/call/CallStatsChildViewHolder.java deleted file mode 100644 index 3d55ee85e..000000000 --- a/app/src/main/java/org/linphone/call/CallStatsChildViewHolder.java +++ /dev/null @@ -1,419 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call; - -import android.content.Context; -import android.text.Html; -import android.view.View; -import android.widget.TextView; -import java.text.DecimalFormat; -import java.util.HashMap; -import java.util.Timer; -import java.util.TimerTask; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.AddressFamily; -import org.linphone.core.Call; -import org.linphone.core.CallListenerStub; -import org.linphone.core.CallParams; -import org.linphone.core.CallStats; -import org.linphone.core.PayloadType; -import org.linphone.core.StreamType; -import org.linphone.utils.LinphoneUtils; - -public class CallStatsChildViewHolder { - private Timer mTimer; - private Call mCall; - private CallListenerStub mListener; - private HashMap mEncoderTexts; - private HashMap mDecoderTexts; - private Context mContext; - - private TextView mTitleAudio; - private TextView mTitleVideo; - private TextView mCodecAudio; - private TextView mCodecVideo; - private TextView mEncoderAudio; - private TextView mDecoderAudio; - private TextView mEncoderVideo; - private TextView mDecoderVideo; - private TextView mAudioCaptureFilter; - private TextView mAudioPlayerFilter; - private TextView mVideoCaptureFilter; - private TextView mVideoDisplayFilter; - private TextView mDlAudio; - private TextView mUlAudio; - private TextView mDlVideo; - private TextView mUlVideo; - private TextView mEdlVideo; - private TextView mIceAudio; - private TextView mIceVideo; - private TextView mVideoResolutionSent; - private TextView mVideoResolutionReceived; - private TextView mVideoFpsSent; - private TextView mVideoFpsReceived; - private TextView mSenderLossRateAudio; - private TextView mReceiverLossRateAudio; - private TextView mSenderLossRateVideo; - private TextView mReceiverLossRateVideo; - private TextView mIpAudio; - private TextView mIpVideo; - private TextView mJitterBufferAudio; - private View mVideoLayout; - private View mAudioLayout; - - public CallStatsChildViewHolder(View view, Context context) { - mContext = context; - - mEncoderTexts = new HashMap<>(); - mDecoderTexts = new HashMap<>(); - - mTitleAudio = view.findViewById(R.id.call_stats_audio); - mTitleVideo = view.findViewById(R.id.call_stats_video); - mCodecAudio = view.findViewById(R.id.codec_audio); - mCodecVideo = view.findViewById(R.id.codec_video); - mEncoderAudio = view.findViewById(R.id.encoder_audio); - mDecoderAudio = view.findViewById(R.id.decoder_audio); - mEncoderVideo = view.findViewById(R.id.encoder_video); - mDecoderVideo = view.findViewById(R.id.decoder_video); - mAudioCaptureFilter = view.findViewById(R.id.audio_capture_filter); - mAudioPlayerFilter = view.findViewById(R.id.audio_player_filter); - mVideoCaptureFilter = view.findViewById(R.id.video_capture_device); - mVideoDisplayFilter = view.findViewById(R.id.display_filter); - mDlAudio = view.findViewById(R.id.downloadBandwith_audio); - mUlAudio = view.findViewById(R.id.uploadBandwith_audio); - mDlVideo = view.findViewById(R.id.downloadBandwith_video); - mUlVideo = view.findViewById(R.id.uploadBandwith_video); - mEdlVideo = view.findViewById(R.id.estimatedDownloadBandwidth_video); - mIceAudio = view.findViewById(R.id.ice_audio); - mIceVideo = view.findViewById(R.id.ice_video); - mVideoResolutionSent = view.findViewById(R.id.video_resolution_sent); - mVideoResolutionReceived = view.findViewById(R.id.video_resolution_received); - mVideoFpsSent = view.findViewById(R.id.video_fps_sent); - mVideoFpsReceived = view.findViewById(R.id.video_fps_received); - mSenderLossRateAudio = view.findViewById(R.id.senderLossRateAudio); - mReceiverLossRateAudio = view.findViewById(R.id.receiverLossRateAudio); - mSenderLossRateVideo = view.findViewById(R.id.senderLossRateVideo); - mReceiverLossRateVideo = view.findViewById(R.id.receiverLossRateVideo); - mIpAudio = view.findViewById(R.id.ip_audio); - mIpVideo = view.findViewById(R.id.ip_video); - mJitterBufferAudio = view.findViewById(R.id.jitterBufferAudio); - mVideoLayout = view.findViewById(R.id.callStatsVideo); - mAudioLayout = view.findViewById(R.id.callStatsAudio); - - mListener = - new CallListenerStub() { - public void onStateChanged(Call call, Call.State cstate, String message) { - if (cstate == Call.State.End || cstate == Call.State.Error) { - if (mTimer != null) { - org.linphone.core.tools.Log.i( - "[Call Stats] Call is terminated, stopping mCountDownTimer in charge of stats refreshing."); - mTimer.cancel(); - } - } - } - }; - } - - public void setCall(Call call) { - if (mCall != null) { - mCall.removeListener(mListener); - } - mCall = call; - mCall.addListener(mListener); - - init(); - } - - private void init() { - TimerTask mTask; - - mTimer = new Timer(); - mTask = - new TimerTask() { - @Override - public void run() { - if (mCall == null) { - mTimer.cancel(); - return; - } - - if (mTitleAudio == null - || mCodecAudio == null - || mDlVideo == null - || mEdlVideo == null - || mIceAudio == null - || mVideoResolutionSent == null - || mVideoLayout == null - || mTitleVideo == null - || mIpVideo == null - || mIpAudio == null - || mCodecVideo == null - || mDlAudio == null - || mUlAudio == null - || mUlVideo == null - || mIceVideo == null - || mVideoResolutionReceived == null) { - mTimer.cancel(); - return; - } - - LinphoneUtils.dispatchOnUIThread( - new Runnable() { - @Override - public void run() { - if (mCall == null) { - return; - } - - if (mCall.getState() != Call.State.Released) { - CallParams params = mCall.getCurrentParams(); - if (params != null) { - CallStats audioStats = - mCall.getStats(StreamType.Audio); - CallStats videoStats = null; - - if (params.videoEnabled()) - videoStats = mCall.getStats(StreamType.Video); - - PayloadType payloadAudio = - params.getUsedAudioPayloadType(); - PayloadType payloadVideo = - params.getUsedVideoPayloadType(); - - formatText( - mAudioPlayerFilter, - mContext.getString( - R.string.call_stats_player_filter), - mCall.getCore().getPlaybackDevice()); - - formatText( - mAudioCaptureFilter, - mContext.getString( - R.string.call_stats_capture_filter), - mCall.getCore().getCaptureDevice()); - - formatText( - mVideoDisplayFilter, - mContext.getString( - R.string.call_stats_display_filter), - mCall.getCore().getVideoDisplayFilter()); - - formatText( - mVideoCaptureFilter, - mContext.getString( - R.string.call_stats_capture_filter), - mCall.getCore().getVideoDevice()); - - displayMediaStats( - params, - audioStats, - payloadAudio, - mAudioLayout, - mTitleAudio, - mCodecAudio, - mDlAudio, - mUlAudio, - null, - mIceAudio, - mIpAudio, - mSenderLossRateAudio, - mReceiverLossRateAudio, - mEncoderAudio, - mDecoderAudio, - null, - null, - null, - null, - false, - mJitterBufferAudio); - - displayMediaStats( - params, - videoStats, - payloadVideo, - mVideoLayout, - mTitleVideo, - mCodecVideo, - mDlVideo, - mUlVideo, - mEdlVideo, - mIceVideo, - mIpVideo, - mSenderLossRateVideo, - mReceiverLossRateVideo, - mEncoderVideo, - mDecoderVideo, - mVideoResolutionSent, - mVideoResolutionReceived, - mVideoFpsSent, - mVideoFpsReceived, - true, - null); - } - } - } - }); - } - }; - mTimer.scheduleAtFixedRate(mTask, 0, 1000); - } - - private void displayMediaStats( - CallParams params, - CallStats stats, - PayloadType media, - View layout, - TextView title, - TextView codec, - TextView dl, - TextView ul, - TextView edl, - TextView ice, - TextView ip, - TextView senderLossRate, - TextView receiverLossRate, - TextView enc, - TextView dec, - TextView videoResolutionSent, - TextView videoResolutionReceived, - TextView videoFpsSent, - TextView videoFpsReceived, - boolean isVideo, - TextView jitterBuffer) { - - if (stats != null) { - String mime = null; - - layout.setVisibility(View.VISIBLE); - title.setVisibility(TextView.VISIBLE); - if (media != null) { - mime = media.getMimeType(); - formatText( - codec, - mContext.getString(R.string.call_stats_codec), - mime + " / " + (media.getClockRate() / 1000) + "kHz"); - } - if (mime != null) { - - formatText( - enc, - mContext.getString(R.string.call_stats_encoder_name), - getEncoderText(mime)); - formatText( - dec, - mContext.getString(R.string.call_stats_decoder_name), - getDecoderText(mime)); - } - formatText( - dl, - mContext.getString(R.string.call_stats_download), - (int) stats.getDownloadBandwidth() + " kbits/s"); - formatText( - ul, - mContext.getString(R.string.call_stats_upload), - (int) stats.getUploadBandwidth() + " kbits/s"); - if (isVideo) { - formatText( - edl, - mContext.getString(R.string.call_stats_estimated_download), - stats.getEstimatedDownloadBandwidth() + " kbits/s"); - } - formatText( - ice, - mContext.getString(R.string.call_stats_ice), - stats.getIceState().toString()); - formatText( - ip, - mContext.getString(R.string.call_stats_ip), - (stats.getIpFamilyOfRemote() == AddressFamily.Inet6) - ? "IpV6" - : (stats.getIpFamilyOfRemote() == AddressFamily.Inet) - ? "IpV4" - : "Unknown"); - - formatText( - senderLossRate, - mContext.getString(R.string.call_stats_sender_loss_rate), - new DecimalFormat("##.##").format(stats.getSenderLossRate()) + "%"); - formatText( - receiverLossRate, - mContext.getString(R.string.call_stats_receiver_loss_rate), - new DecimalFormat("##.##").format(stats.getReceiverLossRate()) + "%"); - - if (isVideo) { - formatText( - videoResolutionSent, - mContext.getString(R.string.call_stats_video_resolution_sent), - "\u2191 " + params.getSentVideoDefinition() != null - ? params.getSentVideoDefinition().getName() - : ""); - formatText( - videoResolutionReceived, - mContext.getString(R.string.call_stats_video_resolution_received), - "\u2193 " + params.getReceivedVideoDefinition() != null - ? params.getReceivedVideoDefinition().getName() - : ""); - formatText( - videoFpsSent, - mContext.getString(R.string.call_stats_video_fps_sent), - "\u2191 " + params.getSentFramerate()); - formatText( - videoFpsReceived, - mContext.getString(R.string.call_stats_video_fps_received), - "\u2193 " + params.getReceivedFramerate()); - } else { - formatText( - jitterBuffer, - mContext.getString(R.string.call_stats_jitter_buffer), - new DecimalFormat("##.##").format(stats.getJitterBufferSizeMs()) + " ms"); - } - } else { - layout.setVisibility(View.GONE); - title.setVisibility(TextView.GONE); - } - } - - private String getEncoderText(String mime) { - String ret = mEncoderTexts.get(mime); - if (ret == null) { - org.linphone.mediastream.Factory msfactory = - LinphoneManager.getCore().getMediastreamerFactory(); - ret = msfactory.getEncoderText(mime); - mEncoderTexts.put(mime, ret); - } - return ret; - } - - private String getDecoderText(String mime) { - String ret = mDecoderTexts.get(mime); - if (ret == null) { - org.linphone.mediastream.Factory msfactory = - LinphoneManager.getCore().getMediastreamerFactory(); - ret = msfactory.getDecoderText(mime); - mDecoderTexts.put(mime, ret); - } - return ret; - } - - private void formatText(TextView tv, String name, String value) { - tv.setText(Html.fromHtml("" + name + " " + value)); - } -} diff --git a/app/src/main/java/org/linphone/call/CallStatsFragment.java b/app/src/main/java/org/linphone/call/CallStatsFragment.java deleted file mode 100644 index 25c076762..000000000 --- a/app/src/main/java/org/linphone/call/CallStatsFragment.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call; - -import android.app.Fragment; -import android.os.Bundle; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ExpandableListView; -import android.widget.RelativeLayout; -import androidx.annotation.Nullable; -import androidx.drawerlayout.widget.DrawerLayout; -import java.util.Arrays; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Call; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; - -public class CallStatsFragment extends Fragment { - private DrawerLayout mSideMenu; - private RelativeLayout mSideMenuContent;; - private ExpandableListView mExpandableList; - private CallStatsAdapter mAdapter; - private CoreListenerStub mListener; - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.call_stats, container, false); - - mExpandableList = view.findViewById(R.id.devices_list); - - mListener = - new CoreListenerStub() { - @Override - public void onCallStateChanged( - Core core, Call call, Call.State state, String message) { - if (state == Call.State.End || state == Call.State.Error) { - mAdapter.updateListItems( - Arrays.asList(LinphoneManager.getCore().getCalls())); - } - } - }; - return view; - } - - @Override - public void onResume() { - super.onResume(); - Core core = LinphoneManager.getCore(); - - if (mAdapter == null) { - mAdapter = new CallStatsAdapter(getActivity()); - - mExpandableList.setAdapter(mAdapter); - // allows you to open the first child in the list - mExpandableList.expandGroup(0); - } - - // Sends calls from the list to the adapter - if (core != null && core.getCallsNb() >= 1) { - mAdapter.updateListItems(Arrays.asList(core.getCalls())); - } - - core.addListener(mListener); - } - - @Override - public void onPause() { - super.onPause(); - - Core core = LinphoneManager.getCore(); - if (core != null) { - core.removeListener(mListener); - } - } - - public void setDrawer(DrawerLayout drawer, RelativeLayout content) { - mSideMenu = drawer; - mSideMenuContent = content; - - if (getResources().getBoolean(R.bool.hide_in_call_stats)) { - drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); - } - } - - public boolean isOpened() { - return mSideMenu != null && mSideMenu.isDrawerVisible(Gravity.LEFT); - } - - public void closeDrawer() { - openOrCloseSideMenu(false, false); - } - - public void openOrCloseSideMenu(boolean open, boolean animate) { - if (mSideMenu == null || mSideMenuContent == null) return; - if (getResources().getBoolean(R.bool.hide_in_call_stats)) return; - - if (open) { - mSideMenu.openDrawer(mSideMenuContent, animate); - } else { - mSideMenu.closeDrawer(mSideMenuContent, animate); - } - } -} diff --git a/app/src/main/java/org/linphone/call/CallStatusBarFragment.java b/app/src/main/java/org/linphone/call/CallStatusBarFragment.java deleted file mode 100644 index 0724c3ae5..000000000 --- a/app/src/main/java/org/linphone/call/CallStatusBarFragment.java +++ /dev/null @@ -1,441 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call; - -import android.app.Dialog; -import android.app.Fragment; -import android.content.Context; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; -import androidx.core.content.ContextCompat; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Call; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.MediaEncryption; -import org.linphone.core.ProxyConfig; -import org.linphone.core.RegistrationState; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.LinphoneUtils; - -public class CallStatusBarFragment extends Fragment { - private TextView mStatusText; - private ImageView mStatusLed, mCallQuality, mEncryption; - private Runnable mCallQualityUpdater; - private CoreListenerStub mListener; - private Dialog mZrtpDialog = null; - private int mDisplayedQuality = -1; - private StatsClikedListener mStatsListener; - - @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.call_status_bar, container, false); - - mStatusText = view.findViewById(R.id.status_text); - mStatusLed = view.findViewById(R.id.status_led); - mCallQuality = view.findViewById(R.id.call_quality); - mEncryption = view.findViewById(R.id.encryption); - - mStatsListener = null; - mCallQuality.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - if (mStatsListener != null) { - mStatsListener.onStatsClicked(); - } - } - }); - - mListener = - new CoreListenerStub() { - @Override - public void onRegistrationStateChanged( - final Core core, - final ProxyConfig proxy, - final RegistrationState state, - String message) { - if (core.getProxyConfigList() == null) { - mStatusLed.setImageResource(R.drawable.led_disconnected); - mStatusText.setText(getString(R.string.no_account)); - } else { - mStatusLed.setVisibility(View.VISIBLE); - } - - if (core.getDefaultProxyConfig() != null - && core.getDefaultProxyConfig().equals(proxy)) { - mStatusLed.setImageResource(getStatusIconResource(state)); - mStatusText.setText(getStatusIconText(state)); - } else if (core.getDefaultProxyConfig() == null) { - mStatusLed.setImageResource(getStatusIconResource(state)); - mStatusText.setText(getStatusIconText(state)); - } - - try { - mStatusText.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.refreshRegisters(); - } - } - }); - } catch (IllegalStateException ise) { - Log.e(ise); - } - } - - @Override - public void onCallStateChanged( - Core core, Call call, Call.State state, String message) { - if (state == Call.State.Resuming || state == Call.State.StreamsRunning) { - refreshStatusItems(call); - } - } - - @Override - public void onCallEncryptionChanged( - Core core, Call call, boolean on, String authenticationToken) { - if (call.getCurrentParams() - .getMediaEncryption() - .equals(MediaEncryption.ZRTP) - && !call.getAuthenticationTokenVerified()) { - showZRTPDialog(call); - } - refreshStatusItems(call); - } - }; - - return view; - } - - @Override - public void onResume() { - super.onResume(); - - Core core = LinphoneManager.getCore(); - if (core != null) { - core.addListener(mListener); - ProxyConfig lpc = core.getDefaultProxyConfig(); - if (lpc != null) { - mListener.onRegistrationStateChanged(core, lpc, lpc.getState(), null); - } - - Call call = core.getCurrentCall(); - if (call != null || core.getConferenceSize() > 1 || core.getCallsNb() > 0) { - if (call != null) { - startCallQuality(); - refreshStatusItems(call); - - if (!call.getAuthenticationTokenVerified()) { - showZRTPDialog(call); - } - } - - // We are obviously connected - if (core.getDefaultProxyConfig() == null) { - mStatusLed.setImageResource(R.drawable.led_disconnected); - mStatusText.setText(getString(R.string.no_account)); - } else { - mStatusLed.setImageResource( - getStatusIconResource(core.getDefaultProxyConfig().getState())); - mStatusText.setText(getStatusIconText(core.getDefaultProxyConfig().getState())); - } - } - } else { - mStatusText.setVisibility(View.VISIBLE); - mEncryption.setVisibility(View.GONE); - } - } - - @Override - public void onPause() { - super.onPause(); - - if (LinphoneContext.isReady()) { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.removeListener(mListener); - } - } - - if (mCallQualityUpdater != null) { - LinphoneUtils.removeFromUIThreadDispatcher(mCallQualityUpdater); - mCallQualityUpdater = null; - } - } - - public void setStatsListener(StatsClikedListener listener) { - mStatsListener = listener; - } - - private int getStatusIconResource(RegistrationState state) { - try { - Core core = LinphoneManager.getCore(); - boolean defaultAccountConnected = - (core != null - && core.getDefaultProxyConfig() != null - && core.getDefaultProxyConfig().getState() == RegistrationState.Ok); - if (state == RegistrationState.Ok && defaultAccountConnected) { - return R.drawable.led_connected; - } else if (state == RegistrationState.Progress) { - return R.drawable.led_inprogress; - } else if (state == RegistrationState.Failed) { - return R.drawable.led_error; - } else { - return R.drawable.led_disconnected; - } - } catch (Exception e) { - Log.e(e); - } - - return R.drawable.led_disconnected; - } - - private String getStatusIconText(RegistrationState state) { - Context context = getActivity(); - try { - if (state == RegistrationState.Ok - && LinphoneManager.getCore().getDefaultProxyConfig().getState() - == RegistrationState.Ok) { - return context.getString(R.string.status_connected); - } else if (state == RegistrationState.Progress) { - return context.getString(R.string.status_in_progress); - } else if (state == RegistrationState.Failed) { - return context.getString(R.string.status_error); - } else { - return context.getString(R.string.status_not_connected); - } - } catch (Exception e) { - Log.e(e); - } - - return context.getString(R.string.status_not_connected); - } - - private void startCallQuality() { - LinphoneUtils.dispatchOnUIThreadAfter( - mCallQualityUpdater = - new Runnable() { - final Call mCurrentCall = LinphoneManager.getCore().getCurrentCall(); - - public void run() { - if (mCurrentCall == null) { - mCallQualityUpdater = null; - return; - } - float newQuality = mCurrentCall.getCurrentQuality(); - updateQualityOfSignalIcon(newQuality); - - LinphoneUtils.dispatchOnUIThreadAfter(this, 1000); - } - }, - 1000); - } - - private void updateQualityOfSignalIcon(float quality) { - int iQuality = (int) quality; - - if (iQuality == mDisplayedQuality) return; - if (quality >= 4) // Good Quality - { - mCallQuality.setImageResource(R.drawable.call_quality_indicator_4); - } else if (quality >= 3) // Average quality - { - mCallQuality.setImageResource(R.drawable.call_quality_indicator_3); - } else if (quality >= 2) // Low quality - { - mCallQuality.setImageResource(R.drawable.call_quality_indicator_2); - } else if (quality >= 1) // Very low quality - { - mCallQuality.setImageResource(R.drawable.call_quality_indicator_1); - } else // Worst quality - { - mCallQuality.setImageResource(R.drawable.call_quality_indicator_0); - } - mDisplayedQuality = iQuality; - } - - public void refreshStatusItems(final Call call) { - if (call != null) { - if (call.getDir() == Call.Dir.Incoming - && call.getState() == Call.State.IncomingReceived - && LinphonePreferences.instance().isMediaEncryptionMandatory()) { - // If the incoming call view is displayed while encryption is mandatory, - // we can safely show the security_ok icon - mEncryption.setImageResource(R.drawable.security_ok); - mEncryption.setVisibility(View.VISIBLE); - return; - } - - MediaEncryption mediaEncryption = call.getCurrentParams().getMediaEncryption(); - mEncryption.setVisibility(View.VISIBLE); - - if (mediaEncryption == MediaEncryption.SRTP - || (mediaEncryption == MediaEncryption.ZRTP - && call.getAuthenticationTokenVerified()) - || mediaEncryption == MediaEncryption.DTLS) { - mEncryption.setImageResource(R.drawable.security_ok); - } else if (mediaEncryption == MediaEncryption.ZRTP - && !call.getAuthenticationTokenVerified()) { - mEncryption.setImageResource(R.drawable.security_pending); - } else { - mEncryption.setImageResource(R.drawable.security_ko); - // Do not show the unsecure icon if user doesn't want to do call mEncryption - if (LinphonePreferences.instance().getMediaEncryption() == MediaEncryption.None) { - mEncryption.setVisibility(View.GONE); - } - } - - if (mediaEncryption == MediaEncryption.ZRTP) { - mEncryption.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - showZRTPDialog(call); - } - }); - } else { - mEncryption.setOnClickListener(null); - } - } - } - - public void showZRTPDialog(final Call call) { - if (getActivity() == null) { - Log.w("[Status Fragment] Can't display ZRTP popup, no Activity"); - return; - } - - if (mZrtpDialog == null || !mZrtpDialog.isShowing()) { - String token = call.getAuthenticationToken(); - - if (token == null) { - Log.w("[Status Fragment] Can't display ZRTP popup, no token !"); - return; - } - if (token.length() < 4) { - Log.w( - "[Status Fragment] Can't display ZRTP popup, token is invalid (" - + token - + ")"); - return; - } - - mZrtpDialog = new Dialog(getActivity()); - mZrtpDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); - mZrtpDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); - mZrtpDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - mZrtpDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - Drawable d = - new ColorDrawable( - ContextCompat.getColor(getActivity(), R.color.dark_grey_color)); - d.setAlpha(200); - mZrtpDialog.setContentView(R.layout.dialog); - mZrtpDialog - .getWindow() - .setLayout( - WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.MATCH_PARENT); - mZrtpDialog.getWindow().setBackgroundDrawable(d); - String zrtpToRead, zrtpToListen; - - if (call.getDir().equals(Call.Dir.Incoming)) { - zrtpToRead = token.substring(0, 2); - zrtpToListen = token.substring(2); - } else { - zrtpToListen = token.substring(0, 2); - zrtpToRead = token.substring(2); - } - - TextView localSas = mZrtpDialog.findViewById(R.id.zrtp_sas_local); - localSas.setText(zrtpToRead.toUpperCase()); - TextView remoteSas = mZrtpDialog.findViewById(R.id.zrtp_sas_remote); - remoteSas.setText(zrtpToListen.toUpperCase()); - TextView message = mZrtpDialog.findViewById(R.id.dialog_message); - message.setVisibility(View.GONE); - mZrtpDialog.findViewById(R.id.dialog_zrtp_layout).setVisibility(View.VISIBLE); - - TextView title = mZrtpDialog.findViewById(R.id.dialog_title); - title.setText(getString(R.string.zrtp_dialog_title)); - title.setVisibility(View.VISIBLE); - - Button delete = mZrtpDialog.findViewById(R.id.dialog_delete_button); - delete.setText(R.string.deny); - Button cancel = mZrtpDialog.findViewById(R.id.dialog_cancel_button); - cancel.setVisibility(View.GONE); - Button accept = mZrtpDialog.findViewById(R.id.dialog_ok_button); - accept.setVisibility(View.VISIBLE); - accept.setText(R.string.accept); - - ImageView icon = mZrtpDialog.findViewById(R.id.dialog_icon); - icon.setVisibility(View.VISIBLE); - icon.setImageResource(R.drawable.security_2_indicator); - - delete.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View view) { - if (call != null) { - LinphoneManager.getInstance().lastCallSasRejected(true); - call.setAuthenticationTokenVerified(false); - if (mEncryption != null) { - mEncryption.setImageResource(R.drawable.security_ko); - } - } - mZrtpDialog.dismiss(); - } - }); - - accept.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View view) { - call.setAuthenticationTokenVerified(true); - if (mEncryption != null) { - mEncryption.setImageResource(R.drawable.security_ok); - } - mZrtpDialog.dismiss(); - } - }); - mZrtpDialog.show(); - } - } - - public interface StatsClikedListener { - void onStatsClicked(); - } -} diff --git a/app/src/main/java/org/linphone/call/VideoZoomHelper.java b/app/src/main/java/org/linphone/call/VideoZoomHelper.java deleted file mode 100644 index 9ea719d3e..000000000 --- a/app/src/main/java/org/linphone/call/VideoZoomHelper.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call; - -import android.content.Context; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.view.View; -import org.linphone.LinphoneManager; -import org.linphone.compatibility.CompatibilityScaleGestureDetector; -import org.linphone.compatibility.CompatibilityScaleGestureListener; -import org.linphone.core.Call; -import org.linphone.core.Core; -import org.linphone.utils.LinphoneUtils; - -public class VideoZoomHelper extends GestureDetector.SimpleOnGestureListener - implements CompatibilityScaleGestureListener { - private View mVideoView; - private GestureDetector mGestureDetector; - private float mZoomFactor = 1.f; - private float mZoomCenterX, mZoomCenterY; - private CompatibilityScaleGestureDetector mScaleDetector; - - public VideoZoomHelper(Context context, View videoView) { - mGestureDetector = new GestureDetector(context, this); - mScaleDetector = new CompatibilityScaleGestureDetector(context); - mScaleDetector.setOnScaleListener(this); - - mVideoView = videoView; - mVideoView.setOnTouchListener( - new View.OnTouchListener() { - public boolean onTouch(View v, MotionEvent event) { - float currentZoomFactor = mZoomFactor; - if (mScaleDetector != null) { - mScaleDetector.onTouchEvent(event); - } - if (currentZoomFactor != mZoomFactor) { - // We did scale, prevent touch event from going further - return true; - } - - boolean touch = mGestureDetector.onTouchEvent(event); - // If true, gesture detected, prevent touch event from going further - // Otherwise it seems we didn't use event, - // allow it to be dispatched somewhere else - return touch; - } - }); - } - - public boolean onScale(CompatibilityScaleGestureDetector detector) { - mZoomFactor *= detector.getScaleFactor(); - // Don't let the object get too small or too large. - // Zoom to make the video fill the screen vertically - float portraitZoomFactor = - ((float) mVideoView.getHeight()) / (float) ((3 * mVideoView.getWidth()) / 4); - // Zoom to make the video fill the screen horizontally - float landscapeZoomFactor = - ((float) mVideoView.getWidth()) / (float) ((3 * mVideoView.getHeight()) / 4); - mZoomFactor = - Math.max( - 0.1f, - Math.min(mZoomFactor, Math.max(portraitZoomFactor, landscapeZoomFactor))); - - Call currentCall = LinphoneManager.getCore().getCurrentCall(); - if (currentCall != null) { - currentCall.zoom(mZoomFactor, mZoomCenterX, mZoomCenterY); - return true; - } - return false; - } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - Core core = LinphoneManager.getCore(); - if (LinphoneUtils.isCallEstablished(core.getCurrentCall())) { - if (mZoomFactor > 1) { - // Video is zoomed, slide is used to change center of zoom - if (distanceX > 0 && mZoomCenterX < 1) { - mZoomCenterX += 0.01; - } else if (distanceX < 0 && mZoomCenterX > 0) { - mZoomCenterX -= 0.01; - } - if (distanceY < 0 && mZoomCenterY < 1) { - mZoomCenterY += 0.01; - } else if (distanceY > 0 && mZoomCenterY > 0) { - mZoomCenterY -= 0.01; - } - - if (mZoomCenterX > 1) mZoomCenterX = 1; - if (mZoomCenterX < 0) mZoomCenterX = 0; - if (mZoomCenterY > 1) mZoomCenterY = 1; - if (mZoomCenterY < 0) mZoomCenterY = 0; - - core.getCurrentCall().zoom(mZoomFactor, mZoomCenterX, mZoomCenterY); - return true; - } - } - - return false; - } - - @Override - public boolean onDoubleTap(MotionEvent e) { - Core core = LinphoneManager.getCore(); - if (LinphoneUtils.isCallEstablished(core.getCurrentCall())) { - if (mZoomFactor == 1.f) { - // Zoom to make the video fill the screen vertically - float portraitZoomFactor = - ((float) mVideoView.getHeight()) - / (float) ((3 * mVideoView.getWidth()) / 4); - // Zoom to make the video fill the screen horizontally - float landscapeZoomFactor = - ((float) mVideoView.getWidth()) - / (float) ((3 * mVideoView.getHeight()) / 4); - - mZoomFactor = Math.max(portraitZoomFactor, landscapeZoomFactor); - } else { - resetZoom(); - } - - core.getCurrentCall().zoom(mZoomFactor, mZoomCenterX, mZoomCenterY); - return true; - } - - return false; - } - - public void destroy() { - if (mVideoView != null) { - mVideoView.setOnTouchListener(null); - mVideoView = null; - } - if (mGestureDetector != null) { - mGestureDetector.setOnDoubleTapListener(null); - mGestureDetector = null; - } - if (mScaleDetector != null) { - mScaleDetector.destroy(); - mScaleDetector = null; - } - } - - private void resetZoom() { - mZoomFactor = 1.f; - mZoomCenterX = mZoomCenterY = 0.5f; - } -} diff --git a/app/src/main/java/org/linphone/call/views/CallButton.java b/app/src/main/java/org/linphone/call/views/CallButton.java deleted file mode 100644 index 347289b2d..000000000 --- a/app/src/main/java/org/linphone/call/views/CallButton.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call.views; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.ImageView; -import org.linphone.LinphoneManager; -import org.linphone.core.Call; -import org.linphone.core.CallLog; -import org.linphone.core.Core; -import org.linphone.core.ProxyConfig; -import org.linphone.dialer.views.AddressAware; -import org.linphone.dialer.views.AddressText; -import org.linphone.settings.LinphonePreferences; - -@SuppressLint("AppCompatCustomView") -public class CallButton extends ImageView implements OnClickListener, AddressAware { - private AddressText mAddress; - private boolean mIsTransfer; - - public CallButton(Context context, AttributeSet attrs) { - super(context, attrs); - - mIsTransfer = false; - setOnClickListener(this); - } - - public void setAddressWidget(AddressText a) { - mAddress = a; - } - - public void setIsTransfer(boolean isTransfer) { - mIsTransfer = isTransfer; - } - - public void onClick(View v) { - if (mAddress.getText().length() > 0) { - if (mIsTransfer) { - Core core = LinphoneManager.getCore(); - if (core.getCurrentCall() == null) { - return; - } - core.getCurrentCall().transfer(mAddress.getText().toString()); - } else { - LinphoneManager.getCallManager().newOutgoingCall(mAddress); - } - } else { - if (LinphonePreferences.instance().isBisFeatureEnabled()) { - Core core = LinphoneManager.getCore(); - CallLog[] logs = core.getCallLogs(); - CallLog log = null; - for (CallLog l : logs) { - if (l.getDir() == Call.Dir.Outgoing) { - log = l; - break; - } - } - if (log == null) { - return; - } - - ProxyConfig lpc = core.getDefaultProxyConfig(); - if (lpc != null && log.getToAddress().getDomain().equals(lpc.getDomain())) { - mAddress.setText(log.getToAddress().getUsername()); - } else { - mAddress.setText(log.getToAddress().asStringUriOnly()); - } - mAddress.setSelection(mAddress.getText().toString().length()); - mAddress.setDisplayedName(log.getToAddress().getDisplayName()); - } - } - } -} diff --git a/app/src/main/java/org/linphone/call/views/CallIncomingAnswerButton.java b/app/src/main/java/org/linphone/call/views/CallIncomingAnswerButton.java deleted file mode 100644 index 9645ac45d..000000000 --- a/app/src/main/java/org/linphone/call/views/CallIncomingAnswerButton.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call.views; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.widget.LinearLayout; -import androidx.annotation.Nullable; -import org.linphone.R; - -public class CallIncomingAnswerButton extends LinearLayout - implements View.OnClickListener, View.OnTouchListener { - private LinearLayout mRoot; - private boolean mUseSliderMode = false; - private CallIncomingButtonListener mListener; - private View mDeclineButton; - - private int mScreenWidth; - private boolean mBegin; - private float mAnswerX, mOldSize; - - public CallIncomingAnswerButton(Context context) { - super(context); - init(); - } - - public CallIncomingAnswerButton(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - init(); - } - - public CallIncomingAnswerButton( - Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); - } - - public void setSliderMode(boolean enabled) { - mUseSliderMode = enabled; - findViewById(R.id.acceptUnlock).setVisibility(enabled ? VISIBLE : GONE); - } - - public void setListener(CallIncomingButtonListener listener) { - mListener = listener; - } - - public void setDeclineButton(View decline) { - mDeclineButton = decline; - } - - private void init() { - inflate(getContext(), R.layout.call_incoming_answer_button, this); - mRoot = findViewById(R.id.root); - mRoot.setOnClickListener(this); - mRoot.setOnTouchListener(this); - mScreenWidth = getResources().getDisplayMetrics().widthPixels; - } - - @Override - public void onClick(View v) { - if (!mUseSliderMode) { - performClick(); - } - } - - @Override - public boolean onTouch(View view, MotionEvent motionEvent) { - if (mUseSliderMode) { - float curX; - switch (motionEvent.getAction()) { - case MotionEvent.ACTION_DOWN: - mDeclineButton.setVisibility(View.GONE); - mAnswerX = motionEvent.getX() - mRoot.getWidth(); - mBegin = true; - mOldSize = 0; - break; - case MotionEvent.ACTION_MOVE: - curX = motionEvent.getX() - mRoot.getWidth(); - view.scrollBy((int) (mAnswerX - curX), view.getScrollY()); - mOldSize -= mAnswerX - curX; - mAnswerX = curX; - if (mOldSize < -25) mBegin = false; - if (curX < (mScreenWidth / 4) - mRoot.getWidth() && !mBegin) { - performClick(); - return true; - } - break; - case MotionEvent.ACTION_UP: - mDeclineButton.setVisibility(View.VISIBLE); - view.scrollTo(0, view.getScrollY()); - break; - } - return true; - } - return false; - } - - @Override - public boolean performClick() { - super.performClick(); - if (mListener != null) { - mListener.onAction(); - } - return true; - } -} diff --git a/app/src/main/java/org/linphone/call/views/CallIncomingDeclineButton.java b/app/src/main/java/org/linphone/call/views/CallIncomingDeclineButton.java deleted file mode 100644 index 65b422c34..000000000 --- a/app/src/main/java/org/linphone/call/views/CallIncomingDeclineButton.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call.views; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.widget.LinearLayout; -import androidx.annotation.Nullable; -import org.linphone.R; - -public class CallIncomingDeclineButton extends LinearLayout - implements View.OnClickListener, View.OnTouchListener { - private boolean mUseSliderMode = false; - private CallIncomingButtonListener mListener; - private View mAnswerButton; - - private int mScreenWidth; - private float mDeclineX; - - public CallIncomingDeclineButton(Context context) { - super(context); - init(); - } - - public CallIncomingDeclineButton(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - init(); - } - - public CallIncomingDeclineButton( - Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); - } - - public void setSliderMode(boolean enabled) { - mUseSliderMode = enabled; - findViewById(R.id.declineUnlock).setVisibility(enabled ? VISIBLE : GONE); - } - - public void setListener(CallIncomingButtonListener listener) { - mListener = listener; - } - - public void setAnswerButton(View answer) { - mAnswerButton = answer; - } - - private void init() { - inflate(getContext(), R.layout.call_incoming_decline_button, this); - LinearLayout root = findViewById(R.id.root); - root.setOnClickListener(this); - root.setOnTouchListener(this); - mScreenWidth = getResources().getDisplayMetrics().widthPixels; - } - - @Override - public void onClick(View v) { - if (!mUseSliderMode) { - performClick(); - } - } - - @Override - public boolean onTouch(View view, MotionEvent motionEvent) { - if (mUseSliderMode) { - float curX; - switch (motionEvent.getAction()) { - case MotionEvent.ACTION_DOWN: - mAnswerButton.setVisibility(View.GONE); - mDeclineX = motionEvent.getX(); - break; - case MotionEvent.ACTION_MOVE: - curX = motionEvent.getX(); - view.scrollBy((int) (mDeclineX - curX), view.getScrollY()); - mDeclineX = curX; - if (curX > (3 * mScreenWidth / 4)) { - performClick(); - return true; - } - break; - case MotionEvent.ACTION_UP: - mAnswerButton.setVisibility(View.VISIBLE); - view.scrollTo(0, view.getScrollY()); - break; - } - return true; - } - return false; - } - - @Override - public boolean performClick() { - super.performClick(); - if (mListener != null) { - mListener.onAction(); - } - return true; - } -} diff --git a/app/src/main/java/org/linphone/call/views/LinphoneGL2JNIViewOverlay.java b/app/src/main/java/org/linphone/call/views/LinphoneGL2JNIViewOverlay.java deleted file mode 100644 index b18e7165a..000000000 --- a/app/src/main/java/org/linphone/call/views/LinphoneGL2JNIViewOverlay.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call.views; - -import android.content.Context; -import android.content.Intent; -import android.graphics.PixelFormat; -import android.os.Build; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.SurfaceView; -import android.view.View; -import android.view.WindowManager; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.call.CallActivity; -import org.linphone.core.Call; -import org.linphone.core.CallParams; -import org.linphone.core.Core; -import org.linphone.mediastream.Version; -import org.linphone.mediastream.video.AndroidVideoWindowImpl; - -public class LinphoneGL2JNIViewOverlay extends org.linphone.mediastream.video.display.GL2JNIView - implements LinphoneOverlay { - private final WindowManager mWindowManager; - private final WindowManager.LayoutParams mParams; - private final DisplayMetrics mMetrics; - private float mX, mY, mTouchX, mTouchY; - private boolean mDragEnabled; - private final AndroidVideoWindowImpl mAndroidVideoWindowImpl; - - public LinphoneGL2JNIViewOverlay(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs); - mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - - int LAYOUT_FLAG; - if (Build.VERSION.SDK_INT >= Version.API26_O_80) { - LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - } else { - LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_PHONE; - } - - mParams = - new WindowManager.LayoutParams( - WindowManager.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.WRAP_CONTENT, - LAYOUT_FLAG, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSLUCENT); - mParams.gravity = Gravity.TOP | Gravity.LEFT; - mMetrics = new DisplayMetrics(); - mWindowManager.getDefaultDisplay().getMetrics(mMetrics); - - mAndroidVideoWindowImpl = - new AndroidVideoWindowImpl( - this, - null, - new AndroidVideoWindowImpl.VideoWindowListener() { - public void onVideoRenderingSurfaceReady( - AndroidVideoWindowImpl vw, SurfaceView surface) { - LinphoneManager.getCore().setNativeVideoWindowId(vw); - } - - public void onVideoRenderingSurfaceDestroyed( - AndroidVideoWindowImpl vw) {} - - public void onVideoPreviewSurfaceReady( - AndroidVideoWindowImpl vw, SurfaceView surface) {} - - public void onVideoPreviewSurfaceDestroyed(AndroidVideoWindowImpl vw) {} - }); - - Core core = LinphoneManager.getCore(); - Call call = core.getCurrentCall(); - CallParams callParams = call.getCurrentParams(); - mParams.width = callParams.getReceivedVideoDefinition().getWidth(); - mParams.height = callParams.getReceivedVideoDefinition().getHeight(); - core.setNativeVideoWindowId(mAndroidVideoWindowImpl); - - setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - Context context = LinphoneContext.instance().getApplicationContext(); - Intent intent = new Intent(context, CallActivity.class); - // This flag is required to start an Activity from a Service context - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } - }); - setOnLongClickListener( - new OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - mDragEnabled = true; - return true; - } - }); - } - - public LinphoneGL2JNIViewOverlay(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public LinphoneGL2JNIViewOverlay(Context context) { - this(context, null); - } - - @Override - public void destroy() { - mAndroidVideoWindowImpl.release(); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - mX = event.getRawX(); - mY = event.getRawY(); - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - mTouchX = event.getX(); - mTouchY = event.getY(); - break; - case MotionEvent.ACTION_MOVE: - if (mDragEnabled) { - updateViewPostion(); - } - break; - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - mTouchX = mTouchY = 0; - mDragEnabled = false; - break; - default: - break; - } - return super.onTouchEvent(event); - } - - private void updateViewPostion() { - mParams.x = - Math.min( - Math.max(0, (int) (mX - mTouchX)), - mMetrics.widthPixels - getMeasuredWidth()); - mParams.y = - Math.min( - Math.max(0, (int) (mY - mTouchY)), - mMetrics.heightPixels - getMeasuredHeight()); - mWindowManager.updateViewLayout(this, mParams); - } - - @Override - public WindowManager.LayoutParams getWindowManagerLayoutParams() { - return mParams; - } - - @Override - public void addToWindowManager(WindowManager windowManager, WindowManager.LayoutParams params) { - windowManager.addView(this, params); - } - - @Override - public void removeFromWindowManager(WindowManager windowManager) { - windowManager.removeViewImmediate(this); - } -} diff --git a/app/src/main/java/org/linphone/call/views/LinphoneLinearLayoutManager.java b/app/src/main/java/org/linphone/call/views/LinphoneLinearLayoutManager.java deleted file mode 100644 index 4694ac63d..000000000 --- a/app/src/main/java/org/linphone/call/views/LinphoneLinearLayoutManager.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call.views; - -import android.content.Context; -import android.util.AttributeSet; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import org.linphone.core.tools.Log; - -/* https://stackoverflow.com/questions/30220771/recyclerview-inconsistency-detected-invalid-item-position */ -public class LinphoneLinearLayoutManager extends LinearLayoutManager { - /** - * Disable predictive animations. There is a bug in RecyclerView which causes views that are - * being reloaded to pull invalid ViewHolders from the internal recycler stack if the adapter - * size has decreased since the ViewHolder was recycled. - */ - @Override - public boolean supportsPredictiveItemAnimations() { - return false; - } - - public LinphoneLinearLayoutManager(Context context) { - super(context); - } - - public LinphoneLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { - super(context, orientation, reverseLayout); - } - - public LinphoneLinearLayoutManager( - Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - @Override - public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { - try { - super.onLayoutChildren(recycler, state); - } catch (IndexOutOfBoundsException e) { - Log.w("[LinearLayoutManager] Index out of bounds exception successfully catched..."); - } - } -} diff --git a/app/src/main/java/org/linphone/call/views/LinphoneTextureViewOverlay.java b/app/src/main/java/org/linphone/call/views/LinphoneTextureViewOverlay.java deleted file mode 100644 index c5b30ab0d..000000000 --- a/app/src/main/java/org/linphone/call/views/LinphoneTextureViewOverlay.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.call.views; - -import android.content.Context; -import android.content.Intent; -import android.graphics.PixelFormat; -import android.os.Build; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.TextureView; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.RelativeLayout; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.call.CallActivity; -import org.linphone.core.Call; -import org.linphone.core.CallParams; -import org.linphone.core.Core; -import org.linphone.core.VideoDefinition; -import org.linphone.mediastream.Version; - -public class LinphoneTextureViewOverlay extends RelativeLayout implements LinphoneOverlay { - private final WindowManager mWindowManager; - private final WindowManager.LayoutParams mParams; - private final DisplayMetrics mMetrics; - private float mX, mY, mTouchX, mTouchY; - private boolean mDragEnabled; - - public LinphoneTextureViewOverlay(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs); - mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - - int LAYOUT_FLAG; - if (Build.VERSION.SDK_INT >= Version.API26_O_80) { - LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - } else { - LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_PHONE; - } - - mParams = - new WindowManager.LayoutParams( - WindowManager.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.WRAP_CONTENT, - LAYOUT_FLAG, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSLUCENT); - mParams.gravity = Gravity.TOP | Gravity.LEFT; - mMetrics = new DisplayMetrics(); - mWindowManager.getDefaultDisplay().getMetrics(mMetrics); - - Core core = LinphoneManager.getCore(); - Call call = core.getCurrentCall(); - CallParams callParams = call.getCurrentParams(); - mParams.width = callParams.getReceivedVideoDefinition().getWidth(); - mParams.height = callParams.getReceivedVideoDefinition().getHeight(); - - TextureView remoteVideo = new TextureView(context); - addView(remoteVideo); - TextureView localPreview = new TextureView(context); - addView(localPreview); - - RelativeLayout.LayoutParams remoteVideoParams = - new RelativeLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - remoteVideo.setLayoutParams(remoteVideoParams); - - VideoDefinition videoSize = call.getCurrentParams().getSentVideoDefinition(); - int localPreviewWidth = videoSize.getWidth(); - int localPreviewHeight = videoSize.getHeight(); - int localPreviewMaxHeight = mParams.height / 4; - localPreviewWidth = localPreviewWidth * localPreviewMaxHeight / localPreviewHeight; - localPreviewHeight = localPreviewMaxHeight; - - RelativeLayout.LayoutParams localPreviewParams = - new RelativeLayout.LayoutParams(localPreviewWidth, localPreviewHeight); - localPreviewParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, TRUE); - localPreviewParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE); - localPreview.setLayoutParams(localPreviewParams); - - core.setNativeVideoWindowId(remoteVideo); - core.setNativePreviewWindowId(localPreview); - - setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - Context context = LinphoneContext.instance().getApplicationContext(); - Intent intent = new Intent(context, CallActivity.class); - // This flag is required to start an Activity from a Service context - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } - }); - setOnLongClickListener( - new OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - mDragEnabled = true; - return true; - } - }); - } - - public LinphoneTextureViewOverlay(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public LinphoneTextureViewOverlay(Context context) { - this(context, null); - } - - @Override - public void destroy() {} - - @Override - public boolean onTouchEvent(MotionEvent event) { - mX = event.getRawX(); - mY = event.getRawY(); - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - mTouchX = event.getX(); - mTouchY = event.getY(); - break; - case MotionEvent.ACTION_MOVE: - if (mDragEnabled) { - updateViewPostion(); - } - break; - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - mTouchX = mTouchY = 0; - mDragEnabled = false; - break; - default: - break; - } - return super.onTouchEvent(event); - } - - private void updateViewPostion() { - mParams.x = - Math.min( - Math.max(0, (int) (mX - mTouchX)), - mMetrics.widthPixels - getMeasuredWidth()); - mParams.y = - Math.min( - Math.max(0, (int) (mY - mTouchY)), - mMetrics.heightPixels - getMeasuredHeight()); - mWindowManager.updateViewLayout(this, mParams); - } - - @Override - public WindowManager.LayoutParams getWindowManagerLayoutParams() { - return mParams; - } - - @Override - public void addToWindowManager(WindowManager windowManager, WindowManager.LayoutParams params) { - windowManager.addView(this, params); - } - - @Override - public void removeFromWindowManager(WindowManager windowManager) { - windowManager.removeViewImmediate(this); - } -} diff --git a/app/src/main/java/org/linphone/chat/ChatActivity.java b/app/src/main/java/org/linphone/chat/ChatActivity.java deleted file mode 100644 index 56c494edc..000000000 --- a/app/src/main/java/org/linphone/chat/ChatActivity.java +++ /dev/null @@ -1,390 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentManager; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.View; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.Toast; -import java.util.ArrayList; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.activities.MainActivity; -import org.linphone.contacts.ContactAddress; -import org.linphone.core.Address; -import org.linphone.core.ChatMessage; -import org.linphone.core.ChatRoom; -import org.linphone.core.Factory; -import org.linphone.core.tools.Log; -import org.linphone.utils.FileUtils; -import org.linphone.utils.LinphoneUtils; - -public class ChatActivity extends MainActivity { - public static final String NAME = "Chat"; - - private String mSharedText, mSharedFiles; - private ChatMessage mForwardMessage; - - @Override - protected void onCreate(Bundle savedInstanceState) { - getIntent().putExtra("Activity", NAME); - super.onCreate(savedInstanceState); - } - - @Override - protected void onStart() { - super.onStart(); - - Fragment currentFragment = getFragmentManager().findFragmentById(R.id.fragmentContainer); - if (currentFragment == null) { - showChatRooms(); - - if (getIntent() != null && getIntent().getExtras() != null) { - handleIntentExtras(getIntent()); - // Remove the SIP Uri from the intent so a click on chat button will go back to list - getIntent().removeExtra("RemoteSipUri"); - } else { - if (isTablet()) { - showEmptyChildFragment(); - } - } - } - } - - @Override - protected void onResume() { - super.onResume(); - mChatSelected.setVisibility(View.VISIBLE); - } - - @Override - protected void onPause() { - super.onPause(); - getIntent().setAction(""); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putString("SharedText", mSharedText); - outState.putString("SharedFiles", mSharedFiles); - } - - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - mSharedText = savedInstanceState.getString("SharedText", null); - mSharedFiles = savedInstanceState.getString("SharedFiles", null); - } - - @Override - public void goBack() { - // 1 is for the empty fragment on tablets - if (!isTablet() || getFragmentManager().getBackStackEntryCount() > 1) { - if (popBackStack()) { - return; - } - } - super.goBack(); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - - // Clean fragments stack upon return - while (getFragmentManager().getBackStackEntryCount() > 0) { - getFragmentManager().popBackStackImmediate(); - } - - handleIntentExtras(intent); - } - - private void handleIntentExtras(Intent intent) { - if (intent == null) return; - - Fragment currentFragment = getFragmentManager().findFragmentById(R.id.fragmentContainer); - if (currentFragment == null || !(currentFragment instanceof ChatRoomsFragment)) { - showChatRooms(); - } - - String sharedText = null; - String sharedFiles = null; - - String action = intent.getAction(); - String type = intent.getType(); - if (Intent.ACTION_SEND.equals(action) && type != null) { - if (("text/plain").equals(type) && intent.getStringExtra(Intent.EXTRA_TEXT) != null) { - sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); - Log.i("[Chat Activity] ACTION_SEND with text/plain data: " + sharedText); - } else { - Uri fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); - sharedFiles = FileUtils.getFilePath(this, fileUri); - Log.i("[Chat Activity] ACTION_SEND with file: " + sharedFiles); - } - } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null) { - if (type.startsWith("image/")) { - ArrayList imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); - StringBuilder filePaths = new StringBuilder(); - for (Uri uri : imageUris) { - filePaths.append(FileUtils.getFilePath(this, uri)); - filePaths.append(":"); - } - sharedFiles = filePaths.toString(); - Log.i("[Chat Activity] ACTION_SEND_MULTIPLE with files: " + sharedFiles); - } - } else { - if (intent.getExtras() != null) { - Bundle extras = intent.getExtras(); - handleRemoteSipUriInIntentExtras(extras); - } - } - - if (!getResources().getBoolean(R.bool.disable_chat_send_file)) { - if (sharedText != null || sharedFiles != null) { - mSharedText = sharedText; - mSharedFiles = sharedFiles; - Toast.makeText(this, R.string.toast_choose_chat_room_for_sharing, Toast.LENGTH_LONG) - .show(); - Log.i( - "[Chat Activity] Sharing arguments found: " - + mSharedText - + " / " - + mSharedFiles); - } - } - } - - private void handleRemoteSipUriInIntentExtras(Bundle extras) { - if (extras == null) return; - - if (extras.containsKey("RemoteSipUri")) { - String remoteSipUri = extras.getString("RemoteSipUri", null); - String localSipUri = extras.getString("LocalSipUri", null); - - Address localAddress = null; - Address remoteAddress = null; - if (localSipUri != null) { - localAddress = Factory.instance().createAddress(localSipUri); - } - if (remoteSipUri != null) { - remoteAddress = Factory.instance().createAddress(remoteSipUri); - } - // Don't make it a child on smartphones to have a working back button - showChatRoom(localAddress, remoteAddress); - } - } - - private void showChatRooms() { - ChatRoomsFragment fragment = new ChatRoomsFragment(); - changeFragment(fragment, "Chat rooms", false); - } - - private void showChatRoom(Address localAddress, Address peerAddress, boolean isChild) { - Bundle extras = new Bundle(); - if (localAddress != null) { - extras.putSerializable("LocalSipUri", localAddress.asStringUriOnly()); - } - if (peerAddress != null) { - extras.putSerializable("RemoteSipUri", peerAddress.asStringUriOnly()); - } - if (mSharedText != null) { - extras.putString("SharedText", mSharedText); - mSharedText = null; - } - if (mSharedFiles != null) { - extras.putString("SharedFiles", mSharedFiles); - mSharedFiles = null; - } - - ChatMessagesFragment fragment = new ChatMessagesFragment(); - fragment.setArguments(extras); - changeFragment(fragment, "Chat room", isChild); - - showForwardDialog(localAddress, peerAddress); - } - - public void showChatRoom(Address localAddress, Address peerAddress) { - showChatRoom(localAddress, peerAddress, true); - } - - public void showImdn(Address localAddress, Address peerAddress, String messageId) { - Bundle extras = new Bundle(); - if (localAddress != null) { - extras.putSerializable("LocalSipUri", localAddress.asStringUriOnly()); - } - if (peerAddress != null) { - extras.putSerializable("RemoteSipUri", peerAddress.asStringUriOnly()); - } - extras.putString("MessageId", messageId); - - ImdnFragment fragment = new ImdnFragment(); - fragment.setArguments(extras); - changeFragment(fragment, "Chat message IMDN", true); - } - - public void showDevices(Address localAddress, Address peerAddress) { - showDevices(localAddress, peerAddress, true); - } - - private void showDevices(Address localAddress, Address peerAddress, boolean isChild) { - Bundle extras = new Bundle(); - if (localAddress != null) { - extras.putSerializable("LocalSipUri", localAddress.asStringUriOnly()); - } - if (peerAddress != null) { - extras.putSerializable("RemoteSipUri", peerAddress.asStringUriOnly()); - } - - DevicesFragment fragment = new DevicesFragment(); - fragment.setArguments(extras); - changeFragment(fragment, "Chat room devices", isChild); - } - - public void showChatRoomCreation( - Address peerAddress, - ArrayList participants, - String subject, - boolean encrypted, - boolean isGroupChatRoom, - boolean cleanBackStack) { - if (cleanBackStack) { - FragmentManager fm = getFragmentManager(); - while (fm.getBackStackEntryCount() > 0) { - fm.popBackStackImmediate(); - } - if (isTablet()) { - showEmptyChildFragment(); - } - } - - Bundle extras = new Bundle(); - if (peerAddress != null) { - extras.putSerializable("RemoteSipUri", peerAddress.asStringUriOnly()); - } - extras.putSerializable("Participants", participants); - extras.putString("Subject", subject); - extras.putBoolean("Encrypted", encrypted); - extras.putBoolean("IsGroupChatRoom", isGroupChatRoom); - - ChatRoomCreationFragment fragment = new ChatRoomCreationFragment(); - fragment.setArguments(extras); - changeFragment(fragment, "Chat room creation", true); - } - - public void showChatRoomGroupInfo( - Address peerAddress, - Address localAddress, - ArrayList participants, - String subject, - boolean encrypted) { - Bundle extras = new Bundle(); - if (peerAddress != null) { - extras.putSerializable("RemoteSipUri", peerAddress.asStringUriOnly()); - } - if (localAddress != null) { - extras.putSerializable("LocalSipUri", localAddress.asStringUriOnly()); - } - extras.putSerializable("Participants", participants); - extras.putString("Subject", subject); - extras.putBoolean("Encrypted", encrypted); - - GroupInfoFragment fragment = new GroupInfoFragment(); - fragment.setArguments(extras); - changeFragment(fragment, "Chat room group info", true); - } - - public void showChatRoomEphemeral(Address peerAddress, Address localAddress) { - Bundle extras = new Bundle(); - if (peerAddress != null) { - extras.putSerializable("RemoteSipUri", peerAddress.asStringUriOnly()); - } - if (localAddress != null) { - extras.putSerializable("LocalSipUri", localAddress.asStringUriOnly()); - } - EphemeralFragment fragment = new EphemeralFragment(); - fragment.setArguments(extras); - changeFragment(fragment, "Chat room ephemeral", true); - } - - public void forwardMessage(ChatMessage message) { - Log.i("[Chat] Message forwarding enabled"); - goBack(); - mForwardMessage = message; - Toast.makeText(this, R.string.toast_choose_chat_room_for_sharing, Toast.LENGTH_LONG).show(); - } - - private void showForwardDialog(final Address localAddress, final Address peerAddress) { - if (mForwardMessage == null) return; - - final Dialog dialog = - LinphoneUtils.getDialog( - this, getString(R.string.chat_message_forward_confirmation_dialog)); - dialog.findViewById(R.id.dialog_delete_button).setVisibility(View.GONE); - - ImageView icon = dialog.findViewById(R.id.dialog_icon); - icon.setVisibility(View.VISIBLE); - icon.setImageResource(R.drawable.forward_message_dialog_default); - - Button cancel = dialog.findViewById(R.id.dialog_cancel_button); - cancel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - mForwardMessage = null; - dialog.dismiss(); - } - }); - - Button ok = dialog.findViewById(R.id.dialog_ok_button); - ok.setVisibility(View.VISIBLE); - ok.setText(getString(R.string.chat_message_context_menu_forward)); - ok.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - doMessageForwarding(localAddress, peerAddress); - dialog.dismiss(); - } - }); - - dialog.show(); - } - - private void doMessageForwarding(Address localAddress, Address peerAddress) { - if (mForwardMessage != null) { - Log.i("[Chat] Found message to forward"); - ChatRoom room = LinphoneManager.getCore().getChatRoom(peerAddress, localAddress); - if (room != null) { - Log.i("[Chat] Found chat room in which to forward message"); - ChatMessage message = room.createForwardMessage(mForwardMessage); - message.send(); - mForwardMessage = null; - Log.i("[Chat] Message forwarded"); - } - } - } -} diff --git a/app/src/main/java/org/linphone/chat/ChatMessageViewHolder.java b/app/src/main/java/org/linphone/chat/ChatMessageViewHolder.java deleted file mode 100644 index 667d741ac..000000000 --- a/app/src/main/java/org/linphone/chat/ChatMessageViewHolder.java +++ /dev/null @@ -1,479 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; - -import android.Manifest; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.CountDownTimer; -import android.text.method.LinkMovementMethod; -import android.text.util.Linkify; -import android.view.LayoutInflater; -import android.view.View; -import android.webkit.MimeTypeMap; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.Toast; -import androidx.core.content.FileProvider; -import androidx.recyclerview.widget.RecyclerView; -import com.bumptech.glide.Glide; -import com.google.android.flexbox.FlexboxLayout; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import org.linphone.R; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.core.Address; -import org.linphone.core.ChatMessage; -import org.linphone.core.Content; -import org.linphone.core.tools.Log; -import org.linphone.utils.FileUtils; -import org.linphone.utils.ImageUtils; -import org.linphone.utils.LinphoneUtils; - -public class ChatMessageViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { - - public final LinearLayout eventLayout; - public final TextView eventMessage; - - public final LinearLayout securityEventLayout; - public final TextView securityEventMessage; - - public final View rightAnchor; - public final RelativeLayout bubbleLayout; - public final LinearLayout background; - public final RelativeLayout avatarLayout; - - private final ProgressBar downloadInProgress; - public final ProgressBar sendInProgress; - public final TextView timeText; - private final ImageView outgoingImdn; - private final TextView messageText; - - private final FlexboxLayout multiFileContents; - private final RelativeLayout singleFileContent; - - private final LinearLayout forwardLayout; - private final LinearLayout ephemeralLayout; - private final TextView ephemeralCountdown; - private CountDownTimer countDownTimer; - - public final CheckBox delete; - public boolean isEditionEnabled; - - private Context mContext; - private ChatMessageViewHolderClickListener mListener; - - public ChatMessageViewHolder( - Context context, View view, ChatMessageViewHolderClickListener listener) { - this(view); - mContext = context; - mListener = listener; - view.setOnClickListener(this); - } - - private ChatMessageViewHolder(View view) { - super(view); - eventLayout = view.findViewById(R.id.event); - eventMessage = view.findViewById(R.id.event_text); - - securityEventLayout = view.findViewById(R.id.security_event); - securityEventMessage = view.findViewById(R.id.security_event_text); - - rightAnchor = view.findViewById(R.id.rightAnchor); - bubbleLayout = view.findViewById(R.id.bubble); - background = view.findViewById(R.id.background); - avatarLayout = view.findViewById(R.id.avatar_layout); - - downloadInProgress = view.findViewById(R.id.download_in_progress); - sendInProgress = view.findViewById(R.id.send_in_progress); - timeText = view.findViewById(R.id.time); - outgoingImdn = view.findViewById(R.id.imdn); - messageText = view.findViewById(R.id.message); - - singleFileContent = view.findViewById(R.id.single_content); - multiFileContents = view.findViewById(R.id.multi_content); - - forwardLayout = view.findViewById(R.id.forward_layout); - ephemeralLayout = view.findViewById(R.id.ephemeral_layout); - ephemeralCountdown = view.findViewById(R.id.ephemeral_time); - countDownTimer = null; - - delete = view.findViewById(R.id.delete_event); - } - - @Override - public void onClick(View v) { - if (mListener != null) { - mListener.onItemClicked(getAdapterPosition()); - } - } - - public void bindMessage(final ChatMessage message, LinphoneContact contact) { - eventLayout.setVisibility(View.GONE); - securityEventLayout.setVisibility(View.GONE); - rightAnchor.setVisibility(View.VISIBLE); - bubbleLayout.setVisibility(View.VISIBLE); - messageText.setVisibility(View.GONE); - timeText.setVisibility(View.VISIBLE); - outgoingImdn.setVisibility(View.GONE); - avatarLayout.setVisibility(View.GONE); - sendInProgress.setVisibility(View.GONE); - downloadInProgress.setVisibility(View.GONE); - singleFileContent.setVisibility(View.GONE); - multiFileContents.setVisibility(View.GONE); - - forwardLayout.setVisibility(message.isForward() ? View.VISIBLE : View.GONE); - ephemeralLayout.setVisibility(message.isEphemeral() ? View.VISIBLE : View.GONE); - updateEphemeralTimer(message); - - ChatMessage.State status = message.getState(); - Address remoteSender = message.getFromAddress(); - String displayName; - String time = - LinphoneUtils.timestampToHumanDate( - mContext, message.getTime(), R.string.messages_date_format); - - if (message.isOutgoing()) { - bubbleLayout.setPadding(0, 0, 0, 0); // Reset padding - outgoingImdn.setVisibility(View.INVISIBLE); // For anchoring purposes - - if (status == ChatMessage.State.DeliveredToUser) { - outgoingImdn.setVisibility(View.VISIBLE); - outgoingImdn.setImageResource(R.drawable.imdn_received); - } else if (status == ChatMessage.State.Displayed) { - outgoingImdn.setVisibility(View.VISIBLE); - outgoingImdn.setImageResource(R.drawable.imdn_read); - } else if (status == ChatMessage.State.NotDelivered) { - outgoingImdn.setVisibility(View.VISIBLE); - outgoingImdn.setImageResource(R.drawable.imdn_error); - } else if (status == ChatMessage.State.FileTransferError) { - outgoingImdn.setVisibility(View.VISIBLE); - outgoingImdn.setImageResource(R.drawable.imdn_error); - } else if (status == ChatMessage.State.InProgress - || status == ChatMessage.State.FileTransferInProgress) { - sendInProgress.setVisibility(View.VISIBLE); - } - - timeText.setVisibility(View.VISIBLE); - background.setBackgroundResource(R.drawable.chat_bubble_outgoing_full); - } else { - rightAnchor.setVisibility(View.GONE); - avatarLayout.setVisibility(View.VISIBLE); - background.setBackgroundResource(R.drawable.chat_bubble_incoming_full); - - // Can't anchor incoming messages, setting this to align max width with LIME icon - bubbleLayout.setPadding(0, 0, (int) ImageUtils.dpToPixels(mContext, 18), 0); - - if (status == ChatMessage.State.FileTransferInProgress) { - downloadInProgress.setVisibility(View.VISIBLE); - } - } - - if (contact == null) { - contact = ContactsManager.getInstance().findContactFromAddress(remoteSender); - } - if (contact != null) { - if (contact.getFullName() != null) { - displayName = contact.getFullName(); - } else { - displayName = LinphoneUtils.getAddressDisplayName(remoteSender); - } - ContactAvatar.displayAvatar(contact, avatarLayout); - } else { - displayName = LinphoneUtils.getAddressDisplayName(remoteSender); - ContactAvatar.displayAvatar(displayName, avatarLayout); - } - - if (message.isOutgoing()) { - timeText.setText(time); - } else { - timeText.setText(time + " - " + displayName); - } - - if (message.hasTextContent()) { - messageText.setText(message.getTextContent()); - Linkify.addLinks(messageText, Linkify.ALL); - messageText.setMovementMethod(LinkMovementMethod.getInstance()); - messageText.setVisibility(View.VISIBLE); - } - - List fileContents = new ArrayList<>(); - for (Content c : message.getContents()) { - if (c.isFile() || c.isFileTransfer()) { - fileContents.add(c); - } - } - - if (fileContents.size() == 1) { - singleFileContent.setVisibility(View.VISIBLE); - displayContent(message, fileContents.get(0), singleFileContent, false); - } else if (fileContents.size() > 1) { - multiFileContents.removeAllViews(); - multiFileContents.setVisibility(View.VISIBLE); - - for (Content c : fileContents) { - View content = - LayoutInflater.from(mContext) - .inflate(R.layout.chat_bubble_content, null, false); - - displayContent(message, c, content, true); - - multiFileContents.addView(content); - } - } - } - - private void displayContent( - final ChatMessage message, Content c, View content, boolean isMultiContent) { - final Button downloadOrCancel = content.findViewById(R.id.download); - downloadOrCancel.setVisibility(View.GONE); - final ImageView bigImage = content.findViewById(R.id.bigImage); - bigImage.setVisibility(View.GONE); - final ImageView smallImage = content.findViewById(R.id.image); - smallImage.setVisibility(View.GONE); - final TextView fileName = content.findViewById(R.id.file); - fileName.setVisibility(View.GONE); - - if (c.isFile() - || (c.isFileTransfer() && message.isOutgoing() && !c.getFilePath().isEmpty())) { - // If message is outgoing, even if content - // is file transfer we have the file available - final String filePath = c.getFilePath(); - - View v; - if (FileUtils.isExtensionImage(filePath)) { - if (!isMultiContent - && mContext.getResources() - .getBoolean( - R.bool.use_big_pictures_to_preview_images_file_transfers)) { - loadBitmap(c.getFilePath(), bigImage); - v = bigImage; - } else { - loadBitmap(c.getFilePath(), smallImage); - v = smallImage; - } - } else { - fileName.setText(FileUtils.getNameFromFilePath(filePath)); - v = fileName; - } - v.setVisibility(View.VISIBLE); - v.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - if (isEditionEnabled) { - ChatMessageViewHolder.this.onClick(v); - } else { - openFile(filePath); - } - } - }); - } else { - downloadOrCancel.setVisibility(View.VISIBLE); - - if (mContext.getPackageManager() - .checkPermission( - Manifest.permission.WRITE_EXTERNAL_STORAGE, - mContext.getPackageName()) - == PackageManager.PERMISSION_GRANTED) { - String filename = c.getName(); - File file = new File(FileUtils.getStorageDirectory(mContext), filename); - - int prefix = 1; - while (file.exists()) { - file = - new File( - FileUtils.getStorageDirectory(mContext), - prefix + "_" + filename); - Log.w( - "[Chat Message View] File with that name already exists, renamed to " - + prefix - + "_" - + filename); - prefix += 1; - } - c.setFilePath(file.getPath()); - - downloadOrCancel.setTag(c); - if (!message.isFileTransferInProgress()) { - downloadOrCancel.setText(R.string.download_file); - } else { - downloadOrCancel.setText(R.string.cancel); - } - - downloadOrCancel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - if (isEditionEnabled) { - ChatMessageViewHolder.this.onClick(v); - } else { - Content c = (Content) v.getTag(); - if (!message.isFileTransferInProgress()) { - message.downloadContent(c); - } else { - message.cancelFileTransfer(); - } - } - } - }); - } else { - Log.w( - "[Chat Message View] WRITE_EXTERNAL_STORAGE permission not granted, won't be able to store the downloaded file"); - ((ChatActivity) mContext) - .requestPermissionIfNotGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE); - } - } - } - - private void openFile(String path) { - Intent intent = new Intent(Intent.ACTION_VIEW); - File file; - Uri contentUri; - if (path.startsWith("file://")) { - path = path.substring("file://".length()); - file = new File(path); - contentUri = - FileProvider.getUriForFile( - mContext, - mContext.getResources().getString(R.string.file_provider), - file); - } else if (path.startsWith("content://")) { - contentUri = Uri.parse(path); - } else { - file = new File(path); - try { - contentUri = - FileProvider.getUriForFile( - mContext, - mContext.getResources().getString(R.string.file_provider), - file); - } catch (Exception e) { - Log.e( - "[Chat Message View] Couldn't get URI for file " - + file - + " using file provider " - + mContext.getResources().getString(R.string.file_provider)); - contentUri = Uri.parse(path); - } - } - - String filePath = contentUri.toString(); - Log.i("[Chat Message View] Trying to open file: " + filePath); - String type = null; - String extension = FileUtils.getExtensionFromFileName(filePath); - - if (extension != null && !extension.isEmpty()) { - Log.i("[Chat Message View] Found extension " + extension); - type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - } else { - Log.e("[Chat Message View] Couldn't find extension"); - } - - if (type != null) { - Log.i("[Chat Message View] Found matching MIME type " + type); - } else { - type = FileUtils.getMimeFromFile(filePath); - Log.e( - "[Chat Message View] Can't get MIME type from extension: " - + extension - + ", will use " - + type); - } - - intent.setDataAndType(contentUri, type); - intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); - - try { - mContext.startActivity(intent); - } catch (ActivityNotFoundException anfe) { - Log.e("[Chat Message View] Couldn't find an activity to handle MIME type: " + type); - Toast.makeText(mContext, R.string.cant_open_file_no_app_found, Toast.LENGTH_LONG) - .show(); - } - } - - private void loadBitmap(String path, ImageView imageView) { - Glide.with(mContext).load(path).into(imageView); - } - - private void updateEphemeralTimer(ChatMessage message) { - if (!message.isEphemeral()) { - if (countDownTimer != null) { - countDownTimer.cancel(); - countDownTimer = null; - } - return; - } - - if (message.getEphemeralExpireTime() == 0) { - // This means the message hasn't been read by all participants yet, so the countdown - // hasn't started - // In this case we simply display the configured value for lifetime - ephemeralCountdown.setText(formatLifetime(message.getEphemeralLifetime())); - if (countDownTimer != null) { - countDownTimer.cancel(); - countDownTimer = null; - } - } else { - // Countdown has started, display remaining time - long remaining = message.getEphemeralExpireTime() - (System.currentTimeMillis() / 1000); - ephemeralCountdown.setText(formatLifetime(remaining)); - - if (countDownTimer == null) { - countDownTimer = - new CountDownTimer(remaining * 1000, 1000) { - @Override - public void onTick(long millisUntilFinished) { - ephemeralCountdown.setText( - formatLifetime(millisUntilFinished / 1000)); - } - - @Override - public void onFinish() {} - }; - countDownTimer.start(); - } - } - } - - private String formatLifetime(long seconds) { - long days = seconds / 86400; - if (days == 0) { - return String.format( - "%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, (seconds % 60)); - } else { - return mContext.getResources().getQuantityString(R.plurals.days, (int) days, days); - } - } -} diff --git a/app/src/main/java/org/linphone/chat/ChatMessageViewHolderClickListener.java b/app/src/main/java/org/linphone/chat/ChatMessageViewHolderClickListener.java deleted file mode 100644 index 0ac8bb490..000000000 --- a/app/src/main/java/org/linphone/chat/ChatMessageViewHolderClickListener.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -interface ChatMessageViewHolderClickListener { - void onItemClicked(int position); -} diff --git a/app/src/main/java/org/linphone/chat/ChatMessagesAdapter.java b/app/src/main/java/org/linphone/chat/ChatMessagesAdapter.java deleted file mode 100644 index 9a17efd3f..000000000 --- a/app/src/main/java/org/linphone/chat/ChatMessagesAdapter.java +++ /dev/null @@ -1,452 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import android.content.ContentValues; -import android.content.Context; -import android.provider.MediaStore; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.linphone.R; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.core.Address; -import org.linphone.core.ChatMessage; -import org.linphone.core.ChatMessageListenerStub; -import org.linphone.core.Content; -import org.linphone.core.EventLog; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.FileUtils; -import org.linphone.utils.LinphoneUtils; -import org.linphone.utils.SelectableAdapter; -import org.linphone.utils.SelectableHelper; - -public class ChatMessagesAdapter extends SelectableAdapter - implements ChatMessagesGenericAdapter { - private static final int MAX_TIME_TO_GROUP_MESSAGES = 300; // 5 minutes - - private final Context mContext; - private List mHistory; - private List mParticipants; - private final int mItemResource; - private final ChatMessagesFragment mFragment; - - private final List mTransientMessages; - - private final ChatMessageViewHolderClickListener mClickListener; - private final ChatMessageListenerStub mListener; - - public ChatMessagesAdapter( - ChatMessagesFragment fragment, - SelectableHelper helper, - int itemResource, - EventLog[] history, - ArrayList participants, - ChatMessageViewHolderClickListener clickListener) { - super(helper); - mFragment = fragment; - mContext = mFragment.getActivity(); - mItemResource = itemResource; - mHistory = new ArrayList<>(Arrays.asList(history)); - Collections.reverse(mHistory); - mParticipants = participants; - mClickListener = clickListener; - mTransientMessages = new ArrayList<>(); - - mListener = - new ChatMessageListenerStub() { - @Override - public void onMsgStateChanged(ChatMessage message, ChatMessage.State state) { - ChatMessageViewHolder holder = - (ChatMessageViewHolder) message.getUserData(); - if (holder != null) { - int position = holder.getAdapterPosition(); - if (position >= 0) { - notifyItemChanged(position); - } else { - notifyDataSetChanged(); - } - } else { - // Just in case, better to refresh the whole view than to miss - // an update - notifyDataSetChanged(); - } - if (state == ChatMessage.State.Displayed) { - mTransientMessages.remove(message); - } else if (state == ChatMessage.State.FileTransferDone) { - Log.i("[Chat Message] File transfer done"); - - // Do not do it for ephemeral messages of if setting is disabled - if (!message.isEphemeral() - && LinphonePreferences.instance() - .makeDownloadedImagesVisibleInNativeGallery()) { - for (Content content : message.getContents()) { - if (content.isFile() && content.getFilePath() != null) { - addImageToNativeGalery(content.getFilePath()); - } - } - } - } - } - }; - } - - private void addImageToNativeGalery(String filePath) { - if (!FileUtils.isExtensionImage(filePath)) return; - - String mime = FileUtils.getMimeFromFile(filePath); - Log.i("[Chat Message] Adding file ", filePath, " to native gallery with MIME ", mime); - - ContentValues values = new ContentValues(); - values.put(MediaStore.Images.Media.DATA, filePath); - values.put(MediaStore.Images.Media.MIME_TYPE, mime); - mContext.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); - } - - @Override - public ChatMessageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(mItemResource, parent, false); - ChatMessageViewHolder VH = new ChatMessageViewHolder(mContext, v, mClickListener); - - // Allows onLongClick ContextMenu on bubbles - mFragment.registerForContextMenu(v); - v.setTag(VH); - return VH; - } - - @Override - public void onBindViewHolder(@NonNull ChatMessageViewHolder holder, int position) { - if (position < 0) return; - EventLog event = mHistory.get(position); - - holder.delete.setVisibility(View.GONE); - holder.eventLayout.setVisibility(View.GONE); - holder.securityEventLayout.setVisibility(View.GONE); - holder.rightAnchor.setVisibility(View.GONE); - holder.bubbleLayout.setVisibility(View.GONE); - holder.sendInProgress.setVisibility(View.GONE); - - holder.isEditionEnabled = isEditionEnabled(); - if (isEditionEnabled()) { - holder.delete.setVisibility(View.VISIBLE); - holder.delete.setChecked(isSelected(position)); - holder.delete.setTag(position); - } - - if (event.getType() == EventLog.Type.ConferenceChatMessage) { - ChatMessage message = event.getChatMessage(); - - if ((message.isOutgoing() && message.getState() != ChatMessage.State.Displayed) - || (!message.isOutgoing() && message.isFileTransfer())) { - if (!mTransientMessages.contains(message)) { - mTransientMessages.add(message); - } - // This only works if JAVA object is kept, hence the transient list - message.setUserData(holder); - message.addListener(mListener); - } - - LinphoneContact contact = null; - Address remoteSender = message.getFromAddress(); - if (!message.isOutgoing()) { - for (LinphoneContact c : mParticipants) { - if (c != null && c.hasAddress(remoteSender.asStringUriOnly())) { - contact = c; - break; - } - } - } - holder.bindMessage(message, contact); - changeBackgroundDependingOnPreviousAndNextEvents(message, holder, position); - } else { // Event is not chat message - Address address = event.getParticipantAddress(); - if (address == null && event.getType() == EventLog.Type.ConferenceSecurityEvent) { - address = event.getSecurityEventFaultyDeviceAddress(); - } - String displayName = ""; - if (address != null) { - LinphoneContact contact = - ContactsManager.getInstance().findContactFromAddress(address); - if (contact != null) { - displayName = contact.getFullName(); - } else { - displayName = LinphoneUtils.getAddressDisplayName(address); - } - } - - switch (event.getType()) { - case ConferenceCreated: - holder.eventLayout.setVisibility(View.VISIBLE); - holder.eventMessage.setText(mContext.getString(R.string.conference_created)); - break; - case ConferenceTerminated: - holder.eventLayout.setVisibility(View.VISIBLE); - holder.eventMessage.setText(mContext.getString(R.string.conference_destroyed)); - break; - case ConferenceParticipantAdded: - holder.eventLayout.setVisibility(View.VISIBLE); - holder.eventMessage.setText( - mContext.getString(R.string.participant_added) - .replace("%s", displayName)); - break; - case ConferenceParticipantRemoved: - holder.eventLayout.setVisibility(View.VISIBLE); - holder.eventMessage.setText( - mContext.getString(R.string.participant_removed) - .replace("%s", displayName)); - break; - case ConferenceSubjectChanged: - holder.eventLayout.setVisibility(View.VISIBLE); - holder.eventMessage.setText( - mContext.getString(R.string.subject_changed) - .replace("%s", event.getSubject())); - break; - case ConferenceParticipantSetAdmin: - holder.eventLayout.setVisibility(View.VISIBLE); - holder.eventMessage.setText( - mContext.getString(R.string.admin_set).replace("%s", displayName)); - break; - case ConferenceParticipantUnsetAdmin: - holder.eventLayout.setVisibility(View.VISIBLE); - holder.eventMessage.setText( - mContext.getString(R.string.admin_unset).replace("%s", displayName)); - break; - case ConferenceParticipantDeviceAdded: - holder.eventLayout.setVisibility(View.VISIBLE); - holder.eventMessage.setText( - mContext.getString(R.string.device_added).replace("%s", displayName)); - break; - case ConferenceParticipantDeviceRemoved: - holder.eventLayout.setVisibility(View.VISIBLE); - holder.eventMessage.setText( - mContext.getString(R.string.device_removed).replace("%s", displayName)); - break; - - case ConferenceEphemeralMessageDisabled: - holder.eventLayout.setVisibility(View.VISIBLE); - holder.eventMessage.setText( - mContext.getString(R.string.chat_event_ephemeral_disabled)); - break; - case ConferenceEphemeralMessageEnabled: - holder.eventLayout.setVisibility(View.VISIBLE); - holder.eventMessage.setText( - mContext.getString(R.string.chat_event_ephemeral_enabled) - .replace( - "%s", - formatEphemeralExpiration( - event.getEphemeralMessageLifetime()))); - break; - case ConferenceEphemeralMessageLifetimeChanged: - holder.eventLayout.setVisibility(View.VISIBLE); - holder.eventMessage.setText( - mContext.getString(R.string.chat_event_ephemeral_lifetime_changed) - .replace( - "%s", - formatEphemeralExpiration( - event.getEphemeralMessageLifetime()))); - break; - case ConferenceSecurityEvent: - holder.securityEventLayout.setVisibility(View.VISIBLE); - - switch (event.getSecurityEventType()) { - case EncryptionIdentityKeyChanged: - holder.securityEventMessage.setText( - mContext.getString(R.string.lime_identity_key_changed) - .replace("%s", displayName)); - break; - case ManInTheMiddleDetected: - holder.securityEventMessage.setText( - mContext.getString(R.string.man_in_the_middle_detected) - .replace("%s", displayName)); - break; - case SecurityLevelDowngraded: - holder.securityEventMessage.setText( - mContext.getString(R.string.security_level_downgraded) - .replace("%s", displayName)); - break; - case ParticipantMaxDeviceCountExceeded: - holder.securityEventMessage.setText( - mContext.getString(R.string.participant_max_count_exceeded) - .replace("%s", displayName)); - break; - case None: - default: - break; - } - break; - case None: - default: - holder.eventLayout.setVisibility(View.VISIBLE); - holder.eventMessage.setText( - mContext.getString(R.string.unexpected_event) - .replace("%s", displayName) - .replace("%i", String.valueOf(event.getType().toInt()))); - break; - } - } - } - - private String formatEphemeralExpiration(long duration) { - if (duration == 0) { - return mContext.getString(R.string.chat_room_ephemeral_message_disabled); - } else if (duration == 60) { - return mContext.getString(R.string.chat_room_ephemeral_message_one_minute); - } else if (duration == 3600) { - return mContext.getString(R.string.chat_room_ephemeral_message_one_hour); - } else if (duration == 86400) { - return mContext.getString(R.string.chat_room_ephemeral_message_one_day); - } else if (duration == 259200) { - return mContext.getString(R.string.chat_room_ephemeral_message_three_days); - } else if (duration == 604800) { - return mContext.getString(R.string.chat_room_ephemeral_message_one_week); - } else { - return "Unexpected duration"; - } - } - - @Override - public int getItemCount() { - return mHistory.size(); - } - - public void addToHistory(EventLog log) { - if (!mHistory.contains(log)) { - mHistory.add(0, log); - notifyItemInserted(0); - notifyItemChanged(1); // Update second to last item just in case for grouping purposes - } - } - - public void addAllToHistory(ArrayList logs) { - int currentSize = mHistory.size() - 1; - Collections.reverse(logs); - mHistory.addAll(logs); - notifyItemRangeInserted(currentSize + 1, logs.size()); - } - - public void setContacts(ArrayList participants) { - mParticipants = participants; - } - - public void refresh(EventLog[] history) { - mHistory = new ArrayList<>(Arrays.asList(history)); - Collections.reverse(mHistory); - notifyDataSetChanged(); - } - - public void clear() { - for (EventLog event : mHistory) { - if (event.getType() == EventLog.Type.ConferenceChatMessage) { - ChatMessage message = event.getChatMessage(); - message.removeListener(mListener); - } - } - mTransientMessages.clear(); - mHistory.clear(); - } - - public Object getItem(int i) { - return mHistory.get(i); - } - - public void removeItem(int i) { - mHistory.remove(i); - notifyItemRemoved(i); - } - - @Override - public boolean removeFromHistory(EventLog eventLog) { - int index = mHistory.indexOf(eventLog); - if (index >= 0) { - removeItem(index); - return true; - } - return false; - } - - private void changeBackgroundDependingOnPreviousAndNextEvents( - ChatMessage message, ChatMessageViewHolder holder, int position) { - boolean hasPrevious = false, hasNext = false; - - // Do not forget history is reversed, so previous in order is next in list display and - // chronology ! - if (position > 0 - && mContext.getResources() - .getBoolean(R.bool.lower_space_between_chat_bubbles_if_same_person)) { - EventLog previousEvent = (EventLog) getItem(position - 1); - if (previousEvent.getType() == EventLog.Type.ConferenceChatMessage) { - ChatMessage previousMessage = previousEvent.getChatMessage(); - if (previousMessage.getFromAddress().weakEqual(message.getFromAddress())) { - if (previousMessage.getTime() - message.getTime() - < MAX_TIME_TO_GROUP_MESSAGES) { - hasPrevious = true; - } - } - } - } - if (position >= 0 - && position < mHistory.size() - 1 - && mContext.getResources() - .getBoolean(R.bool.lower_space_between_chat_bubbles_if_same_person)) { - EventLog nextEvent = (EventLog) getItem(position + 1); - if (nextEvent.getType() == EventLog.Type.ConferenceChatMessage) { - ChatMessage nextMessage = nextEvent.getChatMessage(); - if (nextMessage.getFromAddress().weakEqual(message.getFromAddress())) { - if (message.getTime() - nextMessage.getTime() < MAX_TIME_TO_GROUP_MESSAGES) { - holder.timeText.setVisibility(View.GONE); - if (!message.isOutgoing()) { - holder.avatarLayout.setVisibility(View.INVISIBLE); - } - hasNext = true; - } - } - } - } - - if (message.isOutgoing()) { - if (hasNext && hasPrevious) { - holder.background.setBackgroundResource(R.drawable.chat_bubble_outgoing_split_2); - } else if (hasNext) { - holder.background.setBackgroundResource(R.drawable.chat_bubble_outgoing_split_3); - } else if (hasPrevious) { - holder.background.setBackgroundResource(R.drawable.chat_bubble_outgoing_split_1); - } else { - holder.background.setBackgroundResource(R.drawable.chat_bubble_outgoing_full); - } - } else { - if (hasNext && hasPrevious) { - holder.background.setBackgroundResource(R.drawable.chat_bubble_incoming_split_2); - } else if (hasNext) { - holder.background.setBackgroundResource(R.drawable.chat_bubble_incoming_split_3); - } else if (hasPrevious) { - holder.background.setBackgroundResource(R.drawable.chat_bubble_incoming_split_1); - } else { - holder.background.setBackgroundResource(R.drawable.chat_bubble_incoming_full); - } - } - } -} diff --git a/app/src/main/java/org/linphone/chat/ChatMessagesFragment.java b/app/src/main/java/org/linphone/chat/ChatMessagesFragment.java deleted file mode 100644 index 440f85b6d..000000000 --- a/app/src/main/java/org/linphone/chat/ChatMessagesFragment.java +++ /dev/null @@ -1,1561 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import static android.content.Context.INPUT_METHOD_SERVICE; - -import android.Manifest; -import android.app.Activity; -import android.app.Dialog; -import android.app.Fragment; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.Intent; -import android.graphics.Rect; -import android.net.Uri; -import android.os.Bundle; -import android.os.Parcelable; -import android.provider.MediaStore; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.view.WindowManager; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.appcompat.view.menu.MenuBuilder; -import androidx.appcompat.view.menu.MenuPopupHelper; -import androidx.core.view.inputmethod.InputConnectionCompat; -import androidx.core.view.inputmethod.InputContentInfoCompat; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import com.bumptech.glide.Glide; -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.activities.MainActivity; -import org.linphone.contacts.ContactAddress; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.ContactsUpdatedListener; -import org.linphone.contacts.LinphoneContact; -import org.linphone.core.Address; -import org.linphone.core.Call; -import org.linphone.core.ChatMessage; -import org.linphone.core.ChatRoom; -import org.linphone.core.ChatRoomCapabilities; -import org.linphone.core.ChatRoomListener; -import org.linphone.core.ChatRoomSecurityLevel; -import org.linphone.core.Content; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.EventLog; -import org.linphone.core.Factory; -import org.linphone.core.Participant; -import org.linphone.core.ParticipantDevice; -import org.linphone.core.ParticipantImdnState; -import org.linphone.core.Reason; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.FileUtils; -import org.linphone.utils.LinphoneUtils; -import org.linphone.utils.SelectableHelper; -import org.linphone.views.RichEditText; - -public class ChatMessagesFragment extends Fragment - implements ChatRoomListener, - ContactsUpdatedListener, - ChatMessageViewHolderClickListener, - SelectableHelper.DeleteListener, - RichEditText.RichInputListener { - private static final int ADD_PHOTO = 1337; - private static final int MESSAGES_PER_PAGE = 20; - private static final String INPUT_CONTENT_INFO_KEY = "COMMIT_CONTENT_INPUT_CONTENT_INFO"; - private static final String COMMIT_CONTENT_FLAGS_KEY = "COMMIT_CONTENT_FLAGS"; - - private ImageView mCallButton; - private ImageView mBackToCallButton; - private ImageView mPopupMenu; - private ImageView mAttachImageButton, mSendMessageButton, mSendEphemeralIcon; - private TextView mRoomLabel, mParticipantsLabel, mSipUriLabel, mRemoteComposing; - private RichEditText mMessageTextToSend; - private LayoutInflater mInflater; - private RecyclerView mChatEventsList; - private LinearLayout mFilesUploadLayout; - private SelectableHelper mSelectionHelper; - private Context mContext; - private ViewTreeObserver.OnGlobalLayoutListener mKeyboardListener; - private Uri mImageToUploadUri; - private String mRemoteSipUri; - private Address mLocalSipAddress, mRemoteSipAddress, mRemoteParticipantAddress; - private ChatRoom mChatRoom; - private ArrayList mParticipants; - private int mContextMenuMessagePosition; - private LinearLayout mTopBar; - private ImageView mChatRoomSecurityLevel; - private CoreListenerStub mCoreListener; - - private InputContentInfoCompat mCurrentInputContentInfo; - - @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // Retain the fragment across configuration changes - setRetainInstance(true); - - if (getArguments() != null) { - if (getArguments().getString("LocalSipUri") != null) { - String mLocalSipUri = getArguments().getString("LocalSipUri"); - mLocalSipAddress = Factory.instance().createAddress(mLocalSipUri); - } - if (getArguments().getString("RemoteSipUri") != null) { - mRemoteSipUri = getArguments().getString("RemoteSipUri"); - mRemoteSipAddress = Factory.instance().createAddress(mRemoteSipUri); - } - } - - mContext = getActivity().getApplicationContext(); - mInflater = inflater; - View view = inflater.inflate(R.layout.chat, container, false); - - mTopBar = view.findViewById(R.id.top_bar); - - mChatRoomSecurityLevel = view.findViewById(R.id.room_security_level); - mChatRoomSecurityLevel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - goToDevices(); - } - }); - - ImageView backButton = view.findViewById(R.id.back); - backButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - ((ChatActivity) getActivity()).goBack(); - } - }); - backButton.setVisibility( - getResources().getBoolean(R.bool.isTablet) ? View.INVISIBLE : View.VISIBLE); - - mCallButton = view.findViewById(R.id.start_call); - mCallButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - LinphoneManager.getCallManager() - .newOutgoingCall(mRemoteParticipantAddress.asString(), null); - } - }); - - mBackToCallButton = view.findViewById(R.id.back_to_call); - mBackToCallButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - ((MainActivity) getActivity()).goBackToCall(); - } - }); - - mPopupMenu = view.findViewById(R.id.menu); - mPopupMenu.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - showPopupMenu(); - } - }); - - mRoomLabel = view.findViewById(R.id.subject); - mParticipantsLabel = view.findViewById(R.id.participants); - mSipUriLabel = view.findViewById(R.id.sipUri); - - mFilesUploadLayout = view.findViewById(R.id.file_upload_layout); - - mAttachImageButton = view.findViewById(R.id.send_picture); - mAttachImageButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - String[] permissions = { - Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE - }; - ((ChatActivity) getActivity()).requestPermissionsIfNotGranted(permissions); - pickFile(); - } - }); - if (getResources().getBoolean(R.bool.disable_chat_send_file)) { - mAttachImageButton.setEnabled(false); - mAttachImageButton.setVisibility(View.GONE); - } - - mSendEphemeralIcon = view.findViewById(R.id.send_ephemeral_message); - mSendMessageButton = view.findViewById(R.id.send_message); - mSendMessageButton.setEnabled(false); - mSendEphemeralIcon.setEnabled(mSendMessageButton.isEnabled()); - mSendMessageButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - sendMessage(); - } - }); - - mMessageTextToSend = view.findViewById(R.id.message); - mMessageTextToSend.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence charSequence, int i, int i1, int i2) {} - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { - mSendMessageButton.setEnabled( - mMessageTextToSend.getText().length() > 0 - || mFilesUploadLayout.getChildCount() > 0); - mSendEphemeralIcon.setEnabled(mSendMessageButton.isEnabled()); - if (mChatRoom != null && mMessageTextToSend.getText().length() > 0) { - if (!getResources().getBoolean(R.bool.allow_multiple_images_and_text)) { - mAttachImageButton.setEnabled(false); - } - mChatRoom.compose(); - } else { - mAttachImageButton.setEnabled(true); - } - } - - @Override - public void afterTextChanged(Editable editable) {} - }); - mMessageTextToSend.clearFocus(); - mMessageTextToSend.setListener(this); - - mRemoteComposing = view.findViewById(R.id.remote_composing); - - mChatEventsList = view.findViewById(R.id.chat_message_list); - mSelectionHelper = new SelectableHelper(view, this); - LinearLayoutManager layoutManager = - new LinphoneLinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, true); - mChatEventsList.setLayoutManager(layoutManager); - - ChatScrollListener chatScrollListener = - new ChatScrollListener(layoutManager) { - @Override - public void onLoadMore(int totalItemsCount) { - loadMoreData(totalItemsCount); - } - }; - mChatEventsList.addOnScrollListener(chatScrollListener); - - if (getArguments() != null) { - String fileSharedUri = getArguments().getString("SharedFiles"); - if (fileSharedUri != null) { - Log.i("[Chat Messages Fragment] Found shared file(s): " + fileSharedUri); - if (fileSharedUri.contains(":")) { - String[] files = fileSharedUri.split(":"); - for (String file : files) { - addFileIntoSharingArea(file); - } - } else { - addFileIntoSharingArea(fileSharedUri); - } - } - - if (getArguments().containsKey("SharedText")) { - String sharedText = getArguments().getString("SharedText"); - mMessageTextToSend.setText(sharedText); - Log.i("[Chat Messages Fragment] Found shared text: " + sharedText); - } - getArguments().clear(); - } - - if (savedInstanceState != null) { - onRestoreInstanceState(savedInstanceState); - } - - mCoreListener = - new CoreListenerStub() { - @Override - public void onCallStateChanged( - Core core, Call call, Call.State state, String message) { - displayChatRoomHeader(); - } - }; - - return view; - } - - @Override - public void onResume() { - super.onResume(); - - Core core = LinphoneManager.getCore(); - if (core != null) { - core.addListener(mCoreListener); - } - - ContactsManager.getInstance().addContactsListener(this); - - addVirtualKeyboardVisiblityListener(); - // Force hide keyboard - getActivity() - .getWindow() - .setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN - | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); - InputMethodManager inputMethodManager = - (InputMethodManager) getActivity().getSystemService(INPUT_METHOD_SERVICE); - if (getActivity().getCurrentFocus() != null) { - inputMethodManager.hideSoftInputFromWindow( - getActivity().getCurrentFocus().getWindowToken(), 0); - } - - initChatRoom(); - displayChatRoomHeader(); - displayChatRoomHistory(); - - LinphoneContext.instance() - .getNotificationManager() - .setCurrentlyDisplayedChatRoom( - mRemoteSipAddress != null ? mRemoteSipAddress.asStringUriOnly() : null); - } - - @Override - public void onPause() { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.removeListener(mCoreListener); - } - - ContactsManager.getInstance().removeContactsListener(this); - removeVirtualKeyboardVisiblityListener(); - LinphoneContext.instance().getNotificationManager().setCurrentlyDisplayedChatRoom(null); - if (mChatRoom != null) mChatRoom.removeListener(this); - if (mChatEventsList.getAdapter() != null) - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).clear(); - - super.onPause(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putString("LocalSipUri", mChatRoom.getLocalAddress().asStringUriOnly()); - outState.putString("RemoteSipUri", mChatRoom.getPeerAddress().asStringUriOnly()); - - ArrayList files = new ArrayList<>(); - for (int i = 0; i < mFilesUploadLayout.getChildCount(); i++) { - View child = mFilesUploadLayout.getChildAt(i); - String filePath = (String) child.getTag(); - files.add(filePath); - } - outState.putStringArrayList("Files", files); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (data != null) { - if (requestCode == ADD_PHOTO && resultCode == Activity.RESULT_OK) { - String fileToUploadPath = null; - if (data.getData() != null) { - Log.i( - "[Chat Messages Fragment] Intent data after picking file is " - + data.getData().toString()); - if (data.getData().toString().contains("com.android.contacts/contacts/")) { - Uri cvsPath = FileUtils.getCVSPathFromLookupUri(data.getData().toString()); - if (cvsPath != null) { - fileToUploadPath = cvsPath.toString(); - Log.i("[Chat Messages Fragment] Found CVS path: " + fileToUploadPath); - } else { - // TODO Error - return; - } - } else { - fileToUploadPath = - FileUtils.getRealPathFromURI(getActivity(), data.getData()); - Log.i( - "[Chat Messages Fragment] Resolved path for data is: " - + fileToUploadPath); - } - if (fileToUploadPath == null) { - fileToUploadPath = data.getData().toString(); - Log.i( - "[Chat Messages Fragment] Couldn't resolve path, using as-is: " - + fileToUploadPath); - } - } else if (mImageToUploadUri != null) { - fileToUploadPath = mImageToUploadUri.getPath(); - Log.i( - "[Chat Messages Fragment] Using pre-created path for dynamic capture " - + fileToUploadPath); - } - - if (fileToUploadPath.startsWith("content://") - || fileToUploadPath.startsWith("file://")) { - Uri uriToParse = Uri.parse(fileToUploadPath); - fileToUploadPath = - FileUtils.getFilePath( - getActivity().getApplicationContext(), uriToParse); - Log.i( - "[Chat Messages Fragment] Path was using a content or file scheme, real path is: " - + fileToUploadPath); - if (fileToUploadPath == null) { - Log.e( - "[Chat Messages Fragment] Failed to get access to file " - + uriToParse.toString()); - } - } else if (fileToUploadPath.contains("com.android.contacts/contacts/")) { - fileToUploadPath = - FileUtils.getCVSPathFromLookupUri(fileToUploadPath).toString(); - Log.i( - "[Chat Messages Fragment] Path was using a contact scheme, real path is: " - + fileToUploadPath); - } - - if (fileToUploadPath != null) { - if (FileUtils.isExtensionImage(fileToUploadPath)) { - addImageToPendingList(fileToUploadPath); - } else { - addFileToPendingList(fileToUploadPath); - } - } else { - Log.e( - "[Chat Messages Fragment] Failed to get a path that we could use, aborting attachment"); - } - } else { - super.onActivityResult(requestCode, resultCode, data); - } - } else { - if (FileUtils.isExtensionImage(mImageToUploadUri.getPath())) { - File file = new File(mImageToUploadUri.getPath()); - if (file.exists()) { - addImageToPendingList(mImageToUploadUri.getPath()); - } - } - } - } - - @Override - public void onDeleteSelection(Object[] objectsToDelete) { - for (Object obj : objectsToDelete) { - EventLog eventLog = (EventLog) obj; - LinphoneUtils.deleteFileContentIfExists(eventLog); - eventLog.deleteFromDatabase(); - } - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()) - .refresh(mChatRoom.getHistoryEvents(MESSAGES_PER_PAGE)); - } - - @Override - public void onCreateContextMenu( - ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - - ChatMessageViewHolder holder = (ChatMessageViewHolder) v.getTag(); - mContextMenuMessagePosition = holder.getAdapterPosition(); - - EventLog event = - (EventLog) - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()) - .getItem(mContextMenuMessagePosition); - if (event.getType() != EventLog.Type.ConferenceChatMessage) { - return; - } - - MenuInflater inflater = getActivity().getMenuInflater(); - ChatMessage message = event.getChatMessage(); - if (message.getState() == ChatMessage.State.NotDelivered) { - inflater.inflate(R.menu.chat_bubble_menu_with_resend, menu); - } else { - inflater.inflate(R.menu.chat_bubble_menu, menu); - } - - if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { - // Do not show incoming messages IDMN state in 1 to 1 chat room as we don't receive IMDN - // for them - menu.removeItem(R.id.imdn_infos); - } - if (!message.hasTextContent()) { - // Do not show copy text option if message doesn't have any text - menu.removeItem(R.id.copy_text); - } - - if (!message.isOutgoing()) { - Address address = message.getFromAddress(); - LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(address); - if (contact != null) { - menu.removeItem(R.id.add_to_contacts); - } - } else { - menu.removeItem(R.id.add_to_contacts); - } - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - EventLog event = - (EventLog) - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()) - .getItem(mContextMenuMessagePosition); - - if (event.getType() != EventLog.Type.ConferenceChatMessage) { - return super.onContextItemSelected(item); - } - - ChatMessage message = event.getChatMessage(); - String messageId = message.getMessageId(); - - if (item.getItemId() == R.id.resend) { - message.send(); - return true; - } - if (item.getItemId() == R.id.imdn_infos) { - ((ChatActivity) getActivity()).showImdn(mLocalSipAddress, mRemoteSipAddress, messageId); - return true; - } - if (item.getItemId() == R.id.forward) { - ((ChatActivity) getActivity()).forwardMessage(message); - return true; - } - if (item.getItemId() == R.id.copy_text) { - if (message.hasTextContent()) { - ClipboardManager clipboard = - (ClipboardManager) - getActivity().getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("Message", message.getTextContent()); - clipboard.setPrimaryClip(clip); - } - return true; - } - if (item.getItemId() == R.id.delete_message) { - LinphoneUtils.deleteFileContentIfExists(event); - mChatRoom.deleteMessage(message); - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()) - .removeItem(mContextMenuMessagePosition); - return true; - } - if (item.getItemId() == R.id.add_to_contacts) { - Address address = message.getFromAddress(); - if (address == null) return true; - address.clean(); - ((ChatActivity) getActivity()).showContactsListForCreationOrEdition(address); - return true; - } - return super.onContextItemSelected(item); - } - - private void addFileIntoSharingArea(String fileSharedUri) { - if (FileUtils.isExtensionImage(fileSharedUri)) { - addImageToPendingList(fileSharedUri); - } else { - if (fileSharedUri.startsWith("content://") || fileSharedUri.startsWith("file://")) { - fileSharedUri = - FileUtils.getFilePath( - getActivity().getApplicationContext(), Uri.parse(fileSharedUri)); - } else if (fileSharedUri.contains("com.android.contacts/contacts/")) { - fileSharedUri = FileUtils.getCVSPathFromLookupUri(fileSharedUri).toString(); - } - addFileToPendingList(fileSharedUri); - } - } - - private void loadMoreData(final int totalItemsCount) { - LinphoneUtils.dispatchOnUIThread( - new Runnable() { - @Override - public void run() { - int maxSize = mChatRoom.getHistoryEventsSize(); - if (totalItemsCount < maxSize) { - int upperBound = totalItemsCount + MESSAGES_PER_PAGE; - if (upperBound > maxSize) { - upperBound = maxSize; - } - EventLog[] newLogs; - newLogs = mChatRoom.getHistoryRangeEvents(totalItemsCount, upperBound); - ArrayList logsList = new ArrayList<>(Arrays.asList(newLogs)); - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()) - .addAllToHistory(logsList); - } - } - }); - } - - /** Keyboard management */ - private void addVirtualKeyboardVisiblityListener() { - mKeyboardListener = - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - Rect visibleArea = new Rect(); - getActivity() - .getWindow() - .getDecorView() - .getWindowVisibleDisplayFrame(visibleArea); - - int screenHeight = - getActivity().getWindow().getDecorView().getRootView().getHeight(); - int heightDiff = screenHeight - (visibleArea.bottom - visibleArea.top); - if (heightDiff > screenHeight * 0.15) { - showKeyboardVisibleMode(); - } else { - hideKeyboardVisibleMode(); - } - } - }; - getActivity() - .getWindow() - .getDecorView() - .getViewTreeObserver() - .addOnGlobalLayoutListener(mKeyboardListener); - } - - private void removeVirtualKeyboardVisiblityListener() { - getActivity() - .getWindow() - .getDecorView() - .getViewTreeObserver() - .removeOnGlobalLayoutListener(mKeyboardListener); - hideKeyboardVisibleMode(); - } - - private void showKeyboardVisibleMode() { - ((ChatActivity) getActivity()).hideTabBar(); - ((ChatActivity) getActivity()).hideStatusBar(); - mTopBar.setVisibility(View.GONE); - } - - private void hideKeyboardVisibleMode() { - if (!getResources().getBoolean(R.bool.hide_bottom_bar_on_second_level_views)) { - ((ChatActivity) getActivity()).showTabBar(); - } - ((ChatActivity) getActivity()).showStatusBar(); - mTopBar.setVisibility(View.VISIBLE); - } - - /** View initialization */ - private void setReadOnly(boolean readOnly) { - if (readOnly) { - mMessageTextToSend.setText(""); - mFilesUploadLayout.removeAllViews(); - } - - mMessageTextToSend.setEnabled(!readOnly); - mAttachImageButton.setEnabled(!readOnly); - mSendMessageButton.setEnabled(!readOnly); - mSendEphemeralIcon.setEnabled(mSendMessageButton.isEnabled()); - } - - private void getContactsForParticipants() { - mParticipants = new ArrayList<>(); - if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { - LinphoneContact c = - ContactsManager.getInstance().findContactFromAddress(mRemoteParticipantAddress); - if (c != null) { - mParticipants.add(c); - } - } else { - int index = 0; - StringBuilder participantsLabel = new StringBuilder(); - for (Participant p : mChatRoom.getParticipants()) { - LinphoneContact c = - ContactsManager.getInstance().findContactFromAddress(p.getAddress()); - if (c != null) { - mParticipants.add(c); - participantsLabel.append(c.getFullName()); - } else { - String displayName = LinphoneUtils.getAddressDisplayName(p.getAddress()); - participantsLabel.append(displayName); - } - index++; - if (index != mChatRoom.getNbParticipants()) participantsLabel.append(", "); - } - mParticipantsLabel.setText(participantsLabel.toString()); - } - - if (mChatEventsList.getAdapter() != null) { - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).setContacts(mParticipants); - } - } - - private void initChatRoom() { - if (mChatRoom != null) { - // Required on tablets - mChatRoom.removeListener(this); - } - - Core core = LinphoneManager.getCore(); - if (mRemoteSipAddress == null - || mRemoteSipUri == null - || mRemoteSipUri.isEmpty() - || core == null) { - Log.e("[Chat Messages Fragment] No local/remote SIP URI found!"); - // TODO error - return; - } - - if (mLocalSipAddress != null) { - mChatRoom = core.getChatRoom(mRemoteSipAddress, mLocalSipAddress); - } else { - mChatRoom = core.getChatRoomFromUri(mRemoteSipAddress.asStringUriOnly()); - } - mChatRoom.addListener(this); - mChatRoom.markAsRead(); - - ((ChatActivity) getActivity()).displayMissedChats(); - - mRemoteParticipantAddress = mRemoteSipAddress; - if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) - && mChatRoom.getParticipants().length > 0) { - mRemoteParticipantAddress = mChatRoom.getParticipants()[0].getAddress(); - } - - getContactsForParticipants(); - - mRemoteComposing.setVisibility(View.GONE); - } - - private void displayChatRoomHeader() { - Core core = LinphoneManager.getCore(); - if (core == null || mChatRoom == null) return; - - if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { - mCallButton.setVisibility(View.VISIBLE); - - if (mChatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())) { - mPopupMenu.setVisibility(View.GONE); - mSelectionHelper.setEditButtonVisibility(true); - } else { - mPopupMenu.setVisibility(View.VISIBLE); - mSelectionHelper.setEditButtonVisibility(false); - } - - mParticipantsLabel.setVisibility(View.GONE); - - if (mContext.getResources().getBoolean(R.bool.show_sip_uri_in_chat)) { - mSipUriLabel.setVisibility(View.VISIBLE); - } else { - mSipUriLabel.setVisibility(View.GONE); - mRoomLabel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - mSipUriLabel.setVisibility( - mSipUriLabel.getVisibility() == View.VISIBLE - ? View.GONE - : View.VISIBLE); - } - }); - } - - if (mParticipants.isEmpty()) { - // Contact not found - String displayName = LinphoneUtils.getAddressDisplayName(mRemoteParticipantAddress); - mRoomLabel.setText(displayName); - } else { - mRoomLabel.setText(mParticipants.get(0).getFullName()); - } - mSipUriLabel.setText(mRemoteParticipantAddress.asStringUriOnly()); - } else { - mCallButton.setVisibility(View.GONE); - mPopupMenu.setVisibility(View.VISIBLE); - mSelectionHelper.setEditButtonVisibility(false); - mRoomLabel.setText(mChatRoom.getSubject()); - mParticipantsLabel.setVisibility(View.VISIBLE); - mSipUriLabel.setVisibility(View.GONE); - } - - mBackToCallButton.setVisibility(View.GONE); - if (core.getCallsNb() > 0) { - mBackToCallButton.setVisibility(View.VISIBLE); - } - - mSendEphemeralIcon.setVisibility(mChatRoom.ephemeralEnabled() ? View.VISIBLE : View.GONE); - if (mChatRoom.hasBeenLeft()) { - setReadOnly(true); - } - - updateSecurityLevelIcon(); - } - - private void updateSecurityLevelIcon() { - if (!mChatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) { - mChatRoomSecurityLevel.setVisibility(View.GONE); - } else { - mChatRoomSecurityLevel.setVisibility(View.VISIBLE); - ChatRoomSecurityLevel level = mChatRoom.getSecurityLevel(); - switch (level) { - case Safe: - mChatRoomSecurityLevel.setImageResource(R.drawable.security_2_indicator); - break; - case Encrypted: - mChatRoomSecurityLevel.setImageResource(R.drawable.security_1_indicator); - break; - case ClearText: - case Unsafe: - mChatRoomSecurityLevel.setImageResource(R.drawable.security_alert_indicator); - break; - } - } - } - - private void displayChatRoomHistory() { - if (mChatRoom == null) return; - ChatMessagesAdapter mEventsAdapter = - new ChatMessagesAdapter( - this, - mSelectionHelper, - R.layout.chat_bubble, - mChatRoom.getHistoryEvents(MESSAGES_PER_PAGE), - mParticipants, - this); - mSelectionHelper.setAdapter(mEventsAdapter); - mChatEventsList.setAdapter(mEventsAdapter); - scrollToBottom(); - } - - private void showSecurityDialog(boolean oneParticipantOneDevice) { - final Dialog dialog = - ((ChatActivity) getActivity()) - .displayDialog(getString(R.string.lime_security_popup)); - Button delete = dialog.findViewById(R.id.dialog_delete_button); - delete.setVisibility(View.GONE); - Button ok = dialog.findViewById(R.id.dialog_ok_button); - ok.setText(oneParticipantOneDevice ? getString(R.string.call) : getString(R.string.ok)); - ok.setVisibility(View.VISIBLE); - Button cancel = dialog.findViewById(R.id.dialog_cancel_button); - cancel.setText(getString(R.string.cancel)); - - dialog.findViewById(R.id.dialog_do_not_ask_again_layout).setVisibility(View.VISIBLE); - final CheckBox doNotAskAgain = dialog.findViewById(R.id.doNotAskAgain); - dialog.findViewById(R.id.doNotAskAgainLabel) - .setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - doNotAskAgain.setChecked(!doNotAskAgain.isChecked()); - } - }); - - ok.setTag(oneParticipantOneDevice); - ok.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - boolean oneParticipantOneDevice = (boolean) view.getTag(); - if (doNotAskAgain.isChecked()) { - LinphonePreferences.instance().enableLimeSecurityPopup(false); - } - - if (oneParticipantOneDevice) { - ParticipantDevice device = - mChatRoom.getParticipants()[0].getDevices()[0]; - LinphoneManager.getCallManager() - .inviteAddress(device.getAddress(), true); - } else { - ((ChatActivity) getActivity()) - .showDevices(mLocalSipAddress, mRemoteSipAddress); - } - - dialog.dismiss(); - } - }); - - cancel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - if (doNotAskAgain.isChecked()) { - LinphonePreferences.instance().enableLimeSecurityPopup(false); - } - dialog.dismiss(); - } - }); - dialog.show(); - } - - private void scrollToBottom() { - mChatEventsList.getLayoutManager().scrollToPosition(0); - } - - @Override - public void onItemClicked(int position) { - if (mSelectionHelper.getAdapter().isEditionEnabled()) { - mSelectionHelper.getAdapter().toggleSelection(position); - } - } - - private void onRestoreInstanceState(Bundle savedInstanceState) { - - String localSipUri = savedInstanceState.getString("LocalSipUri"); - mRemoteSipUri = savedInstanceState.getString("RemoteSipUri"); - mLocalSipAddress = Factory.instance().createAddress(localSipUri); - mRemoteSipAddress = Factory.instance().createAddress(mRemoteSipUri); - - ArrayList files = savedInstanceState.getStringArrayList("Files"); - if (files != null && !files.isEmpty()) { - for (String file : files) { - if (FileUtils.isExtensionImage(file)) { - addImageToPendingList(file); - } else { - addFileToPendingList(file); - } - } - } - - final InputContentInfoCompat previousInputContentInfo = - InputContentInfoCompat.wrap( - savedInstanceState.getParcelable(INPUT_CONTENT_INFO_KEY)); - final int previousFlags = savedInstanceState.getInt(COMMIT_CONTENT_FLAGS_KEY); - if (previousInputContentInfo != null) { - onCommitContentInternal(previousInputContentInfo, previousFlags); - } - } - - private void pickFile() { - List cameraIntents = new ArrayList<>(); - - // Handles image & video picking - Intent galleryIntent = new Intent(Intent.ACTION_PICK); - galleryIntent.setType("*/*"); - galleryIntent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"image/*", "video/*"}); - - // Allows to capture directly from the camera - Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - File file = - new File( - FileUtils.getStorageDirectory(mContext), - getString(R.string.temp_photo_name_with_date) - .replace("%s", System.currentTimeMillis() + ".jpeg")); - mImageToUploadUri = Uri.fromFile(file); - captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, mImageToUploadUri); - cameraIntents.add(captureIntent); - - // Finally allow any kind of file - Intent fileIntent = new Intent(Intent.ACTION_GET_CONTENT); - fileIntent.setType("*/*"); - cameraIntents.add(fileIntent); - - Intent chooserIntent = - Intent.createChooser(galleryIntent, getString(R.string.image_picker_title)); - chooserIntent.putExtra( - Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toArray(new Parcelable[] {})); - - startActivityForResult(chooserIntent, ADD_PHOTO); - } - - private void addFileToPendingList(String path) { - if (path == null) { - Log.e( - "[Chat Messages Fragment] Can't add file to pending list because it's path is null..."); - return; - } - - View pendingFile = mInflater.inflate(R.layout.file_upload_cell, mFilesUploadLayout, false); - pendingFile.setTag(path); - - TextView text = pendingFile.findViewById(R.id.pendingFileForUpload); - String extension = path.substring(path.lastIndexOf('.')); - text.setText(extension); - - ImageView remove = pendingFile.findViewById(R.id.remove); - remove.setTag(pendingFile); - remove.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - View pendingImage = (View) view.getTag(); - mFilesUploadLayout.removeView(pendingImage); - mAttachImageButton.setEnabled(true); - mMessageTextToSend.setEnabled(true); - mSendMessageButton.setEnabled( - mMessageTextToSend.getText().length() > 0 - || mFilesUploadLayout.getChildCount() > 0); - mSendEphemeralIcon.setEnabled(mSendMessageButton.isEnabled()); - } - }); - - mFilesUploadLayout.addView(pendingFile); - - if (!getResources().getBoolean(R.bool.allow_multiple_images_and_text)) { - mAttachImageButton.setEnabled(false); - mMessageTextToSend.setEnabled(false); - } - mSendMessageButton.setEnabled(true); - mSendEphemeralIcon.setEnabled(mSendMessageButton.isEnabled()); - } - - private void addImageToPendingList(String path) { - if (path == null) { - Log.e( - "[Chat Messages Fragment] Can't add image to pending list because it's path is null..."); - return; - } - - View pendingImage = - mInflater.inflate(R.layout.image_upload_cell, mFilesUploadLayout, false); - pendingImage.setTag(path); - - ImageView image = pendingImage.findViewById(R.id.pendingImageForUpload); - Glide.with(mContext).load(path).into(image); - - ImageView remove = pendingImage.findViewById(R.id.remove); - remove.setTag(pendingImage); - remove.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - View pendingImage = (View) view.getTag(); - mFilesUploadLayout.removeView(pendingImage); - mAttachImageButton.setEnabled(true); - mMessageTextToSend.setEnabled(true); - mSendMessageButton.setEnabled( - mMessageTextToSend.getText().length() > 0 - || mFilesUploadLayout.getChildCount() > 0); - mSendEphemeralIcon.setEnabled(mSendMessageButton.isEnabled()); - } - }); - - mFilesUploadLayout.addView(pendingImage); - - if (!getResources().getBoolean(R.bool.allow_multiple_images_and_text)) { - mAttachImageButton.setEnabled(false); - mMessageTextToSend.setEnabled(false); - } - mSendMessageButton.setEnabled(true); - mSendEphemeralIcon.setEnabled(mSendMessageButton.isEnabled()); - } - - /** Message sending */ - private void sendMessage() { - ChatMessage msg = mChatRoom.createEmptyMessage(); - boolean isBasicChatRoom = mChatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt()); - boolean sendMultipleImagesAsDifferentMessages = - getResources().getBoolean(R.bool.send_multiple_images_as_different_messages); - boolean sendImageAndTextAsDifferentMessages = - getResources().getBoolean(R.bool.send_text_and_images_as_different_messages); - - String text = mMessageTextToSend.getText().toString(); - boolean hasText = text != null && text.length() > 0; - - int filesCount = mFilesUploadLayout.getChildCount(); - for (int i = 0; i < filesCount; i++) { - String filePath = (String) mFilesUploadLayout.getChildAt(i).getTag(); - String fileName = filePath.substring(filePath.lastIndexOf("/") + 1); - String extension = FileUtils.getExtensionFromFileName(fileName); - Content content = Factory.instance().createContent(); - if (FileUtils.isExtensionImage(fileName)) { - content.setType("image"); - } else { - content.setType("file"); - } - content.setSubtype(extension); - content.setName(fileName); - content.setFilePath(filePath); // Let the file body handler take care of the upload - - boolean split = - isBasicChatRoom; // Always split contents in basic chat rooms for compatibility - if (!split) { - if (hasText && sendImageAndTextAsDifferentMessages) { - split = true; - } else if (mFilesUploadLayout.getChildCount() > 1 - && sendMultipleImagesAsDifferentMessages) { - split = true; - - // Allow the last image to be sent with text if image and text at the same time - // OK - if (hasText && i == filesCount - 1) { - split = false; - } - } - } - - if (split) { - ChatMessage fileMessage = mChatRoom.createFileTransferMessage(content); - fileMessage.send(); - } else { - msg.addFileContent(content); - } - } - - if (hasText) { - msg.addTextContent(text); - } - - // Set listener not required here anymore, message will be added to messages list and - // adapter will set the listener - if (msg.getContents().length > 0) { - msg.send(); - } - - mFilesUploadLayout.removeAllViews(); - mAttachImageButton.setEnabled(true); - mMessageTextToSend.setEnabled(true); - mMessageTextToSend.setText(""); - } - - private void showPopupMenu() { - MenuBuilder builder = new MenuBuilder(getActivity()); - MenuPopupHelper popupMenu = new MenuPopupHelper(getActivity(), builder, mPopupMenu); - popupMenu.setForceShowIcon(true); - - new MenuInflater(getActivity()).inflate(R.menu.chat_room_menu, builder); - - if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { - builder.removeItem(R.id.chat_room_group_info); - } - - if (!mChatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) { - builder.removeItem(R.id.chat_room_participants_devices); - builder.removeItem(R.id.chat_room_ephemeral_messages); - } else { - if (!LinphonePreferences.instance().isEphemeralMessagesEnabled()) { - builder.removeItem(R.id.chat_room_ephemeral_messages); - } - } - - builder.setCallback( - new MenuBuilder.Callback() { - @Override - public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { - if (item.getItemId() == R.id.chat_room_group_info) { - goToGroupInfo(); - return true; - } else if (item.getItemId() == R.id.chat_room_participants_devices) { - goToDevices(); - return true; - } else if (item.getItemId() == R.id.chat_room_ephemeral_messages) { - goToEphemeral(); - return true; - } else if (item.getItemId() == R.id.chat_room_delete_messages) { - mSelectionHelper.enterEditionMode(); - return true; - } - return false; - } - - @Override - public void onMenuModeChange(MenuBuilder menu) {} - }); - - popupMenu.show(); - } - - private void goToDevices() { - boolean oneParticipantOneDevice = false; - if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { - ParticipantDevice[] devices = mChatRoom.getParticipants()[0].getDevices(); - // Only start a call automatically if both ourselves and the remote - // have 1 device exactly, otherwise show devices list. - oneParticipantOneDevice = - devices.length == 1 && mChatRoom.getMe().getDevices().length == 1; - } - - if (LinphonePreferences.instance().isLimeSecurityPopupEnabled()) { - showSecurityDialog(oneParticipantOneDevice); - } else { - if (oneParticipantOneDevice) { - ParticipantDevice device = mChatRoom.getParticipants()[0].getDevices()[0]; - LinphoneManager.getCallManager().inviteAddress(device.getAddress(), true); - } else { - ((ChatActivity) getActivity()).showDevices(mLocalSipAddress, mRemoteSipAddress); - } - } - } - - private void goToGroupInfo() { - if (mChatRoom == null) return; - ArrayList participants = new ArrayList<>(); - for (Participant p : mChatRoom.getParticipants()) { - Address a = p.getAddress(); - LinphoneContact c = ContactsManager.getInstance().findContactFromAddress(a); - if (c == null) { - c = new LinphoneContact(); - String displayName = LinphoneUtils.getAddressDisplayName(a); - c.setFullName(displayName); - } - ContactAddress ca = new ContactAddress(c, a.asString(), "", p.isAdmin()); - participants.add(ca); - } - - boolean encrypted = mChatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt()); - ((ChatActivity) getActivity()) - .showChatRoomGroupInfo( - mRemoteSipAddress, - mLocalSipAddress, - participants, - mChatRoom.getSubject(), - encrypted); - } - - private void goToEphemeral() { - if (mChatRoom == null) return; - ((ChatActivity) getActivity()).showChatRoomEphemeral(mRemoteSipAddress, mLocalSipAddress); - } - - /* - * Chat room callbacks - */ - - @Override - public void onNewEvent(@NonNull ChatRoom chatRoom, @NonNull EventLog eventLog) {} - - @Override - public void onChatMessageSending(ChatRoom cr, EventLog event) { - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); - scrollToBottom(); - } - - @Override - public void onChatMessageSent(@NonNull ChatRoom chatRoom, @NonNull EventLog eventLog) {} - - @Override - public void onConferenceAddressGeneration(ChatRoom cr) {} - - @Override - public void onParticipantRegistrationSubscriptionRequested( - ChatRoom cr, Address participantAddr) {} - - @Override - public void onParticipantRegistrationUnsubscriptionRequested( - ChatRoom cr, Address participantAddr) {} - - @Override - public void onUndecryptableMessageReceived(ChatRoom cr, ChatMessage msg) { - final Address from = msg.getFromAddress(); - final LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(from); - - if (LinphoneManager.getCore().limeX3DhEnabled()) { - final Dialog dialog = - ((ChatActivity) getActivity()) - .displayDialog( - getString(R.string.message_cant_be_decrypted) - .replace( - "%s", - (contact != null) - ? contact.getFullName() - : from.getUsername())); - Button delete = dialog.findViewById(R.id.dialog_delete_button); - delete.setText(getString(R.string.call)); - Button cancel = dialog.findViewById(R.id.dialog_cancel_button); - cancel.setText(getString(R.string.ok)); - delete.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - LinphoneManager.getCallManager() - .newOutgoingCall( - from.asStringUriOnly(), - (contact != null) - ? contact.getFullName() - : from.getUsername()); - dialog.dismiss(); - } - }); - - cancel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - dialog.dismiss(); - } - }); - dialog.show(); - } - } - - @Override - public void onChatMessageReceived(ChatRoom cr, EventLog event) { - cr.markAsRead(); - ((ChatActivity) getActivity()).displayMissedChats(); - - ChatMessage msg = event.getChatMessage(); - if (msg.getErrorInfo() != null - && msg.getErrorInfo().getReason() == Reason.UnsupportedContent) { - Log.w( - "[Chat Messages Fragment] Message received but content is unsupported, do not display it"); - return; - } - - if (!msg.hasTextContent() && msg.getFileTransferInformation() == null) { - Log.w( - "[Chat Messages Fragment] Message has no text or file transfer information to display, ignoring it..."); - return; - } - - String externalBodyUrl = msg.getExternalBodyUrl(); - Content fileTransferContent = msg.getFileTransferInformation(); - if (externalBodyUrl != null || fileTransferContent != null) { - ((ChatActivity) getActivity()) - .requestPermissionIfNotGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE); - } - - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); - scrollToBottom(); - } - - @Override - public void onIsComposingReceived(ChatRoom cr, Address remoteAddr, boolean isComposing) { - ArrayList composing = new ArrayList<>(); - for (Address a : cr.getComposingAddresses()) { - boolean found = false; - for (LinphoneContact c : mParticipants) { - if (c.hasAddress(a.asStringUriOnly())) { - composing.add(c.getFullName()); - found = true; - break; - } - } - if (!found) { - LinphoneContact contact = - ContactsManager.getInstance().findContactFromAddress(remoteAddr); - String displayName; - if (contact != null) { - if (contact.getFullName() != null) { - displayName = contact.getFullName(); - } else { - displayName = LinphoneUtils.getAddressDisplayName(remoteAddr); - } - } else { - displayName = LinphoneUtils.getAddressDisplayName(a); - } - composing.add(displayName); - } - } - - mRemoteComposing.setVisibility(View.VISIBLE); - if (composing.isEmpty()) { - mRemoteComposing.setVisibility(View.GONE); - } else if (composing.size() == 1) { - mRemoteComposing.setText( - getString(R.string.remote_composing_single).replace("%s", composing.get(0))); - } else { - StringBuilder remotes = new StringBuilder(); - int i = 0; - for (String remote : composing) { - remotes.append(remote); - i++; - if (i != composing.size()) { - remotes.append(", "); - } - } - mRemoteComposing.setText( - getString(R.string.remote_composing_multiple) - .replace("%s", remotes.toString())); - } - } - - @Override - public void onMessageReceived(ChatRoom cr, ChatMessage msg) {} - - @Override - public void onConferenceJoined(ChatRoom cr, EventLog event) { - // Currently flexisip doesn't send the participants list in the INVITE - // So we have to refresh the display when information is available - // In the meantime header will be chatroom-xxxxxxx - if (mChatRoom == null) mChatRoom = cr; - if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) - && mChatRoom.getParticipants().length > 0) { - mRemoteParticipantAddress = mChatRoom.getParticipants()[0].getAddress(); - } - getContactsForParticipants(); - displayChatRoomHeader(); - - if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) return; - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); - scrollToBottom(); - } - - @Override - public void onConferenceLeft(ChatRoom cr, EventLog event) { - if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) return; - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); - scrollToBottom(); - } - - @Override - public void onEphemeralEvent(ChatRoom chatRoom, EventLog eventLog) { - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(eventLog); - scrollToBottom(); - } - - @Override - public void onEphemeralMessageTimerStarted(ChatRoom chatRoom, EventLog eventLog) {} - - @Override - public void onEphemeralMessageDeleted(ChatRoom chatRoom, EventLog eventLog) { - Log.i("[Chat Room] Ephemeral message expired"); - LinphoneUtils.deleteFileContentIfExists(eventLog); - - if (!((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()) - .removeFromHistory(eventLog)) { - Log.w("[Chat Room] Ephemeral message not found, refresh list"); - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()) - .refresh(mChatRoom.getHistoryEvents(MESSAGES_PER_PAGE)); - } - } - - @Override - public void onParticipantAdminStatusChanged(ChatRoom cr, EventLog event) { - if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) return; - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); - scrollToBottom(); - } - - @Override - public void onParticipantDeviceRemoved(ChatRoom cr, EventLog event) {} - - @Override - public void onParticipantRemoved(ChatRoom cr, EventLog event) { - if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) return; - getContactsForParticipants(); - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); - scrollToBottom(); - } - - @Override - public void onChatMessageShouldBeStored(ChatRoom cr, ChatMessage msg) {} - - @Override - public void onParticipantDeviceAdded(ChatRoom cr, EventLog event) {} - - @Override - public void onSecurityEvent(ChatRoom cr, EventLog eventLog) { - updateSecurityLevelIcon(); - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(eventLog); - scrollToBottom(); - } - - @Override - public void onStateChanged(ChatRoom cr, ChatRoom.State newState) { - setReadOnly(mChatRoom.hasBeenLeft()); - } - - @Override - public void onParticipantAdded(ChatRoom cr, EventLog event) { - if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) return; - getContactsForParticipants(); - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); - scrollToBottom(); - } - - @Override - public void onChatMessageParticipantImdnStateChanged( - ChatRoom cr, ChatMessage msg, ParticipantImdnState state) {} - - @Override - public void onSubjectChanged(ChatRoom cr, EventLog event) { - if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) return; - mRoomLabel.setText(event.getSubject()); - ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); - scrollToBottom(); - } - - @Override - public void onContactsUpdated() { - getContactsForParticipants(); - displayChatRoomHeader(); - mChatEventsList.getAdapter().notifyDataSetChanged(); - } - - @Override - public boolean onCommitContent( - InputContentInfoCompat inputContentInfo, - int flags, - Bundle opts, - String[] contentMimeTypes) { - try { - if (mCurrentInputContentInfo != null) { - mCurrentInputContentInfo.releasePermission(); - } - } catch (Exception e) { - Log.e("[Chat Messages Fragment] releasePermission failed : ", e); - } finally { - mCurrentInputContentInfo = null; - } - - boolean supported = false; - for (final String mimeType : contentMimeTypes) { - if (inputContentInfo.getDescription().hasMimeType(mimeType)) { - supported = true; - break; - } - } - if (!supported) { - return false; - } - - return onCommitContentInternal(inputContentInfo, flags); - } - - private boolean onCommitContentInternal(InputContentInfoCompat inputContentInfo, int flags) { - if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) { - try { - inputContentInfo.requestPermission(); - } catch (Exception e) { - Log.e("[Chat Messages Fragment] requestPermission failed : ", e); - return false; - } - } - - if (inputContentInfo.getContentUri() != null) { - String contentUri = FileUtils.getFilePath(mContext, inputContentInfo.getContentUri()); - addImageToPendingList(contentUri); - } - - mCurrentInputContentInfo = inputContentInfo; - - return true; - } - - // This is a workaround to prevent a crash from happening while rotating the device - private class LinphoneLinearLayoutManager extends LinearLayoutManager { - LinphoneLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { - super(context, orientation, reverseLayout); - } - - @Override - public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { - try { - super.onLayoutChildren(recycler, state); - } catch (IndexOutOfBoundsException e) { - Log.e( - "[Chat Messages Fragment] InvalidIndexOutOfBound Exception, probably while rotating the device"); - } - } - } -} diff --git a/app/src/main/java/org/linphone/chat/ChatMessagesGenericAdapter.java b/app/src/main/java/org/linphone/chat/ChatMessagesGenericAdapter.java deleted file mode 100644 index 0052c27e8..000000000 --- a/app/src/main/java/org/linphone/chat/ChatMessagesGenericAdapter.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import java.util.ArrayList; -import org.linphone.contacts.LinphoneContact; -import org.linphone.core.EventLog; - -interface ChatMessagesGenericAdapter { - void addToHistory(EventLog log); - - void addAllToHistory(ArrayList logs); - - void setContacts(ArrayList participants); - - void refresh(EventLog[] history); - - void clear(); - - Object getItem(int i); - - void removeItem(int i); - - boolean removeFromHistory(EventLog eventLog); -} diff --git a/app/src/main/java/org/linphone/chat/ChatRoomCreationFragment.java b/app/src/main/java/org/linphone/chat/ChatRoomCreationFragment.java deleted file mode 100644 index 190ee1476..000000000 --- a/app/src/main/java/org/linphone/chat/ChatRoomCreationFragment.java +++ /dev/null @@ -1,654 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import static android.content.Context.INPUT_METHOD_SERVICE; - -import android.app.Fragment; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.CompoundButton; -import android.widget.HorizontalScrollView; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import android.widget.SearchView; -import android.widget.Switch; -import android.widget.TextView; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import java.util.ArrayList; -import java.util.List; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.call.views.LinphoneLinearLayoutManager; -import org.linphone.contacts.ContactAddress; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.ContactsUpdatedListener; -import org.linphone.contacts.LinphoneContact; -import org.linphone.contacts.SearchContactViewHolder; -import org.linphone.contacts.SearchContactsAdapter; -import org.linphone.contacts.views.ContactSelectView; -import org.linphone.core.Address; -import org.linphone.core.ChatRoom; -import org.linphone.core.ChatRoomBackend; -import org.linphone.core.ChatRoomListenerStub; -import org.linphone.core.ChatRoomParams; -import org.linphone.core.Core; -import org.linphone.core.Factory; -import org.linphone.core.FriendCapability; -import org.linphone.core.ProxyConfig; -import org.linphone.core.SearchResult; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; - -public class ChatRoomCreationFragment extends Fragment - implements View.OnClickListener, - SearchContactViewHolder.ClickListener, - ContactsUpdatedListener { - private RecyclerView mContactsList; - private LinearLayout mContactsSelectedLayout; - private HorizontalScrollView mContactsSelectLayout; - private ImageView mAllContactsButton; - private ImageView mLinphoneContactsButton; - private ImageView mNextButton; - private boolean mOnlyDisplayLinphoneContacts; - private View mAllContactsSelected, mLinphoneContactsSelected; - private RelativeLayout mSearchLayout, mWaitLayout, mLinphoneContactsToggle, mAllContactsToggle; - private SearchView mSearchField; - private SearchContactsAdapter mSearchAdapter; - private String mChatRoomSubject, mChatRoomAddress; - private ChatRoom mChatRoom; - private ChatRoomListenerStub mChatRoomCreationListener; - private Switch mSecurityToggle; - private ArrayList mParticipants; - private boolean mCreateGroupChatRoom; - private boolean mChatRoomEncrypted; - - @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - View view = inflater.inflate(R.layout.chat_create, container, false); - setRetainInstance(true); - - mParticipants = new ArrayList<>(); - mChatRoomSubject = null; - mChatRoomAddress = null; - mCreateGroupChatRoom = false; - - if (getArguments() != null) { - if (getArguments().getSerializable("Participants") != null) { - mParticipants = - (ArrayList) getArguments().getSerializable("Participants"); - } - mChatRoomSubject = getArguments().getString("Subject"); - mChatRoomAddress = getArguments().getString("RemoteSipUri"); - mCreateGroupChatRoom = getArguments().getBoolean("IsGroupChatRoom", false); - mChatRoomEncrypted = getArguments().getBoolean("Encrypted", false); - } - - mWaitLayout = view.findViewById(R.id.waitScreen); - mWaitLayout.setVisibility(View.GONE); - - mContactsList = view.findViewById(R.id.contactsList); - mContactsSelectedLayout = view.findViewById(R.id.contactsSelected); - mContactsSelectLayout = view.findViewById(R.id.layoutContactsSelected); - - mAllContactsButton = view.findViewById(R.id.all_contacts); - mAllContactsButton.setOnClickListener(this); - - mLinphoneContactsButton = view.findViewById(R.id.linphone_contacts); - mLinphoneContactsButton.setOnClickListener(this); - - mAllContactsSelected = view.findViewById(R.id.all_contacts_select); - mLinphoneContactsSelected = view.findViewById(R.id.linphone_contacts_select); - - ImageView backButton = view.findViewById(R.id.back); - backButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - ((ChatActivity) getActivity()).goBack(); - } - }); - - mNextButton = view.findViewById(R.id.next); - mNextButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - if (mChatRoomAddress == null && mChatRoomSubject == null) { - mContactsSelectedLayout.removeAllViews(); - } else { - // Pop the back stack twice so we don't have in stack - // Group -> Creation -> Group - getFragmentManager().popBackStack(); - getFragmentManager().popBackStack(); - } - ((ChatActivity) getActivity()) - .showChatRoomGroupInfo( - mChatRoomAddress == null - ? null - : Factory.instance() - .createAddress(mChatRoomAddress), - mChatRoom == null ? null : mChatRoom.getLocalAddress(), - mSearchAdapter.getContactsSelectedList(), - mChatRoomSubject, - mSecurityToggle.isChecked()); - } - }); - mNextButton.setEnabled(false); - mSearchLayout = view.findViewById(R.id.layoutSearchField); - - ProgressBar contactsFetchInProgress = view.findViewById(R.id.contactsFetchInProgress); - contactsFetchInProgress.setVisibility(View.GONE); - - mSearchAdapter = new SearchContactsAdapter(this, !mCreateGroupChatRoom, mChatRoomEncrypted); - - mSearchField = view.findViewById(R.id.searchField); - mSearchField.setOnQueryTextListener( - new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - return true; - } - - @Override - public boolean onQueryTextChange(String newText) { - mSearchAdapter.searchContacts(newText); - return true; - } - }); - - mLinphoneContactsToggle = view.findViewById(R.id.layout_linphone_contacts); - mAllContactsToggle = view.findViewById(R.id.layout_all_contacts); - - mSecurityToggle = view.findViewById(R.id.security_toogle); - mSecurityToggle.setOnCheckedChangeListener( - new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - setSecurityEnabled(isChecked); - } - }); - ImageView securityToggleOn = view.findViewById(R.id.security_toogle_on); - ImageView securityToggleOff = view.findViewById(R.id.security_toogle_off); - securityToggleOn.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - setSecurityEnabled(true); - } - }); - securityToggleOff.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - setSecurityEnabled(false); - } - }); - - mSecurityToggle.setChecked(mChatRoomEncrypted); - mSearchAdapter.setSecurityEnabled(mChatRoomEncrypted); - ProxyConfig lpc = LinphoneManager.getCore().getDefaultProxyConfig(); - if ((mChatRoomSubject != null && mChatRoomAddress != null) - || (lpc == null || lpc.getConferenceFactoryUri() == null)) { - mSecurityToggle.setVisibility(View.GONE); - securityToggleOn.setVisibility(View.GONE); - securityToggleOff.setVisibility(View.GONE); - } - - if (getResources().getBoolean(R.bool.force_end_to_end_encryption_in_chat)) { - mSecurityToggle.setChecked(true); - mSearchAdapter.setSecurityEnabled(true); - mSecurityToggle.setVisibility(View.GONE); - securityToggleOn.setVisibility(View.GONE); - securityToggleOff.setVisibility(View.GONE); - } - - LinearLayoutManager layoutManager = - new LinphoneLinearLayoutManager(getActivity().getApplicationContext()); - - mContactsList.setAdapter(mSearchAdapter); - - DividerItemDecoration dividerItemDecoration = - new DividerItemDecoration( - mContactsList.getContext(), layoutManager.getOrientation()); - dividerItemDecoration.setDrawable( - getActivity() - .getApplicationContext() - .getResources() - .getDrawable(R.drawable.divider)); - mContactsList.addItemDecoration(dividerItemDecoration); - - mContactsList.setLayoutManager(layoutManager); - - mOnlyDisplayLinphoneContacts = - ContactsManager.getInstance().isLinphoneContactsPrefered() - || getResources().getBoolean(R.bool.hide_non_linphone_contacts); - - if (savedInstanceState != null) { - if (mParticipants.isEmpty() - && savedInstanceState.getStringArrayList("Participants") != null) { - mContactsSelectedLayout.removeAllViews(); - // We need to get all contacts not only sip - mParticipants = - (ArrayList) - savedInstanceState.getSerializable("Participants"); - } - mOnlyDisplayLinphoneContacts = - savedInstanceState.getBoolean("onlySipContact", mOnlyDisplayLinphoneContacts); - } - - mChatRoomCreationListener = - new ChatRoomListenerStub() { - @Override - public void onStateChanged(ChatRoom cr, ChatRoom.State newState) { - if (newState == ChatRoom.State.Created) { - mWaitLayout.setVisibility(View.GONE); - // Pop back stack so back button takes to the chat rooms list - getFragmentManager().popBackStack(); - ((ChatActivity) getActivity()) - .showChatRoom( - mChatRoom.getLocalAddress(), - mChatRoom.getPeerAddress()); - } else if (newState == ChatRoom.State.CreationFailed) { - mWaitLayout.setVisibility(View.GONE); - ((ChatActivity) getActivity()).displayChatRoomError(); - Log.e( - "[Chat Room Creation] Group chat room for address " - + cr.getPeerAddress() - + " has failed !"); - } - } - }; - - return view; - } - - @Override - public void onResume() { - super.onResume(); - ContactsManager.getInstance().addContactsListener(this); - - updateLayout(); - - InputMethodManager inputMethodManager = - (InputMethodManager) getActivity().getSystemService(INPUT_METHOD_SERVICE); - if (getActivity().getCurrentFocus() != null) { - inputMethodManager.hideSoftInputFromWindow( - getActivity().getCurrentFocus().getWindowToken(), 0); - } - } - - @Override - public void onPause() { - if (mChatRoom != null) { - mChatRoom.removeListener(mChatRoomCreationListener); - } - ContactsManager.getInstance().removeContactsListener(this); - super.onPause(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (mSearchAdapter.getContactsSelectedList().size() > 0) { - outState.putSerializable("Participants", mSearchAdapter.getContactsSelectedList()); - } - outState.putBoolean("onlySipContact", mOnlyDisplayLinphoneContacts); - } - - @Override - public void onClick(View view) { - int id = view.getId(); - if (id == R.id.all_contacts) { - mOnlyDisplayLinphoneContacts = false; - mSearchAdapter.setOnlySipContact(false); - mAllContactsSelected.setVisibility(View.VISIBLE); - mAllContactsButton.setEnabled(false); - mLinphoneContactsButton.setEnabled(true); - mLinphoneContactsSelected.setVisibility(View.INVISIBLE); - updateList(); - resetAndResearch(); - } else if (id == R.id.linphone_contacts) { - mSearchAdapter.setOnlySipContact(true); - mLinphoneContactsSelected.setVisibility(View.VISIBLE); - mLinphoneContactsButton.setEnabled(false); - mOnlyDisplayLinphoneContacts = true; - mAllContactsButton.setEnabled(true); - mAllContactsSelected.setVisibility(View.INVISIBLE); - updateList(); - resetAndResearch(); - } else if (id == R.id.contactChatDelete) { - ContactAddress ca = (ContactAddress) view.getTag(); - addOrRemoveContactFromSelection(ca); - } - } - - @Override - public void onItemClicked(int position) { - SearchResult searchResult = mSearchAdapter.getContacts().get(position); - Core core = LinphoneManager.getCore(); - ProxyConfig lpc = core.getDefaultProxyConfig(); - boolean createEncryptedChatRoom = mSecurityToggle.isChecked(); - - if (createEncryptedChatRoom && !searchResult.hasCapability(FriendCapability.LimeX3Dh)) { - Log.w( - "[Chat Room Creation] Contact " - + searchResult.getFriend() - + " doesn't have LIME X3DH capability !"); - return; - } else if (mCreateGroupChatRoom - && !searchResult.hasCapability(FriendCapability.GroupChat)) { - Log.w( - "[Chat Room Creation] Contact " - + searchResult.getFriend() - + " doesn't have group chat capability !"); - return; - } - - if (lpc == null || lpc.getConferenceFactoryUri() == null || !mCreateGroupChatRoom) { - Address address = searchResult.getAddress(); - if (address == null) { - Log.w( - "[Chat Room Creation] Using search result without an address, trying with phone number..."); - address = core.interpretUrl(searchResult.getPhoneNumber()); - } - if (address == null) { - Log.e("[Chat Room Creation] Can't create a chat room without a valid address !"); - return; - } - if (lpc != null && lpc.getIdentityAddress().weakEqual(address)) { - Log.e("[Chat Room Creation] Can't create a 1-to-1 chat room with myself !"); - return; - } - - if (createEncryptedChatRoom && lpc != null && lpc.getConferenceFactoryUri() != null) { - mChatRoom = core.findOneToOneChatRoom(lpc.getIdentityAddress(), address, true); - if (mChatRoom != null) { - ((ChatActivity) getActivity()) - .showChatRoom(mChatRoom.getLocalAddress(), mChatRoom.getPeerAddress()); - } else { - ChatRoomParams params = core.createDefaultChatRoomParams(); - // This will set the backend to FlexisipChat automatically - params.enableEncryption(true); - params.enableGroup(false); - - Address[] participants = new Address[1]; - participants[0] = address; - - mChatRoom = - core.createChatRoom( - params, - getString(R.string.dummy_group_chat_subject), - participants); - if (mChatRoom != null) { - mChatRoom.addListener(mChatRoomCreationListener); - } else { - Log.w("[Chat Room Creation Fragment] createChatRoom returned null..."); - mWaitLayout.setVisibility(View.GONE); - } - } - } else { - if (lpc != null - && lpc.getConferenceFactoryUri() != null - && !LinphonePreferences.instance().useBasicChatRoomFor1To1()) { - mChatRoom = core.findOneToOneChatRoom(lpc.getIdentityAddress(), address, false); - if (mChatRoom == null) { - mWaitLayout.setVisibility(View.VISIBLE); - - ChatRoomParams params = core.createDefaultChatRoomParams(); - params.enableEncryption(false); - params.enableGroup(false); - // We don't want a basic chat room - params.setBackend(ChatRoomBackend.FlexisipChat); - - Address[] participants = new Address[1]; - participants[0] = address; - - mChatRoom = - core.createChatRoom( - params, - getString(R.string.dummy_group_chat_subject), - participants); - if (mChatRoom != null) { - mChatRoom.addListener(mChatRoomCreationListener); - } else { - Log.w("[Chat Room Creation Fragment] createChatRoom returned null..."); - mWaitLayout.setVisibility(View.GONE); - } - } else { - // Pop back stack so back button takes to the chat rooms list - getFragmentManager().popBackStack(); - ((ChatActivity) getActivity()) - .showChatRoom( - mChatRoom.getLocalAddress(), mChatRoom.getPeerAddress()); - } - } else { - ChatRoom chatRoom = null; - if (lpc != null) chatRoom = core.getChatRoom(address, lpc.getIdentityAddress()); - else chatRoom = core.getChatRoom(address); - - if (chatRoom != null) { - // Pop back stack so back button takes to the chat rooms list - getFragmentManager().popBackStack(); - ((ChatActivity) getActivity()) - .showChatRoom( - chatRoom.getLocalAddress(), chatRoom.getPeerAddress()); - } - } - } - } else { - LinphoneContact c = - searchResult.getFriend() != null - ? (LinphoneContact) searchResult.getFriend().getUserData() - : null; - if (c == null) { - c = ContactsManager.getInstance().findContactFromAddress(searchResult.getAddress()); - if (c == null) { - c = - ContactsManager.getInstance() - .findContactFromPhoneNumber(searchResult.getPhoneNumber()); - } - } - addOrRemoveContactFromSelection( - new ContactAddress( - c, - searchResult.getAddress().asStringUriOnly(), - searchResult.getPhoneNumber())); - } - } - - @Override - public void onContactsUpdated() { - updateList(); - } - - private void updateLayout() { - if (!mParticipants.isEmpty()) { - mSearchAdapter.setContactsSelectedList(mParticipants); - updateList(); - updateListSelected(); - } - - mSearchAdapter.setOnlySipContact(mOnlyDisplayLinphoneContacts); - updateList(); - - displayChatCreation(); - } - - private void setSecurityEnabled(boolean enabled) { - mChatRoomEncrypted = enabled; - mSecurityToggle.setChecked(mChatRoomEncrypted); - mSearchAdapter.setSecurityEnabled(mChatRoomEncrypted); - - if (enabled) { - // Remove all contacts added before LIME switch was set - // and that can stay because they don't have the capability - mContactsSelectedLayout.removeAllViews(); - List toToggle = new ArrayList<>(); - for (ContactAddress ca : mSearchAdapter.getContactsSelectedList()) { - // If the ContactAddress doesn't have a contact keep it anyway - if (ca.getContact() != null && !ca.hasCapability(FriendCapability.LimeX3Dh)) { - toToggle.add(ca); - } else { - if (ca.getView() != null) { - mContactsSelectedLayout.addView(ca.getView()); - } - } - } - for (ContactAddress ca : toToggle) { - mSearchAdapter.toggleContactSelection(ca); - } - mContactsSelectedLayout.invalidate(); - } - } - - private void displayChatCreation() { - mNextButton.setVisibility(View.VISIBLE); - mNextButton.setEnabled(mSearchAdapter.getContactsSelectedList().size() > 0); - - mContactsList.setVisibility(View.VISIBLE); - mSearchLayout.setVisibility(View.VISIBLE); - - if (mCreateGroupChatRoom) { - mLinphoneContactsToggle.setVisibility(View.GONE); - mAllContactsToggle.setVisibility(View.GONE); - mContactsSelectLayout.setVisibility(View.VISIBLE); - mNextButton.setVisibility(View.VISIBLE); - } else { - mLinphoneContactsToggle.setVisibility(View.VISIBLE); - mAllContactsToggle.setVisibility(View.VISIBLE); - mContactsSelectLayout.setVisibility(View.GONE); - mNextButton.setVisibility(View.GONE); - } - - if (getResources().getBoolean(R.bool.hide_non_linphone_contacts)) { - mLinphoneContactsToggle.setVisibility(View.GONE); - mLinphoneContactsButton.setVisibility(View.INVISIBLE); - - mAllContactsButton.setEnabled(false); - mLinphoneContactsButton.setEnabled(false); - - mOnlyDisplayLinphoneContacts = true; - - mAllContactsButton.setOnClickListener(null); - mLinphoneContactsButton.setOnClickListener(null); - - mLinphoneContactsSelected.setVisibility(View.INVISIBLE); - mLinphoneContactsSelected.setVisibility(View.INVISIBLE); - } else { - mAllContactsButton.setVisibility(View.VISIBLE); - mLinphoneContactsButton.setVisibility(View.VISIBLE); - - if (mOnlyDisplayLinphoneContacts) { - mAllContactsSelected.setVisibility(View.INVISIBLE); - mLinphoneContactsSelected.setVisibility(View.VISIBLE); - } else { - mAllContactsSelected.setVisibility(View.VISIBLE); - mLinphoneContactsSelected.setVisibility(View.INVISIBLE); - } - - mAllContactsButton.setEnabled(mOnlyDisplayLinphoneContacts); - mLinphoneContactsButton.setEnabled(!mAllContactsButton.isEnabled()); - } - - mContactsSelectedLayout.removeAllViews(); - if (mSearchAdapter.getContactsSelectedList().size() > 0) { - for (ContactAddress ca : mSearchAdapter.getContactsSelectedList()) { - addSelectedContactAddress(ca); - } - } - } - - private void updateList() { - mSearchAdapter.searchContacts(mSearchField.getQuery().toString()); - mSearchAdapter.notifyDataSetChanged(); - } - - private void updateListSelected() { - if (mSearchAdapter.getContactsSelectedList().size() > 0) { - mContactsSelectLayout.invalidate(); - mNextButton.setEnabled(true); - } else { - mNextButton.setEnabled(false); - } - } - - private void resetAndResearch() { - ContactsManager.getInstance().getMagicSearch().resetSearchCache(); - mSearchAdapter.searchContacts(mSearchField.getQuery().toString()); - } - - private void addSelectedContactAddress(ContactAddress ca) { - View viewContact = - LayoutInflater.from(getActivity()).inflate(R.layout.contact_selected, null); - if (ca.getContact() != null) { - String name = - (ca.getContact().getFullName() != null - && !ca.getContact().getFullName().isEmpty()) - ? ca.getContact().getFullName() - : (ca.getDisplayName() != null) - ? ca.getDisplayName() - : (ca.getUsername() != null) ? ca.getUsername() : ""; - ((TextView) viewContact.findViewById(R.id.sipUri)).setText(name); - } else { - ((TextView) viewContact.findViewById(R.id.sipUri)) - .setText(ca.getAddressAsDisplayableString()); - } - View removeContact = viewContact.findViewById(R.id.contactChatDelete); - removeContact.setTag(ca); - removeContact.setOnClickListener(this); - viewContact.setOnClickListener(this); - ca.setView(viewContact); - mContactsSelectedLayout.addView(viewContact); - mContactsSelectedLayout.invalidate(); - } - - private void updateContactsClick(ContactAddress ca) { - boolean isSelected = mSearchAdapter.toggleContactSelection(ca); - if (isSelected) { - ContactSelectView csv = new ContactSelectView(getActivity()); - csv.setListener(this); - csv.setContactName(ca); - addSelectedContactAddress(ca); - } else { - mContactsSelectedLayout.removeAllViews(); - for (ContactAddress contactAddress : mSearchAdapter.getContactsSelectedList()) { - if (contactAddress.getView() != null) - mContactsSelectedLayout.addView(contactAddress.getView()); - } - } - mContactsSelectedLayout.invalidate(); - } - - private void addOrRemoveContactFromSelection(ContactAddress ca) { - updateContactsClick(ca); - mSearchAdapter.notifyDataSetChanged(); - updateListSelected(); - } -} diff --git a/app/src/main/java/org/linphone/chat/ChatRoomViewHolder.java b/app/src/main/java/org/linphone/chat/ChatRoomViewHolder.java deleted file mode 100644 index b02d146cc..000000000 --- a/app/src/main/java/org/linphone/chat/ChatRoomViewHolder.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import android.content.Context; -import android.view.View; -import android.widget.CheckBox; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; -import androidx.recyclerview.widget.RecyclerView; -import org.linphone.R; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.core.Address; -import org.linphone.core.ChatMessage; -import org.linphone.core.ChatRoom; -import org.linphone.core.ChatRoomCapabilities; -import org.linphone.core.Content; -import org.linphone.core.Participant; -import org.linphone.utils.LinphoneUtils; - -public class ChatRoomViewHolder extends RecyclerView.ViewHolder - implements View.OnClickListener, View.OnLongClickListener { - private final TextView lastMessageView; - private final TextView date; - private final TextView displayName; - public final TextView unreadMessages; - public final CheckBox delete; - private final RelativeLayout avatarLayout; - public final ImageView ephemeral; - - private final Context mContext; - private final ClickListener mListener; - - public ChatRoomViewHolder(Context context, View itemView, ClickListener listener) { - super(itemView); - - mContext = context; - lastMessageView = itemView.findViewById(R.id.lastMessage); - date = itemView.findViewById(R.id.date); - displayName = itemView.findViewById(R.id.sipUri); - unreadMessages = itemView.findViewById(R.id.unreadMessages); - delete = itemView.findViewById(R.id.delete_chatroom); - avatarLayout = itemView.findViewById(R.id.avatar_layout); - ephemeral = itemView.findViewById(R.id.ephemeral); - mListener = listener; - - itemView.setOnClickListener(this); - itemView.setOnLongClickListener(this); - } - - public void bindChatRoom(ChatRoom room) { - ChatMessage lastMessage = room.getLastMessageInHistory(); - - if (lastMessage != null) { - StringBuilder messageContent = new StringBuilder(); - for (Content c : lastMessage.getContents()) { - if (c.isFile() || c.isFileTransfer()) { - messageContent.append(c.getName()).append(" "); - } else if (c.isText()) { - messageContent.insert(0, c.getStringBuffer() + " "); - } - } - lastMessageView.setText(getSender(lastMessage) + messageContent); - date.setText( - LinphoneUtils.timestampToHumanDate( - mContext, - room.getLastUpdateTime(), - R.string.messages_list_date_format)); - } else { - date.setText(""); - lastMessageView.setText(""); - } - - ephemeral.setVisibility(room.ephemeralEnabled() ? View.VISIBLE : View.GONE); - displayName.setText(getContact(room)); - unreadMessages.setText(String.valueOf(room.getUnreadMessagesCount())); - getAvatar(room); - } - - public void onClick(View v) { - if (mListener != null) { - mListener.onItemClicked(getAdapterPosition()); - } - } - - public boolean onLongClick(View v) { - if (mListener != null) { - return mListener.onItemLongClicked(getAdapterPosition()); - } - return false; - } - - private String getSender(ChatMessage lastMessage) { - if (lastMessage != null) { - LinphoneContact contact = - ContactsManager.getInstance() - .findContactFromAddress(lastMessage.getFromAddress()); - if (contact != null) { - return (contact.getFullName() + mContext.getString(R.string.separator)); - } - return (LinphoneUtils.getAddressDisplayName(lastMessage.getFromAddress()) - + mContext.getString(R.string.separator)); - } - return null; - } - - private String getContact(ChatRoom mRoom) { - Address contactAddress = mRoom.getPeerAddress(); - if (mRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) - && mRoom.getParticipants().length > 0) { - contactAddress = mRoom.getParticipants()[0].getAddress(); - } - - if (mRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { - LinphoneContact contact; - contact = ContactsManager.getInstance().findContactFromAddress(contactAddress); - if (contact != null) { - return contact.getFullName(); - } - return LinphoneUtils.getAddressDisplayName(contactAddress); - } - return mRoom.getSubject(); - } - - private void getAvatar(ChatRoom mRoom) { - if (mRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { - LinphoneContact contact = null; - if (mRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())) { - contact = - ContactsManager.getInstance() - .findContactFromAddress(mRoom.getPeerAddress()); - } else { - Participant[] participants = mRoom.getParticipants(); - if (participants != null && participants.length > 0) { - contact = - ContactsManager.getInstance() - .findContactFromAddress(participants[0].getAddress()); - } - } - if (contact != null) { - if (mRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) { - ContactAvatar.displayAvatar(contact, mRoom.getSecurityLevel(), avatarLayout); - } else { - ContactAvatar.displayAvatar(contact, avatarLayout); - } - } else { - Address remoteAddr = null; - if (mRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) { - Participant[] participants = mRoom.getParticipants(); - if (participants.length > 0) { - remoteAddr = participants[0].getAddress(); - } else { - // TODO: error - } - } else { - remoteAddr = mRoom.getPeerAddress(); - } - - String username = LinphoneUtils.getAddressDisplayName(remoteAddr); - if (mRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) { - ContactAvatar.displayAvatar(username, mRoom.getSecurityLevel(), avatarLayout); - } else { - ContactAvatar.displayAvatar(username, avatarLayout); - } - } - } else { - if (mRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) { - ContactAvatar.displayGroupChatAvatar(mRoom.getSecurityLevel(), avatarLayout); - } else { - ContactAvatar.displayGroupChatAvatar(avatarLayout); - } - } - } - - public interface ClickListener { - void onItemClicked(int position); - - boolean onItemLongClicked(int position); - } -} diff --git a/app/src/main/java/org/linphone/chat/ChatRoomsAdapter.java b/app/src/main/java/org/linphone/chat/ChatRoomsAdapter.java deleted file mode 100644 index 28c3303c2..000000000 --- a/app/src/main/java/org/linphone/chat/ChatRoomsAdapter.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.recyclerview.widget.DiffUtil; -import java.util.Arrays; -import java.util.List; -import org.linphone.LinphoneManager; -import org.linphone.core.ChatRoom; -import org.linphone.utils.SelectableAdapter; -import org.linphone.utils.SelectableHelper; - -public class ChatRoomsAdapter extends SelectableAdapter { - private final Context mContext; - private List mRooms; - private final int mItemResource; - private final ChatRoomViewHolder.ClickListener mClickListener; - - public ChatRoomsAdapter( - Context context, - int itemResource, - List rooms, - ChatRoomViewHolder.ClickListener clickListener, - SelectableHelper helper) { - super(helper); - mClickListener = clickListener; - mRooms = rooms; - mContext = context; - mItemResource = itemResource; - } - - @Override - public ChatRoomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(mItemResource, parent, false); - return new ChatRoomViewHolder(mContext, view, mClickListener); - } - - @Override - public void onBindViewHolder(ChatRoomViewHolder holder, int position) { - ChatRoom room = mRooms.get(position); - holder.delete.setVisibility(isEditionEnabled() ? View.VISIBLE : View.INVISIBLE); - holder.unreadMessages.setVisibility( - isEditionEnabled() - ? View.INVISIBLE - : (room.getUnreadMessagesCount() > 0 ? View.VISIBLE : View.INVISIBLE)); - holder.delete.setChecked(isSelected(position)); - room.setUserData(holder); - holder.bindChatRoom(room); - } - - public void refresh() { - refresh(false); - } - - public void refresh(boolean force) { - ChatRoom[] rooms = LinphoneManager.getCore().getChatRooms(); - List roomsList = Arrays.asList(rooms); - - if (!force) { - DiffUtil.DiffResult diffResult = - DiffUtil.calculateDiff(new ChatRoomDiffCallback(roomsList, mRooms)); - diffResult.dispatchUpdatesTo(this); - mRooms = roomsList; - } else { - mRooms = roomsList; - notifyDataSetChanged(); - } - } - - /** Adapter's methods */ - @Override - public int getItemCount() { - return mRooms.size(); - } - - @Override - public Object getItem(int position) { - if (position < mRooms.size()) return mRooms.get(position); - return null; - } - - @Override - public long getItemId(int position) { - return position; - } - - class ChatRoomDiffCallback extends DiffUtil.Callback { - List oldChatRooms; - List newChatRooms; - - public ChatRoomDiffCallback(List newRooms, List oldRooms) { - oldChatRooms = oldRooms; - newChatRooms = newRooms; - } - - @Override - public int getOldListSize() { - return oldChatRooms.size(); - } - - @Override - public int getNewListSize() { - return newChatRooms.size(); - } - - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - return oldChatRooms.get(oldItemPosition) == (newChatRooms.get(newItemPosition)); - } - - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - return newChatRooms.get(newItemPosition).getUnreadMessagesCount() == 0; - } - } -} diff --git a/app/src/main/java/org/linphone/chat/ChatRoomsFragment.java b/app/src/main/java/org/linphone/chat/ChatRoomsFragment.java deleted file mode 100644 index 871b7bc20..000000000 --- a/app/src/main/java/org/linphone/chat/ChatRoomsFragment.java +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import android.app.Fragment; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import java.util.Arrays; -import java.util.List; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.activities.MainActivity; -import org.linphone.call.views.LinphoneLinearLayoutManager; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.ContactsUpdatedListener; -import org.linphone.core.ChatMessage; -import org.linphone.core.ChatRoom; -import org.linphone.core.ChatRoomListenerStub; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.EventLog; -import org.linphone.core.ProxyConfig; -import org.linphone.utils.LinphoneUtils; -import org.linphone.utils.SelectableHelper; - -public class ChatRoomsFragment extends Fragment - implements ContactsUpdatedListener, - ChatRoomViewHolder.ClickListener, - SelectableHelper.DeleteListener { - - private RecyclerView mChatRoomsList; - private ImageView mNewGroupDiscussionButton; - private ImageView mBackToCallButton; - private ChatRoomsAdapter mChatRoomsAdapter; - private CoreListenerStub mListener; - private RelativeLayout mWaitLayout; - private int mChatRoomDeletionPendingCount; - private ChatRoomListenerStub mChatRoomListener; - private SelectableHelper mSelectionHelper; - private TextView mNoChatHistory; - - @Override - public View onCreateView( - final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - View view = inflater.inflate(R.layout.chatlist, container, false); - - mChatRoomsList = view.findViewById(R.id.chatList); - mWaitLayout = view.findViewById(R.id.waitScreen); - ImageView newDiscussionButton = view.findViewById(R.id.new_discussion); - mNewGroupDiscussionButton = view.findViewById(R.id.new_group_discussion); - mBackToCallButton = view.findViewById(R.id.back_in_call); - mNoChatHistory = view.findViewById(R.id.noChatHistory); - - ChatRoom[] rooms = LinphoneManager.getCore().getChatRooms(); - List mRooms = Arrays.asList(rooms); - - mSelectionHelper = new SelectableHelper(view, this); - mChatRoomsAdapter = - new ChatRoomsAdapter( - getActivity(), R.layout.chatlist_cell, mRooms, this, mSelectionHelper); - - mChatRoomsList.setAdapter(mChatRoomsAdapter); - mSelectionHelper.setAdapter(mChatRoomsAdapter); - mSelectionHelper.setDialogMessage(R.string.chat_room_delete_dialog); - - LinearLayoutManager layoutManager = new LinphoneLinearLayoutManager(getActivity()); - mChatRoomsList.setLayoutManager(layoutManager); - - DividerItemDecoration dividerItemDecoration = - new DividerItemDecoration( - mChatRoomsList.getContext(), layoutManager.getOrientation()); - dividerItemDecoration.setDrawable( - getActivity() - .getApplicationContext() - .getResources() - .getDrawable(R.drawable.divider)); - mChatRoomsList.addItemDecoration(dividerItemDecoration); - - mWaitLayout.setVisibility(View.GONE); - - newDiscussionButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - ((ChatActivity) getActivity()) - .showChatRoomCreation(null, null, null, false, false, false); - } - }); - - mNewGroupDiscussionButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - ((ChatActivity) getActivity()) - .showChatRoomCreation(null, null, null, false, true, false); - } - }); - - mBackToCallButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - ((MainActivity) getActivity()).goBackToCall(); - } - }); - - mListener = - new CoreListenerStub() { - @Override - public void onMessageSent(Core core, ChatRoom room, ChatMessage message) { - refreshChatRoom(room); - } - - @Override - public void onMessageReceived(Core core, ChatRoom room, ChatMessage message) { - refreshChatRoom(room); - } - - @Override - public void onChatRoomSubjectChanged(Core core, ChatRoom room) { - refreshChatRoom(room); - } - - @Override - public void onMessageReceivedUnableDecrypt( - Core core, ChatRoom room, ChatMessage message) { - refreshChatRoom(room); - } - - @Override - public void onChatRoomEphemeralMessageDeleted(Core lc, ChatRoom cr) { - refreshChatRoom(cr); - } - - @Override - public void onChatRoomRead(Core core, ChatRoom room) { - refreshChatRoom(room); - } - - @Override - public void onChatRoomStateChanged( - Core core, ChatRoom room, ChatRoom.State state) { - if (state == ChatRoom.State.Created) { - refreshChatRoom(room); - scrollToTop(); - } - } - }; - - mChatRoomListener = - new ChatRoomListenerStub() { - @Override - public void onStateChanged(ChatRoom room, ChatRoom.State state) { - super.onStateChanged(room, state); - if (state == ChatRoom.State.Deleted - || state == ChatRoom.State.TerminationFailed) { - mChatRoomDeletionPendingCount -= 1; - - if (state == ChatRoom.State.TerminationFailed) { - // TODO error message - } - - if (mChatRoomDeletionPendingCount == 0) { - mWaitLayout.setVisibility(View.GONE); - refreshChatRoomsList(); - } - } - } - }; - - return view; - } - - @Override - public void onItemClicked(int position) { - if (mChatRoomsAdapter.isEditionEnabled()) { - mChatRoomsAdapter.toggleSelection(position); - } else { - ChatRoom room = (ChatRoom) mChatRoomsAdapter.getItem(position); - if (room != null) { - ((ChatActivity) getActivity()) - .showChatRoom(room.getLocalAddress(), room.getPeerAddress()); - refreshChatRoom(room); - } - } - } - - @Override - public boolean onItemLongClicked(int position) { - if (!mChatRoomsAdapter.isEditionEnabled()) { - mSelectionHelper.enterEditionMode(); - } - mChatRoomsAdapter.toggleSelection(position); - return true; - } - - @Override - public void onResume() { - super.onResume(); - ContactsManager.getInstance().addContactsListener(this); - - mBackToCallButton.setVisibility(View.INVISIBLE); - Core core = LinphoneManager.getCore(); - if (core != null) { - core.addListener(mListener); - - if (core.getCallsNb() > 0) { - mBackToCallButton.setVisibility(View.VISIBLE); - } - } - - refreshChatRoomsList(); - - ProxyConfig lpc = core.getDefaultProxyConfig(); - mNewGroupDiscussionButton.setVisibility( - (lpc != null && lpc.getConferenceFactoryUri() != null) ? View.VISIBLE : View.GONE); - } - - @Override - public void onPause() { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.removeListener(mListener); - } - ContactsManager.getInstance().removeContactsListener(this); - super.onPause(); - } - - @Override - public void onDeleteSelection(Object[] objectsToDelete) { - Core core = LinphoneManager.getCore(); - mChatRoomDeletionPendingCount = objectsToDelete.length; - for (Object obj : objectsToDelete) { - ChatRoom room = (ChatRoom) obj; - room.addListener(mChatRoomListener); - - for (EventLog eventLog : room.getHistoryMessageEvents(0)) { - LinphoneUtils.deleteFileContentIfExists(eventLog); - } - - core.deleteChatRoom(room); - } - if (mChatRoomDeletionPendingCount > 0) { - mWaitLayout.setVisibility(View.VISIBLE); - } - ((ChatActivity) getActivity()).displayMissedChats(); - - if (getResources().getBoolean(R.bool.isTablet)) - ((ChatActivity) getActivity()).showEmptyChildFragment(); - } - - @Override - public void onContactsUpdated() { - ChatRoomsAdapter adapter = (ChatRoomsAdapter) mChatRoomsList.getAdapter(); - if (adapter != null) { - adapter.refresh(true); - } - } - - private void scrollToTop() { - mChatRoomsList.getLayoutManager().scrollToPosition(0); - } - - private void refreshChatRoom(ChatRoom cr) { - ChatRoomViewHolder holder = (ChatRoomViewHolder) cr.getUserData(); - if (holder != null) { - int position = holder.getAdapterPosition(); - if (position == 0) { - mChatRoomsAdapter.notifyItemChanged(0); - } else { - refreshChatRoomsList(); - } - } else { - refreshChatRoomsList(); - } - } - - private void refreshChatRoomsList() { - mChatRoomsAdapter.refresh(); - mNoChatHistory.setVisibility( - mChatRoomsAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); - } -} diff --git a/app/src/main/java/org/linphone/chat/ChatScrollListener.java b/app/src/main/java/org/linphone/chat/ChatScrollListener.java deleted file mode 100644 index 9a8cec4f1..000000000 --- a/app/src/main/java/org/linphone/chat/ChatScrollListener.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -abstract class ChatScrollListener extends RecyclerView.OnScrollListener { - // The minimum amount of items to have below your current scroll position - // before mLoading more. - private static final int mVisibleThreshold = 5; - // The total number of items in the dataset after the last load - private int mPreviousTotalItemCount = 0; - // True if we are still waiting for the last set of data to load. - private boolean mLoading = true; - - private final LinearLayoutManager mLayoutManager; - - public ChatScrollListener(LinearLayoutManager layoutManager) { - mLayoutManager = layoutManager; - } - - // This happens many times a second during a scroll, so be wary of the code you place here. - // We are given a few useful parameters to help us work out if we need to load some more data, - // but first we check if we are waiting for the previous load to finish. - @Override - public void onScrolled(RecyclerView view, int dx, int dy) { - int lastVisibleItemPosition; - int totalItemCount = mLayoutManager.getItemCount(); - - lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition(); - - // If the total item count is zero and the previous isn't, assume the - // list is invalidated and should be reset back to initial state - if (totalItemCount < mPreviousTotalItemCount) { - this.mPreviousTotalItemCount = totalItemCount; - if (totalItemCount == 0) { - this.mLoading = true; - } - } - // If it’s still mLoading, we check to see if the dataset count has - // changed, if so we conclude it has finished mLoading and update the current page - // number and total item count. - if (mLoading && (totalItemCount > mPreviousTotalItemCount)) { - mLoading = false; - mPreviousTotalItemCount = totalItemCount; - } - - // If it isn’t currently mLoading, we check to see if we have breached - // the mVisibleThreshold and need to reload more data. - // If we do need to reload some more data, we execute onLoadMore to fetch the data. - // threshold should reflect how many total columns there are too - if (!mLoading && (lastVisibleItemPosition + mVisibleThreshold) > totalItemCount) { - onLoadMore(totalItemCount); - mLoading = true; - } - } - - // Defines the process for actually mLoading more data based on page - protected abstract void onLoadMore(int totalItemsCount); -} diff --git a/app/src/main/java/org/linphone/chat/DeviceGroupViewHolder.java b/app/src/main/java/org/linphone/chat/DeviceGroupViewHolder.java deleted file mode 100644 index ee0527029..000000000 --- a/app/src/main/java/org/linphone/chat/DeviceGroupViewHolder.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import android.view.View; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; -import org.linphone.R; - -class DeviceGroupViewHolder { - public final RelativeLayout avatarLayout; - public final TextView participantName, sipUri; - public final ImageView groupExpander, securityLevel; - - public DeviceGroupViewHolder(View v) { - avatarLayout = v.findViewById(R.id.avatar_layout); - participantName = v.findViewById(R.id.name); - sipUri = v.findViewById(R.id.sipUri); - groupExpander = v.findViewById(R.id.dropdown); - securityLevel = v.findViewById(R.id.device_security_level); - } -} diff --git a/app/src/main/java/org/linphone/chat/DevicesAdapter.java b/app/src/main/java/org/linphone/chat/DevicesAdapter.java deleted file mode 100644 index 57ee8eb86..000000000 --- a/app/src/main/java/org/linphone/chat/DevicesAdapter.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseExpandableListAdapter; -import java.util.ArrayList; -import java.util.List; -import org.linphone.R; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.core.Address; -import org.linphone.core.ChatRoomSecurityLevel; -import org.linphone.core.Participant; -import org.linphone.core.ParticipantDevice; -import org.linphone.utils.LinphoneUtils; - -class DevicesAdapter extends BaseExpandableListAdapter { - private final Context mContext; - private List mParticipants; - - public DevicesAdapter(Context context) { - mContext = context; - mParticipants = new ArrayList<>(); - } - - public void updateListItems(List participants) { - mParticipants = participants; - notifyDataSetChanged(); - } - - @Override - public View getGroupView( - int groupPosition, boolean isExpanded, View view, ViewGroup viewGroup) { - Participant participant = (Participant) getGroup(groupPosition); - - // Group position 0 is reserved for ME participant & devices - DeviceGroupViewHolder holder = null; - if (view != null) { - Object possibleHolder = view.getTag(); - if (possibleHolder instanceof DeviceGroupViewHolder) { - holder = (DeviceGroupViewHolder) possibleHolder; - } - } else { - LayoutInflater inflater = LayoutInflater.from(mContext); - view = inflater.inflate(R.layout.chat_device_group, viewGroup, false); - } - if (holder == null) { - holder = new DeviceGroupViewHolder(view); - view.setTag(holder); - } - - Address participantAddress = participant.getAddress(); - LinphoneContact contact = - ContactsManager.getInstance().findContactFromAddress(participantAddress); - if (contact != null) { - ContactAvatar.displayAvatar( - contact, participant.getSecurityLevel(), holder.avatarLayout); - holder.participantName.setText(contact.getFullName()); - } else { - String displayName = LinphoneUtils.getAddressDisplayName(participantAddress); - ContactAvatar.displayAvatar( - displayName, participant.getSecurityLevel(), holder.avatarLayout); - holder.participantName.setText(displayName); - } - - holder.sipUri.setText(participantAddress.asStringUriOnly()); - if (!mContext.getResources().getBoolean(R.bool.show_sip_uri_in_chat)) { - holder.sipUri.setVisibility(View.GONE); - } - - if (getChildrenCount(groupPosition) == 1) { - holder.securityLevel.setVisibility(View.VISIBLE); - holder.groupExpander.setVisibility(View.GONE); - - ParticipantDevice device = (ParticipantDevice) getChild(groupPosition, 0); - ChatRoomSecurityLevel level = device.getSecurityLevel(); - switch (level) { - case Safe: - holder.securityLevel.setImageResource(R.drawable.security_2_indicator); - break; - case Encrypted: - holder.securityLevel.setImageResource(R.drawable.security_1_indicator); - break; - case ClearText: - case Unsafe: - default: - holder.securityLevel.setImageResource(R.drawable.security_alert_indicator); - break; - } - } else { - holder.securityLevel.setVisibility(View.GONE); - holder.groupExpander.setVisibility(View.VISIBLE); - holder.groupExpander.setImageResource( - isExpanded ? R.drawable.chevron_list_open : R.drawable.chevron_list_close); - } - - return view; - } - - @Override - public View getChildView( - int groupPosition, int childPosition, boolean b, View view, ViewGroup viewGroup) { - ParticipantDevice device = (ParticipantDevice) getChild(groupPosition, childPosition); - - DeviceChildViewHolder holder = null; - if (view != null) { - Object possibleHolder = view.getTag(); - if (possibleHolder instanceof DeviceChildViewHolder) { - holder = (DeviceChildViewHolder) possibleHolder; - } - } else { - LayoutInflater inflater = LayoutInflater.from(mContext); - view = inflater.inflate(R.layout.chat_device_cell, viewGroup, false); - } - if (holder == null) { - holder = new DeviceChildViewHolder(view); - view.setTag(holder); - } - - holder.deviceName.setText(device.getName()); - - ChatRoomSecurityLevel level = device.getSecurityLevel(); - switch (level) { - case Safe: - holder.securityLevel.setImageResource(R.drawable.security_2_indicator); - break; - case Encrypted: - holder.securityLevel.setImageResource(R.drawable.security_1_indicator); - break; - case ClearText: - case Unsafe: - default: - holder.securityLevel.setImageResource(R.drawable.security_alert_indicator); - break; - } - - return view; - } - - @Override - public int getGroupCount() { - return mParticipants.size(); - } - - @Override - public int getChildrenCount(int groupPosition) { - if (groupPosition >= mParticipants.size()) return 0; - return mParticipants.get(groupPosition).getDevices().length; - } - - @Override - public Object getGroup(int groupPosition) { - if (groupPosition >= mParticipants.size()) return null; - return mParticipants.get(groupPosition); - } - - @Override - public Object getChild(int groupPosition, int childPosition) { - if (groupPosition >= mParticipants.size()) return null; - ParticipantDevice[] devices = mParticipants.get(groupPosition).getDevices(); - if (devices.length == 0 || childPosition >= devices.length) return null; - return devices[childPosition]; - } - - @Override - public long getGroupId(int groupPosition) { - return groupPosition; - } - - @Override - public long getChildId(int groupPosition, int childPosition) { - return childPosition; - } - - @Override - public boolean isChildSelectable(int groupPosition, int childPosition) { - return true; - } - - @Override - public boolean hasStableIds() { - return false; - } -} diff --git a/app/src/main/java/org/linphone/chat/DevicesFragment.java b/app/src/main/java/org/linphone/chat/DevicesFragment.java deleted file mode 100644 index 72d3f1d18..000000000 --- a/app/src/main/java/org/linphone/chat/DevicesFragment.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import android.app.Fragment; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ExpandableListView; -import android.widget.ImageView; -import androidx.annotation.Nullable; -import java.util.ArrayList; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Address; -import org.linphone.core.ChatRoom; -import org.linphone.core.Core; -import org.linphone.core.Factory; -import org.linphone.core.Participant; -import org.linphone.core.ParticipantDevice; -import org.linphone.utils.LinphoneUtils; - -public class DevicesFragment extends Fragment { - private ExpandableListView mExpandableList; - private DevicesAdapter mAdapter; - - private Address mLocalSipAddr, mRoomAddr; - private ChatRoom mRoom; - private boolean mOnlyDisplayChilds; - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (getArguments() != null) { - String localSipUri = getArguments().getString("LocalSipUri"); - mLocalSipAddr = Factory.instance().createAddress(localSipUri); - String roomUri = getArguments().getString("RemoteSipUri"); - mRoomAddr = Factory.instance().createAddress(roomUri); - } - - View view = inflater.inflate(R.layout.chat_devices, container, false); - - mOnlyDisplayChilds = false; - - mExpandableList = view.findViewById(R.id.devices_list); - mExpandableList.setOnChildClickListener( - new ExpandableListView.OnChildClickListener() { - @Override - public boolean onChildClick( - ExpandableListView expandableListView, - View view, - int groupPosition, - int childPosition, - long l) { - ParticipantDevice device = - (ParticipantDevice) mAdapter.getChild(groupPosition, childPosition); - LinphoneManager.getCallManager().inviteAddress(device.getAddress(), true); - return false; - } - }); - mExpandableList.setOnGroupClickListener( - new ExpandableListView.OnGroupClickListener() { - @Override - public boolean onGroupClick( - ExpandableListView expandableListView, - View view, - int groupPosition, - long l) { - if (mOnlyDisplayChilds) { - // in this case groups are childs, so call on click - ParticipantDevice device = - (ParticipantDevice) mAdapter.getGroup(groupPosition); - LinphoneManager.getCallManager() - .inviteAddress(device.getAddress(), true); - return true; - } else { - if (mAdapter.getChildrenCount(groupPosition) == 1) { - ParticipantDevice device = - (ParticipantDevice) mAdapter.getChild(groupPosition, 0); - LinphoneManager.getCallManager() - .inviteAddress(device.getAddress(), true); - return true; - } - } - return false; - } - }); - - initChatRoom(); - - ImageView backButton = view.findViewById(R.id.back); - backButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - ((ChatActivity) getActivity()).goBack(); - } - }); - - return view; - } - - @Override - public void onResume() { - super.onResume(); - - initValues(); - - if (LinphoneManager.getInstance().hasLastCallSasBeenRejected()) { - LinphoneManager.getInstance().lastCallSasRejected(false); - LinphoneUtils.showTrustDeniedDialog(getActivity()); - } - } - - private void initChatRoom() { - Core core = LinphoneManager.getCore(); - mRoom = core.getChatRoom(mRoomAddr, mLocalSipAddr); - } - - private void initValues() { - if (mAdapter == null) { - mAdapter = new DevicesAdapter(getActivity()); - mExpandableList.setAdapter(mAdapter); - } - if (mRoom == null) { - initChatRoom(); - } - - if (mRoom != null && mRoom.getNbParticipants() > 0) { - ArrayList participantLists = new ArrayList<>(); - // Group position 0 is reserved for ME participant & devices - participantLists.add(mRoom.getMe()); - for (Participant participant : mRoom.getParticipants()) { - participantLists.add(participant); - } - mAdapter.updateListItems(participantLists); - } - } -} diff --git a/app/src/main/java/org/linphone/chat/EphemeralFragment.java b/app/src/main/java/org/linphone/chat/EphemeralFragment.java deleted file mode 100644 index ef9a3906b..000000000 --- a/app/src/main/java/org/linphone/chat/EphemeralFragment.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import android.app.Fragment; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.TextView; -import androidx.annotation.Nullable; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Address; -import org.linphone.core.ChatRoom; -import org.linphone.core.Factory; -import org.linphone.core.tools.Log; - -public class EphemeralFragment extends Fragment { - private ChatRoom mChatRoom; - private long mCurrentValue; - - private LayoutInflater mInflater; - private ViewGroup mContainer; - private LinearLayout mItems; - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - View view = inflater.inflate(R.layout.chat_ephemeral, container, false); - mInflater = inflater; - mContainer = container; - - if (getArguments() == null || getArguments().isEmpty()) { - return null; - } - - String address = getArguments().getString("RemoteSipUri"); - Address peerAddress = null; - String localSipUri = getArguments().getString("LocalSipUri"); - Address localSipAddress = Factory.instance().createAddress(localSipUri); - mChatRoom = null; - if (address != null && address.length() > 0) { - peerAddress = Factory.instance().createAddress(address); - } - if (peerAddress != null) { - mChatRoom = LinphoneManager.getCore().getChatRoom(peerAddress, localSipAddress); - } - if (mChatRoom == null) { - return null; - } - mCurrentValue = mChatRoom.ephemeralEnabled() ? mChatRoom.getEphemeralLifetime() : 0; - Log.i( - "[Ephemeral Messages] Current duration is ", - mCurrentValue, - ", ephemeral enabled? ", - mChatRoom.ephemeralEnabled()); - - view.findViewById(R.id.back) - .setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - getFragmentManager().popBackStack(); - } - }); - - view.findViewById(R.id.valid) - .setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - Log.i("[Ephemeral Messages] Selected value is ", mCurrentValue); - if (mCurrentValue > 0) { - if (mChatRoom.getEphemeralLifetime() != mCurrentValue) { - Log.i( - "[Ephemeral Messages] Setting new lifetime for ephemeral messages to ", - mCurrentValue); - mChatRoom.setEphemeralLifetime(mCurrentValue); - } else { - Log.i( - "[Ephemeral Messages] Configured lifetime for ephemeral messages was already ", - mCurrentValue); - } - - if (!mChatRoom.ephemeralEnabled()) { - Log.i( - "[Ephemeral Messages] Ephemeral messages were disabled, enable them"); - mChatRoom.enableEphemeral(true); - } - } else if (mChatRoom.ephemeralEnabled()) { - Log.i( - "[Ephemeral Messages] Ephemeral messages were enabled, disable them"); - mChatRoom.enableEphemeral(false); - } - - getFragmentManager().popBackStack(); - } - }); - - mItems = view.findViewById(R.id.items); - - computeItems(); - - return view; - } - - private View getView(int id, final long value) { - View view = mInflater.inflate(R.layout.chat_ephemeral_item, mContainer, false); - ((TextView) view.findViewById(R.id.text)).setText(id); - ((TextView) view.findViewById(R.id.text)) - .setTextAppearance( - getActivity(), - mCurrentValue == value - ? R.style.chat_room_ephemeral_selected_item_font - : R.style.chat_room_ephemeral_item_font); - view.findViewById(R.id.selected) - .setVisibility(mCurrentValue == value ? View.VISIBLE : View.GONE); - view.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - if (mCurrentValue != value) { - mCurrentValue = value; - computeItems(); - } - } - }); - return view; - } - - private void computeItems() { - mItems.removeAllViews(); - mItems.addView(getView(R.string.chat_room_ephemeral_message_disabled, 0)); - mItems.addView(getView(R.string.chat_room_ephemeral_message_one_minute, 60)); - mItems.addView(getView(R.string.chat_room_ephemeral_message_one_hour, 3600)); - mItems.addView(getView(R.string.chat_room_ephemeral_message_one_day, 86400)); - mItems.addView(getView(R.string.chat_room_ephemeral_message_three_days, 259200)); - mItems.addView(getView(R.string.chat_room_ephemeral_message_one_week, 604800)); - } -} diff --git a/app/src/main/java/org/linphone/chat/GroupInfoAdapter.java b/app/src/main/java/org/linphone/chat/GroupInfoAdapter.java deleted file mode 100644 index efbed24b5..000000000 --- a/app/src/main/java/org/linphone/chat/GroupInfoAdapter.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import java.util.ArrayList; -import java.util.List; -import org.linphone.LinphoneContext; -import org.linphone.R; -import org.linphone.contacts.ContactAddress; -import org.linphone.contacts.LinphoneContact; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.core.ChatRoom; -import org.linphone.core.Participant; - -class GroupInfoAdapter extends RecyclerView.Adapter { - private List mItems; - private View.OnClickListener mDeleteListener; - private boolean mHideAdminFeatures; - private ChatRoom mChatRoom; - - public GroupInfoAdapter( - List items, boolean hideAdminFeatures, boolean isCreation) { - mItems = items; - mHideAdminFeatures = hideAdminFeatures || isCreation; - } - - @NonNull - @Override - public GroupInfoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = - LayoutInflater.from(parent.getContext()) - .inflate(R.layout.chat_infos_cell, parent, false); - return new GroupInfoViewHolder(v); - } - - @Override - public void onBindViewHolder(@NonNull final GroupInfoViewHolder holder, int position) { - final ContactAddress ca = (ContactAddress) getItem(position); - LinphoneContact c = ca.getContact(); - - holder.name.setText( - (c != null && c.getFullName() != null) - ? c.getFullName() - : (ca.getDisplayName() != null) ? ca.getDisplayName() : ca.getUsername()); - - if (c != null) { - ContactAvatar.displayAvatar(c, holder.avatarLayout); - } else { - ContactAvatar.displayAvatar(holder.name.getText().toString(), holder.avatarLayout); - } - - holder.sipUri.setText(ca.getAddressAsDisplayableString()); - - if (!LinphoneContext.instance() - .getApplicationContext() - .getResources() - .getBoolean(R.bool.show_sip_uri_in_chat)) { - holder.sipUri.setVisibility(View.GONE); - holder.name.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - holder.sipUri.setVisibility( - holder.sipUri.getVisibility() == View.VISIBLE - ? View.GONE - : View.VISIBLE); - } - }); - } - - holder.delete.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - if (mDeleteListener != null) { - mDeleteListener.onClick(view); - } - } - }); - holder.delete.setTag(ca); - - holder.isAdmin.setVisibility(ca.isAdmin() ? View.VISIBLE : View.GONE); - holder.isNotAdmin.setVisibility(ca.isAdmin() ? View.GONE : View.VISIBLE); - - holder.isAdmin.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - holder.isNotAdmin.setVisibility(View.VISIBLE); - holder.isAdmin.setVisibility(View.GONE); - ca.setAdmin(false); - } - }); - - holder.isNotAdmin.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - holder.isNotAdmin.setVisibility(View.GONE); - holder.isAdmin.setVisibility(View.VISIBLE); - ca.setAdmin(true); - } - }); - - holder.delete.setVisibility(View.VISIBLE); - if (mHideAdminFeatures) { - holder.delete.setVisibility(View.INVISIBLE); - holder.isAdmin.setOnClickListener( - null); // Do not allow not admin to remove it's rights but display admins - holder.isNotAdmin.setVisibility( - View.GONE); // Hide not admin button for not admin participants - } else if (mChatRoom != null) { - boolean found = false; - for (Participant p : mChatRoom.getParticipants()) { - if (p.getAddress().weakEqual(ca.getAddress())) { - found = true; - break; - } - } - if (!found) { - holder.isNotAdmin.setVisibility( - View.GONE); // Hide not admin button for participant not yet added so - // even if user click it it won't have any effect - } - } - } - - @Override - public int getItemCount() { - return mItems.size(); - } - - public void setChatRoom(ChatRoom room) { - mChatRoom = room; - } - - private Object getItem(int i) { - return mItems.get(i); - } - - @Override - public long getItemId(int i) { - return i; - } - - public void setOnDeleteClickListener(View.OnClickListener onClickListener) { - mDeleteListener = onClickListener; - } - - public void updateDataSet(ArrayList mParticipants) { - mItems = mParticipants; - notifyDataSetChanged(); - } - - public void setAdminFeaturesVisible(boolean visible) { - mHideAdminFeatures = !visible; - notifyDataSetChanged(); - } -} diff --git a/app/src/main/java/org/linphone/chat/GroupInfoFragment.java b/app/src/main/java/org/linphone/chat/GroupInfoFragment.java deleted file mode 100644 index 127fde021..000000000 --- a/app/src/main/java/org/linphone/chat/GroupInfoFragment.java +++ /dev/null @@ -1,503 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import static android.content.Context.INPUT_METHOD_SERVICE; - -import android.app.Dialog; -import android.app.Fragment; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import java.util.ArrayList; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.call.views.LinphoneLinearLayoutManager; -import org.linphone.contacts.ContactAddress; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.core.Address; -import org.linphone.core.ChatRoom; -import org.linphone.core.ChatRoomCapabilities; -import org.linphone.core.ChatRoomListenerStub; -import org.linphone.core.ChatRoomParams; -import org.linphone.core.Core; -import org.linphone.core.EventLog; -import org.linphone.core.Factory; -import org.linphone.core.Participant; -import org.linphone.core.tools.Log; -import org.linphone.utils.LinphoneUtils; - -public class GroupInfoFragment extends Fragment { - private ImageView mConfirmButton; - private ImageView mAddParticipantsButton; - private Address mGroupChatRoomAddress; - private EditText mSubjectField; - - private RecyclerView mParticipantsList; - - private RelativeLayout mWaitLayout; - private GroupInfoAdapter mAdapter; - private boolean mIsAlreadyCreatedGroup; - private boolean mIsEditionEnabled; - private ArrayList mParticipants; - private String mSubject; - private ChatRoom mChatRoom, mTempChatRoom; - private Dialog mAdminStateChangedDialog; - private ChatRoomListenerStub mChatRoomCreationListener; - private boolean mIsEncryptionEnabled; - private ChatRoomListenerStub mListener; - - @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.chat_infos, container, false); - - if (getArguments() == null || getArguments().isEmpty()) { - return null; - } - - mParticipants = (ArrayList) getArguments().getSerializable("Participants"); - mGroupChatRoomAddress = null; - mChatRoom = null; - - String address = getArguments().getString("RemoteSipUri"); - if (address != null && address.length() > 0) { - mGroupChatRoomAddress = Factory.instance().createAddress(address); - } - - Address localSipAddress = null; - String localSipUri = getArguments().getString("LocalSipUri"); - if (localSipUri != null && localSipUri.length() > 0) { - localSipAddress = Factory.instance().createAddress(localSipUri); - } - - mIsAlreadyCreatedGroup = mGroupChatRoomAddress != null; - if (mIsAlreadyCreatedGroup) { - mChatRoom = - LinphoneManager.getCore().getChatRoom(mGroupChatRoomAddress, localSipAddress); - } - - if (mChatRoom == null) { - mIsAlreadyCreatedGroup = false; - mIsEditionEnabled = true; - mSubject = getArguments().getString("Subject", ""); - mIsEncryptionEnabled = getArguments().getBoolean("Encrypted", false); - } else { - mIsEditionEnabled = - mChatRoom.getMe() != null - && mChatRoom.getMe().isAdmin() - && !mChatRoom.hasBeenLeft(); - mSubject = mChatRoom.getSubject(); - mIsEncryptionEnabled = mChatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt()); - } - - mParticipantsList = view.findViewById(R.id.chat_room_participants); - mAdapter = new GroupInfoAdapter(mParticipants, !mIsEditionEnabled, !mIsAlreadyCreatedGroup); - mAdapter.setOnDeleteClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - ContactAddress ca = (ContactAddress) view.getTag(); - mParticipants.remove(ca); - mAdapter.updateDataSet(mParticipants); - mParticipantsList.setAdapter(mAdapter); - mConfirmButton.setEnabled( - mSubjectField.getText().length() > 0 && mParticipants.size() > 0); - } - }); - mParticipantsList.setAdapter(mAdapter); - mAdapter.setChatRoom(mChatRoom); - LinearLayoutManager layoutManager = new LinphoneLinearLayoutManager(getActivity()); - mParticipantsList.setLayoutManager(layoutManager); - - // Divider between items - DividerItemDecoration dividerItemDecoration = - new DividerItemDecoration( - mParticipantsList.getContext(), layoutManager.getOrientation()); - dividerItemDecoration.setDrawable(getResources().getDrawable(R.drawable.divider)); - mParticipantsList.addItemDecoration(dividerItemDecoration); - - ImageView backButton = view.findViewById(R.id.back); - backButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - if (mIsAlreadyCreatedGroup) { - ((ChatActivity) getActivity()).goBack(); - } else { - goBackToChatCreationFragment(); - } - } - }); - - mConfirmButton = view.findViewById(R.id.confirm); - mConfirmButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - applyChanges(); - } - }); - mConfirmButton.setEnabled(!mSubject.isEmpty() && mParticipants.size() > 0); - - LinearLayout leaveGroupButton = view.findViewById(R.id.leaveGroupLayout); - leaveGroupButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - showLeaveGroupDialog(); - } - }); - leaveGroupButton.setVisibility( - mIsAlreadyCreatedGroup && mChatRoom.hasBeenLeft() - ? View.GONE - : mIsAlreadyCreatedGroup ? View.VISIBLE : View.GONE); - - RelativeLayout addParticipantsLayout = view.findViewById(R.id.addParticipantsLayout); - addParticipantsLayout.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - if (mIsEditionEnabled && mIsAlreadyCreatedGroup) { - goBackToChatCreationFragment(); - } - } - }); - mAddParticipantsButton = view.findViewById(R.id.addParticipants); - mAddParticipantsButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - if (mIsEditionEnabled && mIsAlreadyCreatedGroup) { - goBackToChatCreationFragment(); - } - } - }); - mAddParticipantsButton.setVisibility(mIsAlreadyCreatedGroup ? View.VISIBLE : View.GONE); - - mSubjectField = view.findViewById(R.id.subjectField); - mSubjectField.addTextChangedListener( - new TextWatcher() { - @Override - public void beforeTextChanged( - CharSequence charSequence, int i, int i1, int i2) {} - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} - - @Override - public void afterTextChanged(Editable editable) { - mConfirmButton.setEnabled( - mSubjectField.getText().length() > 0 && mParticipants.size() > 0); - } - }); - mSubjectField.setText(mSubject); - - mChatRoomCreationListener = - new ChatRoomListenerStub() { - @Override - public void onStateChanged(ChatRoom cr, ChatRoom.State newState) { - if (newState == ChatRoom.State.Created) { - mWaitLayout.setVisibility(View.GONE); - // Pop the back stack twice so we don't have in stack Creation -> Group - // Behind the chat room, so a back press will take us back to the rooms - getFragmentManager().popBackStack(); - getFragmentManager().popBackStack(); - ((ChatActivity) getActivity()) - .showChatRoom(cr.getLocalAddress(), cr.getPeerAddress()); - } else if (newState == ChatRoom.State.CreationFailed) { - mWaitLayout.setVisibility(View.GONE); - ((ChatActivity) getActivity()).displayChatRoomError(); - Log.e( - "[Group Info] Group chat room for address " - + cr.getPeerAddress() - + " has failed !"); - } - } - }; - - if (!mIsEditionEnabled) { - mSubjectField.setEnabled(false); - mConfirmButton.setVisibility(View.INVISIBLE); - mAddParticipantsButton.setVisibility(View.GONE); - } - - mWaitLayout = view.findViewById(R.id.waitScreen); - mWaitLayout.setVisibility(View.GONE); - - mListener = - new ChatRoomListenerStub() { - @Override - public void onParticipantAdminStatusChanged(ChatRoom cr, EventLog event_log) { - if (mChatRoom.getMe().isAdmin() != mIsEditionEnabled) { - // Either we weren't admin and we are now or the other way around - mIsEditionEnabled = mChatRoom.getMe().isAdmin(); - displayMeAdminStatusUpdated(); - refreshAdminRights(); - } - refreshParticipantsList(); - } - - @Override - public void onSubjectChanged(ChatRoom cr, EventLog event_log) { - mSubjectField.setText(event_log.getSubject()); - } - - @Override - public void onParticipantAdded(ChatRoom cr, EventLog eventLog) { - refreshParticipantsList(); - } - - @Override - public void onParticipantRemoved(ChatRoom cr, EventLog eventLog) { - refreshParticipantsList(); - } - }; - - if (mChatRoom != null) { - mChatRoom.addListener(mListener); - } - - return view; - } - - @Override - public void onResume() { - super.onResume(); - - InputMethodManager inputMethodManager = - (InputMethodManager) getActivity().getSystemService(INPUT_METHOD_SERVICE); - if (getActivity().getCurrentFocus() != null) { - inputMethodManager.hideSoftInputFromWindow( - getActivity().getCurrentFocus().getWindowToken(), 0); - } - } - - @Override - public void onPause() { - if (mTempChatRoom != null) { - mTempChatRoom.removeListener(mChatRoomCreationListener); - } - super.onPause(); - } - - @Override - public void onDestroy() { - if (mChatRoom != null) { - mChatRoom.removeListener(mListener); - } - super.onDestroy(); - } - - private void goBackToChatCreationFragment() { - ((ChatActivity) getActivity()) - .showChatRoomCreation( - mGroupChatRoomAddress, - mParticipants, - mSubject, - mIsEncryptionEnabled, - true, - !mIsAlreadyCreatedGroup); - } - - private void refreshParticipantsList() { - if (mChatRoom == null) return; - mParticipants = new ArrayList<>(); - for (Participant p : mChatRoom.getParticipants()) { - Address a = p.getAddress(); - LinphoneContact c = ContactsManager.getInstance().findContactFromAddress(a); - if (c == null) { - c = new LinphoneContact(); - String displayName = LinphoneUtils.getAddressDisplayName(a); - c.setFullName(displayName); - } - ContactAddress ca = new ContactAddress(c, a.asString(), "", p.isAdmin()); - mParticipants.add(ca); - } - - mAdapter.updateDataSet(mParticipants); - mAdapter.setChatRoom(mChatRoom); - } - - private void refreshAdminRights() { - mAdapter.setAdminFeaturesVisible(mIsEditionEnabled); - mAdapter.setChatRoom(mChatRoom); - mSubjectField.setEnabled(mIsEditionEnabled); - mConfirmButton.setVisibility(mIsEditionEnabled ? View.VISIBLE : View.INVISIBLE); - mAddParticipantsButton.setVisibility(mIsEditionEnabled ? View.VISIBLE : View.GONE); - } - - private void displayMeAdminStatusUpdated() { - if (mAdminStateChangedDialog != null) mAdminStateChangedDialog.dismiss(); - - mAdminStateChangedDialog = - ((ChatActivity) getActivity()) - .displayDialog( - getString( - mIsEditionEnabled - ? R.string.chat_room_you_are_now_admin - : R.string.chat_room_you_are_no_longer_admin)); - Button cancel = mAdminStateChangedDialog.findViewById(R.id.dialog_cancel_button); - mAdminStateChangedDialog.findViewById(R.id.dialog_delete_button).setVisibility(View.GONE); - cancel.setText(getString(R.string.ok)); - cancel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - mAdminStateChangedDialog.dismiss(); - } - }); - - mAdminStateChangedDialog.show(); - } - - private void showLeaveGroupDialog() { - final Dialog dialog = - ((ChatActivity) getActivity()) - .displayDialog(getString(R.string.chat_room_leave_dialog)); - Button delete = dialog.findViewById(R.id.dialog_delete_button); - delete.setText(getString(R.string.chat_room_leave_button)); - Button cancel = dialog.findViewById(R.id.dialog_cancel_button); - - delete.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - if (mChatRoom != null) { - mChatRoom.leave(); - ((ChatActivity) getActivity()) - .showChatRoom( - mChatRoom.getLocalAddress(), - mChatRoom.getPeerAddress()); - } else { - Log.e( - "[Group Info] Can't leave, chatRoom for address " - + mGroupChatRoomAddress.asString() - + " is null..."); - } - dialog.dismiss(); - } - }); - - cancel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - dialog.dismiss(); - } - }); - dialog.show(); - } - - private void applyChanges() { - if (!mIsAlreadyCreatedGroup) { - mWaitLayout.setVisibility(View.VISIBLE); - Core core = LinphoneManager.getCore(); - - int i = 0; - Address[] participants = new Address[mParticipants.size()]; - for (ContactAddress ca : mParticipants) { - participants[i] = ca.getAddress(); - i++; - } - - ChatRoomParams params = core.createDefaultChatRoomParams(); - params.enableEncryption(mIsEncryptionEnabled); - params.enableGroup(true); - - mTempChatRoom = - core.createChatRoom(params, mSubjectField.getText().toString(), participants); - if (mTempChatRoom != null) { - mTempChatRoom.addListener(mChatRoomCreationListener); - } else { - Log.w("[Group Info] createChatRoom returned null..."); - mWaitLayout.setVisibility(View.GONE); - } - } else { - // Subject - String newSubject = mSubjectField.getText().toString(); - if (!newSubject.equals(mSubject)) { - mChatRoom.setSubject(newSubject); - } - - // Participants removed - ArrayList toRemove = new ArrayList<>(); - for (Participant p : mChatRoom.getParticipants()) { - boolean found = false; - for (ContactAddress c : mParticipants) { - if (c.getAddress().weakEqual(p.getAddress())) { - found = true; - break; - } - } - if (!found) { - toRemove.add(p); - } - } - Participant[] participantsToRemove = new Participant[toRemove.size()]; - toRemove.toArray(participantsToRemove); - mChatRoom.removeParticipants(participantsToRemove); - - // Participants added - ArrayList
toAdd = new ArrayList<>(); - for (ContactAddress c : mParticipants) { - boolean found = false; - for (Participant p : mChatRoom.getParticipants()) { - if (p.getAddress().weakEqual(c.getAddress())) { - // Admin rights - if (c.isAdmin() != p.isAdmin()) { - mChatRoom.setParticipantAdminStatus(p, c.isAdmin()); - } - found = true; - break; - } - } - if (!found) { - Address addr = c.getAddress(); - if (addr != null) { - toAdd.add(addr); - } else { - // TODO error - } - } - } - Address[] participantsToAdd = new Address[toAdd.size()]; - toAdd.toArray(participantsToAdd); - if (!mChatRoom.addParticipants(participantsToAdd)) { - // TODO error - } - // Pop back stack to go back to the Messages fragment - getFragmentManager().popBackStack(); - } - } -} diff --git a/app/src/main/java/org/linphone/chat/GroupInfoViewHolder.java b/app/src/main/java/org/linphone/chat/GroupInfoViewHolder.java deleted file mode 100644 index b7dcaeaec..000000000 --- a/app/src/main/java/org/linphone/chat/GroupInfoViewHolder.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; -import androidx.recyclerview.widget.RecyclerView; -import org.linphone.R; - -public class GroupInfoViewHolder extends RecyclerView.ViewHolder { - public final TextView name, sipUri; - public final RelativeLayout avatarLayout; - public final ImageView delete; - public final LinearLayout isAdmin; - public final LinearLayout isNotAdmin; - - public GroupInfoViewHolder(View view) { - super(view); - name = view.findViewById(R.id.name); - sipUri = view.findViewById(R.id.sipUri); - avatarLayout = view.findViewById(R.id.avatar_layout); - delete = view.findViewById(R.id.delete); - isAdmin = view.findViewById(R.id.isAdminLayout); - isNotAdmin = view.findViewById(R.id.isNotAdminLayout); - } -} diff --git a/app/src/main/java/org/linphone/chat/ImdnFragment.java b/app/src/main/java/org/linphone/chat/ImdnFragment.java deleted file mode 100644 index 149a6a96d..000000000 --- a/app/src/main/java/org/linphone/chat/ImdnFragment.java +++ /dev/null @@ -1,355 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.chat; - -import android.app.Fragment; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; -import androidx.annotation.Nullable; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.core.Address; -import org.linphone.core.ChatMessage; -import org.linphone.core.ChatMessageListenerStub; -import org.linphone.core.ChatRoom; -import org.linphone.core.Core; -import org.linphone.core.Factory; -import org.linphone.core.ParticipantImdnState; -import org.linphone.utils.LinphoneUtils; - -public class ImdnFragment extends Fragment { - private LayoutInflater mInflater; - private LinearLayout mRead, - mReadHeader, - mDelivered, - mDeliveredHeader, - mSent, - mSentHeader, - mUndelivered, - mUndeliveredHeader; - private ChatMessageViewHolder mBubble; - private ViewGroup mContainer; - - private String mMessageId; - private Address mLocalSipAddr, mRoomAddr; - private ChatMessage mMessage; - private ChatMessageListenerStub mListener; - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (getArguments() != null) { - String localSipuri = getArguments().getString("LocalSipUri"); - mLocalSipAddr = Factory.instance().createAddress(localSipuri); - String roomUri = getArguments().getString("RemoteSipUri"); - mRoomAddr = Factory.instance().createAddress(roomUri); - mMessageId = getArguments().getString("MessageId"); - } - - Core core = LinphoneManager.getCore(); - ChatRoom room = core.getChatRoom(mRoomAddr, mLocalSipAddr); - - mInflater = inflater; - mContainer = container; - View view = mInflater.inflate(R.layout.chat_imdn, container, false); - - ImageView backButton = view.findViewById(R.id.back); - backButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - if (getResources().getBoolean(R.bool.isTablet)) { - ((ChatActivity) getActivity()).showChatRoom(mLocalSipAddr, mRoomAddr); - } else { - ((ChatActivity) getActivity()).popBackStack(); - } - } - }); - - mRead = view.findViewById(R.id.read_layout); - mDelivered = view.findViewById(R.id.delivered_layout); - mSent = view.findViewById(R.id.sent_layout); - mUndelivered = view.findViewById(R.id.undelivered_layout); - mReadHeader = view.findViewById(R.id.read_layout_header); - mDeliveredHeader = view.findViewById(R.id.delivered_layout_header); - mSentHeader = view.findViewById(R.id.sent_layout_header); - mUndeliveredHeader = view.findViewById(R.id.undelivered_layout_header); - - mBubble = new ChatMessageViewHolder(getActivity(), view.findViewById(R.id.bubble), null); - - mMessage = room.findMessage(mMessageId); - mListener = - new ChatMessageListenerStub() { - @Override - public void onParticipantImdnStateChanged( - ChatMessage msg, ParticipantImdnState state) { - refreshInfo(); - } - }; - - return view; - } - - @Override - public void onResume() { - super.onResume(); - - refreshInfo(); - if (mMessage != null) { - mMessage.addListener(mListener); - } - } - - @Override - public void onPause() { - if (mMessage != null) { - mMessage.removeListener(mListener); - } - super.onPause(); - } - - private void refreshInfo() { - if (mMessage == null) { - // TODO: error - return; - } - Address remoteSender = mMessage.getFromAddress(); - LinphoneContact contact = - ContactsManager.getInstance().findContactFromAddress(remoteSender); - - mBubble.delete.setVisibility(View.GONE); - mBubble.eventLayout.setVisibility(View.GONE); - mBubble.securityEventLayout.setVisibility(View.GONE); - mBubble.rightAnchor.setVisibility(View.GONE); - mBubble.bubbleLayout.setVisibility(View.GONE); - mBubble.bindMessage(mMessage, contact); - - mRead.removeAllViews(); - mDelivered.removeAllViews(); - mSent.removeAllViews(); - mUndelivered.removeAllViews(); - - ParticipantImdnState[] participants = - mMessage.getParticipantsByImdnState(ChatMessage.State.Displayed); - mReadHeader.setVisibility(participants.length == 0 ? View.GONE : View.VISIBLE); - boolean first = true; - for (ParticipantImdnState participant : participants) { - Address address = participant.getParticipant().getAddress(); - - LinphoneContact participantContact = - ContactsManager.getInstance().findContactFromAddress(address); - String participantDisplayName = - participantContact != null - ? participantContact.getFullName() - : LinphoneUtils.getAddressDisplayName(address); - - View v = mInflater.inflate(R.layout.chat_imdn_cell, mContainer, false); - v.findViewById(R.id.separator).setVisibility(first ? View.GONE : View.VISIBLE); - ((TextView) v.findViewById(R.id.time)) - .setText( - LinphoneUtils.timestampToHumanDate( - getActivity(), - participant.getStateChangeTime(), - R.string.messages_date_format)); - TextView name = v.findViewById(R.id.name); - name.setText(participantDisplayName); - if (participantContact != null) { - ContactAvatar.displayAvatar(participantContact, v.findViewById(R.id.avatar_layout)); - } else { - ContactAvatar.displayAvatar( - participantDisplayName, v.findViewById(R.id.avatar_layout)); - } - - final TextView sipUri = v.findViewById(R.id.sipUri); - sipUri.setText(address.asStringUriOnly()); - if (!getResources().getBoolean(R.bool.show_sip_uri_in_chat)) { - sipUri.setVisibility(View.GONE); - name.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - sipUri.setVisibility( - sipUri.getVisibility() == View.VISIBLE - ? View.GONE - : View.VISIBLE); - } - }); - } - - mRead.addView(v); - first = false; - } - - participants = mMessage.getParticipantsByImdnState(ChatMessage.State.DeliveredToUser); - mDeliveredHeader.setVisibility(participants.length == 0 ? View.GONE : View.VISIBLE); - first = true; - for (ParticipantImdnState participant : participants) { - Address address = participant.getParticipant().getAddress(); - - LinphoneContact participantContact = - ContactsManager.getInstance().findContactFromAddress(address); - String participantDisplayName = - participantContact != null - ? participantContact.getFullName() - : LinphoneUtils.getAddressDisplayName(address); - - View v = mInflater.inflate(R.layout.chat_imdn_cell, mContainer, false); - v.findViewById(R.id.separator).setVisibility(first ? View.GONE : View.VISIBLE); - ((TextView) v.findViewById(R.id.time)) - .setText( - LinphoneUtils.timestampToHumanDate( - getActivity(), - participant.getStateChangeTime(), - R.string.messages_date_format)); - TextView name = v.findViewById(R.id.name); - name.setText(participantDisplayName); - if (participantContact != null) { - ContactAvatar.displayAvatar(participantContact, v.findViewById(R.id.avatar_layout)); - } else { - ContactAvatar.displayAvatar( - participantDisplayName, v.findViewById(R.id.avatar_layout)); - } - - final TextView sipUri = v.findViewById(R.id.sipUri); - sipUri.setText(address.asStringUriOnly()); - if (!getResources().getBoolean(R.bool.show_sip_uri_in_chat)) { - sipUri.setVisibility(View.GONE); - name.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - sipUri.setVisibility( - sipUri.getVisibility() == View.VISIBLE - ? View.GONE - : View.VISIBLE); - } - }); - } - - mDelivered.addView(v); - first = false; - } - - participants = mMessage.getParticipantsByImdnState(ChatMessage.State.Delivered); - mSentHeader.setVisibility(participants.length == 0 ? View.GONE : View.VISIBLE); - first = true; - for (ParticipantImdnState participant : participants) { - Address address = participant.getParticipant().getAddress(); - - LinphoneContact participantContact = - ContactsManager.getInstance().findContactFromAddress(address); - String participantDisplayName = - participantContact != null - ? participantContact.getFullName() - : LinphoneUtils.getAddressDisplayName(address); - - View v = mInflater.inflate(R.layout.chat_imdn_cell, mContainer, false); - v.findViewById(R.id.separator).setVisibility(first ? View.GONE : View.VISIBLE); - ((TextView) v.findViewById(R.id.time)) - .setText( - LinphoneUtils.timestampToHumanDate( - getActivity(), - participant.getStateChangeTime(), - R.string.messages_date_format)); - TextView name = v.findViewById(R.id.name); - name.setText(participantDisplayName); - if (participantContact != null) { - ContactAvatar.displayAvatar(participantContact, v.findViewById(R.id.avatar_layout)); - } else { - ContactAvatar.displayAvatar( - participantDisplayName, v.findViewById(R.id.avatar_layout)); - } - - final TextView sipUri = v.findViewById(R.id.sipUri); - sipUri.setText(address.asStringUriOnly()); - if (!getResources().getBoolean(R.bool.show_sip_uri_in_chat)) { - sipUri.setVisibility(View.GONE); - name.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - sipUri.setVisibility( - sipUri.getVisibility() == View.VISIBLE - ? View.GONE - : View.VISIBLE); - } - }); - } - - mSent.addView(v); - first = false; - } - - participants = mMessage.getParticipantsByImdnState(ChatMessage.State.NotDelivered); - mUndeliveredHeader.setVisibility(participants.length == 0 ? View.GONE : View.VISIBLE); - first = true; - for (ParticipantImdnState participant : participants) { - Address address = participant.getParticipant().getAddress(); - - LinphoneContact participantContact = - ContactsManager.getInstance().findContactFromAddress(address); - String participantDisplayName = - participantContact != null - ? participantContact.getFullName() - : LinphoneUtils.getAddressDisplayName(address); - - View v = mInflater.inflate(R.layout.chat_imdn_cell, mContainer, false); - v.findViewById(R.id.separator).setVisibility(first ? View.GONE : View.VISIBLE); - TextView name = v.findViewById(R.id.name); - name.setText(participantDisplayName); - if (participantContact != null) { - ContactAvatar.displayAvatar(participantContact, v.findViewById(R.id.avatar_layout)); - } else { - ContactAvatar.displayAvatar( - participantDisplayName, v.findViewById(R.id.avatar_layout)); - } - - final TextView sipUri = v.findViewById(R.id.sipUri); - sipUri.setText(address.asStringUriOnly()); - if (!getResources().getBoolean(R.bool.show_sip_uri_in_chat)) { - sipUri.setVisibility(View.GONE); - name.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - sipUri.setVisibility( - sipUri.getVisibility() == View.VISIBLE - ? View.GONE - : View.VISIBLE); - } - }); - } - - mUndelivered.addView(v); - first = false; - } - } -} diff --git a/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt new file mode 100644 index 000000000..2763214d9 --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.compatibility + +import android.annotation.TargetApi +import android.bluetooth.BluetoothAdapter +import android.content.ContentValues +import android.content.Context +import android.os.Build +import android.os.Environment +import android.os.Vibrator +import android.provider.MediaStore +import android.provider.Settings +import org.linphone.R +import org.linphone.core.Content +import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils +import org.linphone.utils.FileUtils + +@TargetApi(21) +class Api21Compatibility { + companion object { + fun getDeviceName(context: Context): String { + var name = BluetoothAdapter.getDefaultAdapter().name + if (name == null) { + name = Settings.Secure.getString( + context.contentResolver, + "bluetooth_name" + ) + } + if (name == null) { + name = Build.MANUFACTURER + " " + Build.MODEL + } + return name + } + + fun vibrate(vibrator: Vibrator) { + val pattern = longArrayOf(0, 1000, 1000) + vibrator.vibrate(pattern, 1) + } + + fun addImageToMediaStore(context: Context, content: Content): Boolean { + val filePath = content.filePath + val appName = AppUtils.getString(R.string.app_name) + val relativePath = "${Environment.DIRECTORY_PICTURES}/$appName" + val fileName = content.name + val mime = "${content.type}/${content.subtype}" + Log.i("[Chat Message] Adding image $filePath to Media Store with name $fileName and MIME $mime, asking to be stored in $relativePath") + + val values = ContentValues().apply { + put(MediaStore.Images.Media.DISPLAY_NAME, fileName) + put(MediaStore.Images.Media.MIME_TYPE, mime) + } + val collection = MediaStore.Images.Media.getContentUri("external") + + val fileUri = context.contentResolver.insert(collection, values) + if (fileUri == null) { + Log.e("[Chat Message] Failed to get a URI to where store the file, aborting") + return false + } + + var copyOk = false + context.contentResolver.openOutputStream(fileUri).use { out -> + copyOk = FileUtils.copyFileTo(filePath, out) + } + + if (copyOk) { + content.userData = fileUri.toString() + } + return copyOk + } + + fun addVideoToMediaStore(context: Context, content: Content): Boolean { + val filePath = content.filePath + val appName = AppUtils.getString(R.string.app_name) + val relativePath = "${Environment.DIRECTORY_MOVIES}/$appName" + val fileName = content.name + val mime = "${content.type}/${content.subtype}" + Log.i("[Chat Message] Adding video $filePath to Media Store with name $fileName and MIME $mime, asking to be stored in $relativePath") + + val values = ContentValues().apply { + put(MediaStore.Video.Media.TITLE, fileName) + put(MediaStore.Video.Media.DISPLAY_NAME, fileName) + put(MediaStore.Video.Media.MIME_TYPE, mime) + } + val collection = MediaStore.Video.Media.getContentUri("external") + + val fileUri = context.contentResolver.insert(collection, values) + if (fileUri == null) { + Log.e("[Chat Message] Failed to get a URI to where store the file, aborting") + return false + } + + var copyOk = false + context.contentResolver.openOutputStream(fileUri).use { out -> + copyOk = FileUtils.copyFileTo(filePath, out) + } + + if (copyOk) { + content.userData = fileUri.toString() + } + return copyOk + } + + fun addAudioToMediaStore(context: Context, content: Content): Boolean { + val filePath = content.filePath + val appName = AppUtils.getString(R.string.app_name) + val relativePath = "${Environment.DIRECTORY_MUSIC}/$appName" + val fileName = content.name + val mime = "${content.type}/${content.subtype}" + Log.i("[Chat Message] Adding audio $filePath to Media Store with name $fileName and MIME $mime, asking to be stored in $relativePath") + + val values = ContentValues().apply { + put(MediaStore.Audio.Media.TITLE, fileName) + put(MediaStore.Audio.Media.DISPLAY_NAME, fileName) + put(MediaStore.Audio.Media.MIME_TYPE, mime) + } + val collection = MediaStore.Audio.Media.getContentUri("external") + + val fileUri = context.contentResolver.insert(collection, values) + if (fileUri == null) { + Log.e("[Chat Message] Failed to get a URI to where store the file, aborting") + return false + } + + var copyOk = false + context.contentResolver.openOutputStream(fileUri).use { out -> + copyOk = FileUtils.copyFileTo(filePath, out) + } + + if (copyOk) { + content.userData = fileUri.toString() + } + return copyOk + } + } +} diff --git a/app/src/main/java/org/linphone/compatibility/Api23Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api23Compatibility.kt new file mode 100644 index 000000000..30cc9097d --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/Api23Compatibility.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.compatibility + +import android.annotation.TargetApi +import android.app.NotificationManager +import android.content.Context +import android.content.pm.PackageManager +import android.provider.Settings +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.contact.Contact +import org.linphone.core.Address +import org.linphone.core.tools.Log + +@TargetApi(23) +class Api23Compatibility { + companion object { + fun hasPermission(context: Context, permission: String): Boolean { + return context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED + } + + fun isDoNotDisturbSettingsAccessGranted(context: Context): Boolean { + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + return notificationManager.isNotificationPolicyAccessGranted + } + + fun isDoNotDisturbPolicyAllowingRinging(context: Context, remoteAddress: Address): Boolean { + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val filter = notificationManager.currentInterruptionFilter + if (filter == NotificationManager.INTERRUPTION_FILTER_PRIORITY) { + Log.w("[Audio Manager] Priority interruption filter detected") + if (!notificationManager.isNotificationPolicyAccessGranted) { + Log.e( + "[Audio Manager] Access to policy is denied, let's assume it is not safe for ringing" + ) + return false + } + val callPolicy = notificationManager.notificationPolicy.priorityCallSenders + if (callPolicy == NotificationManager.Policy.PRIORITY_SENDERS_ANY) { + Log.i("[Audio Manager] Priority for calls is Any, we can ring") + } else { + val contact: Contact? = coreContext.contactsManager.findContactByAddress(remoteAddress) + if (callPolicy == NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS) { + Log.i("[Audio Manager] Priority for calls is Contacts, let's check") + if (contact == null) { + Log.w( + "[Audio Manager] Couldn't find a contact for address ${remoteAddress.asStringUriOnly()}" + ) + return false + } else { + Log.i( + "[Audio Manager] Contact found for address ${remoteAddress.asStringUriOnly()}, we can ring" + ) + } + } else if (callPolicy == NotificationManager.Policy.PRIORITY_SENDERS_STARRED) { + Log.i("[Audio Manager] Priority for calls is Starred Contacts, let's check") + if (contact == null) { + Log.w( + "[Audio Manager] Couldn't find a contact for address ${remoteAddress.asStringUriOnly()}" + ) + return false + } else if (!contact.isStarred) { + Log.w( + "[Audio Manager] Contact found for address ${remoteAddress.asStringUriOnly()}, but it isn't starred" + ) + return false + } else { + Log.i( + "[Audio Manager] Starred contact found for address ${remoteAddress.asStringUriOnly()}, we can ring" + ) + } + } + } + } else if (filter == NotificationManager.INTERRUPTION_FILTER_ALARMS) { + Log.w("[Audio Manager] Alarms interruption filter detected") + return false + } else { + Log.i("[Audio Manager] Interruption filter is $filter, we can ring") + } + + return true + } + + fun canDrawOverlay(context: Context): Boolean { + return Settings.canDrawOverlays(context) + } + } +} diff --git a/app/src/main/java/org/linphone/compatibility/Api25Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api25Compatibility.kt new file mode 100644 index 000000000..2d66d357a --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/Api25Compatibility.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.compatibility + +import android.annotation.TargetApi +import android.bluetooth.BluetoothAdapter +import android.content.Context +import android.os.Build +import android.provider.Settings +import org.linphone.contact.ShortcutsHelper + +@TargetApi(25) +class Api25Compatibility { + companion object { + fun getDeviceName(context: Context): String { + var name = Settings.Global.getString( + context.contentResolver, Settings.Global.DEVICE_NAME + ) + if (name == null) { + name = BluetoothAdapter.getDefaultAdapter().name + } + if (name == null) { + name = Settings.Secure.getString( + context.contentResolver, + "bluetooth_name" + ) + } + if (name == null) { + name = Build.MANUFACTURER + " " + Build.MODEL + } + return name + } + + fun createShortcutsToContacts(context: Context) { + ShortcutsHelper.createShortcutsToContacts(context) + } + + fun removeShortcutsToContacts(context: Context) { + ShortcutsHelper.removeShortcuts(context) + } + } +} diff --git a/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt new file mode 100644 index 000000000..cd20aae39 --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.compatibility + +import android.annotation.TargetApi +import android.app.Activity +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PictureInPictureParams +import android.content.Context +import android.content.pm.PackageManager +import android.media.AudioAttributes +import android.os.VibrationEffect +import android.os.Vibrator +import android.view.WindowManager +import androidx.core.app.NotificationManagerCompat +import org.linphone.R +import org.linphone.core.tools.Log + +@TargetApi(26) +class Api26Compatibility { + companion object { + fun enterPipMode(activity: Activity) { + val supportsPip = activity.packageManager + .hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) + Log.i("[Call] Is picture in picture supported: $supportsPip") + if (supportsPip) { + val params = PictureInPictureParams.Builder().build() + if (!activity.enterPictureInPictureMode(params)) { + Log.e("[Call] Failed to enter picture in picture mode") + } + } + } + + fun createServiceChannel(context: Context, notificationManager: NotificationManagerCompat) { + // Create service notification channel + val id = context.getString(R.string.notification_channel_service_id) + val name: CharSequence = + context.getString(R.string.notification_channel_service_name) + val description = + context.getString(R.string.notification_channel_service_name) + val channel = + NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW) + channel.description = description + channel.enableVibration(false) + channel.enableLights(false) + channel.setShowBadge(false) + notificationManager.createNotificationChannel(channel) + } + + fun createIncomingCallChannel( + context: Context, + notificationManager: NotificationManagerCompat + ) { + // Create incoming calls notification channel + val id = context.getString(R.string.notification_channel_incoming_call_id) + val name: CharSequence = + context.getString(R.string.notification_channel_incoming_call_name) + val description = + context.getString(R.string.notification_channel_incoming_call_name) + val channel = + NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH) + channel.description = description + channel.lightColor = context.getColor(R.color.notification_led_color) + channel.enableVibration(true) + channel.enableLights(true) + channel.setShowBadge(true) + notificationManager.createNotificationChannel(channel) + } + + fun createMessageChannel(context: Context, notificationManager: NotificationManagerCompat) { + // Create messages notification channel + val id = context.getString(R.string.notification_channel_chat_id) + val name = context.getString(R.string.notification_channel_chat_name) + val description = context.getString(R.string.notification_channel_chat_name) + val channel = + NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH) + channel.description = description + channel.lightColor = context.getColor(R.color.notification_led_color) + channel.enableLights(true) + channel.enableVibration(true) + channel.setShowBadge(true) + notificationManager.createNotificationChannel(channel) + } + + fun getOverlayType(): Int { + return WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + } + + fun vibrate(vibrator: Vibrator) { + val effect = VibrationEffect.createWaveform(longArrayOf(0L, 1000L, 1000L), intArrayOf(0, VibrationEffect.DEFAULT_AMPLITUDE, 0), 1) + val audioAttrs = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) + .build() + vibrator.vibrate(effect, audioAttrs) + } + } +} diff --git a/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt new file mode 100644 index 000000000..a42de5071 --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.compatibility + +import android.annotation.TargetApi +import android.app.* +import android.app.usage.UsageStatsManager +import android.content.Context + +@TargetApi(28) +class Api28Compatibility { + companion object { + fun isAppUserRestricted(context: Context): Boolean { + val activityManager = + context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + return activityManager.isBackgroundRestricted + } + + fun getAppStandbyBucket(context: Context): Int { + val usageStatsManager = + context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager + return usageStatsManager.appStandbyBucket + } + } +} diff --git a/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt new file mode 100644 index 000000000..fd4ab0fe3 --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.compatibility + +import android.annotation.TargetApi +import android.content.ContentValues +import android.content.Context +import android.os.Environment +import android.provider.MediaStore +import org.linphone.R +import org.linphone.core.Content +import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils +import org.linphone.utils.FileUtils + +@TargetApi(29) +class Api29Compatibility { + companion object { + fun addImageToMediaStore(context: Context, content: Content): Boolean { + val filePath = content.filePath + val appName = AppUtils.getString(R.string.app_name) + val relativePath = "${Environment.DIRECTORY_PICTURES}/$appName" + val fileName = content.name + val mime = "${content.type}/${content.subtype}" + Log.i("[Chat Message] Adding image $filePath to Media Store with name $fileName and MIME $mime, asking to be stored in $relativePath") + + val values = ContentValues().apply { + put(MediaStore.Images.Media.DISPLAY_NAME, fileName) + put(MediaStore.Images.Media.MIME_TYPE, mime) + put(MediaStore.Images.Media.RELATIVE_PATH, relativePath) + put(MediaStore.Images.Media.IS_PENDING, 1) + } + val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + + val fileUri = context.contentResolver.insert(collection, values) + if (fileUri == null) { + Log.e("[Chat Message] Failed to get a URI to where store the file, aborting") + return false + } + + var copyOk = false + context.contentResolver.openOutputStream(fileUri).use { out -> + copyOk = FileUtils.copyFileTo(filePath, out) + } + + values.clear() + values.put(MediaStore.Images.Media.IS_PENDING, 0) + context.contentResolver.update(fileUri, values, null, null) + + if (copyOk) { + content.userData = fileUri.toString() + } + return copyOk + } + + fun addVideoToMediaStore(context: Context, content: Content): Boolean { + val filePath = content.filePath + val appName = AppUtils.getString(R.string.app_name) + val relativePath = "${Environment.DIRECTORY_MOVIES}/$appName" + val fileName = content.name + val mime = "${content.type}/${content.subtype}" + Log.i("[Chat Message] Adding video $filePath to Media Store with name $fileName and MIME $mime, asking to be stored in $relativePath") + + val values = ContentValues().apply { + put(MediaStore.Video.Media.TITLE, fileName) + put(MediaStore.Video.Media.DISPLAY_NAME, fileName) + put(MediaStore.Video.Media.MIME_TYPE, mime) + put(MediaStore.Video.Media.RELATIVE_PATH, relativePath) + put(MediaStore.Video.Media.IS_PENDING, 1) + } + val collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + + val fileUri = context.contentResolver.insert(collection, values) + if (fileUri == null) { + Log.e("[Chat Message] Failed to get a URI to where store the file, aborting") + return false + } + + var copyOk = false + context.contentResolver.openOutputStream(fileUri).use { out -> + copyOk = FileUtils.copyFileTo(filePath, out) + } + + values.clear() + values.put(MediaStore.Video.Media.IS_PENDING, 0) + context.contentResolver.update(fileUri, values, null, null) + + if (copyOk) { + content.userData = fileUri.toString() + } + return copyOk + } + + fun addAudioToMediaStore(context: Context, content: Content): Boolean { + val filePath = content.filePath + val appName = AppUtils.getString(R.string.app_name) + val relativePath = "${Environment.DIRECTORY_MUSIC}/$appName" + val fileName = content.name + val mime = "${content.type}/${content.subtype}" + Log.i("[Chat Message] Adding audio $filePath to Media Store with name $fileName and MIME $mime, asking to be stored in $relativePath") + + val values = ContentValues().apply { + put(MediaStore.Audio.Media.TITLE, fileName) + put(MediaStore.Audio.Media.DISPLAY_NAME, fileName) + put(MediaStore.Audio.Media.MIME_TYPE, mime) + put(MediaStore.Audio.Media.RELATIVE_PATH, relativePath) + put(MediaStore.Audio.Media.IS_PENDING, 1) + } + val collection = MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + + val fileUri = context.contentResolver.insert(collection, values) + if (fileUri == null) { + Log.e("[Chat Message] Failed to get a URI to where store the file, aborting") + return false + } + + var copyOk = false + context.contentResolver.openOutputStream(fileUri).use { out -> + copyOk = FileUtils.copyFileTo(filePath, out) + } + + values.clear() + values.put(MediaStore.Audio.Media.IS_PENDING, 0) + context.contentResolver.update(fileUri, values, null, null) + + if (copyOk) { + content.userData = fileUri.toString() + } + return copyOk + } + } +} diff --git a/app/src/main/java/org/linphone/compatibility/ApiTwentyEightPlus.java b/app/src/main/java/org/linphone/compatibility/ApiTwentyEightPlus.java deleted file mode 100644 index bdc7fc178..000000000 --- a/app/src/main/java/org/linphone/compatibility/ApiTwentyEightPlus.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.compatibility; - -import static org.linphone.compatibility.Compatibility.CHAT_NOTIFICATIONS_GROUP; -import static org.linphone.compatibility.Compatibility.INTENT_LOCAL_IDENTITY; -import static org.linphone.compatibility.Compatibility.INTENT_MARK_AS_READ_ACTION; -import static org.linphone.compatibility.Compatibility.INTENT_NOTIF_ID; -import static org.linphone.compatibility.Compatibility.INTENT_REMOTE_IDENTITY; -import static org.linphone.compatibility.Compatibility.INTENT_REPLY_NOTIF_ACTION; -import static org.linphone.compatibility.Compatibility.KEY_TEXT_REPLY; - -import android.annotation.TargetApi; -import android.app.ActivityManager; -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Person; -import android.app.RemoteInput; -import android.app.usage.UsageStatsManager; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.drawable.Icon; -import org.linphone.R; -import org.linphone.notifications.Notifiable; -import org.linphone.notifications.NotifiableMessage; -import org.linphone.notifications.NotificationBroadcastReceiver; - -@TargetApi(28) -class ApiTwentyEightPlus { - public static Notification createMessageNotification( - Context context, Notifiable notif, Bitmap contactIcon, PendingIntent intent) { - - Person me = new Person.Builder().setName(notif.getMyself()).build(); - Notification.MessagingStyle style = new Notification.MessagingStyle(me); - for (NotifiableMessage message : notif.getMessages()) { - Icon userIcon = null; - if (message.getSenderBitmap() != null) { - userIcon = Icon.createWithBitmap(message.getSenderBitmap()); - } - - Person.Builder builder = new Person.Builder().setName(message.getSender()); - if (userIcon != null) { - builder.setIcon(userIcon); - } - Person user = builder.build(); - - Notification.MessagingStyle.Message msg = - new Notification.MessagingStyle.Message( - message.getMessage(), message.getTime(), user); - if (message.getFilePath() != null) - msg.setData(message.getFileMime(), message.getFilePath()); - style.addMessage(msg); - } - if (notif.isGroup()) { - style.setConversationTitle(notif.getGroupTitle()); - } - style.setGroupConversation(notif.isGroup()); - - return new Notification.Builder( - context, context.getString(R.string.notification_channel_id)) - .setSmallIcon(R.drawable.topbar_chat_notification) - .setAutoCancel(true) - .setContentIntent(intent) - .setDefaults( - Notification.DEFAULT_SOUND - | Notification.DEFAULT_VIBRATE - | Notification.DEFAULT_LIGHTS) - .setLargeIcon(contactIcon) - .setCategory(Notification.CATEGORY_MESSAGE) - .setGroup(CHAT_NOTIFICATIONS_GROUP) - .setVisibility(Notification.VISIBILITY_PRIVATE) - .setPriority(Notification.PRIORITY_HIGH) - .setNumber(notif.getMessages().size()) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .setColor(context.getColor(R.color.notification_led_color)) - .setStyle(style) - .addAction(Compatibility.getReplyMessageAction(context, notif)) - .addAction(Compatibility.getMarkMessageAsReadAction(context, notif)) - .build(); - } - - public static boolean isAppUserRestricted(Context context) { - ActivityManager activityManager = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - return activityManager.isBackgroundRestricted(); - } - - public static int getAppStandbyBucket(Context context) { - UsageStatsManager usageStatsManager = - (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); - return usageStatsManager.getAppStandbyBucket(); - } - - public static String getAppStandbyBucketNameFromValue(int bucket) { - switch (bucket) { - case UsageStatsManager.STANDBY_BUCKET_ACTIVE: - return "STANDBY_BUCKET_ACTIVE"; - case UsageStatsManager.STANDBY_BUCKET_FREQUENT: - return "STANDBY_BUCKET_FREQUENT"; - case UsageStatsManager.STANDBY_BUCKET_RARE: - return "STANDBY_BUCKET_RARE"; - case UsageStatsManager.STANDBY_BUCKET_WORKING_SET: - return "STANDBY_BUCKET_WORKING_SET"; - } - return null; - } - - public static Notification.Action getReplyMessageAction(Context context, Notifiable notif) { - String replyLabel = context.getResources().getString(R.string.notification_reply_label); - RemoteInput remoteInput = - new RemoteInput.Builder(KEY_TEXT_REPLY).setLabel(replyLabel).build(); - - Intent replyIntent = new Intent(context, NotificationBroadcastReceiver.class); - replyIntent.setAction(INTENT_REPLY_NOTIF_ACTION); - replyIntent.putExtra(INTENT_NOTIF_ID, notif.getNotificationId()); - replyIntent.putExtra(INTENT_LOCAL_IDENTITY, notif.getLocalIdentity()); - replyIntent.putExtra(INTENT_REMOTE_IDENTITY, notif.getRemoteIdentity()); - - PendingIntent replyPendingIntent = - PendingIntent.getBroadcast( - context, - notif.getNotificationId(), - replyIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - - return new Notification.Action.Builder( - R.drawable.chat_send_over, - context.getString(R.string.notification_reply_label), - replyPendingIntent) - .addRemoteInput(remoteInput) - .setAllowGeneratedReplies(true) - .setSemanticAction(Notification.Action.SEMANTIC_ACTION_REPLY) - .build(); - } - - public static Notification.Action getMarkMessageAsReadAction( - Context context, Notifiable notif) { - Intent markAsReadIntent = new Intent(context, NotificationBroadcastReceiver.class); - markAsReadIntent.setAction(INTENT_MARK_AS_READ_ACTION); - markAsReadIntent.putExtra(INTENT_NOTIF_ID, notif.getNotificationId()); - markAsReadIntent.putExtra(INTENT_LOCAL_IDENTITY, notif.getLocalIdentity()); - markAsReadIntent.putExtra(INTENT_REMOTE_IDENTITY, notif.getRemoteIdentity()); - - PendingIntent markAsReadPendingIntent = - PendingIntent.getBroadcast( - context, - notif.getNotificationId(), - markAsReadIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - - return new Notification.Action.Builder( - R.drawable.chat_send_over, - context.getString(R.string.notification_mark_as_read_label), - markAsReadPendingIntent) - .setSemanticAction(Notification.Action.SEMANTIC_ACTION_MARK_AS_READ) - .build(); - } -} diff --git a/app/src/main/java/org/linphone/compatibility/ApiTwentyFivePlus.java b/app/src/main/java/org/linphone/compatibility/ApiTwentyFivePlus.java deleted file mode 100644 index 7d0729245..000000000 --- a/app/src/main/java/org/linphone/compatibility/ApiTwentyFivePlus.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.compatibility; - -import static java.lang.Math.min; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import java.util.ArrayList; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.core.Address; -import org.linphone.core.ChatRoom; -import org.linphone.core.ChatRoomCapabilities; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.LinphoneShortcutManager; - -@TargetApi(25) -class ApiTwentyFivePlus { - - public static void removeChatShortcuts(Context context) { - ShortcutManager shortcutManager = - (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); - shortcutManager.removeAllDynamicShortcuts(); - } - - public static void createChatShortcuts(Context context) { - if (!LinphonePreferences.instance().shortcutsCreationEnabled()) return; - - LinphoneShortcutManager manager = new LinphoneShortcutManager(context); - ShortcutManager shortcutManager = - (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); - ArrayList shortcuts = new ArrayList<>(); - - ChatRoom[] rooms = LinphoneManager.getCore().getChatRooms(); - - int i = 0; - int maxShortcuts = min(rooms.length, shortcutManager.getMaxShortcutCountPerActivity()); - ArrayList contacts = new ArrayList<>(); - for (ChatRoom room : rooms) { - // Android can only have around 4-5 shortcuts at a time - if (i >= maxShortcuts) break; - - Address participantAddress = - room.hasCapability(ChatRoomCapabilities.Basic.toInt()) - ? room.getPeerAddress() - : room.getParticipants().length == 0 - ? null - : room.getParticipants()[0].getAddress(); - if (participantAddress == null) continue; - - LinphoneContact contact = - ContactsManager.getInstance().findContactFromAddress(participantAddress); - if (contact != null && !contacts.contains(contact)) { - if (context.getResources().getBoolean(R.bool.shortcut_to_contact)) { - ShortcutInfo shortcut = manager.createContactShortcutInfo(contact); - if (shortcut != null) { - Log.i( - "[Shortcut] Creating launcher shortcut " - + shortcut.getShortLabel() - + " for contact " - + shortcut.getShortLabel()); - shortcuts.add(shortcut); - contacts.add(contact); - i += 1; - } - } else if (context.getResources().getBoolean(R.bool.shortcut_to_chatroom)) { - String peerAddress = room.getPeerAddress().asStringUriOnly(); - ShortcutInfo shortcut = - manager.createChatRoomShortcutInfo(contact, peerAddress); - if (shortcut != null) { - Log.i( - "[Shortcut] Creating launcher shortcut " - + shortcut.getShortLabel() - + " for room " - + shortcut.getId()); - shortcuts.add(shortcut); - contacts.add(contact); - i += 1; - } - } - } - } - - shortcutManager.setDynamicShortcuts(shortcuts); - manager.destroy(); - } -} diff --git a/app/src/main/java/org/linphone/compatibility/ApiTwentyFourPlus.java b/app/src/main/java/org/linphone/compatibility/ApiTwentyFourPlus.java deleted file mode 100644 index 788dd77b8..000000000 --- a/app/src/main/java/org/linphone/compatibility/ApiTwentyFourPlus.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.compatibility; - -import static org.linphone.compatibility.Compatibility.CHAT_NOTIFICATIONS_GROUP; -import static org.linphone.compatibility.Compatibility.INTENT_ANSWER_CALL_NOTIF_ACTION; -import static org.linphone.compatibility.Compatibility.INTENT_HANGUP_CALL_NOTIF_ACTION; -import static org.linphone.compatibility.Compatibility.INTENT_LOCAL_IDENTITY; -import static org.linphone.compatibility.Compatibility.INTENT_MARK_AS_READ_ACTION; -import static org.linphone.compatibility.Compatibility.INTENT_NOTIF_ID; -import static org.linphone.compatibility.Compatibility.INTENT_REMOTE_IDENTITY; -import static org.linphone.compatibility.Compatibility.INTENT_REPLY_NOTIF_ACTION; -import static org.linphone.compatibility.Compatibility.KEY_TEXT_REPLY; - -import android.annotation.TargetApi; -import android.app.Notification; -import android.app.PendingIntent; -import android.app.RemoteInput; -import android.content.ContentProviderClient; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.widget.RemoteViews; -import org.linphone.R; -import org.linphone.notifications.Notifiable; -import org.linphone.notifications.NotifiableMessage; -import org.linphone.notifications.NotificationBroadcastReceiver; - -@TargetApi(24) -class ApiTwentyFourPlus { - - public static Notification createRepliedNotification(Context context, String reply) { - return new Notification.Builder(context) - .setSmallIcon(R.drawable.topbar_chat_notification) - .setContentText( - context.getString(R.string.notification_replied_label).replace("%s", reply)) - .build(); - } - - public static Notification createMessageNotification( - Context context, Notifiable notif, Bitmap contactIcon, PendingIntent intent) { - - Notification.MessagingStyle style = new Notification.MessagingStyle(notif.getMyself()); - for (NotifiableMessage message : notif.getMessages()) { - Notification.MessagingStyle.Message msg = - new Notification.MessagingStyle.Message( - message.getMessage(), message.getTime(), message.getSender()); - if (message.getFilePath() != null) - msg.setData(message.getFileMime(), message.getFilePath()); - style.addMessage(msg); - } - if (notif.isGroup()) { - style.setConversationTitle(notif.getGroupTitle()); - } - - return new Notification.Builder(context) - .setSmallIcon(R.drawable.topbar_chat_notification) - .setAutoCancel(true) - .setContentIntent(intent) - .setDefaults( - Notification.DEFAULT_SOUND - | Notification.DEFAULT_VIBRATE - | Notification.DEFAULT_LIGHTS) - .setLargeIcon(contactIcon) - .setCategory(Notification.CATEGORY_MESSAGE) - .setGroup(CHAT_NOTIFICATIONS_GROUP) - .setVisibility(Notification.VISIBILITY_PRIVATE) - .setPriority(Notification.PRIORITY_HIGH) - .setNumber(notif.getMessages().size()) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .setColor(context.getColor(R.color.notification_led_color)) - .setStyle(style) - .addAction(Compatibility.getReplyMessageAction(context, notif)) - .addAction(Compatibility.getMarkMessageAsReadAction(context, notif)) - .build(); - } - - public static Notification createInCallNotification( - Context context, - int callId, - String msg, - int iconID, - Bitmap contactIcon, - String contactName, - PendingIntent intent) { - - return new Notification.Builder(context) - .setContentTitle(contactName) - .setContentText(msg) - .setSmallIcon(iconID) - .setAutoCancel(false) - .setContentIntent(intent) - .setLargeIcon(contactIcon) - .setCategory(Notification.CATEGORY_CALL) - .setVisibility(Notification.VISIBILITY_PUBLIC) - .setPriority(Notification.PRIORITY_HIGH) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .setOngoing(true) - .setColor(context.getColor(R.color.notification_led_color)) - .addAction(Compatibility.getCallDeclineAction(context, callId)) - .build(); - } - - public static Notification createIncomingCallNotification( - Context context, - int callId, - Bitmap contactIcon, - String contactName, - String sipUri, - PendingIntent intent) { - RemoteViews notificationLayoutHeadsUp = - new RemoteViews( - context.getPackageName(), R.layout.call_incoming_notification_heads_up); - notificationLayoutHeadsUp.setTextViewText(R.id.caller, contactName); - notificationLayoutHeadsUp.setTextViewText(R.id.sip_uri, sipUri); - notificationLayoutHeadsUp.setTextViewText( - R.id.incoming_call_info, context.getString(R.string.incall_notif_incoming)); - if (contactIcon != null) { - notificationLayoutHeadsUp.setImageViewBitmap(R.id.caller_picture, contactIcon); - } - - return new Notification.Builder(context) - .setStyle(new Notification.DecoratedCustomViewStyle()) - .setSmallIcon(R.drawable.topbar_call_notification) - .setContentTitle(contactName) - .setContentText(context.getString(R.string.incall_notif_incoming)) - .setContentIntent(intent) - .setCategory(Notification.CATEGORY_CALL) - .setVisibility(Notification.VISIBILITY_PUBLIC) - .setPriority(Notification.PRIORITY_HIGH) - .setWhen(System.currentTimeMillis()) - .setAutoCancel(false) - .setShowWhen(true) - .setOngoing(true) - .setColor(context.getColor(R.color.notification_led_color)) - .addAction(Compatibility.getCallDeclineAction(context, callId)) - .addAction(Compatibility.getCallAnswerAction(context, callId)) - .setCustomHeadsUpContentView(notificationLayoutHeadsUp) - .setFullScreenIntent(intent, true) - .build(); - } - - public static Notification.Action getReplyMessageAction(Context context, Notifiable notif) { - String replyLabel = context.getResources().getString(R.string.notification_reply_label); - RemoteInput remoteInput = - new RemoteInput.Builder(KEY_TEXT_REPLY).setLabel(replyLabel).build(); - - Intent replyIntent = new Intent(context, NotificationBroadcastReceiver.class); - replyIntent.setAction(INTENT_REPLY_NOTIF_ACTION); - replyIntent.putExtra(INTENT_NOTIF_ID, notif.getNotificationId()); - replyIntent.putExtra(INTENT_LOCAL_IDENTITY, notif.getLocalIdentity()); - replyIntent.putExtra(INTENT_REMOTE_IDENTITY, notif.getRemoteIdentity()); - - PendingIntent replyPendingIntent = - PendingIntent.getBroadcast( - context, - notif.getNotificationId(), - replyIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - - return new Notification.Action.Builder( - R.drawable.chat_send_over, - context.getString(R.string.notification_reply_label), - replyPendingIntent) - .addRemoteInput(remoteInput) - .setAllowGeneratedReplies(true) - .build(); - } - - public static Notification.Action getMarkMessageAsReadAction( - Context context, Notifiable notif) { - Intent markAsReadIntent = new Intent(context, NotificationBroadcastReceiver.class); - markAsReadIntent.setAction(INTENT_MARK_AS_READ_ACTION); - markAsReadIntent.putExtra(INTENT_NOTIF_ID, notif.getNotificationId()); - markAsReadIntent.putExtra(INTENT_LOCAL_IDENTITY, notif.getLocalIdentity()); - markAsReadIntent.putExtra(INTENT_REMOTE_IDENTITY, notif.getRemoteIdentity()); - - PendingIntent markAsReadPendingIntent = - PendingIntent.getBroadcast( - context, - notif.getNotificationId(), - markAsReadIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - - return new Notification.Action.Builder( - R.drawable.chat_send_over, - context.getString(R.string.notification_mark_as_read_label), - markAsReadPendingIntent) - .build(); - } - - public static Notification.Action getCallAnswerAction(Context context, int callId) { - Intent answerIntent = new Intent(context, NotificationBroadcastReceiver.class); - answerIntent.setAction(INTENT_ANSWER_CALL_NOTIF_ACTION); - answerIntent.putExtra(INTENT_NOTIF_ID, callId); - - PendingIntent answerPendingIntent = - PendingIntent.getBroadcast( - context, callId, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - return new Notification.Action.Builder( - R.drawable.call_audio_start, - context.getString(R.string.notification_call_answer_label), - answerPendingIntent) - .build(); - } - - public static Notification.Action getCallDeclineAction(Context context, int callId) { - Intent hangupIntent = new Intent(context, NotificationBroadcastReceiver.class); - hangupIntent.setAction(INTENT_HANGUP_CALL_NOTIF_ACTION); - hangupIntent.putExtra(INTENT_NOTIF_ID, callId); - - PendingIntent hangupPendingIntent = - PendingIntent.getBroadcast( - context, callId, hangupIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - return new Notification.Action.Builder( - R.drawable.call_hangup, - context.getString(R.string.notification_call_hangup_label), - hangupPendingIntent) - .build(); - } - - public static void closeContentProviderClient(ContentProviderClient client) { - client.close(); - } -} diff --git a/app/src/main/java/org/linphone/compatibility/ApiTwentyNinePlus.java b/app/src/main/java/org/linphone/compatibility/ApiTwentyNinePlus.java deleted file mode 100644 index 8cee33b34..000000000 --- a/app/src/main/java/org/linphone/compatibility/ApiTwentyNinePlus.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.compatibility; - -import static org.linphone.compatibility.Compatibility.INTENT_ANSWER_CALL_NOTIF_ACTION; -import static org.linphone.compatibility.Compatibility.INTENT_HANGUP_CALL_NOTIF_ACTION; -import static org.linphone.compatibility.Compatibility.INTENT_LOCAL_IDENTITY; -import static org.linphone.compatibility.Compatibility.INTENT_MARK_AS_READ_ACTION; -import static org.linphone.compatibility.Compatibility.INTENT_NOTIF_ID; -import static org.linphone.compatibility.Compatibility.INTENT_REMOTE_IDENTITY; -import static org.linphone.compatibility.Compatibility.INTENT_REPLY_NOTIF_ACTION; -import static org.linphone.compatibility.Compatibility.KEY_TEXT_REPLY; - -import android.annotation.TargetApi; -import android.app.Notification; -import android.app.PendingIntent; -import android.app.RemoteInput; -import android.content.Context; -import android.content.Intent; -import org.linphone.R; -import org.linphone.notifications.Notifiable; -import org.linphone.notifications.NotificationBroadcastReceiver; - -@TargetApi(29) -public class ApiTwentyNinePlus { - public static Notification.Action getReplyMessageAction(Context context, Notifiable notif) { - String replyLabel = context.getResources().getString(R.string.notification_reply_label); - RemoteInput remoteInput = - new RemoteInput.Builder(KEY_TEXT_REPLY).setLabel(replyLabel).build(); - - Intent replyIntent = new Intent(context, NotificationBroadcastReceiver.class); - replyIntent.setAction(INTENT_REPLY_NOTIF_ACTION); - replyIntent.putExtra(INTENT_NOTIF_ID, notif.getNotificationId()); - replyIntent.putExtra(INTENT_LOCAL_IDENTITY, notif.getLocalIdentity()); - replyIntent.putExtra(INTENT_REMOTE_IDENTITY, notif.getRemoteIdentity()); - - PendingIntent replyPendingIntent = - PendingIntent.getBroadcast( - context, - notif.getNotificationId(), - replyIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - - return new Notification.Action.Builder( - R.drawable.chat_send_over, - context.getString(R.string.notification_reply_label), - replyPendingIntent) - .addRemoteInput(remoteInput) - .setAllowGeneratedReplies(true) - .setSemanticAction(Notification.Action.SEMANTIC_ACTION_REPLY) - .build(); - } - - public static Notification.Action getMarkMessageAsReadAction( - Context context, Notifiable notif) { - Intent markAsReadIntent = new Intent(context, NotificationBroadcastReceiver.class); - markAsReadIntent.setAction(INTENT_MARK_AS_READ_ACTION); - markAsReadIntent.putExtra(INTENT_NOTIF_ID, notif.getNotificationId()); - markAsReadIntent.putExtra(INTENT_LOCAL_IDENTITY, notif.getLocalIdentity()); - markAsReadIntent.putExtra(INTENT_REMOTE_IDENTITY, notif.getRemoteIdentity()); - - PendingIntent markAsReadPendingIntent = - PendingIntent.getBroadcast( - context, - notif.getNotificationId(), - markAsReadIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - - return new Notification.Action.Builder( - R.drawable.chat_send_over, - context.getString(R.string.notification_mark_as_read_label), - markAsReadPendingIntent) - .setSemanticAction(Notification.Action.SEMANTIC_ACTION_MARK_AS_READ) - .build(); - } - - public static Notification.Action getCallAnswerAction(Context context, int callId) { - Intent answerIntent = new Intent(context, NotificationBroadcastReceiver.class); - answerIntent.setAction(INTENT_ANSWER_CALL_NOTIF_ACTION); - answerIntent.putExtra(INTENT_NOTIF_ID, callId); - - PendingIntent answerPendingIntent = - PendingIntent.getBroadcast( - context, callId, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - return new Notification.Action.Builder( - R.drawable.call_audio_start, - context.getString(R.string.notification_call_answer_label), - answerPendingIntent) - .build(); - } - - public static Notification.Action getCallDeclineAction(Context context, int callId) { - Intent hangupIntent = new Intent(context, NotificationBroadcastReceiver.class); - hangupIntent.setAction(INTENT_HANGUP_CALL_NOTIF_ACTION); - hangupIntent.putExtra(INTENT_NOTIF_ID, callId); - - PendingIntent hangupPendingIntent = - PendingIntent.getBroadcast( - context, callId, hangupIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - return new Notification.Action.Builder( - R.drawable.call_hangup, - context.getString(R.string.notification_call_hangup_label), - hangupPendingIntent) - .build(); - } -} diff --git a/app/src/main/java/org/linphone/compatibility/ApiTwentyOnePlus.java b/app/src/main/java/org/linphone/compatibility/ApiTwentyOnePlus.java deleted file mode 100644 index f72a344dd..000000000 --- a/app/src/main/java/org/linphone/compatibility/ApiTwentyOnePlus.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.compatibility; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.Notification; -import android.app.PendingIntent; -import android.content.ContentProviderClient; -import android.content.Context; -import android.graphics.Bitmap; -import android.os.Vibrator; -import android.view.WindowManager; -import androidx.core.content.ContextCompat; -import org.linphone.R; - -@TargetApi(21) -class ApiTwentyOnePlus { - - public static Notification createMessageNotification( - Context context, - int msgCount, - String msgSender, - String msg, - Bitmap contactIcon, - PendingIntent intent) { - String title; - if (msgCount == 1) { - title = msgSender; - } else { - title = - context.getString(R.string.unread_messages) - .replace("%i", String.valueOf(msgCount)); - } - - return new Notification.Builder(context) - .setContentTitle(title) - .setContentText(msg) - .setSmallIcon(R.drawable.topbar_chat_notification) - .setAutoCancel(true) - .setContentIntent(intent) - .setDefaults( - Notification.DEFAULT_SOUND - | Notification.DEFAULT_VIBRATE - | Notification.DEFAULT_LIGHTS) - .setLargeIcon(contactIcon) - .setLights( - ContextCompat.getColor(context, R.color.notification_led_color), - context.getResources().getInteger(R.integer.notification_ms_on), - context.getResources().getInteger(R.integer.notification_ms_off)) - .setCategory(Notification.CATEGORY_MESSAGE) - .setVisibility(Notification.VISIBILITY_PRIVATE) - .setPriority(Notification.PRIORITY_HIGH) - .setNumber(msgCount) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .build(); - } - - public static Notification createInCallNotification( - Context context, - String msg, - int iconID, - Bitmap contactIcon, - String contactName, - PendingIntent intent) { - - return new Notification.Builder(context) - .setContentTitle(contactName) - .setContentText(msg) - .setSmallIcon(iconID) - .setAutoCancel(false) - .setContentIntent(intent) - .setLargeIcon(contactIcon) - .setCategory(Notification.CATEGORY_CALL) - .setVisibility(Notification.VISIBILITY_PUBLIC) - .setPriority(Notification.PRIORITY_HIGH) - .setOngoing(true) - .setLights( - ContextCompat.getColor(context, R.color.notification_led_color), - context.getResources().getInteger(R.integer.notification_ms_on), - context.getResources().getInteger(R.integer.notification_ms_off)) - .setShowWhen(true) - .build(); - } - - public static Notification createIncomingCallNotification( - Context context, - Bitmap contactIcon, - String contactName, - String sipUri, - PendingIntent intent) { - - return new Notification.Builder(context) - .setContentTitle(contactName) - .setContentText(context.getString(R.string.incall_notif_incoming)) - .setSmallIcon(R.drawable.topbar_call_notification) - .setAutoCancel(false) - .setContentIntent(intent) - .setLargeIcon(contactIcon) - .setCategory(Notification.CATEGORY_CALL) - .setVisibility(Notification.VISIBILITY_PUBLIC) - .setPriority(Notification.PRIORITY_HIGH) - .setOngoing(true) - .setLights( - ContextCompat.getColor(context, R.color.notification_led_color), - context.getResources().getInteger(R.integer.notification_ms_on), - context.getResources().getInteger(R.integer.notification_ms_off)) - .setShowWhen(true) - .setFullScreenIntent(intent, true) - .build(); - } - - public static Notification createNotification( - Context context, - String title, - String message, - int icon, - int level, - Bitmap largeIcon, - PendingIntent intent, - int priority, - boolean ongoing) { - Notification notif; - - if (largeIcon != null) { - notif = - new Notification.Builder(context) - .setContentTitle(title) - .setContentText(message) - .setSmallIcon(icon, level) - .setLargeIcon(largeIcon) - .setContentIntent(intent) - .setCategory(Notification.CATEGORY_SERVICE) - .setVisibility(Notification.VISIBILITY_SECRET) - .setLights( - ContextCompat.getColor(context, R.color.notification_led_color), - context.getResources().getInteger(R.integer.notification_ms_on), - context.getResources() - .getInteger(R.integer.notification_ms_off)) - .setWhen(System.currentTimeMillis()) - .setPriority(priority) - .setShowWhen(true) - .setOngoing(ongoing) - .build(); - } else { - notif = - new Notification.Builder(context) - .setContentTitle(title) - .setContentText(message) - .setSmallIcon(icon, level) - .setContentIntent(intent) - .setCategory(Notification.CATEGORY_SERVICE) - .setVisibility(Notification.VISIBILITY_SECRET) - .setLights( - ContextCompat.getColor(context, R.color.notification_led_color), - context.getResources().getInteger(R.integer.notification_ms_on), - context.getResources() - .getInteger(R.integer.notification_ms_off)) - .setPriority(priority) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .build(); - } - - return notif; - } - - public static Notification createMissedCallNotification( - Context context, String title, String text, PendingIntent intent, int count) { - - return new Notification.Builder(context) - .setContentTitle(title) - .setContentText(text) - .setSmallIcon(R.drawable.call_status_missed) - .setAutoCancel(true) - .setContentIntent(intent) - .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE) - .setCategory(Notification.CATEGORY_EVENT) - .setVisibility(Notification.VISIBILITY_PRIVATE) - .setLights( - ContextCompat.getColor(context, R.color.notification_led_color), - context.getResources().getInteger(R.integer.notification_ms_on), - context.getResources().getInteger(R.integer.notification_ms_off)) - .setPriority(Notification.PRIORITY_HIGH) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .setNumber(count) - .build(); - } - - public static Notification createSimpleNotification( - Context context, String title, String text, PendingIntent intent) { - - return new Notification.Builder(context) - .setContentTitle(title) - .setContentText(text) - .setSmallIcon(R.drawable.linphone_logo) - .setAutoCancel(true) - .setContentIntent(intent) - .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE) - .setCategory(Notification.CATEGORY_MESSAGE) - .setVisibility(Notification.VISIBILITY_PRIVATE) - .setLights( - ContextCompat.getColor(context, R.color.notification_led_color), - context.getResources().getInteger(R.integer.notification_ms_on), - context.getResources().getInteger(R.integer.notification_ms_off)) - .setWhen(System.currentTimeMillis()) - .setPriority(Notification.PRIORITY_HIGH) - .setShowWhen(true) - .build(); - } - - public static void closeContentProviderClient(ContentProviderClient client) { - client.release(); - } - - public static void setShowWhenLocked(Activity activity, boolean enable) { - if (enable) { - activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - } - } - - public static void setTurnScreenOn(Activity activity, boolean enable) { - if (enable) { - activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); - } - } - - public static void vibrate(Vibrator vibrator) { - long[] pattern = {0, 1000, 1000}; - vibrator.vibrate(pattern, 1); - } -} diff --git a/app/src/main/java/org/linphone/compatibility/ApiTwentySixPlus.java b/app/src/main/java/org/linphone/compatibility/ApiTwentySixPlus.java deleted file mode 100644 index 8d58f9e71..000000000 --- a/app/src/main/java/org/linphone/compatibility/ApiTwentySixPlus.java +++ /dev/null @@ -1,348 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.compatibility; - -import static org.linphone.compatibility.Compatibility.CHAT_NOTIFICATIONS_GROUP; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.FragmentTransaction; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.bluetooth.BluetoothAdapter; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.media.AudioAttributes; -import android.os.Build; -import android.os.VibrationEffect; -import android.os.Vibrator; -import android.provider.Settings; -import android.widget.RemoteViews; -import org.linphone.R; -import org.linphone.core.tools.Log; -import org.linphone.notifications.Notifiable; -import org.linphone.notifications.NotifiableMessage; - -@TargetApi(26) -class ApiTwentySixPlus { - public static String getDeviceName(Context context) { - String name = - Settings.Global.getString( - context.getContentResolver(), Settings.Global.DEVICE_NAME); - if (name == null) { - BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - if (adapter != null) { - name = adapter.getName(); - } - } - if (name == null) { - name = Settings.Secure.getString(context.getContentResolver(), "bluetooth_name"); - } - if (name == null) { - name = Build.MANUFACTURER + " " + Build.MODEL; - } - return name; - } - - public static Notification createRepliedNotification(Context context, String reply) { - return new Notification.Builder( - context, context.getString(R.string.notification_channel_id)) - .setSmallIcon(R.drawable.topbar_chat_notification) - .setContentText( - context.getString(R.string.notification_replied_label).replace("%s", reply)) - .build(); - } - - public static void createServiceChannel(Context context) { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - // Create service/call notification channel - String id = context.getString(R.string.notification_service_channel_id); - CharSequence name = context.getString(R.string.content_title_notification_service); - String description = context.getString(R.string.content_title_notification_service); - NotificationChannel channel = - new NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW); - channel.setDescription(description); - channel.enableVibration(false); - channel.enableLights(false); - channel.setShowBadge(false); - notificationManager.createNotificationChannel(channel); - } - - public static void createMessageChannel(Context context) { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - // Create message notification channel - String id = context.getString(R.string.notification_channel_id); - String name = context.getString(R.string.content_title_notification); - String description = context.getString(R.string.content_title_notification); - NotificationChannel channel = - new NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH); - channel.setDescription(description); - channel.setLightColor(context.getColor(R.color.notification_led_color)); - channel.enableLights(true); - channel.enableVibration(true); - channel.setShowBadge(true); - notificationManager.createNotificationChannel(channel); - } - - public static void createMissedCallChannel(Context context) { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - // Create missed call notification channel - String id = context.getString(R.string.notification_missed_call_channel_id); - String name = context.getString(R.string.content_title_notification_missed_call); - String description = context.getString(R.string.content_title_notification_missed_call); - NotificationChannel channel = - new NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW); - channel.setDescription(description); - channel.setLightColor(context.getColor(R.color.notification_led_color)); - channel.enableLights(true); - channel.enableVibration(true); - channel.setShowBadge(true); - notificationManager.createNotificationChannel(channel); - } - - public static Notification createMessageNotification( - Context context, Notifiable notif, Bitmap contactIcon, PendingIntent intent) { - - Notification.MessagingStyle style = new Notification.MessagingStyle(notif.getMyself()); - for (NotifiableMessage message : notif.getMessages()) { - Notification.MessagingStyle.Message msg = - new Notification.MessagingStyle.Message( - message.getMessage(), message.getTime(), message.getSender()); - if (message.getFilePath() != null) - msg.setData(message.getFileMime(), message.getFilePath()); - style.addMessage(msg); - } - if (notif.isGroup()) { - style.setConversationTitle(notif.getGroupTitle()); - } - - return new Notification.Builder( - context, context.getString(R.string.notification_channel_id)) - .setSmallIcon(R.drawable.topbar_chat_notification) - .setAutoCancel(true) - .setContentIntent(intent) - .setDefaults( - Notification.DEFAULT_SOUND - | Notification.DEFAULT_VIBRATE - | Notification.DEFAULT_LIGHTS) - .setLargeIcon(contactIcon) - .setCategory(Notification.CATEGORY_MESSAGE) - .setGroup(CHAT_NOTIFICATIONS_GROUP) - .setVisibility(Notification.VISIBILITY_PRIVATE) - .setPriority(Notification.PRIORITY_HIGH) - .setNumber(notif.getMessages().size()) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .setColor(context.getColor(R.color.notification_led_color)) - .setStyle(style) - .addAction(Compatibility.getReplyMessageAction(context, notif)) - .addAction(Compatibility.getMarkMessageAsReadAction(context, notif)) - .build(); - } - - public static Notification createInCallNotification( - Context context, - int callId, - String msg, - int iconID, - Bitmap contactIcon, - String contactName, - PendingIntent intent) { - - return new Notification.Builder( - context, context.getString(R.string.notification_service_channel_id)) - .setContentTitle(contactName) - .setContentText(msg) - .setSmallIcon(iconID) - .setAutoCancel(false) - .setContentIntent(intent) - .setLargeIcon(contactIcon) - .setCategory(Notification.CATEGORY_CALL) - .setVisibility(Notification.VISIBILITY_PUBLIC) - .setPriority(Notification.PRIORITY_LOW) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .setOngoing(true) - .setColor(context.getColor(R.color.notification_led_color)) - .addAction(Compatibility.getCallDeclineAction(context, callId)) - .build(); - } - - public static Notification createIncomingCallNotification( - Context context, - int callId, - Bitmap contactIcon, - String contactName, - String sipUri, - PendingIntent intent) { - RemoteViews notificationLayoutHeadsUp = - new RemoteViews( - context.getPackageName(), R.layout.call_incoming_notification_heads_up); - notificationLayoutHeadsUp.setTextViewText(R.id.caller, contactName); - notificationLayoutHeadsUp.setTextViewText(R.id.sip_uri, sipUri); - notificationLayoutHeadsUp.setTextViewText( - R.id.incoming_call_info, context.getString(R.string.incall_notif_incoming)); - if (contactIcon != null) { - notificationLayoutHeadsUp.setImageViewBitmap(R.id.caller_picture, contactIcon); - } - - return new Notification.Builder( - context, context.getString(R.string.notification_channel_id)) - .setStyle(new Notification.DecoratedCustomViewStyle()) - .setSmallIcon(R.drawable.topbar_call_notification) - .setContentTitle(contactName) - .setContentText(context.getString(R.string.incall_notif_incoming)) - .setContentIntent(intent) - .setCategory(Notification.CATEGORY_CALL) - .setVisibility(Notification.VISIBILITY_PUBLIC) - .setPriority(Notification.PRIORITY_HIGH) - .setWhen(System.currentTimeMillis()) - .setAutoCancel(false) - .setShowWhen(true) - .setOngoing(true) - .setColor(context.getColor(R.color.notification_led_color)) - .setFullScreenIntent(intent, true) - .addAction(Compatibility.getCallDeclineAction(context, callId)) - .addAction(Compatibility.getCallAnswerAction(context, callId)) - .setCustomHeadsUpContentView(notificationLayoutHeadsUp) - .build(); - } - - public static Notification createNotification( - Context context, - String title, - String message, - int icon, - int level, - Bitmap largeIcon, - PendingIntent intent, - int priority, - boolean ongoing) { - - if (largeIcon != null) { - return new Notification.Builder( - context, context.getString(R.string.notification_service_channel_id)) - .setContentTitle(title) - .setContentText(message) - .setSmallIcon(icon, level) - .setLargeIcon(largeIcon) - .setContentIntent(intent) - .setCategory(Notification.CATEGORY_SERVICE) - .setVisibility(Notification.VISIBILITY_SECRET) - .setPriority(priority) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .setOngoing(ongoing) - .setColor(context.getColor(R.color.notification_led_color)) - .build(); - } else { - return new Notification.Builder( - context, context.getString(R.string.notification_service_channel_id)) - .setContentTitle(title) - .setContentText(message) - .setSmallIcon(icon, level) - .setContentIntent(intent) - .setCategory(Notification.CATEGORY_SERVICE) - .setVisibility(Notification.VISIBILITY_SECRET) - .setPriority(priority) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .setColor(context.getColor(R.color.notification_led_color)) - .build(); - } - } - - public static Notification createMissedCallNotification( - Context context, String title, String text, PendingIntent intent, int count) { - return new Notification.Builder( - context, context.getString(R.string.notification_missed_call_channel_id)) - .setContentTitle(title) - .setContentText(text) - .setSmallIcon(R.drawable.call_status_missed) - .setAutoCancel(true) - .setContentIntent(intent) - .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE) - // .setCategory(Notification.CATEGORY_EVENT) No one really matches "missed call" - .setVisibility(Notification.VISIBILITY_PRIVATE) - .setPriority(Notification.PRIORITY_HIGH) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .setNumber(count) - .setColor(context.getColor(R.color.notification_led_color)) - .build(); - } - - public static Notification createSimpleNotification( - Context context, String title, String text, PendingIntent intent) { - return new Notification.Builder( - context, context.getString(R.string.notification_channel_id)) - .setContentTitle(title) - .setContentText(text) - .setSmallIcon(R.drawable.linphone_logo) - .setAutoCancel(true) - .setContentIntent(intent) - .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE) - .setCategory(Notification.CATEGORY_MESSAGE) - .setVisibility(Notification.VISIBILITY_PRIVATE) - .setPriority(Notification.PRIORITY_HIGH) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .setColorized(true) - .setColor(context.getColor(R.color.notification_led_color)) - .build(); - } - - public static void startService(Context context, Intent intent) { - context.startForegroundService(intent); - } - - public static void setFragmentTransactionReorderingAllowed( - FragmentTransaction transaction, boolean allowed) { - transaction.setReorderingAllowed(allowed); - } - - public static void enterPipMode(Activity activity) { - boolean supportsPip = - activity.getPackageManager() - .hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); - Log.i("[Call] Is picture in picture supported: " + supportsPip); - if (supportsPip) { - activity.enterPictureInPictureMode(); - } - } - - public static void vibrate(Vibrator vibrator) { - long[] timings = {0, 1000, 1000}; - int[] amplitudes = {0, VibrationEffect.DEFAULT_AMPLITUDE, 0}; - VibrationEffect effect = VibrationEffect.createWaveform(timings, amplitudes, 1); - AudioAttributes audioAttrs = - new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) - .build(); - vibrator.vibrate(effect, audioAttrs); - } -} diff --git a/app/src/main/java/org/linphone/compatibility/ApiTwentyThreePlus.java b/app/src/main/java/org/linphone/compatibility/ApiTwentyThreePlus.java deleted file mode 100644 index cb0c0d24b..000000000 --- a/app/src/main/java/org/linphone/compatibility/ApiTwentyThreePlus.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.compatibility; - -import android.annotation.TargetApi; -import android.app.NotificationManager; -import android.content.Context; -import android.os.PowerManager; -import android.service.notification.StatusBarNotification; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.core.Address; -import org.linphone.core.tools.Log; - -@TargetApi(23) -class ApiTwentyThreePlus { - - public static boolean isAppIdleMode(Context context) { - return ((PowerManager) context.getSystemService(Context.POWER_SERVICE)).isDeviceIdleMode(); - } - - public static boolean isDoNotDisturbSettingsAccessGranted(Context context) { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - return notificationManager.isNotificationPolicyAccessGranted(); - } - - public static boolean isDoNotDisturbPolicyAllowingRinging( - Context context, Address remoteAddress) { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - int filter = notificationManager.getCurrentInterruptionFilter(); - if (filter == NotificationManager.INTERRUPTION_FILTER_PRIORITY) { - Log.w("[Audio Manager] Priority interruption filter detected"); - boolean accessGranted = notificationManager.isNotificationPolicyAccessGranted(); - if (!accessGranted) { - Log.e( - "[Audio Manager] Access to policy is denied, let's assume it is not safe for ringing"); - return false; - } - - NotificationManager.Policy policy = notificationManager.getNotificationPolicy(); - int callPolicy = policy.priorityCallSenders; - if (callPolicy == NotificationManager.Policy.PRIORITY_SENDERS_ANY) { - Log.i("[Audio Manager] Priority for calls is Any, we can ring"); - } else { - if (remoteAddress == null) { - Log.e( - "[Audio Manager] Remote address is null, let's assume it is not safe for ringing"); - return false; - } - - LinphoneContact contact = - ContactsManager.getInstance().findContactFromAddress(remoteAddress); - if (callPolicy == NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS) { - Log.i("[Audio Manager] Priority for calls is Contacts, let's check"); - if (contact == null) { - Log.w( - "[Audio Manager] Couldn't find a contact for address " - + remoteAddress.asStringUriOnly()); - return false; - } else { - Log.i( - "[Audio Manager] Contact found for address " - + remoteAddress.asStringUriOnly() - + ", we can ring"); - } - } else if (callPolicy == NotificationManager.Policy.PRIORITY_SENDERS_STARRED) { - Log.i("[Audio Manager] Priority for calls is Starred Contacts, let's check"); - if (contact == null) { - Log.w( - "[Audio Manager] Couldn't find a contact for address " - + remoteAddress.asStringUriOnly()); - return false; - } else if (!contact.isFavourite()) { - Log.w( - "[Audio Manager] Contact found for address " - + remoteAddress.asStringUriOnly() - + ", but it isn't starred"); - return false; - } else { - Log.i( - "[Audio Manager] Starred contact found for address " - + remoteAddress.asStringUriOnly() - + ", we can ring"); - } - } - } - } else if (filter == NotificationManager.INTERRUPTION_FILTER_ALARMS) { - Log.w("[Audio Manager] Alarms interruption filter detected"); - return false; - } else { - Log.i("[Audio Manager] Interruption filter is " + filter + ", we can ring"); - } - - return true; - } - - public static StatusBarNotification[] getActiveNotifications(NotificationManager manager) { - return manager.getActiveNotifications(); - } -} diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.java b/app/src/main/java/org/linphone/compatibility/Compatibility.java deleted file mode 100644 index 9b421d576..000000000 --- a/app/src/main/java/org/linphone/compatibility/Compatibility.java +++ /dev/null @@ -1,338 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.compatibility; - -import android.app.Activity; -import android.app.FragmentTransaction; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.bluetooth.BluetoothAdapter; -import android.content.ContentProviderClient; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.os.Build; -import android.os.Vibrator; -import android.provider.Settings; -import android.service.notification.StatusBarNotification; -import org.linphone.core.Address; -import org.linphone.mediastream.Version; -import org.linphone.notifications.Notifiable; - -public class Compatibility { - public static final String CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP"; - public static final String KEY_TEXT_REPLY = "key_text_reply"; - public static final String INTENT_NOTIF_ID = "NOTIFICATION_ID"; - public static final String INTENT_REPLY_NOTIF_ACTION = "org.linphone.REPLY_ACTION"; - public static final String INTENT_HANGUP_CALL_NOTIF_ACTION = "org.linphone.HANGUP_CALL_ACTION"; - public static final String INTENT_ANSWER_CALL_NOTIF_ACTION = "org.linphone.ANSWER_CALL_ACTION"; - public static final String INTENT_LOCAL_IDENTITY = "LOCAL_IDENTITY"; - public static final String INTENT_REMOTE_IDENTITY = "REMOTE_IDENTITY"; - public static final String INTENT_MARK_AS_READ_ACTION = "org.linphone.MARK_AS_READ_ACTION"; - - public static String getDeviceName(Context context) { - if (Version.sdkAboveOrEqual(25)) { - return ApiTwentySixPlus.getDeviceName(context); - } - - String name = null; - BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - if (adapter != null) { - name = adapter.getName(); - } - - if (name == null) { - name = Settings.Secure.getString(context.getContentResolver(), "bluetooth_name"); - } - if (name == null) { - name = Build.MANUFACTURER + " " + Build.MODEL; - } - return name; - } - - public static void createNotificationChannels(Context context) { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - ApiTwentySixPlus.createServiceChannel(context); - ApiTwentySixPlus.createMessageChannel(context); - ApiTwentySixPlus.createMissedCallChannel(context); - } - } - - public static Notification createSimpleNotification( - Context context, String title, String text, PendingIntent intent) { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - return ApiTwentySixPlus.createSimpleNotification(context, title, text, intent); - } - return ApiTwentyOnePlus.createSimpleNotification(context, title, text, intent); - } - - public static Notification createMessageNotification( - Context context, - Notifiable notif, - String msgSender, - String msg, - Bitmap contactIcon, - PendingIntent intent) { - if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) { - return ApiTwentyEightPlus.createMessageNotification( - context, notif, contactIcon, intent); - } else if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - return ApiTwentySixPlus.createMessageNotification(context, notif, contactIcon, intent); - } else if (Version.sdkAboveOrEqual(Version.API24_NOUGAT_70)) { - return ApiTwentyFourPlus.createMessageNotification(context, notif, contactIcon, intent); - } - return ApiTwentyOnePlus.createMessageNotification( - context, notif.getMessages().size(), msgSender, msg, contactIcon, intent); - } - - public static Notification createRepliedNotification(Context context, String reply) { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - return ApiTwentySixPlus.createRepliedNotification(context, reply); - } else if (Version.sdkAboveOrEqual(Version.API24_NOUGAT_70)) { - return ApiTwentyFourPlus.createRepliedNotification(context, reply); - } - return null; - } - - public static Notification createMissedCallNotification( - Context context, String title, String text, PendingIntent intent, int count) { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - return ApiTwentySixPlus.createMissedCallNotification( - context, title, text, intent, count); - } - return ApiTwentyOnePlus.createMissedCallNotification(context, title, text, intent, count); - } - - public static Notification createInCallNotification( - Context context, - int callId, - String msg, - int iconID, - Bitmap contactIcon, - String contactName, - PendingIntent intent) { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - return ApiTwentySixPlus.createInCallNotification( - context, callId, msg, iconID, contactIcon, contactName, intent); - } else if (Version.sdkAboveOrEqual(Version.API24_NOUGAT_70)) { - return ApiTwentyFourPlus.createInCallNotification( - context, callId, msg, iconID, contactIcon, contactName, intent); - } - return ApiTwentyOnePlus.createInCallNotification( - context, msg, iconID, contactIcon, contactName, intent); - } - - public static Notification createIncomingCallNotification( - Context context, - int callId, - Bitmap contactIcon, - String contactName, - String sipUri, - PendingIntent intent) { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - return ApiTwentySixPlus.createIncomingCallNotification( - context, callId, contactIcon, contactName, sipUri, intent); - } else if (Version.sdkAboveOrEqual(Version.API24_NOUGAT_70)) { - return ApiTwentyFourPlus.createIncomingCallNotification( - context, callId, contactIcon, contactName, sipUri, intent); - } - return ApiTwentyOnePlus.createIncomingCallNotification( - context, contactIcon, contactName, sipUri, intent); - } - - public static Notification createNotification( - Context context, - String title, - String message, - int icon, - int iconLevel, - Bitmap largeIcon, - PendingIntent intent, - int priority, - boolean ongoing) { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - return ApiTwentySixPlus.createNotification( - context, title, message, icon, iconLevel, largeIcon, intent, priority, ongoing); - } - return ApiTwentyOnePlus.createNotification( - context, title, message, icon, iconLevel, largeIcon, intent, priority, ongoing); - } - - public static boolean canDrawOverlays(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return Settings.canDrawOverlays(context); - } - return true; - } - - public static void startService(Context context, Intent intent) { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - ApiTwentySixPlus.startService(context, intent); - } else { - context.startService(intent); - } - } - - public static void setFragmentTransactionReorderingAllowed( - FragmentTransaction transaction, boolean allowed) { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - ApiTwentySixPlus.setFragmentTransactionReorderingAllowed(transaction, allowed); - } - } - - public static void closeContentProviderClient(ContentProviderClient client) { - if (Version.sdkAboveOrEqual(Version.API24_NOUGAT_70)) { - ApiTwentyFourPlus.closeContentProviderClient(client); - } else { - ApiTwentyOnePlus.closeContentProviderClient(client); - } - } - - public static boolean isAppUserRestricted(Context context) { - if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) { - return ApiTwentyEightPlus.isAppUserRestricted(context); - } - return false; - } - - public static boolean isAppIdleMode(Context context) { - if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { - return ApiTwentyThreePlus.isAppIdleMode(context); - } - return false; - } - - public static int getAppStandbyBucket(Context context) { - if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) { - return ApiTwentyEightPlus.getAppStandbyBucket(context); - } - return 0; - } - - public static String getAppStandbyBucketNameFromValue(int bucket) { - if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) { - return ApiTwentyEightPlus.getAppStandbyBucketNameFromValue(bucket); - } - return null; - } - - public static void setShowWhenLocked(Activity activity, boolean enable) { - if (Version.sdkStrictlyBelow(Version.API27_OREO_81)) { - ApiTwentyOnePlus.setShowWhenLocked(activity, enable); - } - } - - public static void setTurnScreenOn(Activity activity, boolean enable) { - if (Version.sdkStrictlyBelow(Version.API27_OREO_81)) { - ApiTwentyOnePlus.setTurnScreenOn(activity, enable); - } - } - - public static boolean isDoNotDisturbSettingsAccessGranted(Context context) { - if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { - return ApiTwentyThreePlus.isDoNotDisturbSettingsAccessGranted(context); - } - return true; - } - - public static boolean isDoNotDisturbPolicyAllowingRinging( - Context context, Address remoteAddress) { - if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { - return ApiTwentyThreePlus.isDoNotDisturbPolicyAllowingRinging(context, remoteAddress); - } - return true; - } - - public static void createChatShortcuts(Context context) { - if (Version.sdkAboveOrEqual(Version.API25_NOUGAT_71)) { - ApiTwentyFivePlus.createChatShortcuts(context); - } - } - - public static void removeChatShortcuts(Context context) { - if (Version.sdkAboveOrEqual(Version.API25_NOUGAT_71)) { - ApiTwentyFivePlus.removeChatShortcuts(context); - } - } - - public static void enterPipMode(Activity activity) { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - ApiTwentySixPlus.enterPipMode(activity); - } - } - - public static Notification.Action getReplyMessageAction(Context context, Notifiable notif) { - if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { - return ApiTwentyNinePlus.getReplyMessageAction(context, notif); - } else if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) { - return ApiTwentyEightPlus.getReplyMessageAction(context, notif); - } else if (Version.sdkAboveOrEqual(Version.API24_NOUGAT_70)) { - return ApiTwentyFourPlus.getReplyMessageAction(context, notif); - } - return null; - } - - public static Notification.Action getMarkMessageAsReadAction( - Context context, Notifiable notif) { - if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { - return ApiTwentyNinePlus.getMarkMessageAsReadAction(context, notif); - } else if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) { - return ApiTwentyEightPlus.getMarkMessageAsReadAction(context, notif); - } else if (Version.sdkAboveOrEqual(Version.API24_NOUGAT_70)) { - return ApiTwentyFourPlus.getMarkMessageAsReadAction(context, notif); - } - return null; - } - - public static Notification.Action getCallAnswerAction(Context context, int callId) { - if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { - return ApiTwentyNinePlus.getCallAnswerAction(context, callId); - } else if (Version.sdkAboveOrEqual(Version.API24_NOUGAT_70)) { - return ApiTwentyFourPlus.getCallAnswerAction(context, callId); - } - return null; - } - - public static Notification.Action getCallDeclineAction(Context context, int callId) { - if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { - return ApiTwentyNinePlus.getCallDeclineAction(context, callId); - } else if (Version.sdkAboveOrEqual(Version.API24_NOUGAT_70)) { - return ApiTwentyFourPlus.getCallDeclineAction(context, callId); - } - return null; - } - - public static StatusBarNotification[] getActiveNotifications(NotificationManager manager) { - if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { - return ApiTwentyThreePlus.getActiveNotifications(manager); - } - - return new StatusBarNotification[0]; - } - - public static void vibrate(Vibrator vibrator) { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - ApiTwentySixPlus.vibrate(vibrator); - } else { - ApiTwentyOnePlus.vibrate(vibrator); - } - } -} diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Compatibility.kt new file mode 100644 index 000000000..8f115069d --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/Compatibility.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.compatibility + +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.os.Vibrator +import android.view.WindowManager +import androidx.core.app.NotificationManagerCompat +import org.linphone.core.Content +import org.linphone.mediastream.Version + +class Compatibility { + companion object { + fun hasPermission(context: Context, permission: String): Boolean { + return when (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { + true -> Api23Compatibility.hasPermission(context, permission) + else -> context.packageManager.checkPermission(permission, context.packageName) == PackageManager.PERMISSION_GRANTED + } + } + + fun getDeviceName(context: Context): String { + return when (Version.sdkAboveOrEqual(Version.API25_NOUGAT_71)) { + true -> Api25Compatibility.getDeviceName(context) + else -> Api21Compatibility.getDeviceName(context) + } + } + + /* Notifications */ + + fun createNotificationChannels( + context: Context, + notificationManager: NotificationManagerCompat + ) { + if (Version.sdkAboveOrEqual(Version.API26_O_80)) { + Api26Compatibility.createServiceChannel(context, notificationManager) + Api26Compatibility.createIncomingCallChannel(context, notificationManager) + Api26Compatibility.createMessageChannel(context, notificationManager) + } + } + + fun getOverlayType(): Int { + if (Version.sdkAboveOrEqual(Version.API26_O_80)) { + return Api26Compatibility.getOverlayType() + } + return WindowManager.LayoutParams.TYPE_PHONE + } + + /* Call */ + + fun canDrawOverlay(context: Context): Boolean { + if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { + return Api23Compatibility.canDrawOverlay(context) + } + return false + } + + fun enterPipMode(activity: Activity) { + if (Version.sdkAboveOrEqual(Version.API26_O_80)) { + Api26Compatibility.enterPipMode(activity) + } + } + + fun vibrate(vibrator: Vibrator) { + if (Version.sdkAboveOrEqual(Version.API26_O_80)) { + Api26Compatibility.vibrate(vibrator) + } else { + Api21Compatibility.vibrate(vibrator) + } + } + + /* Contacts */ + + fun createShortcutsToContacts(context: Context) { + if (Version.sdkAboveOrEqual(Version.API25_NOUGAT_71)) { + Api25Compatibility.createShortcutsToContacts(context) + } + } + + fun removeShortcutsToContacts(context: Context) { + if (Version.sdkAboveOrEqual(Version.API25_NOUGAT_71)) { + Api25Compatibility.removeShortcutsToContacts(context) + } + } + + /* Chat */ + + fun addImageToMediaStore(context: Context, content: Content): Boolean { + if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { + return Api29Compatibility.addImageToMediaStore(context, content) + } + return Api21Compatibility.addImageToMediaStore(context, content) + } + + fun addVideoToMediaStore(context: Context, content: Content): Boolean { + if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { + return Api29Compatibility.addVideoToMediaStore(context, content) + } + return Api21Compatibility.addVideoToMediaStore(context, content) + } + + fun addAudioToMediaStore(context: Context, content: Content): Boolean { + if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { + return Api29Compatibility.addAudioToMediaStore(context, content) + } + return Api21Compatibility.addAudioToMediaStore(context, content) + } + } +} diff --git a/app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureDetector.java b/app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureDetector.java deleted file mode 100644 index f9712ee41..000000000 --- a/app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureDetector.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.compatibility; - -import android.content.Context; -import android.view.MotionEvent; -import android.view.ScaleGestureDetector; - -public class CompatibilityScaleGestureDetector - extends ScaleGestureDetector.SimpleOnScaleGestureListener { - private ScaleGestureDetector detector; - private CompatibilityScaleGestureListener listener; - - public CompatibilityScaleGestureDetector(Context context) { - detector = new ScaleGestureDetector(context, this); - } - - public void setOnScaleListener(CompatibilityScaleGestureListener newListener) { - listener = newListener; - } - - public void onTouchEvent(MotionEvent event) { - detector.onTouchEvent(event); - } - - @Override - public boolean onScale(ScaleGestureDetector detector) { - if (listener == null) { - return false; - } - - return listener.onScale(this); - } - - public float getScaleFactor() { - return detector.getScaleFactor(); - } - - public void destroy() { - listener = null; - detector = null; - } -} diff --git a/app/src/main/java/org/linphone/contact/AsyncContactsLoader.kt b/app/src/main/java/org/linphone/contact/AsyncContactsLoader.kt new file mode 100644 index 000000000..d10c74405 --- /dev/null +++ b/app/src/main/java/org/linphone/contact/AsyncContactsLoader.kt @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.contact + +import android.content.Context +import android.database.Cursor +import android.os.AsyncTask +import android.provider.ContactsContract +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.LinphoneUtils +import org.linphone.utils.PermissionHelper + +class AsyncContactsLoader(private val context: Context) : + AsyncTask() { + companion object { + val projection = arrayOf( + ContactsContract.Data.CONTACT_ID, + ContactsContract.Contacts.DISPLAY_NAME_PRIMARY, + ContactsContract.Data.MIMETYPE, + ContactsContract.Contacts.STARRED, + ContactsContract.Contacts.LOOKUP_KEY, + "data1", // Company, Phone or SIP Address + "data2", // ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME + "data3", // ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME + "data4" + ) + } + + override fun onPreExecute() { + if (isCancelled) return + Log.i("[Contacts Loader] Synchronization started") + + val core = coreContext.core + if (core.isFriendListSubscriptionEnabled) { + val rls: String = corePreferences.rlsUri + for (list in core.friendsLists) { + if (list.rlsAddress == null || list.rlsAddress.asStringUriOnly() != rls) { + Log.i("[Contacts Loader] Friend list RLS URI updated to: $rls") + list.rlsUri = rls + } + } + } + } + + override fun doInBackground(vararg args: Void): AsyncContactsData { + Log.i("[Contacts Loader] Background synchronization started") + + val data = AsyncContactsData() + val core: Core = coreContext.core + + val androidContactsCache: HashMap = HashMap() + val nativeIds = arrayListOf() + + val friendLists = core.friendsLists + for (list in friendLists) { + val friends = list.friends + for (friend in friends) { + if (isCancelled) { + Log.w("[Contacts Loader] Task cancelled") + return data + } + var contact: Contact? = friend.userData as? Contact + if (contact != null) { + if (contact is NativeContact) { + contact.sipAddresses.clear() + contact.rawSipAddresses.clear() + contact.phoneNumbers.clear() + androidContactsCache[contact.nativeId] = contact + nativeIds.add(contact.nativeId) + } else { + data.contacts.add(contact) + if (contact.sipAddresses.isNotEmpty()) { + data.sipContacts.add(contact) + } + } + } else { + if (friend.refKey != null) { + // Friend has a refkey but no LinphoneContact => represents a + // native contact stored in db from a previous version of Linphone, + // remove it + list.removeFriend(friend) + } else { // No refkey so it's a standalone contact + contact = Contact() + contact.friend = friend + contact.syncValuesFromFriend() + friend.userData = contact + data.contacts.add(contact) + if (contact.sipAddresses.isNotEmpty()) { + data.sipContacts.add(contact) + } + } + } + } + } + + if (PermissionHelper.required(context).hasReadContactsPermission()) { + var selection: String? = null + if (corePreferences.fetchContactsFromDefaultDirectory) { + Log.i("[Contacts Loader] Only fetching contacts in default directory") + selection = ContactsContract.Data.IN_DEFAULT_DIRECTORY + " == 1" + } + val cursor: Cursor? = context.contentResolver + .query( + ContactsContract.Data.CONTENT_URI, + projection, + selection, + null, + null + ) + + if (cursor != null) { + Log.i("[Contacts Loader] Found ${cursor.count} entries in cursor") + while (cursor.moveToNext()) { + if (isCancelled) { + Log.w("[Contacts Loader] Task cancelled") + return data + } + + try { + val id: String = + cursor.getString(cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID)) + val starred = + cursor.getInt(cursor.getColumnIndex(ContactsContract.Contacts.STARRED)) == 1 + val lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)) + var contact: Contact? = androidContactsCache[id] + if (contact == null) { + Log.d( + "[Contacts Loader] Creating contact with native ID $id, favorite flag is $starred" + ) + nativeIds.add(id) + contact = NativeContact(id, lookupKey) + contact.isStarred = starred + androidContactsCache[id] = contact + } + contact.syncValuesFromAndroidCursor(cursor) + } catch (ise: IllegalStateException) { + Log.e( + "[Contacts Loader] Couldn't get values from cursor, exception: $ise" + ) + } + } + cursor.close() + } else { + Log.w("[Contacts Loader] Read contacts permission denied, can't fetch native contacts") + } + + for (list in core.friendsLists) { + val friends = list.friends + for (friend in friends) { + if (isCancelled) { + Log.w("[Contacts Loader] Task cancelled") + return data + } + val contact: Contact? = friend.userData as? Contact + if (contact != null && contact is NativeContact) { + if (!nativeIds.contains(contact.nativeId)) { + Log.i("[Contacts Loader] Contact removed since last fetch: ${contact.nativeId}") + // Has been removed since last fetch + androidContactsCache.remove(contact.nativeId) + } + } + } + } + + nativeIds.clear() + } + + val contacts: Collection = androidContactsCache.values + // New friends count will be 0 after the first contacts fetch + Log.i( + "[Contacts Loader] Found ${contacts.size} native contacts plus ${data.contacts.size} friends in the configuration file" + ) + for (contact in contacts) { + if (isCancelled) { + Log.w("[Contacts Loader] Task cancelled") + return data + } + if (contact.sipAddresses.isEmpty() && contact.phoneNumbers.isEmpty()) { + continue + } + + if (contact.fullName == null) { + for (address in contact.sipAddresses) { + contact.fullName = LinphoneUtils.getDisplayName(address) + Log.w( + "[Contacts Loader] Couldn't find a display name for contact ${contact.fullName}, used SIP address display name / username instead..." + ) + } + } + + if (!corePreferences.hideContactsWithoutPresence) { + if (contact.sipAddresses.isNotEmpty() && !data.sipContacts.contains(contact)) { + data.sipContacts.add(contact) + } + } + data.contacts.add(contact) + } + androidContactsCache.clear() + + data.contacts.sort() + data.sipContacts.sort() + + Log.i("[Contacts Loader] Background synchronization finished") + return data + } + + override fun onPostExecute(data: AsyncContactsData) { + if (isCancelled) return + Log.i("[Contacts Loader] ${data.contacts.size} contacts found in which ${data.sipContacts.size} are SIP") + + for (contact in data.contacts) { + if (contact is NativeContact) { + contact.createOrUpdateFriendFromNativeContact() + + if (contact.friend?.presenceModel?.basicStatus == PresenceBasicStatus.Open) { + data.sipContacts.add(contact) + } + } + } + + // Now that contact fetching is asynchronous, this is required to ensure + // presence subscription event will be sent with all friends + val core = coreContext.core + if (core.isFriendListSubscriptionEnabled) { + Log.i("[Contacts Loader] Matching friends created, updating subscription") + for (list in core.friendsLists) { + list.updateSubscriptions() + } + } + + coreContext.contactsManager.updateContacts(data.contacts, data.sipContacts) + + Log.i("[Contacts Loader] Synchronization finished") + } + + class AsyncContactsData { + val contacts = arrayListOf() + val sipContacts = arrayListOf() + } +} diff --git a/app/src/main/java/org/linphone/contact/BigContactAvatarView.kt b/app/src/main/java/org/linphone/contact/BigContactAvatarView.kt new file mode 100644 index 000000000..a6304cb1b --- /dev/null +++ b/app/src/main/java/org/linphone/contact/BigContactAvatarView.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.contact + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import androidx.databinding.DataBindingUtil +import org.linphone.R +import org.linphone.databinding.ContactAvatarBigBinding +import org.linphone.utils.AppUtils + +class BigContactAvatarView : LinearLayout { + lateinit var binding: ContactAvatarBigBinding + + constructor(context: Context) : super(context) { + init(context) + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init(context) + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + init(context) + } + + fun init(context: Context) { + binding = DataBindingUtil.inflate( + LayoutInflater.from(context), R.layout.contact_avatar_big, this, true + ) + } + + fun setViewModel(viewModel: ContactViewModelInterface?) { + if (viewModel == null) { + binding.root.visibility = View.GONE + return + } + binding.root.visibility = View.VISIBLE + + val contact: Contact? = viewModel.contact.value + val initials = if (contact != null) { + AppUtils.getInitials(contact.fullName ?: contact.firstName + " " + contact.lastName) + } else { + AppUtils.getInitials(viewModel.displayName) + } + + binding.initials = initials + binding.generatedAvatarVisibility = initials.isNotEmpty() && initials != "+" + binding.imagePath = contact?.getContactPictureUri() + } +} diff --git a/app/src/main/java/org/linphone/contact/Contact.kt b/app/src/main/java/org/linphone/contact/Contact.kt new file mode 100644 index 000000000..520f7e484 --- /dev/null +++ b/app/src/main/java/org/linphone/contact/Contact.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.contact + +import android.database.Cursor +import android.graphics.Bitmap +import android.net.Uri +import androidx.core.app.Person +import androidx.core.graphics.drawable.IconCompat +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.core.Address +import org.linphone.core.Friend +import org.linphone.core.PresenceBasicStatus +import org.linphone.core.tools.Log +import org.linphone.utils.ImageUtils + +open class Contact : Comparable { + var fullName: String? = null + var firstName: String? = null + var lastName: String? = null + var organization: String? = null + var isStarred: Boolean = false + + var phoneNumbers = arrayListOf() + var sipAddresses = arrayListOf
() + // Raw SIP addresses are only used for contact edition + var rawSipAddresses = arrayListOf() + + var friend: Friend? = null + + override fun compareTo(other: Contact): Int { + val fn = fullName ?: "" + val otherFn = other.fullName ?: "" + if (fn == otherFn) { + if (phoneNumbers.size == other.phoneNumbers.size && phoneNumbers.size > 0) { + if (phoneNumbers != other.phoneNumbers) { + for (i in 0..phoneNumbers.size) { + val compare = phoneNumbers[i].compareTo(other.phoneNumbers[i]) + if (compare != 0) return compare + } + } + } else { + return phoneNumbers.size.compareTo(other.phoneNumbers.size) + } + + if (sipAddresses.size == other.sipAddresses.size && sipAddresses.size > 0) { + if (sipAddresses != other.sipAddresses) { + for (i in 0..sipAddresses.size) { + val compare = sipAddresses[i].asStringUriOnly().compareTo(other.sipAddresses[i].asStringUriOnly()) + if (compare != 0) return compare + } + } + } else { + return sipAddresses.size.compareTo(other.sipAddresses.size) + } + + val org = organization ?: "" + val otherOrg = other.organization ?: "" + return org.compareTo(otherOrg) + } + return fn.compareTo(otherFn) + } + + @Synchronized + fun syncValuesFromFriend() { + val friend = this.friend + friend ?: return + + phoneNumbers.clear() + for (number in friend.phoneNumbers) { + if (!phoneNumbers.contains(number)) phoneNumbers.add(number) + } + sipAddresses.clear() + rawSipAddresses.clear() + for (address in friend.addresses) { + if (!sipAddresses.contains(address)) { + sipAddresses.add(address) + rawSipAddresses.add(address.asStringUriOnly()) + } + } + + fullName = friend.name + val vCard = friend.vcard + if (vCard != null) { + lastName = vCard.familyName + firstName = vCard.givenName + organization = vCard.organization + } + } + + @Synchronized + open fun syncValuesFromAndroidCursor(cursor: Cursor) { + Log.e("[Contact] Not a native contact, skip") + } + + open fun getContactThumbnailPictureUri(): Uri? { + return null + } + + open fun getContactPictureUri(): Uri? { + return null + } + + open fun getPerson(): Person { + val personBuilder = Person.Builder().setName(fullName) + + val bm: Bitmap? = + ImageUtils.getRoundBitmapFromUri( + coreContext.context, + getContactThumbnailPictureUri() + ) + val icon = + if (bm == null) IconCompat.createWithResource( + coreContext.context, + R.drawable.avatar + ) else IconCompat.createWithBitmap(bm) + if (icon != null) { + personBuilder.setIcon(icon) + } + + personBuilder.setImportant(isStarred) + return personBuilder.build() + } + + fun hasPresence(): Boolean { + if (friend == null) return false + for (address in sipAddresses) { + val presenceModel = friend?.getPresenceModelForUriOrTel(address.asStringUriOnly()) + if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return true + } + for (number in phoneNumbers) { + val presenceModel = friend?.getPresenceModelForUriOrTel(number) + if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return true + } + return false + } + + fun getContactForPhoneNumberOrAddress(value: String): String? { + val presenceModel = friend?.getPresenceModelForUriOrTel(value) + if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return presenceModel.contact + return null + } +} diff --git a/app/src/main/java/org/linphone/contact/ContactAvatarView.kt b/app/src/main/java/org/linphone/contact/ContactAvatarView.kt new file mode 100644 index 000000000..86537d66e --- /dev/null +++ b/app/src/main/java/org/linphone/contact/ContactAvatarView.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.contact + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import androidx.databinding.DataBindingUtil +import org.linphone.R +import org.linphone.core.ChatRoomSecurityLevel +import org.linphone.databinding.ContactAvatarBinding +import org.linphone.utils.AppUtils + +class ContactAvatarView : LinearLayout { + lateinit var binding: ContactAvatarBinding + + constructor(context: Context) : super(context) { + init(context) + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init(context) + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + init(context) + } + + fun init(context: Context) { + binding = DataBindingUtil.inflate( + LayoutInflater.from(context), R.layout.contact_avatar, this, true + ) + } + + fun setViewModel(viewModel: ContactViewModelInterface) { + val contact: Contact? = viewModel.contact.value + val initials = if (contact != null) { + AppUtils.getInitials(contact.fullName ?: contact.firstName + " " + contact.lastName) + } else { + AppUtils.getInitials(viewModel.displayName) + } + + binding.initials = initials + binding.generatedAvatarVisibility = initials.isNotEmpty() && initials != "+" + binding.groupChatAvatarVisibility = viewModel.showGroupChatAvatar + + binding.imagePath = contact?.getContactThumbnailPictureUri() + + binding.securityIcon = when (viewModel.securityLevel) { + ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator + ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator + else -> R.drawable.security_alert_indicator + } + binding.securityContentDescription = when (viewModel.securityLevel) { + ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe + ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted + else -> R.string.content_description_security_level_unsafe + } + } +} diff --git a/app/src/main/java/org/linphone/contact/ContactsManager.kt b/app/src/main/java/org/linphone/contact/ContactsManager.kt new file mode 100644 index 000000000..b25590132 --- /dev/null +++ b/app/src/main/java/org/linphone/contact/ContactsManager.kt @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.contact + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.database.ContentObserver +import android.net.Uri +import android.os.AsyncTask.THREAD_POOL_EXECUTOR +import android.provider.ContactsContract +import android.util.Patterns +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.PermissionHelper + +interface ContactsUpdatedListener { + fun onContactsUpdated() + + fun onContactUpdated(contact: Contact) +} + +open class ContactsUpdatedListenerStub : ContactsUpdatedListener { + override fun onContactsUpdated() {} + + override fun onContactUpdated(contact: Contact) {} +} + +class ContactsManager(private val context: Context) { + private val contactsObserver: ContentObserver by lazy { + object : ContentObserver(coreContext.handler) { + override fun onChange(selfChange: Boolean) { + onChange(selfChange, null) + } + + override fun onChange(selfChange: Boolean, uri: Uri?) { + Log.i("[Contacts Observer] At least one contact has changed") + fetchContactsAsync() + } + } + } + + var contacts = ArrayList() + @Synchronized + private set + var sipContacts = ArrayList() + @Synchronized + private set + + val magicSearch: MagicSearch by lazy { + val magicSearch = coreContext.core.createMagicSearch() + magicSearch.limitedSearch = false + magicSearch + } + + private val contactsUpdatedListeners = ArrayList() + + private var loadContactsTask: AsyncContactsLoader? = null + + private val friendListListener: FriendListListenerStub = object : FriendListListenerStub() { + @Synchronized + override fun onPresenceReceived(list: FriendList, friends: Array) { + Log.i("[Contacts Manager] Presence received") + var sipContactsListUpdated = false + for (friend in friends) { + if (refreshContactOnPresenceReceived(friend)) { + sipContactsListUpdated = true + } + } + + if (sipContactsListUpdated) { + sipContacts.sort() + Log.i("[Contacts Manager] Notifying observers that list has changed") + for (listener in contactsUpdatedListeners) { + listener.onContactsUpdated() + } + } + } + } + + init { + if (PermissionHelper.required(context).hasReadContactsPermission()) + context.contentResolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactsObserver) + + initSyncAccount() + + val core = coreContext.core + for (list in core.friendsLists) { + list.addListener(friendListListener) + } + Log.i("[Contacts Manager] Created") + } + + fun fetchContactsAsync() { + loadContactsTask?.cancel(true) + loadContactsTask = AsyncContactsLoader(context) + loadContactsTask?.executeOnExecutor(THREAD_POOL_EXECUTOR) + } + + @Synchronized + fun updateContacts(all: ArrayList, sip: ArrayList) { + contacts.clear() + sipContacts.clear() + + contacts.addAll(all) + sipContacts.addAll(sip) + + Log.i("[Contacts Manager] Async fetching finished, notifying observers") + for (listener in contactsUpdatedListeners) { + listener.onContactsUpdated() + } + } + + fun getAndroidContactIdFromUri(uri: Uri): String? { + val projection = arrayOf(ContactsContract.Data.CONTACT_ID) + val cursor = context.contentResolver.query(uri, projection, null, null, null) + if (cursor?.moveToFirst() == true) { + val nameColumnIndex = cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID) + val id = cursor.getString(nameColumnIndex) + cursor.close() + return id + } + return null + } + + @Synchronized + fun findContactById(id: String): Contact? { + var found: Contact? = null + if (contacts.isNotEmpty()) { + found = contacts.find { contact -> + contact is NativeContact && contact.nativeId == id + } + } + + if (found == null && PermissionHelper.required(context).hasReadContactsPermission()) { + // First of all abort background contacts fetching + loadContactsTask?.cancel(true) + + Log.i("[Contacts Manager] Creating native contact with id $id and fetch information from Android database directly") + found = NativeContact(id) + found.syncValuesFromAndroidContact(context) + // Create a LinphoneFriend to be able to receive presence information + found.createOrUpdateFriendFromNativeContact() + + // Restart contacts async fetching + fetchContactsAsync() + } + return found + } + + @Synchronized + fun findContactByPhoneNumber(number: String): Contact? { + return contacts.find { contact -> + contact.phoneNumbers.contains(number) + } + } + + @Synchronized + fun findContactByAddress(address: Address): Contact? { + val friend: Friend? = coreContext.core.findFriend(address) + val contact: Contact? = friend?.userData as? Contact + if (contact != null) return contact + + val username = address.username + if (username != null && Patterns.PHONE.matcher(username).matches()) { + return findContactByPhoneNumber(username) + } + + return null + } + + fun addListener(listener: ContactsUpdatedListener) { + contactsUpdatedListeners.add(listener) + } + + fun removeListener(listener: ContactsUpdatedListener) { + contactsUpdatedListeners.remove(listener) + } + + @Synchronized + fun destroy() { + context.contentResolver.unregisterContentObserver(contactsObserver) + loadContactsTask?.cancel(true) + + // Contact has a Friend field and Friend can have a Contact has userData + // Friend also keeps a ref on the Core, so we have to clean them + for (contact in contacts) { + contact.friend = null + } + contacts.clear() + for (contact in sipContacts) { + contact.friend = null + } + sipContacts.clear() + + val core = coreContext.core + for (list in core.friendsLists) list.removeListener(friendListListener) + } + + private fun initSyncAccount() { + if (!corePreferences.useLinphoneSyncAccount) { + Log.w("[Contacts Manager] Linphone sync account disabled, skipping initialization") + return + } + + val accountManager = context.getSystemService(Context.ACCOUNT_SERVICE) as AccountManager + val accounts = accountManager.getAccountsByType(context.getString(R.string.sync_account_type)) + if (accounts.isEmpty()) { + val newAccount = Account(context.getString(R.string.sync_account_name), context.getString(R.string.sync_account_type)) + try { + accountManager.addAccountExplicitly(newAccount, null, null) + Log.i("[Contacts Manager] Contact account added") + } catch (e: Exception) { + Log.e("[Contacts Manager] Couldn't initialize sync account: $e") + } + } else { + for (account in accounts) { + Log.i("[Contacts Manager] Found account with name [${account.name}] and type [${account.type}]") + } + } + } + + @Synchronized + private fun refreshContactOnPresenceReceived(friend: Friend): Boolean { + if (friend.userData == null) return false + + val contact: Contact = friend.userData as Contact + for (listener in contactsUpdatedListeners) { + listener.onContactUpdated(contact) + } + + Log.i("[Contacts Manager] Received presence information for contact $contact") + if (!sipContacts.contains(contact)) { + sipContacts.add(contact) + return true + } + + return false + } +} diff --git a/app/src/main/java/org/linphone/contact/DummyAuthenticationService.kt b/app/src/main/java/org/linphone/contact/DummyAuthenticationService.kt new file mode 100644 index 000000000..3cc3cf4b0 --- /dev/null +++ b/app/src/main/java/org/linphone/contact/DummyAuthenticationService.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +package org.linphone.contact + +import android.accounts.AbstractAccountAuthenticator +import android.accounts.Account +import android.accounts.AccountAuthenticatorResponse +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.IBinder + +// Below classes are required to be able to create our DummySyncService... +internal class DummyAuthenticator(context: Context) : AbstractAccountAuthenticator(context) { + override fun getAuthTokenLabel(authTokenType: String?): String { + throw UnsupportedOperationException() + } + + override fun confirmCredentials( + response: AccountAuthenticatorResponse?, + account: Account?, + options: Bundle? + ): Bundle? = null + + override fun updateCredentials( + response: AccountAuthenticatorResponse?, + account: Account?, + authTokenType: String?, + options: Bundle? + ): Bundle { + throw UnsupportedOperationException() + } + + override fun getAuthToken( + response: AccountAuthenticatorResponse?, + account: Account?, + authTokenType: String?, + options: Bundle? + ): Bundle { + throw UnsupportedOperationException() + } + + override fun hasFeatures( + response: AccountAuthenticatorResponse?, + account: Account?, + features: Array? + ): Bundle { + throw UnsupportedOperationException() + } + + override fun editProperties( + response: AccountAuthenticatorResponse?, + accountType: String? + ): Bundle { + throw UnsupportedOperationException() + } + + override fun addAccount( + response: AccountAuthenticatorResponse?, + accountType: String?, + authTokenType: String?, + requiredFeatures: Array?, + options: Bundle? + ): Bundle? = null +} + +class DummyAuthenticationService : Service() { + private lateinit var authenticator: DummyAuthenticator + + override fun onCreate() { + authenticator = DummyAuthenticator(this) + } + + override fun onBind(intent: Intent): IBinder { + return authenticator.iBinder + } +} diff --git a/app/src/main/java/org/linphone/contact/DummySyncService.kt b/app/src/main/java/org/linphone/contact/DummySyncService.kt new file mode 100644 index 000000000..60cec9642 --- /dev/null +++ b/app/src/main/java/org/linphone/contact/DummySyncService.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.contact + +import android.accounts.Account +import android.app.Service +import android.content.* +import android.os.Bundle +import android.os.IBinder + +// Below classes are required to be able to use our own contact MIME type entry... +class DummySyncAdapter(context: Context, autoInit: Boolean) : AbstractThreadedSyncAdapter(context, autoInit) { + override fun onPerformSync( + account: Account?, + extras: Bundle?, + authority: String?, + provider: ContentProviderClient?, + syncResult: SyncResult? + ) { } +} + +class DummySyncService : Service() { + companion object { + private val syncAdapterLock = Any() + private var syncAdapter: DummySyncAdapter? = null + } + + override fun onCreate() { + synchronized(syncAdapterLock) { + if (syncAdapter == null) { + syncAdapter = DummySyncAdapter(applicationContext, true) + } + } + } + + override fun onBind(intent: Intent?): IBinder? { + return syncAdapter?.syncAdapterBinder + } +} diff --git a/app/src/main/java/org/linphone/contact/GenericContactViewModel.kt b/app/src/main/java/org/linphone/contact/GenericContactViewModel.kt new file mode 100644 index 000000000..89ecb34c4 --- /dev/null +++ b/app/src/main/java/org/linphone/contact/GenericContactViewModel.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.contact + +import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.activities.main.viewmodels.ErrorReportingViewModel +import org.linphone.core.Address +import org.linphone.core.ChatRoomSecurityLevel +import org.linphone.core.tools.Log +import org.linphone.utils.LinphoneUtils + +interface ContactViewModelInterface { + val contact: MutableLiveData + + val displayName: String + + val securityLevel: ChatRoomSecurityLevel + get() = ChatRoomSecurityLevel.ClearText + + val showGroupChatAvatar: Boolean + get() = false +} + +abstract class GenericContactViewModel(private val sipAddress: Address) : ErrorReportingViewModel(), ContactViewModelInterface { + override val displayName: String = LinphoneUtils.getDisplayName(sipAddress) + + override val contact = MutableLiveData() + + private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { + override fun onContactUpdated(contact: Contact) { + Log.i("[Generic Contact VM] Contacts have changed") + contactLookup() + } + } + + init { + coreContext.contactsManager.addListener(contactsUpdatedListener) + contactLookup() + } + + override fun onCleared() { + coreContext.contactsManager.removeListener(contactsUpdatedListener) + + super.onCleared() + } + + private fun contactLookup() { + contact.value = coreContext.contactsManager.findContactByAddress(sipAddress) + } +} diff --git a/app/src/main/java/org/linphone/contact/NativeContact.kt b/app/src/main/java/org/linphone/contact/NativeContact.kt new file mode 100644 index 000000000..c48c5b502 --- /dev/null +++ b/app/src/main/java/org/linphone/contact/NativeContact.kt @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.contact + +import android.content.ContentUris +import android.content.Context +import android.database.Cursor +import android.graphics.Bitmap +import android.net.Uri +import android.provider.ContactsContract +import androidx.core.app.Person +import androidx.core.graphics.drawable.IconCompat +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.core.Address +import org.linphone.core.SubscribePolicy +import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils +import org.linphone.utils.ImageUtils + +class NativeContact(val nativeId: String, private val lookupKey: String? = null) : Contact() { + override fun compareTo(other: Contact): Int { + val superResult = super.compareTo(other) + if (superResult == 0 && other is NativeContact) { + return nativeId.compareTo(other.nativeId) + } + return superResult + } + + override fun getContactThumbnailPictureUri(): Uri { + return Uri.withAppendedPath( + ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, nativeId.toLong()), + ContactsContract.Contacts.Photo.CONTENT_DIRECTORY + ) + } + + override fun getContactPictureUri(): Uri { + return Uri.withAppendedPath( + ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, nativeId.toLong()), + ContactsContract.Contacts.Photo.DISPLAY_PHOTO + ) + } + + override fun getPerson(): Person { + val personBuilder = Person.Builder().setName(fullName) + + val bm: Bitmap? = + ImageUtils.getRoundBitmapFromUri( + coreContext.context, + getContactThumbnailPictureUri() + ) + val icon = + if (bm == null) IconCompat.createWithResource( + coreContext.context, + R.drawable.avatar + ) else IconCompat.createWithBitmap(bm) + if (icon != null) { + personBuilder.setIcon(icon) + } + + personBuilder.setImportant(isStarred) + if (lookupKey != null) { + personBuilder.setUri("${ContactsContract.Contacts.CONTENT_LOOKUP_URI}/$lookupKey") + } + + return personBuilder.build() + } + + @Synchronized + override fun syncValuesFromAndroidCursor(cursor: Cursor) { + val displayName: String = + cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME_PRIMARY)) + + val mime: String? = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.MIMETYPE)) + val data1: String? = cursor.getString(cursor.getColumnIndex("data1")) + val data2: String? = cursor.getString(cursor.getColumnIndex("data2")) + val data3: String? = cursor.getString(cursor.getColumnIndex("data3")) + val data4: String? = cursor.getString(cursor.getColumnIndex("data4")) + + if (fullName == null || fullName != displayName) { + Log.d("[Native Contact] Setting display name $displayName") + fullName = displayName + } + + val linphoneMime = AppUtils.getString(R.string.linphone_address_mime_type) + when (mime) { + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> { + if (data1 == null && data4 == null) { + Log.e("[Native Contact] Phone number data is empty") + return + } + + Log.d("[Native Contact] Found phone number $data1 ($data4)") + val number = data4 ?: data1 + if (number != null && !phoneNumbers.contains(number)) phoneNumbers.add(number) + } + linphoneMime, ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE -> { + if (data1 == null) { + Log.e("[Native Contact] SIP address is null !") + return + } + + Log.d("[Native Contact] Found SIP address $data1") + val address: Address? = coreContext.core.interpretUrl(data1) + if (address == null) { + Log.e("[Native Contact] Couldn't parse address $data1 !") + return + } + + if (!sipAddresses.contains(address)) { + sipAddresses.add(address) + rawSipAddresses.add(data1) + } + } + ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE -> { + if (data1 == null) { + Log.e("[Native Contact] Organization is null !") + return + } + + Log.d("[Native Contact] Found organization $data1") + organization = data1 + } + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> { + if (data2 == null && data3 == null) { + Log.e("[Native Contact] First name and last name are both null !") + return + } + + Log.d("[Native Contact] Found first name $data2 and last name $data3") + firstName = data2 + lastName = data3 + } + } + } + + @Synchronized + fun createOrUpdateFriendFromNativeContact() { + var created = false + if (friend == null) { + val friend = coreContext.core.createFriend() + friend.enableSubscribes(false) + friend.incSubscribePolicy = SubscribePolicy.SPDeny + friend.refKey = nativeId + friend.userData = this + + created = true + this.friend = friend + } + + val friend = this.friend + if (friend != null) { + friend.edit() + friend.name = fullName + + val vCard = friend.vcard + if (vCard != null) { + vCard.familyName = lastName + vCard.givenName = firstName + if (organization != null) vCard.organization = organization + } + + if (!created) { + for (address in friend.addresses) friend.removeAddress(address) + for (number in friend.phoneNumbers) friend.removePhoneNumber(number) + } + + for (address in sipAddresses) friend.addAddress(address) + for (number in phoneNumbers) friend.addPhoneNumber(number) + + friend.done() + if (created) coreContext.core.defaultFriendList?.addFriend(friend) + } + } + + @Synchronized + fun syncValuesFromAndroidContact(context: Context) { + Log.d("[Native Contact] Looking for contact cursor with id: $nativeId") + + var selection: String = ContactsContract.Data.CONTACT_ID + " == " + nativeId + if (corePreferences.fetchContactsFromDefaultDirectory) { + Log.d("[Native Contact] Only fetching contacts in default directory") + selection = ContactsContract.Data.IN_DEFAULT_DIRECTORY + " == 1 AND " + selection + } + + val cursor: Cursor? = context.contentResolver + .query( + ContactsContract.Data.CONTENT_URI, + AsyncContactsLoader.projection, + selection, + null, + null + ) + if (cursor != null) { + sipAddresses.clear() + rawSipAddresses.clear() + phoneNumbers.clear() + + while (cursor.moveToNext()) { + syncValuesFromAndroidCursor(cursor) + } + cursor.close() + } + } +} diff --git a/app/src/main/java/org/linphone/contact/NativeContactEditor.kt b/app/src/main/java/org/linphone/contact/NativeContactEditor.kt new file mode 100644 index 000000000..ef7da8bfb --- /dev/null +++ b/app/src/main/java/org/linphone/contact/NativeContactEditor.kt @@ -0,0 +1,410 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.contact + +import android.content.ContentProviderOperation +import android.content.ContentUris +import android.net.Uri +import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds +import android.provider.ContactsContract.RawContacts +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.activities.main.contact.viewmodels.NumberOrAddressEditorViewModel +import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils +import org.linphone.utils.PermissionHelper + +class NativeContactEditor(val contact: NativeContact) { + private val changes = arrayListOf() + private val selection = + "${ContactsContract.Data.CONTACT_ID} =? AND ${ContactsContract.Data.MIMETYPE} =?" + private val phoneNumberSelection = + "$selection AND (${CommonDataKinds.Phone.NUMBER}=? OR ${CommonDataKinds.Phone.NORMALIZED_NUMBER}=?)" + private val sipAddressSelection = + "${ContactsContract.Data.CONTACT_ID} =? AND (${ContactsContract.Data.MIMETYPE} =? OR ${ContactsContract.Data.MIMETYPE} =?) AND data1=?" + private val useLinphoneSyncAccount = corePreferences.useLinphoneSyncAccount + private val contactUri = ContactsContract.Data.CONTENT_URI + + private var rawId: String? = null + private var linphoneRawId: String? = null + private var pictureByteArray: ByteArray? = null + + init { + val contentResolver = coreContext.context.contentResolver + val syncAccountType = AppUtils.getString(R.string.sync_account_type) + val syncAccountName = AppUtils.getString(R.string.sync_account_name) + + val cursor = contentResolver.query( + RawContacts.CONTENT_URI, + arrayOf(RawContacts._ID, RawContacts.ACCOUNT_TYPE), + "${RawContacts.CONTACT_ID} =?", + arrayOf(contact.nativeId), + null + ) + if (cursor?.moveToFirst() == true) { + do { + if (rawId == null) { + rawId = cursor.getString(cursor.getColumnIndex(RawContacts._ID)) + Log.i("[Native Contact Editor] Found raw id $rawId for native contact with id ${contact.nativeId}") + } + + val accountType = cursor.getString(cursor.getColumnIndex(RawContacts.ACCOUNT_TYPE)) + if (accountType == syncAccountType && linphoneRawId == null) { + linphoneRawId = cursor.getString(cursor.getColumnIndex(RawContacts._ID)) + Log.i("[Native Contact Editor] Found linphone raw id $linphoneRawId for native contact with id ${contact.nativeId}") + } + } while (cursor.moveToNext() && linphoneRawId == null) + } + cursor?.close() + + // When contact has been created with AppUtils.createAndroidContact this is required + if (rawId == null) rawId = contact.nativeId + + if (linphoneRawId == null && useLinphoneSyncAccount) { + Log.w("[Native Contact Editor] Linphone raw id not found") + val insert = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI) + .withValue(RawContacts.ACCOUNT_TYPE, syncAccountType) + .withValue(RawContacts.ACCOUNT_NAME, syncAccountName) + .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT) + .build() + addChanges(insert) + val update = + ContentProviderOperation.newUpdate(ContactsContract.AggregationExceptions.CONTENT_URI) + .withValue( + ContactsContract.AggregationExceptions.TYPE, + ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER + ) + .withValue(ContactsContract.AggregationExceptions.RAW_CONTACT_ID1, rawId) + .withValueBackReference( + ContactsContract.AggregationExceptions.RAW_CONTACT_ID2, + 0 + ) + .build() + addChanges(update) + commit() + } + } + + fun setFirstAndLastNames(firstName: String, lastName: String): NativeContactEditor { + if (firstName == contact.firstName && lastName == contact.lastName) { + Log.w("[Native Contact Editor] First & last names haven't changed") + return this + } + + val builder = if (contact.firstName == null && contact.lastName == null) { + // Probably a contact creation + ContentProviderOperation.newInsert(contactUri) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawId) + } else { + ContentProviderOperation.newUpdate(contactUri) + .withSelection( + selection, + arrayOf(contact.nativeId, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) + ) + } + + builder.withValue( + ContactsContract.Data.MIMETYPE, + CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + ) + .withValue( + CommonDataKinds.StructuredName.GIVEN_NAME, firstName + ) + .withValue( + CommonDataKinds.StructuredName.FAMILY_NAME, lastName + ) + addChanges(builder.build()) + return this + } + + fun setOrganization(value: String): NativeContactEditor { + val previousValue = contact.organization + if (value == previousValue) { + Log.w("[Native Contact Editor] Organization hasn't changed") + return this + } + + val builder = if (previousValue?.isNotEmpty() == true) { + ContentProviderOperation.newUpdate(contactUri) + .withSelection( + "$selection AND ${CommonDataKinds.Organization.COMPANY} =?", + arrayOf( + contact.nativeId, + CommonDataKinds.Organization.CONTENT_ITEM_TYPE, + previousValue + ) + ) + } else { + ContentProviderOperation.newInsert(contactUri) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawId) + } + + builder.withValue( + ContactsContract.Data.MIMETYPE, + CommonDataKinds.Organization.CONTENT_ITEM_TYPE + ) + .withValue( + CommonDataKinds.Organization.COMPANY, value + ) + + addChanges(builder.build()) + return this + } + + fun setPhoneNumbers(value: List): NativeContactEditor { + var addCount = 0 + var removeCount = 0 + var editCount = 0 + + for (phoneNumber in value) { + when { + phoneNumber.currentValue.isEmpty() -> { + // New phone number to add + addCount++ + addNumber(phoneNumber) + } + phoneNumber.toRemove.value == true -> { + // Existing number to remove + removeCount++ + removeNumber(phoneNumber) + } + phoneNumber.currentValue != phoneNumber.newValue.value -> { + // Existing number to update + editCount++ + updateNumber(phoneNumber) + } + } + } + + Log.i("[Native Contact Editor] $addCount numbers added, $removeCount numbers removed and $editCount numbers updated") + return this + } + + fun setSipAddresses(value: List): NativeContactEditor { + var addCount = 0 + var removeCount = 0 + var editCount = 0 + + for (sipAddress in value) { + when { + sipAddress.currentValue.isEmpty() -> { + // New address to add + addCount++ + if (useLinphoneSyncAccount) { + addAddress(sipAddress) + } else { + addSipAddress(sipAddress) + } + } + sipAddress.toRemove.value == true -> { + // Existing address to remove + removeCount++ + removeAddress(sipAddress) + } + sipAddress.currentValue != sipAddress.newValue.value -> { + // Existing address to update + editCount++ + updateAddress(sipAddress) + } + } + } + + Log.i("[Native Contact Editor] $addCount addresses added, $removeCount addresses removed and $editCount addresses updated") + return this + } + + fun setPicture(value: ByteArray?): NativeContactEditor { + pictureByteArray = value + if (value != null) Log.i("[Native Contact Editor] Adding operation: picture set/update") + return this + } + + fun commit() { + if (PermissionHelper.get().hasWriteContactsPermission()) { + try { + if (changes.isNotEmpty()) { + val contentResolver = coreContext.context.contentResolver + val results = contentResolver.applyBatch(ContactsContract.AUTHORITY, changes) + for (result in results) { + Log.i("[Native Contact Editor] Result is $result") + if (linphoneRawId == null && useLinphoneSyncAccount && result?.uri != null) { + linphoneRawId = ContentUris.parseId(result.uri).toString() + Log.i("[Native Contact Editor] Linphone raw id is $linphoneRawId") + } + } + } + if (pictureByteArray != null) { + updatePicture() + } + } catch (e: Exception) { + Log.e("[Native Contact Editor] Exception raised while applying changes: $e") + } + } else { + Log.e("[Native Contact Editor] WRITE_CONTACTS permission isn't granted!") + } + changes.clear() + } + + private fun addChanges(operation: ContentProviderOperation) { + Log.i("[Native Contact Editor] Adding operation: $operation") + changes.add(operation) + } + + private fun addNumber(number: NumberOrAddressEditorViewModel) { + val insert = ContentProviderOperation.newInsert(contactUri) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawId) + .withValue( + ContactsContract.Data.MIMETYPE, + CommonDataKinds.Phone.CONTENT_ITEM_TYPE + ) + .withValue(CommonDataKinds.Phone.NUMBER, number.newValue.value) + .withValue( + CommonDataKinds.Phone.TYPE, + CommonDataKinds.Phone.TYPE_MOBILE + ) + .build() + addChanges(insert) + } + + private fun updateNumber(number: NumberOrAddressEditorViewModel) { + val update = ContentProviderOperation.newUpdate(contactUri) + .withSelection( + phoneNumberSelection, + arrayOf( + contact.nativeId, + CommonDataKinds.Phone.CONTENT_ITEM_TYPE, + number.currentValue, + number.currentValue + ) + ) + .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Phone.NUMBER, number.newValue.value) + .withValue( + CommonDataKinds.Phone.TYPE, + CommonDataKinds.Phone.TYPE_MOBILE + ) + .build() + addChanges(update) + } + + private fun removeNumber(number: NumberOrAddressEditorViewModel) { + val delete = ContentProviderOperation.newDelete(contactUri) + .withSelection( + phoneNumberSelection, + arrayOf( + contact.nativeId, + CommonDataKinds.Phone.CONTENT_ITEM_TYPE, + number.currentValue, + number.currentValue + ) + ) + .build() + addChanges(delete) + } + + private fun addAddress(address: NumberOrAddressEditorViewModel) { + val insert = ContentProviderOperation.newInsert(contactUri) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, linphoneRawId) + .withValue( + ContactsContract.Data.MIMETYPE, + AppUtils.getString(R.string.linphone_address_mime_type) + ) + .withValue("data1", address.newValue.value) // value + .withValue("data2", AppUtils.getString(R.string.app_name)) // summary + .withValue("data3", address.newValue.value) // detail + .build() + addChanges(insert) + } + + private fun addSipAddress(address: NumberOrAddressEditorViewModel) { + val insert = ContentProviderOperation.newInsert(contactUri) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawId) + .withValue( + ContactsContract.Data.MIMETYPE, + CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE + ) + .withValue("data1", address.newValue.value) // value + .build() + addChanges(insert) + } + + private fun updateAddress(address: NumberOrAddressEditorViewModel) { + val update = ContentProviderOperation.newUpdate(contactUri) + .withSelection( + sipAddressSelection, + arrayOf( + contact.nativeId, + CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE, + AppUtils.getString(R.string.linphone_address_mime_type), + address.currentValue + ) + ) + .withValue( + ContactsContract.Data.MIMETYPE, + AppUtils.getString(R.string.linphone_address_mime_type) + ) + .withValue("data1", address.newValue.value) // value + .withValue("data2", AppUtils.getString(R.string.app_name)) // summary + .withValue("data3", address.newValue.value) // detail + .build() + addChanges(update) + } + + private fun removeAddress(address: NumberOrAddressEditorViewModel) { + val delete = ContentProviderOperation.newDelete(contactUri) + .withSelection( + sipAddressSelection, + arrayOf( + contact.nativeId, + CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE, + AppUtils.getString(R.string.linphone_address_mime_type), + address.currentValue + ) + ) + .build() + addChanges(delete) + } + + private fun updatePicture() { + val value = pictureByteArray + val id = rawId + if (value == null || id == null) return + + try { + val uri = Uri.withAppendedPath( + ContentUris.withAppendedId(RawContacts.CONTENT_URI, id.toLong()), + RawContacts.DisplayPhoto.CONTENT_DIRECTORY + ) + val contentResolver = coreContext.context.contentResolver + val assetFileDescriptor = contentResolver.openAssetFileDescriptor(uri, "rw") + val outputStream = assetFileDescriptor?.createOutputStream() + outputStream?.write(value) + outputStream?.close() + assetFileDescriptor?.close() + Log.i("[Native Contact Editor] Picture updated") + } catch (e: Exception) { + Log.e("[Native Contact Editor] Failed to update picture, raised exception: $e") + } + + pictureByteArray = null + } +} diff --git a/app/src/main/java/org/linphone/contact/ShortcutsHelper.kt b/app/src/main/java/org/linphone/contact/ShortcutsHelper.kt new file mode 100644 index 000000000..c0bc9d4b0 --- /dev/null +++ b/app/src/main/java/org/linphone/contact/ShortcutsHelper.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.contact + +import android.annotation.TargetApi +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import androidx.collection.ArraySet +import androidx.core.content.pm.ShortcutInfoCompat +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.activities.main.MainActivity +import org.linphone.core.Address +import org.linphone.core.ChatRoomCapabilities +import org.linphone.core.tools.Log + +@TargetApi(25) +class ShortcutsHelper(val context: Context) { + companion object { + fun createShortcutsToContacts(context: Context) { + val shortcuts = ArrayList() + val shortcutManager = context.getSystemService(ShortcutManager::class.java) + if (shortcutManager.isRateLimitingActive) { + Log.e("[Shortcut Helper] Rate limiting is active, aborting") + return + } + + val maxShortcuts = shortcutManager.maxShortcutCountPerActivity + var count = 0 + val processedAddresses = arrayListOf() + for (room in coreContext.core.chatRooms) { + // Android can usually only have around 4-5 shortcuts at a time + if (count >= maxShortcuts) { + Log.w("[Shortcut Helper] Max amount of shortcuts reached ($count)") + break + } + + val addresses: ArrayList
= arrayListOf(room.peerAddress) + if (!room.hasCapability(ChatRoomCapabilities.Basic.toInt())) { + addresses.clear() + for (participant in room.participants) { + addresses.add(participant.address) + } + } + for (address in addresses) { + if (count >= maxShortcuts) { + Log.w("[Shortcut Helper] Max amount of shortcuts reached ($count)") + break + } + + val stringAddress = address.asStringUriOnly() + if (!processedAddresses.contains(stringAddress)) { + processedAddresses.add(stringAddress) + val contact: Contact? = + coreContext.contactsManager.findContactByAddress(address) + + if (contact != null && contact is NativeContact) { + val shortcut: ShortcutInfo? = createContactShortcut(context, contact) + if (shortcut != null) { + Log.i("[Shortcut Helper] Creating launcher shortcut for ${shortcut.shortLabel}") + shortcuts.add(shortcut) + count += 1 + } + } else { + Log.w("[Shortcut Helper] Contact not found for address: $stringAddress") + } + } + } + } + shortcutManager.dynamicShortcuts = shortcuts + } + + private fun createContactShortcut(context: Context, contact: NativeContact): ShortcutInfo? { + try { + val categories: ArraySet = ArraySet() + categories.add(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION) + + val person = contact.getPerson() + val icon = person.icon + + val intent = Intent(Intent.ACTION_MAIN) + intent.setClass(context, MainActivity::class.java) + intent.putExtra("ContactId", contact.nativeId) + + return ShortcutInfoCompat.Builder(context, contact.nativeId) + .setShortLabel(contact.fullName ?: "${contact.firstName} ${contact.lastName}") + .setIcon(icon) + .setPerson(person) + .setCategories(categories) + .setIntent(intent) + .build().toShortcutInfo() + } catch (e: Exception) { + Log.e("[Shortcuts Helper] ShortcutInfo.Builder exception: $e") + } + + return null + } + + fun removeShortcuts(context: Context) { + Log.w("[Shortcut Helper] Removing all contacts shortcuts") + val shortcutManager = context.getSystemService(ShortcutManager::class.java) + shortcutManager.removeAllDynamicShortcuts() + } + } +} diff --git a/app/src/main/java/org/linphone/contacts/AndroidContact.java b/app/src/main/java/org/linphone/contacts/AndroidContact.java deleted file mode 100644 index 0470a72d9..000000000 --- a/app/src/main/java/org/linphone/contacts/AndroidContact.java +++ /dev/null @@ -1,725 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import android.content.ContentProviderOperation; -import android.content.ContentProviderResult; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.res.AssetFileDescriptor; -import android.database.Cursor; -import android.net.Uri; -import android.provider.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.RawContacts; -import java.io.IOException; -import java.io.OutputStream; -import java.io.Serializable; -import java.util.ArrayList; -import org.linphone.LinphoneContext; -import org.linphone.R; -import org.linphone.core.tools.Log; - -class AndroidContact implements Serializable { - String mAndroidId; - private String mAndroidRawId; - private boolean isAndroidRawIdLinphone; - private transient ArrayList mChangesToCommit; - private byte[] mTempPicture; - - AndroidContact() { - mChangesToCommit = new ArrayList<>(); - isAndroidRawIdLinphone = false; - mTempPicture = null; - } - - String getAndroidId() { - return mAndroidId; - } - - void setAndroidId(String id) { - mAndroidId = id; - } - - boolean isAndroidContact() { - return mAndroidId != null; - } - - private void addChangesToCommit(ContentProviderOperation operation) { - Log.i("[Contact] Added operation " + operation); - if (mChangesToCommit == null) { - mChangesToCommit = new ArrayList<>(); - } - mChangesToCommit.add(operation); - } - - void saveChangesCommited() { - if (ContactsManager.getInstance().hasReadContactsAccess() && mChangesToCommit.size() > 0) { - try { - ContentResolver contentResolver = - LinphoneContext.instance().getApplicationContext().getContentResolver(); - ContentProviderResult[] results = - contentResolver.applyBatch(ContactsContract.AUTHORITY, mChangesToCommit); - if (results != null - && results.length > 0 - && results[0] != null - && results[0].uri != null) { - String rawId = String.valueOf(ContentUris.parseId(results[0].uri)); - if (mAndroidId == null) { - Log.i("[Contact] Contact created with RAW ID " + rawId); - mAndroidRawId = rawId; - if (mTempPicture != null) { - Log.i( - "[Contact] Contact has been created, raw is is available, time to set the photo"); - setPhoto(mTempPicture); - } - - final String[] projection = - new String[] {ContactsContract.RawContacts.CONTACT_ID}; - final Cursor cursor = - contentResolver.query(results[0].uri, projection, null, null, null); - if (cursor != null) { - cursor.moveToNext(); - long contactId = cursor.getLong(0); - mAndroidId = String.valueOf(contactId); - cursor.close(); - Log.i("[Contact] Contact created with ID " + mAndroidId); - } - } else { - if (mAndroidRawId == null || !isAndroidRawIdLinphone) { - Log.i( - "[Contact] Linphone RAW ID " - + rawId - + " created from existing RAW ID " - + mAndroidRawId); - mAndroidRawId = rawId; - isAndroidRawIdLinphone = true; - } - } - } - } catch (Exception e) { - Log.e("[Contact] Exception while saving changes: " + e); - } finally { - mChangesToCommit.clear(); - } - } - } - - void createAndroidContact() { - if (LinphoneContext.instance() - .getApplicationContext() - .getResources() - .getBoolean(R.bool.use_linphone_tag)) { - Log.i("[Contact] Creating contact using linphone account type"); - addChangesToCommit( - ContentProviderOperation.newInsert(RawContacts.CONTENT_URI) - .withValue( - RawContacts.ACCOUNT_TYPE, - ContactsManager.getInstance() - .getString(R.string.sync_account_type)) - .withValue( - RawContacts.ACCOUNT_NAME, - ContactsManager.getInstance() - .getString(R.string.sync_account_name)) - .withValue( - RawContacts.AGGREGATION_MODE, - RawContacts.AGGREGATION_MODE_DEFAULT) - .build()); - isAndroidRawIdLinphone = true; - - } else { - Log.i("[Contact] Creating contact using default account type"); - addChangesToCommit( - ContentProviderOperation.newInsert(RawContacts.CONTENT_URI) - .withValue(RawContacts.ACCOUNT_TYPE, null) - .withValue(RawContacts.ACCOUNT_NAME, null) - .withValue( - RawContacts.AGGREGATION_MODE, - RawContacts.AGGREGATION_MODE_DEFAULT) - .build()); - } - } - - void deleteAndroidContact() { - Log.i("[Contact] Deleting Android contact ", this); - ContactsManager.getInstance().delete(mAndroidId); - } - - Uri getContactThumbnailPictureUri() { - Uri person = ContentUris.withAppendedId(Contacts.CONTENT_URI, Long.parseLong(mAndroidId)); - return Uri.withAppendedPath(person, Contacts.Photo.CONTENT_DIRECTORY); - } - - Uri getContactPictureUri() { - Uri person = ContentUris.withAppendedId(Contacts.CONTENT_URI, Long.parseLong(mAndroidId)); - return Uri.withAppendedPath(person, Contacts.Photo.DISPLAY_PHOTO); - } - - void setName(String fn, String ln) { - if ((fn == null || fn.isEmpty()) && (ln == null || ln.isEmpty())) { - Log.e("[Contact] Can't set both first and last name to null or empty"); - return; - } - - if (mAndroidId == null) { - Log.i("[Contact] Setting given & family name " + fn + " " + ln + " to new contact."); - addChangesToCommit( - ContentProviderOperation.newInsert(Data.CONTENT_URI) - .withValueBackReference(Data.RAW_CONTACT_ID, 0) - .withValue( - Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) - .withValue(CommonDataKinds.StructuredName.GIVEN_NAME, fn) - .withValue(CommonDataKinds.StructuredName.FAMILY_NAME, ln) - .build()); - } else { - Log.i( - "[Contact] Setting given & family name " - + fn - + " " - + ln - + " to existing contact " - + mAndroidId - + " (" - + mAndroidRawId - + ")"); - String select = - ContactsContract.Data.CONTACT_ID - + "=? AND " - + ContactsContract.Data.MIMETYPE - + "=?"; - String[] args = - new String[] { - getAndroidId(), - ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE - }; - - addChangesToCommit( - ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) - .withSelection(select, args) - .withValue( - ContactsContract.Data.MIMETYPE, - ContactsContract.CommonDataKinds.StructuredName - .CONTENT_ITEM_TYPE) - .withValue( - ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, fn) - .withValue( - ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, ln) - .build()); - } - } - - void updateNativeContactWithPresenceInfo(String value) { - Log.d("[Contact] Adding presence information " + value); - addChangesToCommit( - ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, mAndroidRawId) - .withValue( - ContactsContract.Data.MIMETYPE, - ContactsManager.getInstance() - .getString(R.string.linphone_address_mime_type)) - .withValue("data1", value) // phone number - .withValue( - "data2", - ContactsManager.getInstance() - .getString(R.string.app_name)) // Summary - .withValue("data3", value) // Detail - .build()); - } - - boolean isLinphoneAddressMimeEntryAlreadyExisting(String value) { - boolean result = false; - - ContentResolver resolver = - LinphoneContext.instance().getApplicationContext().getContentResolver(); - String[] projection = {"data1", "data3"}; - String selection = - ContactsContract.Data.RAW_CONTACT_ID - + " = ? AND " - + Data.MIMETYPE - + " = ? AND data1 = ?"; - - Cursor c = - resolver.query( - ContactsContract.Data.CONTENT_URI, - projection, - selection, - new String[] { - mAndroidRawId, - ContactsManager.getInstance() - .getString(R.string.linphone_address_mime_type), - value - }, - null); - if (c != null) { - if (c.moveToFirst()) { - result = true; - } - c.close(); - } - return result; - } - - void addNumberOrAddress(String value, String oldValueToReplace, boolean isSIP) { - if (value == null || value.isEmpty()) { - Log.e("[Contact] Can't add null or empty number or address"); - return; - } - - if (oldValueToReplace != null) { - if (mAndroidId == null) { - Log.e("[Contact] Can't update a number or address in non existing contact"); - return; - } - - Log.i( - "[Contact] Updating " - + oldValueToReplace - + " by " - + value - + " in contact " - + mAndroidId - + " (" - + mAndroidRawId - + ")"); - if (isSIP) { - String select = - ContactsContract.Data.CONTACT_ID - + "=? AND (" - + ContactsContract.Data.MIMETYPE - + "=? OR " - + ContactsContract.Data.MIMETYPE - + "=? OR " - + ContactsContract.Data.MIMETYPE - + "=?) AND data1=?"; - String[] args = - new String[] { - mAndroidId, - "vnd.android.cursor.item/org.linphone.profile", // Old value - ContactsManager.getInstance() - .getString(R.string.linphone_address_mime_type), - ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE, - oldValueToReplace - }; - - addChangesToCommit( - ContentProviderOperation.newUpdate(Data.CONTENT_URI) - .withSelection(select, args) - .withValue( - Data.MIMETYPE, - ContactsManager.getInstance() - .getString(R.string.linphone_address_mime_type)) - .withValue("data1", value) // Value - .withValue( - "data2", - ContactsManager.getInstance() - .getString(R.string.app_name)) // Summary - .withValue("data3", value) // Detail - .build()); - } else { - String select = - ContactsContract.Data.CONTACT_ID - + "=? AND " - + ContactsContract.Data.MIMETYPE - + "=? AND data1=?"; - String[] args = - new String[] { - mAndroidId, - ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, - oldValueToReplace - }; - - addChangesToCommit( - ContentProviderOperation.newUpdate(Data.CONTENT_URI) - .withSelection(select, args) - .withValue(Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, value) - .withValue( - ContactsContract.CommonDataKinds.Phone.TYPE, - CommonDataKinds.Phone.TYPE_MOBILE) - .build()); - } - } else { - if (mAndroidId == null) { - Log.i("[Contact] Adding number or address " + value + " to new contact."); - if (isSIP) { - addChangesToCommit( - ContentProviderOperation.newInsert(Data.CONTENT_URI) - .withValueBackReference(Data.RAW_CONTACT_ID, 0) - .withValue( - Data.MIMETYPE, - ContactsManager.getInstance() - .getString(R.string.linphone_address_mime_type)) - .withValue("data1", value) // Value - .withValue( - "data2", - ContactsManager.getInstance() - .getString(R.string.app_name)) // Summary - .withValue("data3", value) // Detail - .build()); - } else { - addChangesToCommit( - ContentProviderOperation.newInsert(Data.CONTENT_URI) - .withValueBackReference(Data.RAW_CONTACT_ID, 0) - .withValue( - Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, value) - .withValue( - ContactsContract.CommonDataKinds.Phone.TYPE, - CommonDataKinds.Phone.TYPE_MOBILE) - .build()); - } - } else { - Log.i( - "[Contact] Adding number or address " - + value - + " to existing contact " - + mAndroidId - + " (" - + mAndroidRawId - + ")"); - if (isSIP) { - addChangesToCommit( - ContentProviderOperation.newInsert(Data.CONTENT_URI) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, mAndroidRawId) - .withValue( - Data.MIMETYPE, - ContactsManager.getInstance() - .getString(R.string.linphone_address_mime_type)) - .withValue("data1", value) // Value - .withValue( - "data2", - ContactsManager.getInstance() - .getString(R.string.app_name)) // Summary - .withValue("data3", value) // Detail - .build()); - } else { - addChangesToCommit( - ContentProviderOperation.newInsert(Data.CONTENT_URI) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, mAndroidRawId) - .withValue( - Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, value) - .withValue( - ContactsContract.CommonDataKinds.Phone.TYPE, - CommonDataKinds.Phone.TYPE_MOBILE) - .build()); - } - } - } - } - - void removeNumberOrAddress(String noa, boolean isSIP) { - if (noa == null || noa.isEmpty()) { - Log.e("[Contact] Can't remove null or empty number or address."); - return; - } - - if (mAndroidId == null) { - Log.e("[Contact] Can't remove a number or address from non existing contact"); - } else { - Log.i( - "[Contact] Removing number or address " - + noa - + " from existing contact " - + mAndroidId - + " (" - + mAndroidRawId - + ")"); - if (isSIP) { - String select = - ContactsContract.Data.CONTACT_ID - + "=? AND (" - + ContactsContract.Data.MIMETYPE - + "=? OR " - + ContactsContract.Data.MIMETYPE - + "=?) AND data1=?"; - String[] args = - new String[] { - mAndroidId, - ContactsManager.getInstance() - .getString(R.string.linphone_address_mime_type), - ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE, - noa - }; - - addChangesToCommit( - ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) - .withSelection(select, args) - .build()); - } else { - String select = - ContactsContract.Data.CONTACT_ID - + "=? AND " - + ContactsContract.Data.MIMETYPE - + "=? AND data1=?"; - String[] args = - new String[] { - mAndroidId, - ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, - noa - }; - - addChangesToCommit( - ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) - .withSelection(select, args) - .build()); - } - } - } - - void setOrganization(String org, String previousValue) { - if (org == null || org.isEmpty()) { - if (mAndroidId == null) { - Log.e("[Contact] Can't set organization to null or empty for new contact"); - return; - } - } - if (mAndroidId == null) { - Log.i("[Contact] Setting organization " + org + " to new contact."); - addChangesToCommit( - ContentProviderOperation.newInsert(Data.CONTENT_URI) - .withValueBackReference(Data.RAW_CONTACT_ID, 0) - .withValue( - Data.MIMETYPE, CommonDataKinds.Organization.CONTENT_ITEM_TYPE) - .withValue(CommonDataKinds.Organization.COMPANY, org) - .build()); - } else { - if (previousValue != null) { - String select = - ContactsContract.Data.CONTACT_ID - + "=? AND " - + ContactsContract.Data.MIMETYPE - + "=? AND " - + ContactsContract.CommonDataKinds.Organization.COMPANY - + "=?"; - String[] args = - new String[] { - getAndroidId(), - ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE, - previousValue - }; - - Log.i( - "[Contact] Updating organization " - + org - + " to existing contact " - + mAndroidId - + " (" - + mAndroidRawId - + ")"); - addChangesToCommit( - ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) - .withSelection(select, args) - .withValue( - ContactsContract.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Organization - .CONTENT_ITEM_TYPE) - .withValue( - ContactsContract.CommonDataKinds.Organization.COMPANY, org) - .build()); - } else { - Log.i( - "[Contact] Setting organization " - + org - + " to existing contact " - + mAndroidId - + " (" - + mAndroidRawId - + ")"); - addChangesToCommit( - ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, mAndroidRawId) - .withValue( - ContactsContract.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Organization - .CONTENT_ITEM_TYPE) - .withValue( - ContactsContract.CommonDataKinds.Organization.COMPANY, org) - .build()); - } - } - } - - void setPhoto(byte[] photo) { - if (photo == null) { - Log.e("[Contact] Can't set null picture."); - return; - } - - if (mAndroidRawId == null) { - Log.w("[Contact] Can't set picture for not already created contact, will do it later"); - mTempPicture = photo; - } else { - Log.i( - "[Contact] Setting picture to an already created raw contact [", - mAndroidRawId, - "]"); - try { - long rawId = Long.parseLong(mAndroidRawId); - - Uri rawContactPhotoUri = - Uri.withAppendedPath( - ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawId), - RawContacts.DisplayPhoto.CONTENT_DIRECTORY); - - if (rawContactPhotoUri != null) { - ContentResolver resolver = - LinphoneContext.instance().getApplicationContext().getContentResolver(); - AssetFileDescriptor fd = - resolver.openAssetFileDescriptor(rawContactPhotoUri, "rw"); - OutputStream os = fd.createOutputStream(); - os.write(photo); - os.close(); - fd.close(); - } else { - Log.e( - "[Contact] Failed to get raw contact photo URI for raw contact id [", - rawId, - "], aborting"); - } - } catch (NumberFormatException nfe) { - Log.e("[Contact] Couldn't parse raw id [", mAndroidId, "], aborting"); - } catch (IOException ioe) { - Log.e("[Contact] Couldn't set picture, IO error: ", ioe); - } catch (Exception e) { - Log.e("[Contact] Couldn't set picture, unknown error: ", e); - } - } - } - - private String findRawContactID() { - ContentResolver resolver = - LinphoneContext.instance().getApplicationContext().getContentResolver(); - String result = null; - String[] projection = {ContactsContract.RawContacts._ID}; - - String selection = ContactsContract.RawContacts.CONTACT_ID + "=?"; - Cursor c = - resolver.query( - ContactsContract.RawContacts.CONTENT_URI, - projection, - selection, - new String[] {mAndroidId}, - null); - if (c != null) { - if (c.moveToFirst()) { - result = c.getString(c.getColumnIndex(ContactsContract.RawContacts._ID)); - } - c.close(); - } - return result; - } - - void createRawLinphoneContactFromExistingAndroidContactIfNeeded() { - if (LinphoneContext.instance() - .getApplicationContext() - .getResources() - .getBoolean(R.bool.use_linphone_tag)) { - if (mAndroidId != null && (mAndroidRawId == null || !isAndroidRawIdLinphone)) { - if (mAndroidRawId == null) { - Log.d("[Contact] RAW ID not found for contact " + mAndroidId); - mAndroidRawId = findRawContactID(); - } - Log.d("[Contact] Found RAW ID for contact " + mAndroidId + " : " + mAndroidRawId); - - String linphoneRawId = findLinphoneRawContactId(); - if (linphoneRawId == null) { - Log.d("[Contact] Linphone RAW ID not found for contact " + mAndroidId); - createRawLinphoneContactFromExistingAndroidContact(); - } else { - Log.d( - "[Contact] Linphone RAW ID found for contact " - + mAndroidId - + " : " - + linphoneRawId); - mAndroidRawId = linphoneRawId; - } - isAndroidRawIdLinphone = true; - } - } - } - - private void createRawLinphoneContactFromExistingAndroidContact() { - addChangesToCommit( - ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) - .withValue( - ContactsContract.RawContacts.ACCOUNT_TYPE, - ContactsManager.getInstance().getString(R.string.sync_account_type)) - .withValue( - ContactsContract.RawContacts.ACCOUNT_NAME, - ContactsManager.getInstance().getString(R.string.sync_account_name)) - .withValue( - ContactsContract.RawContacts.AGGREGATION_MODE, - ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT) - .build()); - - addChangesToCommit( - ContentProviderOperation.newUpdate( - ContactsContract.AggregationExceptions.CONTENT_URI) - .withValue( - ContactsContract.AggregationExceptions.TYPE, - ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER) - .withValue( - ContactsContract.AggregationExceptions.RAW_CONTACT_ID1, - mAndroidRawId) - .withValueBackReference( - ContactsContract.AggregationExceptions.RAW_CONTACT_ID2, 0) - .build()); - - Log.i( - "[Contact] Creating linphone RAW contact for contact " - + mAndroidId - + " linked with existing RAW contact " - + mAndroidRawId); - saveChangesCommited(); - } - - private String findLinphoneRawContactId() { - ContentResolver resolver = - LinphoneContext.instance().getApplicationContext().getContentResolver(); - String result = null; - String[] projection = {ContactsContract.RawContacts._ID}; - - String selection = - ContactsContract.RawContacts.CONTACT_ID - + "=? AND " - + ContactsContract.RawContacts.ACCOUNT_TYPE - + "=?"; - Cursor c = - resolver.query( - ContactsContract.RawContacts.CONTENT_URI, - projection, - selection, - new String[] { - mAndroidId, - ContactsManager.getInstance().getString(R.string.sync_account_type) - }, - null); - if (c != null) { - if (c.moveToFirst()) { - result = c.getString(c.getColumnIndex(ContactsContract.RawContacts._ID)); - } - c.close(); - } - return result; - } -} diff --git a/app/src/main/java/org/linphone/contacts/AsyncContactPresence.java b/app/src/main/java/org/linphone/contacts/AsyncContactPresence.java deleted file mode 100644 index c65473b99..000000000 --- a/app/src/main/java/org/linphone/contacts/AsyncContactPresence.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import android.os.AsyncTask; - -public class AsyncContactPresence extends AsyncTask { - - private LinphoneContact mLinphoneContact; - private String mAlias; - - public AsyncContactPresence(LinphoneContact linphoneContact, String alias) { - mLinphoneContact = linphoneContact; - mAlias = alias; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - } - - @Override - protected Void doInBackground(Void... voids) { - mLinphoneContact.addPresenceInfoToNativeContact(mAlias); - return null; - } -} diff --git a/app/src/main/java/org/linphone/contacts/AsyncContactsLoader.java b/app/src/main/java/org/linphone/contacts/AsyncContactsLoader.java deleted file mode 100644 index e9c616e2c..000000000 --- a/app/src/main/java/org/linphone/contacts/AsyncContactsLoader.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.database.Cursor; -import android.os.AsyncTask; -import android.provider.ContactsContract; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.compatibility.Compatibility; -import org.linphone.core.Core; -import org.linphone.core.Friend; -import org.linphone.core.FriendList; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.LinphoneUtils; - -class AsyncContactsLoader extends AsyncTask { - @SuppressLint("InlinedApi") - public static final String[] PROJECTION = { - ContactsContract.Data.CONTACT_ID, - ContactsContract.Contacts.DISPLAY_NAME_PRIMARY, - ContactsContract.Data.MIMETYPE, - ContactsContract.Contacts.STARRED, - "data1", // Company, Phone or SIP Address - "data2", // ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME - "data3", // ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME - "data4", // Normalized phone number - }; - - private Context mContext; - - public AsyncContactsLoader(Context context) { - mContext = context; - } - - @Override - protected void onPreExecute() { - Log.i("[Contacts Manager] Synchronization started"); - if (LinphonePreferences.instance().isFriendlistsubscriptionEnabled()) { - String rls = mContext.getString(R.string.rls_uri); - for (FriendList list : LinphoneManager.getCore().getFriendsLists()) { - if (list.getRlsAddress() == null - || !list.getRlsAddress().asStringUriOnly().equals(rls)) { - list.setRlsUri(rls); - } - list.addListener(ContactsManager.getInstance()); - } - } - } - - @Override - protected AsyncContactsData doInBackground(Void... params) { - Log.i("[Contacts Manager] Background synchronization started"); - - HashMap androidContactsCache = new HashMap<>(); - AsyncContactsData data = new AsyncContactsData(); - List nativeIds = new ArrayList<>(); - - Core core = LinphoneManager.getCore(); - if (core != null) { - FriendList[] friendLists = core.getFriendsLists(); - for (FriendList list : friendLists) { - Friend[] friends = list.getFriends(); - for (Friend friend : friends) { - if (isCancelled()) { - Log.w("[Contacts Manager] Task cancelled"); - return data; - } - - LinphoneContact contact = (LinphoneContact) friend.getUserData(); - if (contact != null) { - if (contact.getAndroidId() != null) { - contact.clearAddresses(); - androidContactsCache.put(contact.getAndroidId(), contact); - nativeIds.add(contact.getAndroidId()); - } else { - data.contacts.add(contact); - } - } else { - if (friend.getRefKey() != null) { - // Friend has a refkey but no LinphoneContact => represents a - // native contact stored in db from a previous version of Linphone, - // remove it - list.removeFriend(friend); - } else { - // No refkey so it's a standalone contact - contact = new LinphoneContact(); - contact.setFriend(friend); - contact.syncValuesFromFriend(); - data.contacts.add(contact); - } - } - } - } - } - - if (ContactsManager.getInstance().hasReadContactsAccess()) { - String selection = null; - if (mContext.getResources().getBoolean(R.bool.fetch_contacts_from_default_directory)) { - Log.i("[Contacts Manager] Only fetching contacts in default directory"); - selection = ContactsContract.Data.IN_DEFAULT_DIRECTORY + " == 1"; - } - - Cursor c = - mContext.getContentResolver() - .query( - ContactsContract.Data.CONTENT_URI, - PROJECTION, - selection, - null, - null); - if (c != null) { - Log.i("[Contacts Manager] Found " + c.getCount() + " entries in cursor"); - while (c.moveToNext()) { - if (isCancelled()) { - Log.w("[Contacts Manager] Task cancelled"); - return data; - } - - try { - String id = c.getString(c.getColumnIndex(ContactsContract.Data.CONTACT_ID)); - boolean starred = - c.getInt(c.getColumnIndex(ContactsContract.Contacts.STARRED)) == 1; - - LinphoneContact contact = androidContactsCache.get(id); - if (contact == null) { - Log.d( - "[Contacts Manager] Creating LinphoneContact with native ID " - + id - + ", favorite flag is " - + starred); - nativeIds.add(id); - contact = new LinphoneContact(); - contact.setAndroidId(id); - contact.setIsFavourite(starred); - androidContactsCache.put(id, contact); - } - - contact.syncValuesFromAndroidCusor(c); - } catch (IllegalStateException ise) { - Log.e( - "[Contacts Manager] Couldn't get values from cursor, exception: ", - ise); - } - } - c.close(); - } - - FriendList[] friendLists = core.getFriendsLists(); - for (FriendList list : friendLists) { - Friend[] friends = list.getFriends(); - for (Friend friend : friends) { - if (isCancelled()) { - Log.w("[Contacts Manager] Task cancelled"); - return data; - } - - LinphoneContact contact = (LinphoneContact) friend.getUserData(); - if (contact != null && contact.isAndroidContact()) { - String id = contact.getAndroidId(); - if (id != null && !nativeIds.contains(id)) { - Log.i("[Contacts Manager] Contact removed since last fetch: " + id); - // Has been removed since last fetch - androidContactsCache.remove(id); - } - } - } - } - nativeIds.clear(); - } - - Collection contacts = androidContactsCache.values(); - // New friends count will be 0 after the first contacts fetch - Log.i( - "[Contacts Manager] Found " - + contacts.size() - + " native contacts plus " - + data.contacts.size() - + " friends in the configuration file"); - for (LinphoneContact contact : contacts) { - if (isCancelled()) { - Log.w("[Contacts Manager] Task cancelled"); - return data; - } - if (contact.getNumbersOrAddresses().isEmpty()) { - continue; - } - - if (contact.getFullName() == null) { - for (LinphoneNumberOrAddress noa : contact.getNumbersOrAddresses()) { - if (noa.isSIPAddress()) { - contact.setFullName(LinphoneUtils.getAddressDisplayName(noa.getValue())); - Log.w( - "[Contacts Manager] Couldn't find a display name for contact " - + contact.getFullName() - + ", used SIP address display name / username instead..."); - break; - } - } - } - - /*if (contact.getFriend() != null) { - for (LinphoneNumberOrAddress noa : contact.getNumbersOrAddresses()) { - PresenceModel pm = - contact.getFriend().getPresenceModelForUriOrTel(noa.getValue()); - if (pm != null && pm.getBasicStatus().equals(PresenceBasicStatus.Open)) { - data.sipContacts.add(contact); - break; - } - } - }*/ - - if (!mContext.getResources().getBoolean(R.bool.hide_sip_contacts_without_presence)) { - if (contact.hasAddress() && !data.sipContacts.contains(contact)) { - data.sipContacts.add(contact); - } - } - - data.contacts.add(contact); - } - - androidContactsCache.clear(); - - Collections.sort(data.contacts); - Collections.sort(data.sipContacts); - - Log.i("[Contacts Manager] Background synchronization finished"); - return data; - } - - @Override - protected void onPostExecute(AsyncContactsData data) { - Log.i( - "[Contacts Manager] " - + data.contacts.size() - + " contacts found in which " - + data.sipContacts.size() - + " are SIP"); - - for (LinphoneContact contact : data.contacts) { - contact.createOrUpdateFriendFromNativeContact(); - } - - // Now that contact fetching is asynchronous, this is required to ensure - // presence subscription event will be sent with all friends - if (LinphonePreferences.instance().isFriendlistsubscriptionEnabled()) { - Log.i("[Contacts Manager] Matching friends created, updating subscription"); - FriendList[] friendLists = LinphoneManager.getCore().getFriendsLists(); - for (FriendList list : friendLists) { - list.updateSubscriptions(); - } - } - - ContactsManager.getInstance().setContacts(data.contacts); - ContactsManager.getInstance().setSipContacts(data.sipContacts); - - for (ContactsUpdatedListener listener : - ContactsManager.getInstance().getContactsListeners()) { - listener.onContactsUpdated(); - } - - Compatibility.createChatShortcuts(mContext); - Log.i("[Contacts Manager] Synchronization finished"); - } - - class AsyncContactsData { - final List contacts; - final List sipContacts; - - AsyncContactsData() { - contacts = new ArrayList<>(); - sipContacts = new ArrayList<>(); - } - } -} diff --git a/app/src/main/java/org/linphone/contacts/ContactAddress.java b/app/src/main/java/org/linphone/contacts/ContactAddress.java deleted file mode 100644 index cb85b7e04..000000000 --- a/app/src/main/java/org/linphone/contacts/ContactAddress.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import android.view.View; -import java.io.Serializable; -import org.linphone.core.Address; -import org.linphone.core.Factory; -import org.linphone.core.FriendCapability; - -public class ContactAddress implements Serializable { - private LinphoneContact mContact; - private String mAddress; - private String mPhoneNumber; - private boolean mIsAdmin = false; - private transient View mView; - - public ContactAddress(LinphoneContact c, String a, String pn) { - init(c, a, pn); - } - - public ContactAddress(LinphoneContact c, String a, String pn, boolean isAdmin) { - init(c, a, pn); - mIsAdmin = isAdmin; - } - - public boolean isAdmin() { - return mIsAdmin; - } - - public void setAdmin(boolean admin) { - mIsAdmin = admin; - } - - public View getView() { - return mView; - } - - public void setView(View v) { - mView = v; - } - - public LinphoneContact getContact() { - return mContact; - } - - public String getAddressAsDisplayableString() { - Address addr = getAddress(); - if (addr != null && addr.getUsername() != null) return addr.asStringUriOnly(); - return mAddress; - } - - public Address getAddress() { - String presence = null; - if (mContact != null) { - presence = - mContact.getContactFromPresenceModelForUriOrTel( - (mPhoneNumber != null && !mPhoneNumber.isEmpty()) - ? mPhoneNumber - : mAddress); - } - Address addr = Factory.instance().createAddress(presence != null ? presence : mAddress); - // Remove the user=phone URI param if existing, it will break everything otherwise - if (addr.hasUriParam("user")) { - addr.removeUriParam("user"); - } - return addr; - } - - public String getDisplayName() { - if (mAddress != null) { - Address addr = Factory.instance().createAddress(mAddress); - if (addr != null) { - return addr.getDisplayName(); - } - } - return null; - } - - public String getUsername() { - if (mAddress != null) { - Address addr = Factory.instance().createAddress(mAddress); - if (addr != null) { - return addr.getUsername(); - } - } - return null; - } - - public String getPhoneNumber() { - return mPhoneNumber; - } - - public boolean hasCapability(FriendCapability capability) { - return mContact != null && mContact.hasFriendCapability(capability); - } - - private void init(LinphoneContact c, String a, String pn) { - mContact = c; - mAddress = a; - mPhoneNumber = pn; - } - - @Override - public boolean equals(Object other) { - if (other == null) return false; - if (other == this) return true; - if (!(other instanceof ContactAddress)) return false; - return ((ContactAddress) other) - .getAddressAsDisplayableString() - .equals(getAddressAsDisplayableString()); - } -} diff --git a/app/src/main/java/org/linphone/contacts/ContactDetailsFragment.java b/app/src/main/java/org/linphone/contacts/ContactDetailsFragment.java deleted file mode 100644 index 01fc667c9..000000000 --- a/app/src/main/java/org/linphone/contacts/ContactDetailsFragment.java +++ /dev/null @@ -1,433 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import android.annotation.SuppressLint; -import android.app.Dialog; -import android.app.Fragment; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TableLayout; -import android.widget.TextView; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.activities.MainActivity; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.core.Address; -import org.linphone.core.ChatRoom; -import org.linphone.core.ChatRoomBackend; -import org.linphone.core.ChatRoomListenerStub; -import org.linphone.core.ChatRoomParams; -import org.linphone.core.Core; -import org.linphone.core.Factory; -import org.linphone.core.FriendCapability; -import org.linphone.core.PresenceBasicStatus; -import org.linphone.core.PresenceModel; -import org.linphone.core.ProxyConfig; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.LinphoneUtils; - -public class ContactDetailsFragment extends Fragment implements ContactsUpdatedListener { - private LinphoneContact mContact; - private TextView mOrganization; - private RelativeLayout mWaitLayout; - private LayoutInflater mInflater; - private View mView; - private boolean mDisplayChatAddressOnly = false; - private ChatRoom mChatRoom; - private ChatRoomListenerStub mChatRoomCreationListener; - - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - mContact = (LinphoneContact) getArguments().getSerializable("Contact"); - if (mContact == null) { - if (savedInstanceState != null) { - mContact = (LinphoneContact) savedInstanceState.get("Contact"); - } - } - - mInflater = inflater; - mView = inflater.inflate(R.layout.contact, container, false); - - if (getArguments() != null) { - mDisplayChatAddressOnly = getArguments().getBoolean("ChatAddressOnly"); - } - - mWaitLayout = mView.findViewById(R.id.waitScreen); - mWaitLayout.setVisibility(View.GONE); - - ImageView editContact = mView.findViewById(R.id.editContact); - editContact.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - ((ContactsActivity) getActivity()).showContactEdit(mContact); - } - }); - - if (mContact != null - && getResources().getBoolean(R.bool.forbid_pure_linphone_contacts_edition)) { - editContact.setVisibility(mContact.isAndroidContact() ? View.VISIBLE : View.GONE); - } - - ImageView deleteContact = mView.findViewById(R.id.deleteContact); - deleteContact.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - final Dialog dialog = - ((ContactsActivity) getActivity()) - .displayDialog(getString(R.string.delete_text)); - Button delete = dialog.findViewById(R.id.dialog_delete_button); - Button cancel = dialog.findViewById(R.id.dialog_cancel_button); - - delete.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View view) { - mContact.delete(); - // To ensure removed contact won't appear in the contacts - // list anymore - ContactsManager.getInstance().fetchContactsAsync(); - ((ContactsActivity) getActivity()).goBack(); - dialog.dismiss(); - } - }); - - cancel.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View view) { - dialog.dismiss(); - } - }); - dialog.show(); - } - }); - - mOrganization = mView.findViewById(R.id.contactOrganization); - - ImageView back = mView.findViewById(R.id.back); - back.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - ((ContactsActivity) getActivity()).goBack(); - } - }); - back.setVisibility( - getResources().getBoolean(R.bool.isTablet) ? View.INVISIBLE : View.VISIBLE); - - mChatRoomCreationListener = - new ChatRoomListenerStub() { - @Override - public void onStateChanged(ChatRoom cr, ChatRoom.State newState) { - if (newState == ChatRoom.State.Created) { - mWaitLayout.setVisibility(View.GONE); - ((ContactsActivity) getActivity()) - .showChatRoom(cr.getLocalAddress(), cr.getPeerAddress()); - } else if (newState == ChatRoom.State.CreationFailed) { - mWaitLayout.setVisibility(View.GONE); - ((ContactsActivity) getActivity()).displayChatRoomError(); - Log.e( - "Group chat room for address " - + cr.getPeerAddress() - + " has failed !"); - } - } - }; - - return mView; - } - - @Override - public void onContactsUpdated() { - LinphoneContact contact = - ContactsManager.getInstance().findContactFromAndroidId(mContact.getAndroidId()); - if (contact != null) { - changeDisplayedContact(contact); - } - } - - @Override - public void onResume() { - super.onResume(); - - ContactsManager.getInstance().addContactsListener(this); - displayContact(mInflater, mView); - } - - @Override - public void onPause() { - if (mChatRoom != null) { - mChatRoom.removeListener(mChatRoomCreationListener); - } - ContactsManager.getInstance().removeContactsListener(this); - super.onPause(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putSerializable("Contact", mContact); - } - - private void changeDisplayedContact(LinphoneContact newContact) { - mContact = newContact; - displayContact(mInflater, mView); - } - - @SuppressLint("InflateParams") - private void displayContact(LayoutInflater inflater, View view) { - if (mContact == null) return; - ContactAvatar.displayAvatar(mContact, view.findViewById(R.id.avatar_layout)); - - boolean isOrgVisible = LinphonePreferences.instance().isDisplayContactOrganization(); - if (mContact != null - && mContact.getOrganization() != null - && !mContact.getOrganization().isEmpty() - && isOrgVisible) { - mOrganization.setText(mContact.getOrganization()); - } else { - mOrganization.setVisibility(View.GONE); - } - - TextView contactName = view.findViewById(R.id.contact_name); - contactName.setText(mContact.getFullName()); - mOrganization.setText( - (mContact.getOrganization() != null) ? mContact.getOrganization() : ""); - - TableLayout controls = view.findViewById(R.id.controls); - controls.removeAllViews(); - for (LinphoneNumberOrAddress noa : mContact.getNumbersOrAddresses()) { - boolean skip; - View v = inflater.inflate(R.layout.contact_control_cell, null); - - String value = noa.getValue(); - String displayedNumberOrAddress = value; - if (getResources() - .getBoolean(R.bool.only_show_address_username_if_matches_default_domain)) { - displayedNumberOrAddress = LinphoneUtils.getDisplayableUsernameFromAddress(value); - } - - TextView label = v.findViewById(R.id.address_label); - if (noa.isSIPAddress()) { - label.setText(R.string.sip_address); - skip = getResources().getBoolean(R.bool.hide_contact_sip_addresses); - } else { - label.setText(R.string.phone_number); - skip = getResources().getBoolean(R.bool.hide_contact_phone_numbers); - } - - TextView tv = v.findViewById(R.id.numeroOrAddress); - tv.setText(displayedNumberOrAddress); - tv.setSelected(true); - - ProxyConfig lpc = LinphoneManager.getCore().getDefaultProxyConfig(); - if (lpc != null) { - String username = lpc.normalizePhoneNumber(displayedNumberOrAddress); - if (username != null) { - value = LinphoneUtils.getFullAddressFromUsername(username); - } - } - - v.findViewById(R.id.friendLinphone).setVisibility(View.GONE); - if (mContact.getFriend() != null) { - PresenceModel pm = mContact.getFriend().getPresenceModelForUriOrTel(noa.getValue()); - if (pm != null && pm.getBasicStatus().equals(PresenceBasicStatus.Open)) { - v.findViewById(R.id.friendLinphone).setVisibility(View.VISIBLE); - } else { - if (getResources() - .getBoolean(R.bool.hide_numbers_and_addresses_without_presence)) { - skip = true; - } - } - } - - v.findViewById(R.id.inviteFriend).setVisibility(View.GONE); - if (!noa.isSIPAddress() - && v.findViewById(R.id.friendLinphone).getVisibility() == View.GONE - && !getResources().getBoolean(R.bool.hide_invite_contact)) { - v.findViewById(R.id.inviteFriend).setVisibility(View.VISIBLE); - v.findViewById(R.id.inviteFriend).setTag(noa.getNormalizedPhone()); - v.findViewById(R.id.inviteFriend) - .setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - String number = (String) v.getTag(); - Intent smsIntent = new Intent(Intent.ACTION_SENDTO); - smsIntent.putExtra("address", number); - smsIntent.setData(Uri.parse("smsto:" + number)); - String text = - getString(R.string.invite_friend_text) - .replace( - "%s", - getString(R.string.download_link)); - smsIntent.putExtra("sms_body", text); - startActivity(smsIntent); - } - }); - } - - String contactAddress = mContact.getContactFromPresenceModelForUriOrTel(noa.getValue()); - - if (!mDisplayChatAddressOnly) { - v.findViewById(R.id.contact_call) - .setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - String tag = (String) v.getTag(); - ((MainActivity) getActivity()).newOutgoingCall(tag); - } - }); - if (contactAddress != null) { - v.findViewById(R.id.contact_call).setTag(contactAddress); - } else { - v.findViewById(R.id.contact_call).setTag(value); - } - } else { - v.findViewById(R.id.contact_call).setVisibility(View.GONE); - } - - v.findViewById(R.id.contact_chat) - .setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - goToChat((String) v.getTag(), false); - } - }); - v.findViewById(R.id.contact_chat_secured) - .setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - goToChat((String) v.getTag(), true); - } - }); - if (contactAddress != null) { - v.findViewById(R.id.contact_chat).setTag(contactAddress); - v.findViewById(R.id.contact_chat_secured).setTag(contactAddress); - } else { - v.findViewById(R.id.contact_chat).setTag(value); - v.findViewById(R.id.contact_chat_secured).setTag(value); - } - - if (v.findViewById(R.id.friendLinphone).getVisibility() == View.VISIBLE - && mContact.hasPresenceModelForUriOrTelCapability( - noa.getValue(), FriendCapability.LimeX3Dh)) { - v.findViewById(R.id.contact_chat_secured).setVisibility(View.VISIBLE); - } else { - v.findViewById(R.id.contact_chat_secured).setVisibility(View.GONE); - } - - if (getResources().getBoolean(R.bool.force_end_to_end_encryption_in_chat)) { - v.findViewById(R.id.contact_chat).setVisibility(View.GONE); - } - if (getResources().getBoolean(R.bool.disable_chat)) { - v.findViewById(R.id.contact_chat).setVisibility(View.GONE); - v.findViewById(R.id.contact_chat_secured).setVisibility(View.GONE); - } - - if (!skip) { - controls.addView(v); - } - } - } - - private void goToChat(String tag, boolean isSecured) { - Core core = LinphoneManager.getCore(); - if (core == null) return; - - Address participant = Factory.instance().createAddress(tag); - if (participant == null) { - Log.e("[Contact Detail] Couldn't parse ", tag); - return; - } - ProxyConfig defaultProxyConfig = core.getDefaultProxyConfig(); - - if (defaultProxyConfig != null) { - ChatRoom room = - core.findOneToOneChatRoom( - defaultProxyConfig.getIdentityAddress(), participant, isSecured); - if (room != null) { - ((ContactsActivity) getActivity()) - .showChatRoom(room.getLocalAddress(), room.getPeerAddress()); - } else { - if (defaultProxyConfig.getConferenceFactoryUri() != null - && (isSecured - || !LinphonePreferences.instance().useBasicChatRoomFor1To1())) { - mWaitLayout.setVisibility(View.VISIBLE); - - ChatRoomParams params = core.createDefaultChatRoomParams(); - params.enableEncryption(isSecured); - params.enableGroup(false); - // We don't want a basic chat room, - // so if isSecured is false we have to set this manually - params.setBackend(ChatRoomBackend.FlexisipChat); - - Address[] participants = new Address[1]; - participants[0] = participant; - - mChatRoom = - core.createChatRoom( - params, - getString(R.string.dummy_group_chat_subject), - participants); - if (mChatRoom != null) { - mChatRoom.addListener(mChatRoomCreationListener); - } else { - Log.w("[Contact Details Fragment] createChatRoom returned null..."); - mWaitLayout.setVisibility(View.GONE); - } - } else { - room = core.getChatRoom(participant); - if (room != null) { - ((ContactsActivity) getActivity()) - .showChatRoom(room.getLocalAddress(), room.getPeerAddress()); - } - } - } - } else { - if (isSecured) { - Log.e( - "[Contact Details Fragment] Can't create a secured chat room without proxy config"); - return; - } - - ChatRoom room = core.getChatRoom(participant); - if (room != null) { - ((ContactsActivity) getActivity()) - .showChatRoom(room.getLocalAddress(), room.getPeerAddress()); - } - } - } -} diff --git a/app/src/main/java/org/linphone/contacts/ContactEditorFragment.java b/app/src/main/java/org/linphone/contacts/ContactEditorFragment.java deleted file mode 100644 index e8bf17f8a..000000000 --- a/app/src/main/java/org/linphone/contacts/ContactEditorFragment.java +++ /dev/null @@ -1,720 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.Dialog; -import android.app.Fragment; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.ExifInterface; -import android.net.Uri; -import android.os.Bundle; -import android.os.Parcelable; -import android.provider.MediaStore; -import android.text.Editable; -import android.text.InputType; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.LinearLayout; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import org.linphone.R; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.core.tools.Log; -import org.linphone.mediastream.Version; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.FileUtils; -import org.linphone.utils.ImageUtils; -import org.linphone.utils.LinphoneUtils; - -public class ContactEditorFragment extends Fragment { - private static final int ADD_PHOTO = 1337; - private static final int PHOTO_SIZE = 128; - - private View mView; - private ImageView mOk; - private ImageView mContactPicture; - private EditText mFirstName, mLastName, mOrganization; - private LayoutInflater mInflater; - private boolean mIsNewContact; - private LinphoneContact mContact; - private List mNumbersAndAddresses; - private int mFirstSipAddressIndex = -1; - private LinearLayout mSipAddresses, mNumbers; - private String mNewSipOrNumberToAdd, mNewDisplayName; - private Uri mPickedPhotoForContactUri; - private byte[] mPhotoToAdd; - - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - mInflater = inflater; - - mContact = null; - mIsNewContact = true; - - if (getArguments() != null) { - mContact = (LinphoneContact) getArguments().getSerializable("Contact"); - if (getArguments().containsKey("SipUri")) { - mNewSipOrNumberToAdd = getArguments().getString("SipUri"); - } - if (getArguments().containsKey("DisplayName")) { - mNewDisplayName = getArguments().getString("DisplayName"); - } - } else if (savedInstanceState != null) { - mContact = (LinphoneContact) savedInstanceState.get("Contact"); - mNewSipOrNumberToAdd = savedInstanceState.getString("SipUri"); - mNewDisplayName = savedInstanceState.getString("DisplayName"); - } - if (mContact != null) { - mContact.createRawLinphoneContactFromExistingAndroidContactIfNeeded(); - mIsNewContact = false; - } - - mView = inflater.inflate(R.layout.contact_edit, container, false); - - LinearLayout phoneNumbersSection = mView.findViewById(R.id.phone_numbers); - if (getResources().getBoolean(R.bool.hide_phone_numbers_in_editor) - || !ContactsManager.getInstance().hasReadContactsAccess()) { - // Currently linphone friends don't support phone mNumbers, so hide them - phoneNumbersSection.setVisibility(View.GONE); - } - - LinearLayout sipAddressesSection = mView.findViewById(R.id.sip_addresses); - if (getResources().getBoolean(R.bool.hide_sip_addresses_in_editor)) { - sipAddressesSection.setVisibility(View.GONE); - } - - ImageView deleteContact = mView.findViewById(R.id.delete_contact); - - ImageView cancel = mView.findViewById(R.id.cancel); - cancel.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - ((ContactsActivity) getActivity()).goBack(); - } - }); - - mOk = mView.findViewById(R.id.ok); - mOk.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - if (mIsNewContact) { - boolean areAllFielsEmpty = true; - for (LinphoneNumberOrAddress nounoa : mNumbersAndAddresses) { - String value = nounoa.getValue(); - if (value != null && !value.trim().isEmpty()) { - areAllFielsEmpty = false; - break; - } - } - if (areAllFielsEmpty) { - Log.i( - "[Contact Editor] All SIP and phone fields are empty, aborting"); - getFragmentManager().popBackStackImmediate(); - return; - } - mContact = LinphoneContact.createContact(); - } - - mContact.setFirstNameAndLastName( - mFirstName.getText().toString(), - mLastName.getText().toString(), - true); - - if (mPhotoToAdd != null) { - Log.i("[Contact Editor] Found picture to set to contact"); - mContact.setPhoto(mPhotoToAdd); - } - - for (LinphoneNumberOrAddress noa : mNumbersAndAddresses) { - String value = noa.getValue(); - String oldValue = noa.getOldValue(); - - if (value == null || value.trim().isEmpty()) { - if (oldValue != null && !oldValue.isEmpty()) { - Log.i("[Contact Editor] Removing number: ", oldValue); - mContact.removeNumberOrAddress(noa); - } - } else { - if (oldValue != null && oldValue.equals(value)) { - Log.i("[Contact Editor] Keeping existing number: ", value); - continue; - } - - if (noa.isSIPAddress()) { - noa.setValue(LinphoneUtils.getFullAddressFromUsername(value)); - } - Log.i("[Contact Editor] Adding new number: ", value); - - mContact.addOrUpdateNumberOrAddress(noa); - } - } - - if (!mOrganization.getText().toString().isEmpty() || !mIsNewContact) { - Log.i("[Contact Editor] Setting organization field: ", mOrganization); - mContact.setOrganization(mOrganization.getText().toString(), true); - } - - mContact.save(); - - if (mIsNewContact) { - // Ensure fetch will be done so the new contact appears in the contacts - // list: contacts content observer may not be notified if contacts sync - // is disabled at system level - Log.i( - "[Contact Editor] New contact created, starting fetch contacts task"); - ContactsManager.getInstance().fetchContactsAsync(); - } - - getFragmentManager().popBackStack(); - if (mIsNewContact || getResources().getBoolean(R.bool.isTablet)) { - ((ContactsActivity) getActivity()).showContactDetails(mContact); - } - } - }); - - mLastName = mView.findViewById(R.id.contactLastName); - // Hack to display keyboard when touching focused edittext on Nexus One - if (Version.sdkStrictlyBelow(Version.API11_HONEYCOMB_30)) { - mLastName.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - InputMethodManager imm = - (InputMethodManager) - getActivity() - .getSystemService(Context.INPUT_METHOD_SERVICE); - imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); - } - }); - } - mLastName.addTextChangedListener( - new TextWatcher() { - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - mOk.setEnabled( - mLastName.getText().length() > 0 - || mFirstName.getText().length() > 0); - } - - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void afterTextChanged(Editable s) {} - }); - - mFirstName = mView.findViewById(R.id.contactFirstName); - mFirstName.addTextChangedListener( - new TextWatcher() { - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - mOk.setEnabled( - mFirstName.getText().length() > 0 - || mLastName.getText().length() > 0); - } - - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void afterTextChanged(Editable s) {} - }); - - mOrganization = mView.findViewById(R.id.contactOrganization); - - if (!mIsNewContact) { - String fn = mContact.getFirstName(); - String ln = mContact.getLastName(); - if (fn != null || ln != null) { - mFirstName.setText(fn); - mLastName.setText(ln); - } else { - mLastName.setText(mContact.getFullName()); - mFirstName.setText(""); - } - - deleteContact.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - final Dialog dialog = - ((ContactsActivity) getActivity()) - .displayDialog(getString(R.string.delete_text)); - Button delete = dialog.findViewById(R.id.dialog_delete_button); - Button cancel = dialog.findViewById(R.id.dialog_cancel_button); - - delete.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View view) { - mContact.delete(); - ((ContactsActivity) getActivity()).goBack(); - dialog.dismiss(); - } - }); - - cancel.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View view) { - dialog.dismiss(); - } - }); - dialog.show(); - } - }); - } else { - deleteContact.setVisibility(View.INVISIBLE); - } - - mContactPicture = mView.findViewById(R.id.contact_picture); - mContactPicture.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View view) { - ContactsActivity contactsActivity = ((ContactsActivity) getActivity()); - if (contactsActivity != null) { - String[] permissions = { - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.CAMERA - }; - if (contactsActivity.checkPermissions(permissions)) { - pickImage(); - } else { - contactsActivity.requestPermissionsIfNotGranted(permissions); - } - } - } - }); - - mNumbersAndAddresses = new ArrayList<>(); - - ImageView addSipAddress = mView.findViewById(R.id.add_address_field); - if (getResources().getBoolean(R.bool.allow_only_one_sip_address)) { - addSipAddress.setVisibility(View.GONE); - } - addSipAddress.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View view) { - addEmptyRowToAllowNewNumberOrAddress(mSipAddresses, true); - } - }); - - ImageView addNumber = mView.findViewById(R.id.add_number_field); - if (getResources().getBoolean(R.bool.allow_only_one_phone_number)) { - addNumber.setVisibility(View.GONE); - } - addNumber.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View view) { - addEmptyRowToAllowNewNumberOrAddress(mNumbers, false); - } - }); - - mLastName.requestFocus(); - - return mView; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putSerializable("Contact", mContact); - outState.putString("SipUri", mNewSipOrNumberToAdd); - outState.putString("DisplayName", mNewDisplayName); - } - - @Override - public void onResume() { - super.onResume(); - - displayContact(); - - // Force hide keyboard - getActivity() - .getWindow() - .setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN - | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); - } - - @Override - public void onPause() { - // Force hide keyboard - InputMethodManager imm = - (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); - View view = getActivity().getCurrentFocus(); - if (imm != null && view != null) { - imm.hideSoftInputFromWindow(view.getWindowToken(), 0); - } - - super.onPause(); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == ADD_PHOTO && resultCode == Activity.RESULT_OK) { - if (data != null && data.getExtras() != null && data.getExtras().get("data") != null) { - Bitmap bm = (Bitmap) data.getExtras().get("data"); - editContactPicture(null, bm); - } else if (data != null && data.getData() != null) { - Uri selectedImageUri = data.getData(); - String filePath = FileUtils.getRealPathFromURI(getActivity(), selectedImageUri); - if (filePath != null) { - editContactPicture(filePath, null); - } else { - try { - Bitmap selectedImage = - MediaStore.Images.Media.getBitmap( - getActivity().getContentResolver(), selectedImageUri); - editContactPicture(null, selectedImage); - } catch (IOException e) { - Log.e("[Contact Editor] IO error: ", e); - } - } - } else if (mPickedPhotoForContactUri != null) { - String filePath = mPickedPhotoForContactUri.getPath(); - editContactPicture(filePath, null); - } else { - File file = - new File( - FileUtils.getStorageDirectory(getActivity()), - getString(R.string.temp_photo_name)); - if (file.exists()) { - mPickedPhotoForContactUri = Uri.fromFile(file); - String filePath = mPickedPhotoForContactUri.getPath(); - editContactPicture(filePath, null); - } - } - } else { - super.onActivityResult(requestCode, resultCode, data); - } - } - - private void displayContact() { - boolean isOrgVisible = LinphonePreferences.instance().isDisplayContactOrganization(); - if (!isOrgVisible) { - mOrganization.setVisibility(View.GONE); - mView.findViewById(R.id.contactOrganizationTitle).setVisibility(View.GONE); - } else { - if (!mIsNewContact) { - mOrganization.setText(mContact.getOrganization()); - } - } - - if (mPhotoToAdd == null) { - if (mContact != null) { - ContactAvatar.displayAvatar(mContact, mView.findViewById(R.id.avatar_layout)); - } else { - ContactAvatar.displayAvatar("", mView.findViewById(R.id.avatar_layout)); - } - } - - mSipAddresses = initSipAddressFields(mContact); - mNumbers = initNumbersFields(mContact); - } - - private void pickImage() { - mPickedPhotoForContactUri = null; - List cameraIntents = new ArrayList<>(); - - // Handles image & video picking - Intent galleryIntent = new Intent(Intent.ACTION_PICK); - galleryIntent.setType("image/*"); - - // Allows to capture directly from the camera - Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - File file = - new File( - FileUtils.getStorageDirectory(getActivity()), - getString(R.string.temp_photo_name_with_date) - .replace("%s", System.currentTimeMillis() + ".jpeg")); - mPickedPhotoForContactUri = Uri.fromFile(file); - captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, mPickedPhotoForContactUri); - cameraIntents.add(captureIntent); - - Intent chooserIntent = - Intent.createChooser(galleryIntent, getString(R.string.image_picker_title)); - chooserIntent.putExtra( - Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toArray(new Parcelable[] {})); - - startActivityForResult(chooserIntent, ADD_PHOTO); - } - - private void editContactPicture(String filePath, Bitmap image) { - int orientation = ExifInterface.ORIENTATION_NORMAL; - - if (image == null) { - Log.i( - "[Contact Editor] Bitmap is null, trying to decode image from file [", - filePath, - "]"); - image = BitmapFactory.decodeFile(filePath); - - try { - ExifInterface ei = new ExifInterface(filePath); - orientation = - ei.getAttributeInt( - ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); - Log.i("[Contact Editor] Exif rotation is ", orientation); - } catch (IOException e) { - Log.e("[Contact Editor] Failed to get Exif rotation, error is ", e); - } - } - - if (image == null) { - Log.e( - "[Contact Editor] Couldn't get bitmap from either filePath [", - filePath, - "] nor image"); - return; - } - - switch (orientation) { - case ExifInterface.ORIENTATION_ROTATE_90: - image = ImageUtils.rotateImage(image, 90); - break; - case ExifInterface.ORIENTATION_ROTATE_180: - image = ImageUtils.rotateImage(image, 180); - break; - case ExifInterface.ORIENTATION_ROTATE_270: - image = ImageUtils.rotateImage(image, 270); - break; - case ExifInterface.ORIENTATION_NORMAL: - // Nothing to do - break; - default: - Log.w("[Contact Editor] Unexpected orientation ", orientation); - } - - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - image.compress(Bitmap.CompressFormat.JPEG, 100, stream); - mPhotoToAdd = stream.toByteArray(); - - Bitmap roundPicture = ImageUtils.getRoundBitmap(image); - ContactAvatar.displayAvatar(roundPicture, mView.findViewById(R.id.avatar_layout)); - image.recycle(); - } - - private LinearLayout initNumbersFields(final LinphoneContact contact) { - LinearLayout controls = mView.findViewById(R.id.controls_numbers); - controls.removeAllViews(); - - if (contact != null) { - for (LinphoneNumberOrAddress numberOrAddress : contact.getNumbersOrAddresses()) { - if (!numberOrAddress.isSIPAddress()) { - View view = displayNumberOrAddress(controls, numberOrAddress.getValue(), false); - if (view != null) controls.addView(view); - } - } - } - - if (mNewSipOrNumberToAdd != null) { - boolean isSip = - LinphoneUtils.isStrictSipAddress(mNewSipOrNumberToAdd) - || !LinphoneUtils.isNumberAddress(mNewSipOrNumberToAdd); - if (!isSip) { - View view = displayNumberOrAddress(controls, mNewSipOrNumberToAdd, false); - if (view != null) controls.addView(view); - } - } - - if (mNewDisplayName != null) { - EditText lastNameEditText = mView.findViewById(R.id.contactLastName); - if (mView != null) lastNameEditText.setText(mNewDisplayName); - } - - if (controls.getChildCount() == 0) { - addEmptyRowToAllowNewNumberOrAddress(controls, false); - } - - return controls; - } - - private LinearLayout initSipAddressFields(final LinphoneContact contact) { - LinearLayout controls = mView.findViewById(R.id.controls_sip_address); - controls.removeAllViews(); - - if (contact != null) { - for (LinphoneNumberOrAddress numberOrAddress : contact.getNumbersOrAddresses()) { - if (numberOrAddress.isSIPAddress()) { - View view = displayNumberOrAddress(controls, numberOrAddress.getValue(), true); - if (view != null) controls.addView(view); - } - } - } - - if (mNewSipOrNumberToAdd != null) { - boolean isSip = - LinphoneUtils.isStrictSipAddress(mNewSipOrNumberToAdd) - || !LinphoneUtils.isNumberAddress(mNewSipOrNumberToAdd); - if (isSip) { - View view = displayNumberOrAddress(controls, mNewSipOrNumberToAdd, true); - if (view != null) controls.addView(view); - } - } - - if (controls.getChildCount() == 0) { - addEmptyRowToAllowNewNumberOrAddress(controls, true); - } - - return controls; - } - - private View displayNumberOrAddress( - final LinearLayout controls, String numberOrAddress, boolean isSIP) { - String displayedNumberOrAddress = numberOrAddress; - if (isSIP) { - if (mFirstSipAddressIndex == -1) { - mFirstSipAddressIndex = controls.getChildCount(); - } - - if (getResources() - .getBoolean(R.bool.only_show_address_username_if_matches_default_domain)) { - displayedNumberOrAddress = - LinphoneUtils.getDisplayableUsernameFromAddress(numberOrAddress); - } - } - if ((getResources().getBoolean(R.bool.hide_phone_numbers_in_editor) && !isSIP) - || (getResources().getBoolean(R.bool.hide_sip_addresses_in_editor) && isSIP)) { - return null; - } - - LinphoneNumberOrAddress tempNounoa; - if (mIsNewContact || mNewSipOrNumberToAdd != null) { - tempNounoa = new LinphoneNumberOrAddress(numberOrAddress, isSIP); - } else { - tempNounoa = new LinphoneNumberOrAddress(numberOrAddress, isSIP, numberOrAddress); - } - final LinphoneNumberOrAddress nounoa = tempNounoa; - mNumbersAndAddresses.add(nounoa); - - final View view = mInflater.inflate(R.layout.contact_edit_cell, null); - - final EditText noa = view.findViewById(R.id.numoraddr); - if (!isSIP) { - noa.setInputType(InputType.TYPE_CLASS_PHONE); - } - noa.setText(displayedNumberOrAddress); - noa.addTextChangedListener( - new TextWatcher() { - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - nounoa.setValue(noa.getText().toString()); - } - - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void afterTextChanged(Editable s) {} - }); - - ImageView delete = view.findViewById(R.id.delete_field); - if ((getResources().getBoolean(R.bool.allow_only_one_phone_number) && !isSIP) - || (getResources().getBoolean(R.bool.allow_only_one_sip_address) && isSIP)) { - delete.setVisibility(View.GONE); - } - delete.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - if (mContact != null) { - mContact.removeNumberOrAddress(nounoa); - } - mNumbersAndAddresses.remove(nounoa); - view.setVisibility(View.GONE); - } - }); - return view; - } - - @SuppressLint("InflateParams") - private void addEmptyRowToAllowNewNumberOrAddress( - final LinearLayout controls, final boolean isSip) { - final View view = mInflater.inflate(R.layout.contact_edit_cell, null); - final LinphoneNumberOrAddress nounoa = new LinphoneNumberOrAddress(null, isSip); - - final EditText noa = view.findViewById(R.id.numoraddr); - mNumbersAndAddresses.add(nounoa); - noa.setHint(isSip ? getString(R.string.sip_address) : getString(R.string.phone_number)); - if (!isSip) { - noa.setInputType(InputType.TYPE_CLASS_PHONE); - noa.setHint(R.string.phone_number); - } else { - noa.setHint(R.string.sip_address); - } - noa.requestFocus(); - noa.addTextChangedListener( - new TextWatcher() { - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - nounoa.setValue(noa.getText().toString()); - } - - @Override - public void beforeTextChanged( - CharSequence s, int start, int count, int after) {} - - @Override - public void afterTextChanged(Editable s) {} - }); - - final ImageView delete = view.findViewById(R.id.delete_field); - if ((getResources().getBoolean(R.bool.allow_only_one_phone_number) && !isSip) - || (getResources().getBoolean(R.bool.allow_only_one_sip_address) && isSip)) { - delete.setVisibility(View.GONE); - } - delete.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - mNumbersAndAddresses.remove(nounoa); - view.setVisibility(View.GONE); - } - }); - - controls.addView(view); - } -} diff --git a/app/src/main/java/org/linphone/contacts/ContactViewHolder.java b/app/src/main/java/org/linphone/contacts/ContactViewHolder.java deleted file mode 100644 index b18cd6ca5..000000000 --- a/app/src/main/java/org/linphone/contacts/ContactViewHolder.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import android.view.View; -import android.widget.CheckBox; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; -import androidx.recyclerview.widget.RecyclerView; -import org.linphone.R; - -public class ContactViewHolder extends RecyclerView.ViewHolder - implements View.OnClickListener, View.OnLongClickListener { - public final CheckBox delete; - public final ImageView linphoneFriend; - public final TextView name; - public final LinearLayout separator; - public final TextView separatorText; - public final RelativeLayout avatarLayout; - public final TextView organization; - private final ClickListener mListener; - - public ContactViewHolder(View view, ClickListener listener) { - super(view); - - delete = view.findViewById(R.id.delete); - linphoneFriend = view.findViewById(R.id.friendLinphone); - name = view.findViewById(R.id.name); - separator = view.findViewById(R.id.separator); - separatorText = view.findViewById(R.id.separator_text); - avatarLayout = view.findViewById(R.id.avatar_layout); - organization = view.findViewById(R.id.contactOrganization); - // friendStatus = view.findViewById(R.id.friendStatus); - mListener = listener; - view.setOnClickListener(this); - view.setOnLongClickListener(this); - } - - @Override - public void onClick(View view) { - if (mListener != null) { - mListener.onItemClicked(getAdapterPosition()); - } - } - - public boolean onLongClick(View v) { - if (mListener != null) { - return mListener.onItemLongClicked(getAdapterPosition()); - } - return false; - } - - public interface ClickListener { - void onItemClicked(int position); - - boolean onItemLongClicked(int position); - } -} diff --git a/app/src/main/java/org/linphone/contacts/ContactsActivity.java b/app/src/main/java/org/linphone/contacts/ContactsActivity.java deleted file mode 100644 index 5559e19c8..000000000 --- a/app/src/main/java/org/linphone/contacts/ContactsActivity.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import android.Manifest; -import android.app.Fragment; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.View; -import android.widget.Toast; -import org.linphone.R; -import org.linphone.activities.MainActivity; -import org.linphone.core.tools.Log; - -public class ContactsActivity extends MainActivity { - public static final String NAME = "Contacts"; - - private boolean mEditOnClick; - private String mEditSipUri, mEditDisplayName; - - @Override - protected void onCreate(Bundle savedInstanceState) { - getIntent().putExtra("Activity", NAME); - super.onCreate(savedInstanceState); - - mPermissionsToHave = - new String[] { - Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS - }; - } - - @Override - protected void onStart() { - super.onStart(); - - Fragment currentFragment = getFragmentManager().findFragmentById(R.id.fragmentContainer); - if (currentFragment == null) { - showContactsList(); - - if (getIntent() != null && getIntent().getExtras() != null) { - Bundle extras = getIntent().getExtras(); - Uri uri = getIntent().getData(); - if (uri != null) { - extras.putString("ContactUri", uri.toString()); - } - handleIntentExtras(extras); - } else if (getIntent() != null && getIntent().getData() != null) { - Uri uri = getIntent().getData(); - Bundle bundle = new Bundle(); - bundle.putString("ContactUri", uri.toString()); - handleIntentExtras(bundle); - } else { - if (isTablet()) { - showEmptyChildFragment(); - } - } - } - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - - Bundle bundle = intent.getExtras(); - if (bundle == null) { - bundle = new Bundle(); - } - - // Clean fragments stack upon return - while (getFragmentManager().getBackStackEntryCount() > 0) { - getFragmentManager().popBackStackImmediate(); - } - - if (intent.getData() != null) { - bundle.putString("ContactUri", intent.getDataString()); - } - - handleIntentExtras(bundle); - } - - @Override - protected void onResume() { - super.onResume(); - mContactsSelected.setVisibility(View.VISIBLE); - } - - @Override - protected void onPause() { - // From the moment this activity is leaved, clear these values - mEditDisplayName = null; - mEditSipUri = null; - mEditOnClick = false; - super.onPause(); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putString("EditSipUri", mEditSipUri); - outState.putString("EditDisplayName", mEditDisplayName); - outState.putBoolean("EditOnClick", mEditOnClick); - } - - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - mEditOnClick = savedInstanceState.getBoolean("EditOnClick", false); - mEditSipUri = savedInstanceState.getString("EditSipUri", null); - mEditDisplayName = savedInstanceState.getString("EditDisplayName", null); - } - - @Override - public void goBack() { - // 1 is for the empty fragment on tablets - if (!isTablet() || getFragmentManager().getBackStackEntryCount() > 1) { - if (popBackStack()) { - mEditOnClick = false; - return; - } - } - super.goBack(); - } - - private void handleIntentExtras(Bundle extras) { - if (extras == null) return; - - Fragment currentFragment = getFragmentManager().findFragmentById(R.id.fragmentContainer); - if (currentFragment == null || !(currentFragment instanceof ContactsFragment)) { - showContactsList(); - } - - if (extras.containsKey("ContactUri") || extras.containsKey("ContactId")) { - String contactId = extras.getString("ContactId"); - if (contactId == null) { - String uri = extras.getString("ContactUri"); - Log.i("[Contacts Activity] Found Contact URI " + uri); - Uri contactUri = Uri.parse(uri); - contactId = ContactsManager.getInstance().getAndroidContactIdFromUri(contactUri); - } else { - Log.i("[Contacts Activity] Found Contact ID " + contactId); - } - - LinphoneContact linphoneContact = - ContactsManager.getInstance().findContactFromAndroidId(contactId); - if (linphoneContact != null) { - showContactDetails(linphoneContact); - } - } else if (extras.containsKey("Contact")) { - LinphoneContact contact = (LinphoneContact) extras.get("Contact"); - Log.i("[Contacts Activity] Found Contact " + contact); - if (extras.containsKey("Edit")) { - showContactEdit(contact, extras, true); - } else { - showContactDetails(contact); - } - } else if (extras.containsKey("CreateOrEdit")) { - mEditOnClick = extras.getBoolean("CreateOrEdit"); - mEditSipUri = extras.getString("SipUri", null); - mEditDisplayName = extras.getString("DisplayName", null); - Log.i( - "[Contacts Activity] CreateOrEdit with values " - + mEditSipUri - + " / " - + mEditDisplayName); - - Toast.makeText(this, R.string.toast_choose_contact_for_edition, Toast.LENGTH_LONG) - .show(); - } - } - - public void showContactDetails(LinphoneContact contact) { - showContactDetails(contact, true); - } - - public void showContactEdit(LinphoneContact contact) { - showContactEdit(contact, true); - } - - private void showContactsList() { - ContactsFragment fragment = new ContactsFragment(); - changeFragment(fragment, "Contacts", false); - } - - private void showContactDetails(LinphoneContact contact, boolean isChild) { - if (mEditOnClick) { - showContactEdit(contact, isChild); - return; - } - - Bundle extras = new Bundle(); - if (contact != null) { - extras.putSerializable("Contact", contact); - Log.i("[Contacts Activity] Displaying Contact " + contact); - } - - ContactDetailsFragment fragment = new ContactDetailsFragment(); - fragment.setArguments(extras); - changeFragment(fragment, "Contact detail", isChild); - } - - private void showContactEdit(LinphoneContact contact, boolean isChild) { - showContactEdit(contact, new Bundle(), isChild); - } - - private void showContactEdit(LinphoneContact contact, Bundle extras, boolean isChild) { - if (contact != null) { - extras.putSerializable("Contact", contact); - Log.i("[Contacts Activity] Editing Contact " + contact); - } - if (mEditOnClick) { - mEditOnClick = false; - extras.putString("SipUri", mEditSipUri); - extras.putString("DisplayName", mEditDisplayName); - mEditSipUri = null; - mEditDisplayName = null; - } - ContactEditorFragment fragment = new ContactEditorFragment(); - fragment.setArguments(extras); - changeFragment(fragment, "Contact editor", isChild); - } -} diff --git a/app/src/main/java/org/linphone/contacts/ContactsAdapter.java b/app/src/main/java/org/linphone/contacts/ContactsAdapter.java deleted file mode 100644 index af7f7d5b8..000000000 --- a/app/src/main/java/org/linphone/contacts/ContactsAdapter.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.SectionIndexer; -import androidx.annotation.NonNull; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import org.linphone.R; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.SelectableAdapter; -import org.linphone.utils.SelectableHelper; - -public class ContactsAdapter extends SelectableAdapter - implements SectionIndexer { - private List mContacts; - private String[] mSections; - private ArrayList mSectionsList; - private Map mMap = new LinkedHashMap<>(); - private final ContactViewHolder.ClickListener mClickListener; - private final Context mContext; - private boolean mIsSearchMode; - - ContactsAdapter( - Context context, - List contactsList, - ContactViewHolder.ClickListener clickListener, - SelectableHelper helper) { - super(helper); - mContext = context; - updateDataSet(contactsList); - mClickListener = clickListener; - } - - @NonNull - @Override - public ContactViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = - LayoutInflater.from(parent.getContext()) - .inflate(R.layout.contact_cell, parent, false); - return new ContactViewHolder(v, mClickListener); - } - - @Override - public void onBindViewHolder(@NonNull ContactViewHolder holder, final int position) { - LinphoneContact contact = (LinphoneContact) getItem(position); - - holder.name.setText(contact.getFullName()); - - if (!mIsSearchMode) { - String fullName = contact.getFullName(); - if (fullName != null && !fullName.isEmpty()) { - holder.separatorText.setText(String.valueOf(fullName.charAt(0))); - } - } - holder.separator.setVisibility( - mIsSearchMode - || (getPositionForSection(getSectionForPosition(position)) - != position) - ? View.GONE - : View.VISIBLE); - holder.linphoneFriend.setVisibility(contact.isInFriendList() ? View.VISIBLE : View.GONE); - - ContactAvatar.displayAvatar(contact, holder.avatarLayout); - - boolean isOrgVisible = LinphonePreferences.instance().isDisplayContactOrganization(); - String org = contact.getOrganization(); - if (org != null && !org.isEmpty() && isOrgVisible) { - holder.organization.setText(org); - holder.organization.setVisibility(View.VISIBLE); - } else { - holder.organization.setVisibility(View.GONE); - } - - holder.delete.setVisibility(isEditionEnabled() ? View.VISIBLE : View.INVISIBLE); - holder.delete.setChecked(isSelected(position)); - } - - @Override - public int getItemCount() { - return mContacts.size(); - } - - public Object getItem(int position) { - if (position >= getItemCount()) return null; - return mContacts.get(position); - } - - public void setIsSearchMode(boolean set) { - mIsSearchMode = set; - } - - public long getItemId(int position) { - return position; - } - - public void updateDataSet(List contactsList) { - mContacts = contactsList; - - mMap = new LinkedHashMap<>(); - String prevLetter = null; - for (int i = 0; i < mContacts.size(); i++) { - LinphoneContact contact = mContacts.get(i); - String fullName = contact.getFullName(); - if (fullName == null || fullName.isEmpty()) { - continue; - } - String firstLetter = fullName.substring(0, 1).toUpperCase(Locale.getDefault()); - if (!firstLetter.equals(prevLetter)) { - prevLetter = firstLetter; - mMap.put(firstLetter, i); - } - } - mSectionsList = new ArrayList<>(mMap.keySet()); - mSections = new String[mSectionsList.size()]; - mSectionsList.toArray(mSections); - - notifyDataSetChanged(); - } - - @Override - public Object[] getSections() { - return mSections; - } - - @Override - public int getPositionForSection(int sectionIndex) { - if (sectionIndex >= mSections.length || sectionIndex < 0) { - return 0; - } - return mMap.get(mSections[sectionIndex]); - } - - @Override - public int getSectionForPosition(int position) { - if (position >= mContacts.size() || position < 0) { - return 0; - } - LinphoneContact contact = mContacts.get(position); - String fullName = contact.getFullName(); - if (fullName == null || fullName.isEmpty()) { - return 0; - } - String letter = fullName.substring(0, 1).toUpperCase(Locale.getDefault()); - return mSectionsList.indexOf(letter); - } -} diff --git a/app/src/main/java/org/linphone/contacts/ContactsFragment.java b/app/src/main/java/org/linphone/contacts/ContactsFragment.java deleted file mode 100644 index 1ea2d0c63..000000000 --- a/app/src/main/java/org/linphone/contacts/ContactsFragment.java +++ /dev/null @@ -1,370 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import android.app.Fragment; -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.SearchView; -import android.widget.TextView; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import java.util.ArrayList; -import java.util.List; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.call.views.LinphoneLinearLayoutManager; -import org.linphone.utils.SelectableHelper; - -public class ContactsFragment extends Fragment - implements OnItemClickListener, - ContactsUpdatedListener, - ContactViewHolder.ClickListener, - SelectableHelper.DeleteListener { - private RecyclerView mContactsList; - private TextView mNoSipContact, mNoContact; - private ImageView mAllContacts; - private ImageView mLinphoneContacts; - private boolean mOnlyDisplayLinphoneContacts; - private View mAllContactsSelected, mLinphoneContactsSelected; - private int mLastKnownPosition; - private SearchView mSearchView; - private ProgressBar mContactsFetchInProgress; - private LinearLayoutManager mLayoutManager; - private Context mContext; - private SelectableHelper mSelectionHelper; - private ContactsAdapter mContactAdapter; - private SwipeRefreshLayout mContactsRefresher; - - @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.contacts_list, container, false); - mContext = getActivity().getApplicationContext(); - mSelectionHelper = new SelectableHelper(view, this); - mSelectionHelper.setDialogMessage(R.string.delete_contacts_text); - - mNoSipContact = view.findViewById(R.id.noSipContact); - mNoContact = view.findViewById(R.id.noContact); - mContactsList = view.findViewById(R.id.contactsList); - - mAllContacts = view.findViewById(R.id.all_contacts); - mLinphoneContacts = view.findViewById(R.id.linphone_contacts); - mAllContactsSelected = view.findViewById(R.id.all_contacts_select); - mLinphoneContactsSelected = view.findViewById(R.id.linphone_contacts_select); - mContactsFetchInProgress = view.findViewById(R.id.contactsFetchInProgress); - ImageView newContact = view.findViewById(R.id.newContact); - mContactsRefresher = view.findViewById(R.id.contactsListRefresher); - - mContactsRefresher.setOnRefreshListener( - new SwipeRefreshLayout.OnRefreshListener() { - @Override - public void onRefresh() { - ContactsManager.getInstance().fetchContactsAsync(); - } - }); - - mAllContacts.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - mOnlyDisplayLinphoneContacts = false; - mAllContactsSelected.setVisibility(View.VISIBLE); - mAllContacts.setEnabled(false); - mLinphoneContacts.setEnabled(true); - mLinphoneContactsSelected.setVisibility(View.INVISIBLE); - changeContactsAdapter(); - } - }); - - mLinphoneContacts.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - mAllContactsSelected.setVisibility(View.INVISIBLE); - mLinphoneContactsSelected.setVisibility(View.VISIBLE); - mLinphoneContacts.setEnabled(false); - mAllContacts.setEnabled(true); - mOnlyDisplayLinphoneContacts = true; - changeContactsAdapter(); - } - }); - - newContact.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - ((ContactsActivity) getActivity()).showContactEdit(null); - } - }); - - if (getResources().getBoolean(R.bool.hide_non_linphone_contacts)) { - mAllContacts.setEnabled(false); - mLinphoneContacts.setEnabled(false); - mOnlyDisplayLinphoneContacts = true; - mAllContacts.setOnClickListener(null); - mLinphoneContacts.setOnClickListener(null); - mLinphoneContacts.setVisibility(View.INVISIBLE); - mLinphoneContactsSelected.setVisibility(View.INVISIBLE); - } else { - mAllContacts.setEnabled(mOnlyDisplayLinphoneContacts); - mLinphoneContacts.setEnabled(!mAllContacts.isEnabled()); - } - newContact.setEnabled(LinphoneManager.getCore().getCallsNb() == 0); - - if (!ContactsManager.getInstance().contactsFetchedOnce()) { - if (ContactsManager.getInstance().hasReadContactsAccess()) { - mContactsFetchInProgress.setVisibility(View.VISIBLE); - } - } else { - if (!mOnlyDisplayLinphoneContacts - && ContactsManager.getInstance().getContacts().isEmpty()) { - mNoContact.setVisibility(View.VISIBLE); - } else if (mOnlyDisplayLinphoneContacts - && ContactsManager.getInstance().getSIPContacts().isEmpty()) { - mNoSipContact.setVisibility(View.VISIBLE); - } - } - - mSearchView = view.findViewById(R.id.searchField); - mSearchView.setOnQueryTextListener( - new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - return true; - } - - @Override - public boolean onQueryTextChange(String newText) { - searchContacts(newText); - return true; - } - }); - - mLayoutManager = new LinphoneLinearLayoutManager(mContext); - mContactsList.setLayoutManager(mLayoutManager); - - DividerItemDecoration dividerItemDecoration = - new DividerItemDecoration( - mContactsList.getContext(), mLayoutManager.getOrientation()); - dividerItemDecoration.setDrawable( - getActivity().getResources().getDrawable(R.drawable.divider)); - mContactsList.addItemDecoration(dividerItemDecoration); - - return view; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - - @Override - public void onItemClick(AdapterView adapter, View view, int position, long id) { - LinphoneContact contact = (LinphoneContact) adapter.getItemAtPosition(position); - mLastKnownPosition = mLayoutManager.findFirstVisibleItemPosition(); - ((ContactsActivity) getActivity()).showContactDetails(contact); - } - - @Override - public void onItemClicked(int position) { - LinphoneContact contact = (LinphoneContact) mContactAdapter.getItem(position); - - if (mContactAdapter.isEditionEnabled()) { - mContactAdapter.toggleSelection(position); - } else { - mLastKnownPosition = mLayoutManager.findFirstVisibleItemPosition(); - ((ContactsActivity) getActivity()).showContactDetails(contact); - } - } - - @Override - public boolean onItemLongClicked(int position) { - if (!mContactAdapter.isEditionEnabled()) { - mSelectionHelper.enterEditionMode(); - } - mContactAdapter.toggleSelection(position); - return true; - } - - @Override - public void onResume() { - super.onResume(); - - ContactsManager.getInstance().addContactsListener(this); - - mOnlyDisplayLinphoneContacts = - ContactsManager.getInstance().isLinphoneContactsPrefered() - || getResources().getBoolean(R.bool.hide_non_linphone_contacts); - - changeContactsToggle(); - invalidate(); - onContactsUpdated(); - - ((ContactsActivity) (getActivity())).showTabBar(); - } - - @Override - public void onPause() { - ContactsManager.getInstance().removeContactsListener(this); - super.onPause(); - } - - @Override - public void onContactsUpdated() { - if (mContactAdapter != null) { - mContactAdapter.updateDataSet( - mOnlyDisplayLinphoneContacts - ? ContactsManager.getInstance().getSIPContacts() - : ContactsManager.getInstance().getContacts()); - mContactAdapter.notifyDataSetChanged(); - - if (mContactAdapter.getItemCount() > 0) { - mNoContact.setVisibility(View.GONE); - mNoSipContact.setVisibility(View.GONE); - } - } - mContactsFetchInProgress.setVisibility(View.GONE); - mContactsRefresher.setRefreshing(false); - } - - @Override - public void onDeleteSelection(Object[] objectsToDelete) { - ArrayList ids = new ArrayList<>(); - int size = mContactAdapter.getSelectedItemCount(); - for (int i = size - 1; i >= 0; i--) { - LinphoneContact contact = (LinphoneContact) objectsToDelete[i]; - if (contact.isAndroidContact()) { - contact.deleteFriend(); - ids.add(contact.getAndroidId()); - } else { - contact.delete(); - } - } - ContactsManager.getInstance().deleteMultipleContactsAtOnce(ids); - } - - private void searchContacts(String search) { - boolean isEditionEnabled = false; - if (search == null || search.isEmpty()) { - changeContactsAdapter(); - return; - } - changeContactsToggle(); - - List listContact; - - if (mOnlyDisplayLinphoneContacts) { - listContact = ContactsManager.getInstance().getSIPContacts(search); - } else { - listContact = ContactsManager.getInstance().getContacts(search); - } - if (mContactAdapter != null && mContactAdapter.isEditionEnabled()) { - isEditionEnabled = true; - } - - mContactAdapter = new ContactsAdapter(mContext, listContact, this, mSelectionHelper); - mContactAdapter.setIsSearchMode(true); - - mSelectionHelper.setAdapter(mContactAdapter); - if (isEditionEnabled) { - mSelectionHelper.enterEditionMode(); - } - mContactsList.setAdapter(mContactAdapter); - } - - private void changeContactsAdapter() { - changeContactsToggle(); - List listContact; - - mNoSipContact.setVisibility(View.GONE); - mNoContact.setVisibility(View.GONE); - mContactsList.setVisibility(View.VISIBLE); - boolean isEditionEnabled = false; - String query = mSearchView.getQuery().toString(); - if (query.equals("")) { - if (mOnlyDisplayLinphoneContacts) { - listContact = ContactsManager.getInstance().getSIPContacts(); - } else { - listContact = ContactsManager.getInstance().getContacts(); - } - } else { - if (mOnlyDisplayLinphoneContacts) { - listContact = ContactsManager.getInstance().getSIPContacts(query); - } else { - listContact = ContactsManager.getInstance().getContacts(query); - } - } - - if (mContactAdapter != null && mContactAdapter.isEditionEnabled()) { - isEditionEnabled = true; - } - - mContactAdapter = new ContactsAdapter(mContext, listContact, this, mSelectionHelper); - - mSelectionHelper.setAdapter(mContactAdapter); - - if (isEditionEnabled) { - mSelectionHelper.enterEditionMode(); - } - mContactsList.setAdapter(mContactAdapter); - - mContactAdapter.notifyDataSetChanged(); - - if (!mOnlyDisplayLinphoneContacts && mContactAdapter.getItemCount() == 0) { - mNoContact.setVisibility(View.VISIBLE); - } else if (mOnlyDisplayLinphoneContacts && mContactAdapter.getItemCount() == 0) { - mNoSipContact.setVisibility(View.VISIBLE); - } - } - - private void changeContactsToggle() { - if (mOnlyDisplayLinphoneContacts - && !getResources().getBoolean(R.bool.hide_non_linphone_contacts)) { - mAllContacts.setEnabled(true); - mAllContactsSelected.setVisibility(View.INVISIBLE); - mLinphoneContacts.setEnabled(false); - mLinphoneContactsSelected.setVisibility(View.VISIBLE); - } else { - mAllContacts.setEnabled(false); - mAllContactsSelected.setVisibility(View.VISIBLE); - mLinphoneContacts.setEnabled(true); - mLinphoneContactsSelected.setVisibility(View.INVISIBLE); - } - } - - private void invalidate() { - if (mSearchView != null && mSearchView.getQuery().toString().length() > 0) { - searchContacts(mSearchView.getQuery().toString()); - } else { - changeContactsAdapter(); - } - mContactsList.scrollToPosition(mLastKnownPosition); - } -} diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.java b/app/src/main/java/org/linphone/contacts/ContactsManager.java deleted file mode 100644 index 2e95e3dca..000000000 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.java +++ /dev/null @@ -1,595 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import static android.os.AsyncTask.THREAD_POOL_EXECUTOR; - -import android.Manifest; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.ContentProviderClient; -import android.content.ContentProviderOperation; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.pm.PackageManager; -import android.database.ContentObserver; -import android.database.Cursor; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.os.RemoteException; -import android.provider.ContactsContract; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.compatibility.Compatibility; -import org.linphone.core.Address; -import org.linphone.core.Core; -import org.linphone.core.Friend; -import org.linphone.core.FriendList; -import org.linphone.core.FriendListListener; -import org.linphone.core.MagicSearch; -import org.linphone.core.PresenceBasicStatus; -import org.linphone.core.PresenceModel; -import org.linphone.core.ProxyConfig; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; - -public class ContactsManager extends ContentObserver - implements FriendListListener, LinphoneContext.CoreStartedListener { - private List mContacts, mSipContacts; - private final ArrayList mContactsUpdatedListeners; - private MagicSearch mMagicSearch; - private boolean mContactsFetchedOnce = false; - private Context mContext; - private AsyncContactsLoader mLoadContactTask; - private boolean mInitialized = false; - - public static ContactsManager getInstance() { - return LinphoneContext.instance().getContactsManager(); - } - - public ContactsManager(Context context) { - super(new Handler(Looper.getMainLooper())); - mContext = context; - mContactsUpdatedListeners = new ArrayList<>(); - mContacts = new ArrayList<>(); - mSipContacts = new ArrayList<>(); - - if (LinphoneManager.getCore() != null) { - mMagicSearch = LinphoneManager.getCore().createMagicSearch(); - mMagicSearch.setLimitedSearch(false); // Do not limit the number of results - } - - LinphoneContext.instance().addCoreStartedListener(this); - } - - public void addContactsListener(ContactsUpdatedListener listener) { - mContactsUpdatedListeners.add(listener); - } - - public void removeContactsListener(ContactsUpdatedListener listener) { - mContactsUpdatedListeners.remove(listener); - } - - public ArrayList getContactsListeners() { - return mContactsUpdatedListeners; - } - - @Override - public void onChange(boolean selfChange) { - onChange(selfChange, null); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - Log.i("[Contacts Manager] Content observer detected a changing in at least one contact"); - fetchContactsAsync(); - } - - @Override - public void onCoreStarted() { - // Core has been started, fetch contacts again in case there are some - // in the configuration file or remote provisioning - fetchContactsAsync(); - } - - public synchronized List getContacts() { - return mContacts; - } - - synchronized void setContacts(List c) { - mContacts = c; - } - - public synchronized List getSIPContacts() { - return mSipContacts; - } - - synchronized void setSipContacts(List c) { - mSipContacts = c; - } - - public void destroy() { - mContext.getContentResolver().unregisterContentObserver(this); - LinphoneContext.instance().removeCoreStartedListener(this); - - if (mLoadContactTask != null) { - mLoadContactTask.cancel(true); - } - // LinphoneContact has a Friend field and Friend can have a LinphoneContact has userData - // Friend also keeps a ref on the Core, so we have to clean them - for (LinphoneContact c : mContacts) { - c.setFriend(null); - } - mContacts.clear(); - for (LinphoneContact c : mSipContacts) { - c.setFriend(null); - } - mSipContacts.clear(); - - Core core = LinphoneManager.getCore(); - if (core != null) { - for (FriendList list : core.getFriendsLists()) { - list.removeListener(this); - } - } - } - - public void fetchContactsAsync() { - if (mLoadContactTask != null) { - mLoadContactTask.cancel(true); - } - - if (!hasReadContactsAccess()) { - Log.w( - "[Contacts Manager] Can't fetch native contacts without READ_CONTACTS permission"); - } - - mLoadContactTask = new AsyncContactsLoader(mContext); - mContactsFetchedOnce = true; - mLoadContactTask.executeOnExecutor(THREAD_POOL_EXECUTOR); - } - - public MagicSearch getMagicSearch() { - return mMagicSearch; - } - - public boolean contactsFetchedOnce() { - return mContactsFetchedOnce; - } - - public List getContacts(String search) { - search = search.toLowerCase(Locale.getDefault()); - List searchContactsBegin = new ArrayList<>(); - List searchContactsContain = new ArrayList<>(); - for (LinphoneContact contact : getContacts()) { - if (contact.getFullName() != null) { - if (contact.getFullName().toLowerCase(Locale.getDefault()).startsWith(search)) { - searchContactsBegin.add(contact); - } else if (contact.getFullName() - .toLowerCase(Locale.getDefault()) - .contains(search)) { - searchContactsContain.add(contact); - } - } - } - searchContactsBegin.addAll(searchContactsContain); - return searchContactsBegin; - } - - public List getSIPContacts(String search) { - search = search.toLowerCase(Locale.getDefault()); - List searchContactsBegin = new ArrayList<>(); - List searchContactsContain = new ArrayList<>(); - for (LinphoneContact contact : getSIPContacts()) { - if (contact.getFullName() != null) { - if (contact.getFullName().toLowerCase(Locale.getDefault()).startsWith(search)) { - searchContactsBegin.add(contact); - } else if (contact.getFullName() - .toLowerCase(Locale.getDefault()) - .contains(search)) { - searchContactsContain.add(contact); - } - } - } - searchContactsBegin.addAll(searchContactsContain); - return searchContactsBegin; - } - - public void enableContactsAccess() { - LinphonePreferences.instance().disableFriendsStorage(); - } - - public boolean hasReadContactsAccess() { - if (mContext == null || mContext.getPackageManager() == null) { - return false; - } - - boolean contactsR = - (PackageManager.PERMISSION_GRANTED - == mContext.getPackageManager() - .checkPermission( - android.Manifest.permission.READ_CONTACTS, - mContext.getPackageName())); - return contactsR - && !mContext.getResources().getBoolean(R.bool.force_use_of_linphone_friends); - } - - private boolean hasWriteContactsAccess() { - if (mContext == null) { - return false; - } - - return (PackageManager.PERMISSION_GRANTED - == mContext.getPackageManager() - .checkPermission( - Manifest.permission.WRITE_CONTACTS, mContext.getPackageName())); - } - - private boolean hasWriteSyncPermission() { - if (mContext == null) { - return false; - } - - return (PackageManager.PERMISSION_GRANTED - == mContext.getPackageManager() - .checkPermission( - Manifest.permission.WRITE_SYNC_SETTINGS, - mContext.getPackageName())); - } - - public boolean isLinphoneContactsPrefered() { - ProxyConfig lpc = LinphoneManager.getCore().getDefaultProxyConfig(); - return lpc != null - && lpc.getIdentityAddress() - .getDomain() - .equals(mContext.getString(R.string.default_domain)); - } - - public void initializeContactManager() { - if (!mInitialized) { - if (mContext.getResources().getBoolean(R.bool.use_linphone_tag)) { - if (hasReadContactsAccess() - && hasWriteContactsAccess() - && hasWriteSyncPermission()) { - if (LinphoneContext.isReady()) { - initializeSyncAccount(); - mInitialized = true; - } - } - } - } - } - - private void makeContactAccountVisible() { - ContentProviderClient client = - mContext.getContentResolver() - .acquireContentProviderClient(ContactsContract.AUTHORITY_URI); - if (client == null) { - Log.e( - "[Contacts Manager] Failed to get content provider client for contacts authority!"); - return; - } - - ContentValues values = new ContentValues(); - values.put( - ContactsContract.Settings.ACCOUNT_NAME, - mContext.getString(R.string.sync_account_name)); - values.put( - ContactsContract.Settings.ACCOUNT_TYPE, - mContext.getString(R.string.sync_account_type)); - values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, true); - - try { - client.insert( - ContactsContract.Settings.CONTENT_URI - .buildUpon() - .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") - .build(), - values); - Log.i("[Contacts Manager] Contacts account made visible"); - } catch (RemoteException e) { - Log.e("[Contacts Manager] Couldn't make contacts account visible: " + e); - } - Compatibility.closeContentProviderClient(client); - } - - private void initializeSyncAccount() { - AccountManager accountManager = - (AccountManager) mContext.getSystemService(Context.ACCOUNT_SERVICE); - - Account[] accounts = - accountManager.getAccountsByType(mContext.getString(R.string.sync_account_type)); - - if (accounts != null && accounts.length == 0) { - Account newAccount = - new Account( - mContext.getString(R.string.sync_account_name), - mContext.getString(R.string.sync_account_type)); - try { - accountManager.addAccountExplicitly(newAccount, null, null); - Log.i("[Contacts Manager] Contact account added"); - makeContactAccountVisible(); - } catch (Exception e) { - Log.e("[Contacts Manager] Couldn't initialize sync account: " + e); - } - } else if (accounts != null) { - for (Account account : accounts) { - Log.i( - "[Contacts Manager] Found account with name \"" - + account.name - + "\" and type \"" - + account.type - + "\""); - makeContactAccountVisible(); - } - } - } - - public String getAndroidContactIdFromUri(Uri uri) { - String[] projection = {ContactsContract.CommonDataKinds.SipAddress.CONTACT_ID}; - Cursor cursor = - mContext.getApplicationContext() - .getContentResolver() - .query(uri, projection, null, null, null); - cursor.moveToFirst(); - - int nameColumnIndex = - cursor.getColumnIndex(ContactsContract.CommonDataKinds.SipAddress.CONTACT_ID); - String id = cursor.getString(nameColumnIndex); - cursor.close(); - return id; - } - - public synchronized LinphoneContact findContactFromAndroidId(String androidId) { - if (androidId == null) { - return null; - } - - for (LinphoneContact c : getContacts()) { - if (c.getAndroidId() != null && c.getAndroidId().equals(androidId)) { - return c; - } - } - return null; - } - - public synchronized LinphoneContact findContactFromAddress(Address address) { - if (address == null) return null; - Core core = LinphoneManager.getCore(); - - Friend lf = core.findFriend(address); - if (lf != null) { - return (LinphoneContact) lf.getUserData(); - } - - String username = address.getUsername(); - if (username == null) { - Log.w("[Contacts Manager] Address ", address.asString(), " doesn't have a username!"); - return null; - } - - if (android.util.Patterns.PHONE.matcher(username).matches()) { - return findContactFromPhoneNumber(username); - } - - return null; - } - - public synchronized LinphoneContact findContactFromPhoneNumber(String phoneNumber) { - if (phoneNumber == null) return null; - - if (!android.util.Patterns.PHONE.matcher(phoneNumber).matches()) { - Log.w( - "[Contacts Manager] Expected phone number but doesn't look like it: " - + phoneNumber); - return null; - } - - Core core = LinphoneManager.getCore(); - ProxyConfig lpc = null; - if (core != null) { - lpc = core.getDefaultProxyConfig(); - } - if (lpc == null) { - Log.i("[Contacts Manager] Couldn't find default proxy config..."); - return null; - } - - String normalized = lpc.normalizePhoneNumber(phoneNumber); - if (normalized == null) { - Log.w( - "[Contacts Manager] Couldn't normalize phone number " - + phoneNumber - + ", default proxy config prefix is " - + lpc.getDialPrefix()); - normalized = phoneNumber; - } - - Address addr = lpc.normalizeSipUri(normalized); - if (addr == null) { - Log.w("[Contacts Manager] Couldn't normalize SIP URI " + normalized); - return null; - } - - // Without this, the hashmap inside liblinphone won't find it... - addr.setUriParam("user", "phone"); - Friend lf = core.findFriend(addr); - if (lf != null) { - return (LinphoneContact) lf.getUserData(); - } - - Log.w("[Contacts Manager] Couldn't find friend..."); - return null; - } - - public String getAddressOrNumberForAndroidContact(ContentResolver resolver, Uri contactUri) { - if (resolver == null || contactUri == null) return null; - - // Phone Numbers - String[] projection = new String[] {ContactsContract.CommonDataKinds.Phone.NUMBER}; - Cursor c = resolver.query(contactUri, projection, null, null, null); - if (c != null) { - if (c.moveToNext()) { - int numberIndex = c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER); - String number = c.getString(numberIndex); - c.close(); - return number; - } - c.close(); - } - - projection = new String[] {ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS}; - c = resolver.query(contactUri, projection, null, null, null); - if (c != null) { - if (c.moveToNext()) { - int numberIndex = - c.getColumnIndex(ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS); - String address = c.getString(numberIndex); - c.close(); - return address; - } - c.close(); - } - - return null; - } - - private synchronized boolean refreshSipContact(Friend lf) { - if (lf == null) return false; - LinphoneContact contact = (LinphoneContact) lf.getUserData(); - - if (contact != null) { - if (LinphoneContext.instance() - .getApplicationContext() - .getResources() - .getBoolean(R.bool.use_linphone_tag)) { - if (LinphonePreferences.instance() - .isPresenceStorageInNativeAndroidContactEnabled()) { - // Inserting information in Android contact if the parameter is enabled - for (LinphoneNumberOrAddress noa : contact.getNumbersOrAddresses()) { - if (noa.isSIPAddress()) { - // We are only interested in phone numbers - continue; - } - String value = noa.getValue(); - if (value == null || value.isEmpty()) { - continue; - } - - // Test presence of the value - PresenceModel pm = contact.getFriend().getPresenceModelForUriOrTel(value); - // If presence is not null - if (pm != null - && pm.getBasicStatus() != null - && pm.getBasicStatus().equals(PresenceBasicStatus.Open)) { - // Add presence to native contact - AsyncContactPresence asyncContactPresence = - new AsyncContactPresence(contact, value); - asyncContactPresence.execute(); - } - } - } - } - - if (!mSipContacts.contains(contact)) { - mSipContacts.add(contact); - return true; - } - } - - return false; - } - - public void delete(String id) { - ArrayList ids = new ArrayList<>(); - ids.add(id); - deleteMultipleContactsAtOnce(ids); - } - - public void deleteMultipleContactsAtOnce(List ids) { - String select = ContactsContract.Data.CONTACT_ID + " = ?"; - ArrayList ops = new ArrayList<>(); - - for (String id : ids) { - Log.i("[Contacts Manager] Adding Android contact id ", id, " to batch removal"); - String[] args = new String[] {id}; - ops.add( - ContentProviderOperation.newDelete(ContactsContract.RawContacts.CONTENT_URI) - .withSelection(select, args) - .build()); - } - - try { - mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops); - } catch (Exception e) { - Log.e("[Contacts Manager] " + e); - } - - // To ensure removed contacts won't appear in the contacts list anymore - fetchContactsAsync(); - } - - public String getString(int resourceID) { - if (mContext == null) return null; - return mContext.getString(resourceID); - } - - @Override - public void onContactCreated(FriendList list, Friend lf) {} - - @Override - public void onContactDeleted(FriendList list, Friend lf) {} - - @Override - public void onContactUpdated(FriendList list, Friend newFriend, Friend oldFriend) {} - - @Override - public void onSyncStatusChanged(FriendList list, FriendList.SyncStatus status, String msg) {} - - @Override - public void onPresenceReceived(FriendList list, Friend[] friends) { - boolean updated = false; - - for (Friend lf : friends) { - boolean newContact = refreshSipContact(lf); - - if (newContact) { - updated = true; - } - } - - if (updated) { - Collections.sort(mSipContacts); - } - - for (ContactsUpdatedListener listener : mContactsUpdatedListeners) { - listener.onContactsUpdated(); - } - - Compatibility.createChatShortcuts(mContext); - } -} diff --git a/app/src/main/java/org/linphone/contacts/ContactsUpdatedListener.java b/app/src/main/java/org/linphone/contacts/ContactsUpdatedListener.java deleted file mode 100644 index 576cb589c..000000000 --- a/app/src/main/java/org/linphone/contacts/ContactsUpdatedListener.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -public interface ContactsUpdatedListener { - void onContactsUpdated(); -} diff --git a/app/src/main/java/org/linphone/contacts/LinphoneContact.java b/app/src/main/java/org/linphone/contacts/LinphoneContact.java deleted file mode 100644 index 9a8f06ed1..000000000 --- a/app/src/main/java/org/linphone/contacts/LinphoneContact.java +++ /dev/null @@ -1,673 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.provider.ContactsContract; -import java.io.Serializable; -import java.text.Collator; -import java.util.ArrayList; -import java.util.List; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Address; -import org.linphone.core.Core; -import org.linphone.core.Friend; -import org.linphone.core.FriendCapability; -import org.linphone.core.PresenceBasicStatus; -import org.linphone.core.PresenceModel; -import org.linphone.core.SubscribePolicy; -import org.linphone.core.tools.Log; - -public class LinphoneContact extends AndroidContact - implements Serializable, Comparable { - private static final long serialVersionUID = 9015568163905205244L; - - private transient Friend mFriend; - private String mFullName, mFirstName, mLastName, mOrganization; - private transient Uri mPhotoUri, mThumbnailUri; - private List mAddresses; - private boolean mHasSipAddress; - private boolean mIsStarred; - - public LinphoneContact() { - super(); - mFullName = null; - mFirstName = null; - mLastName = null; - mOrganization = null; - mAndroidId = null; - mThumbnailUri = null; - mAddresses = new ArrayList<>(); - mPhotoUri = null; - mHasSipAddress = false; - mIsStarred = false; - } - - public static LinphoneContact createContact() { - LinphoneContact contact = new LinphoneContact(); - - if (ContactsManager.getInstance().hasReadContactsAccess()) { - contact.createAndroidContact(); - - } else { - contact.createFriend(); - } - return contact; - } - - public String getContactId() { - if (isAndroidContact()) { - return getAndroidId(); - } else { - // TODO - } - return null; - } - - @Override - public int compareTo(LinphoneContact contact) { - String fullName = getFullName() != null ? getFullName() : ""; - String contactFullName = contact.getFullName() != null ? contact.getFullName() : ""; - - if (fullName.equals(contactFullName)) { - String id = getAndroidId() != null ? getAndroidId() : ""; - String contactId = contact.getAndroidId() != null ? contact.getAndroidId() : ""; - - if (id.equals(contactId)) { - List noas1 = getNumbersOrAddresses(); - List noas2 = contact.getNumbersOrAddresses(); - if (noas1.size() == noas2.size() && noas1.size() > 0) { - if (!noas1.containsAll(noas2) || !noas2.containsAll(noas1)) { - for (int i = 0; i < noas1.size(); i++) { - int compare = noas1.get(i).compareTo(noas2.get(i)); - if (compare != 0) return compare; - } - } - } else { - return Integer.compare(noas1.size(), noas2.size()); - } - - String org = getOrganization() != null ? getOrganization() : ""; - String contactOrg = - contact.getOrganization() != null ? contact.getOrganization() : ""; - return org.compareTo(contactOrg); - } - return id.compareTo(contactId); - } - - Collator collator = Collator.getInstance(); - collator.setStrength(Collator.NO_DECOMPOSITION); - return collator.compare(fullName, contactFullName); - } - - @Override - public boolean equals(Object obj) { - if (obj.getClass() != LinphoneContact.class) return false; - LinphoneContact contact = (LinphoneContact) obj; - return (this.compareTo(contact) == 0); - } - - /* - Name related - */ - - public String getFullName() { - return mFullName; - } - - public void setFullName(String name) { - mFullName = name; - } - - public void setFirstNameAndLastName(String fn, String ln, boolean commitChanges) { - if (fn != null && fn.isEmpty() && ln != null && ln.isEmpty()) return; - if (fn != null && fn.equals(mFirstName) && ln != null && ln.equals(mLastName)) return; - - if (commitChanges) { - setName(fn, ln); - } - - mFirstName = fn; - mLastName = ln; - if (mFullName == null) { - if (mFirstName != null - && mLastName != null - && mFirstName.length() > 0 - && mLastName.length() > 0) { - mFullName = mFirstName + " " + mLastName; - } else if (mFirstName != null && mFirstName.length() > 0) { - mFullName = mFirstName; - } else if (mLastName != null && mLastName.length() > 0) { - mFullName = mLastName; - } - } - } - - public String getFirstName() { - return mFirstName; - } - - public String getLastName() { - return mLastName; - } - - /* - Organization related - */ - - public String getOrganization() { - return mOrganization; - } - - public void setOrganization(String org, boolean commitChanges) { - if ((org == null || org.isEmpty()) && (mOrganization == null || mOrganization.isEmpty())) - return; - if (org != null && org.equals(mOrganization)) return; - - if (commitChanges) { - setOrganization(org, mOrganization); - } - - mOrganization = org; - } - - /* - Picture related - */ - - public Uri getPhotoUri() { - return mPhotoUri; - } - - private void setPhotoUri(Uri uri) { - if (uri != null && uri.equals(mPhotoUri)) return; - mPhotoUri = uri; - } - - public Uri getThumbnailUri() { - return mThumbnailUri; - } - - private void setThumbnailUri(Uri uri) { - if (uri != null && uri.equals(mThumbnailUri)) return; - mThumbnailUri = uri; - } - - /* - Number or address related - */ - - private synchronized void addNumberOrAddress(LinphoneNumberOrAddress noa) { - if (noa == null) return; - - boolean found = false; - String normalizedPhone = noa.getNormalizedPhone(); - // Check for duplicated phone numbers but with different formats - for (LinphoneNumberOrAddress number : mAddresses) { - if (!number.isSIPAddress()) { - if ((!noa.isSIPAddress() - && normalizedPhone != null - && normalizedPhone.equals(number.getNormalizedPhone())) - || (noa.isSIPAddress() - && noa.getValue().equals(number.getNormalizedPhone())) - || (normalizedPhone != null && normalizedPhone.equals(number.getValue()))) { - Log.d("[Linphone Contact] Duplicated entry detected: " + noa); - found = true; - break; - } - } - } - - if (!found) { - if (noa.isSIPAddress()) { - mHasSipAddress = true; - } - mAddresses.add(noa); - } - } - - public synchronized List getNumbersOrAddresses() { - return mAddresses; - } - - public boolean hasAddress(String address) { - for (LinphoneNumberOrAddress noa : getNumbersOrAddresses()) { - if (noa.isSIPAddress()) { - String value = noa.getValue(); - if (value != null - && (address.startsWith(value) || value.equals("sip:" + address))) { - // Startswith is to workaround the fact that the - // address may have a ;gruu= at the end... - return true; - } - } - } - return false; - } - - public boolean hasAddress() { - return mHasSipAddress; - } - - public synchronized void removeNumberOrAddress(LinphoneNumberOrAddress noa) { - if (noa != null && noa.getOldValue() != null) { - - removeNumberOrAddress(noa.getOldValue(), noa.isSIPAddress()); - - if (isFriend()) { - if (noa.isSIPAddress()) { - if (!noa.getOldValue().startsWith("sip:")) { - noa.setOldValue("sip:" + noa.getOldValue()); - } - } - LinphoneNumberOrAddress toRemove = null; - for (LinphoneNumberOrAddress address : mAddresses) { - if (noa.getOldValue() != null - && noa.getOldValue().equals(address.getValue()) - && noa.isSIPAddress() == address.isSIPAddress()) { - toRemove = address; - break; - } - } - if (toRemove != null) { - mAddresses.remove(toRemove); - } - } - } - } - - public synchronized void addOrUpdateNumberOrAddress(LinphoneNumberOrAddress noa) { - if (noa != null && noa.getValue() != null) { - - addNumberOrAddress(noa.getValue(), noa.getOldValue(), noa.isSIPAddress()); - - if (isFriend()) { - if (noa.isSIPAddress()) { - if (!noa.getValue().startsWith("sip:")) { - noa.setValue("sip:" + noa.getValue()); - } - } - if (noa.getOldValue() != null) { - if (noa.isSIPAddress()) { - if (!noa.getOldValue().startsWith("sip:")) { - noa.setOldValue("sip:" + noa.getOldValue()); - } - } - for (LinphoneNumberOrAddress address : mAddresses) { - if (noa.getOldValue() != null - && noa.getOldValue().equals(address.getValue()) - && noa.isSIPAddress() == address.isSIPAddress()) { - address.setValue(noa.getValue()); - break; - } - } - } else { - mAddresses.add(noa); - } - } - } - } - - public synchronized void clearAddresses() { - mAddresses.clear(); - } - - /* - Friend related - */ - - public Friend getFriend() { - return mFriend; - } - - private synchronized void createOrUpdateFriend() { - boolean created = false; - Core core = LinphoneManager.getCore(); - if (core == null) return; - - if (!isFriend()) { - mFriend = core.createFriend(); - mFriend.enableSubscribes(false); - mFriend.setIncSubscribePolicy(SubscribePolicy.SPDeny); - if (isAndroidContact()) { - mFriend.setRefKey(getAndroidId()); - } - mFriend.setUserData(this); - created = true; - } - if (isFriend()) { - mFriend.edit(); - mFriend.setName(mFullName); - - if (mFriend.getVcard() != null) { - mFriend.getVcard().setFamilyName(mLastName); - mFriend.getVcard().setGivenName(mFirstName); - - if (mOrganization != null) { - mFriend.getVcard().setOrganization(mOrganization); - } - } - - if (!created) { - for (Address address : mFriend.getAddresses()) { - mFriend.removeAddress(address); - } - for (String phone : mFriend.getPhoneNumbers()) { - mFriend.removePhoneNumber(phone); - } - } - - for (LinphoneNumberOrAddress noa : getNumbersOrAddresses()) { - - if (noa.isSIPAddress()) { - - Address addr = core.interpretUrl(noa.getValue()); - - if (addr != null) { - mFriend.addAddress(addr); - } - } else { - mFriend.addPhoneNumber(noa.getValue()); - } - } - mFriend.done(); - } - if (created) { - core.getDefaultFriendList().addFriend(mFriend); - } - - if (!ContactsManager.getInstance().hasReadContactsAccess()) { - // This refresh is only needed if app has no contacts permission to refresh the list of - // Friends. - // Otherwise contacts will be refreshed due to changes in native contact and the handler - // in ContactsManager - ContactsManager.getInstance().fetchContactsAsync(); - } - } - - public void deleteFriend() { - if (mFriend == null) return; - Core core = LinphoneManager.getCore(); - if (core == null) return; - - Log.i("[Contact] Deleting friend ", mFriend.getName(), " for contact ", this); - mFriend.remove(); - } - - public void createOrUpdateFriendFromNativeContact() { - if (isAndroidContact()) { - createOrUpdateFriend(); - } - } - - public boolean isFriend() { - return mFriend != null; - } - - public void setFriend(Friend f) { - if (mFriend != null && (f == null || f != mFriend)) { - mFriend.setUserData(null); - } - mFriend = f; - if (mFriend != null) { - mFriend.setUserData(this); - } - } - - public boolean isInFriendList() { - if (mFriend == null) return false; - for (LinphoneNumberOrAddress noa : getNumbersOrAddresses()) { - PresenceModel pm = mFriend.getPresenceModelForUriOrTel(noa.getValue()); - if (pm != null - && pm.getBasicStatus() != null - && pm.getBasicStatus().equals(PresenceBasicStatus.Open)) { - return true; - } - } - return false; - } - - public String getContactFromPresenceModelForUriOrTel(String uri) { - if (mFriend != null && mFriend.getPresenceModelForUriOrTel(uri) != null) { - return mFriend.getPresenceModelForUriOrTel(uri).getContact(); - } - return null; - } - - public PresenceBasicStatus getBasicStatusFromPresenceModelForUriOrTel(String uri) { - if (mFriend != null && mFriend.getPresenceModelForUriOrTel(uri) != null) { - return mFriend.getPresenceModelForUriOrTel(uri).getBasicStatus(); - } - return PresenceBasicStatus.Closed; - } - - public boolean hasPresenceModelForUriOrTelCapability(String uri, FriendCapability capability) { - if (mFriend == null || uri == null) return false; - - PresenceModel presence = mFriend.getPresenceModelForUriOrTel(uri); - if (presence != null) { - return presence.hasCapability(capability); - } else { - for (LinphoneNumberOrAddress noa : getNumbersOrAddresses()) { - String value = noa.getValue(); - if (value != null) { - String contact = getContactFromPresenceModelForUriOrTel(value); - if (contact != null && contact.equals(uri)) { - presence = mFriend.getPresenceModelForUriOrTel(value); - if (presence != null) { - return presence.hasCapability(capability); - } - } - } - } - } - return false; - } - - private void createFriend() { - LinphoneContact contact = new LinphoneContact(); - Friend friend = LinphoneManager.getCore().createFriend(); - // Disable subscribes for now - friend.enableSubscribes(false); - friend.setIncSubscribePolicy(SubscribePolicy.SPDeny); - contact.mFriend = friend; - friend.setUserData(contact); - } - - /* - Contact related - */ - - protected void setAndroidId(String id) { - super.setAndroidId(id); - setThumbnailUri(getContactThumbnailPictureUri()); - setPhotoUri(getContactPictureUri()); - } - - public synchronized void syncValuesFromFriend() { - if (isFriend()) { - mAddresses = new ArrayList<>(); - mFullName = mFriend.getName(); - mLastName = mFriend.getVcard().getFamilyName(); - mFirstName = mFriend.getVcard().getGivenName(); - mThumbnailUri = null; - mPhotoUri = null; - mHasSipAddress = mFriend.getAddress() != null; - mOrganization = mFriend.getVcard().getOrganization(); - - Core core = LinphoneManager.getCore(); - - if (core != null && core.vcardSupported()) { - for (Address addr : mFriend.getAddresses()) { - if (addr != null) { - addNumberOrAddress( - new LinphoneNumberOrAddress(addr.asStringUriOnly(), true)); - } - } - for (String tel : mFriend.getPhoneNumbers()) { - if (tel != null) { - addNumberOrAddress(new LinphoneNumberOrAddress(tel, false)); - } - } - } else { - Address addr = mFriend.getAddress(); - addNumberOrAddress(new LinphoneNumberOrAddress(addr.asStringUriOnly(), true)); - } - } - } - - private synchronized void syncValuesFromAndroidContact(Context context) { - Cursor c = null; - try { - c = - context.getContentResolver() - .query( - ContactsContract.Data.CONTENT_URI, - AsyncContactsLoader.PROJECTION, - ContactsContract.Data.IN_DEFAULT_DIRECTORY - + " == 1 AND " - + ContactsContract.Data.CONTACT_ID - + " == " - + mAndroidId, - null, - null); - } catch (SecurityException se) { - Log.e("[Contact] Security exception: ", se); - } - - if (c != null) { - mAddresses = new ArrayList<>(); - while (c.moveToNext()) { - syncValuesFromAndroidCusor(c); - } - c.close(); - } - } - - public void syncValuesFromAndroidCusor(Cursor c) { - String displayName = - c.getString(c.getColumnIndex(ContactsContract.Data.DISPLAY_NAME_PRIMARY)); - - String mime = c.getString(c.getColumnIndex(ContactsContract.Data.MIMETYPE)); - String data1 = c.getString(c.getColumnIndex("data1")); - String data2 = c.getString(c.getColumnIndex("data2")); - String data3 = c.getString(c.getColumnIndex("data3")); - String data4 = c.getString(c.getColumnIndex("data4")); - - String fullName = getFullName(); - if (fullName == null || !fullName.equals(displayName)) { - Log.d("[Linphone Contact] Setting display name " + displayName); - setFullName(displayName); - } - - if (ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mime)) { - if (data1 == null && data4 == null) { - Log.e("[Linphone Contact] Phone number data are both null !"); - return; - } - - Log.d("[Linphone Contact] Found phone number " + data1 + " (" + data4 + ")"); - addNumberOrAddress(new LinphoneNumberOrAddress(data1, data4)); - } else if (ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE.equals(mime) - || LinphoneContext.instance() - .getApplicationContext() - .getString(R.string.linphone_address_mime_type) - .equals(mime)) { - if (data1 == null) { - Log.e("[Linphone Contact] SIP address is null !"); - return; - } - - Log.d("[Linphone Contact] Found SIP address " + data1); - addNumberOrAddress(new LinphoneNumberOrAddress(data1, true)); - } else if (ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE.equals(mime)) { - if (data1 == null) { - Log.e("[Linphone Contact] Organization is null !"); - return; - } - - Log.d("[Linphone Contact] Found organization " + data1); - setOrganization(data1, false); - } else if (ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE.equals(mime)) { - if (data2 == null && data3 == null) { - Log.e("[Linphone Contact] Firstname and lastname are both null !"); - return; - } - - Log.d("[Linphone Contact] Found first name " + data2 + " and last name " + data3); - setFirstNameAndLastName(data2, data3, false); - } else { - Log.d("[Linphone Contact] Skipping unused MIME type " + mime); - } - } - - public synchronized void addPresenceInfoToNativeContact(String value) { - Log.d( - "[Contact] Trying to update native contact with presence information for phone number ", - value); - - // Creation of the raw contact with the presence information (tablet) - createRawLinphoneContactFromExistingAndroidContactIfNeeded(); - - if (!isLinphoneAddressMimeEntryAlreadyExisting(value)) { - // Do the action on the contact only once if it has not been done yet - updateNativeContactWithPresenceInfo(value); - } - saveChangesCommited(); - } - - public void save() { - saveChangesCommited(); - if (getAndroidId() != null) { - setThumbnailUri(getContactThumbnailPictureUri()); - setPhotoUri(getContactPictureUri()); - } - syncValuesFromAndroidContact(LinphoneContext.instance().getApplicationContext()); - createOrUpdateFriend(); - } - - public void delete() { - Log.i("[Contact] Deleting contact ", this); - if (isAndroidContact()) { - deleteAndroidContact(); - } - if (isFriend()) { - deleteFriend(); - } - } - - public boolean hasFriendCapability(FriendCapability capability) { - if (!isFriend()) return false; - - return getFriend().hasCapability(capability); - } - - public void setIsFavourite(boolean starred) { - mIsStarred = starred; - } - - public boolean isFavourite() { - return mIsStarred; - } -} diff --git a/app/src/main/java/org/linphone/contacts/LinphoneNumberOrAddress.java b/app/src/main/java/org/linphone/contacts/LinphoneNumberOrAddress.java deleted file mode 100644 index 6785a4921..000000000 --- a/app/src/main/java/org/linphone/contacts/LinphoneNumberOrAddress.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import java.io.Serializable; - -public class LinphoneNumberOrAddress implements Serializable, Comparable { - private static final long serialVersionUID = -2301689469730072896L; - - private final boolean mIsSIPAddress; - private String mValue, mOldValueForUpdatePurpose; - private final String mNormalizedPhone; - - public LinphoneNumberOrAddress(String v, boolean isSIP) { - mValue = v; - mIsSIPAddress = isSIP; - mOldValueForUpdatePurpose = null; - mNormalizedPhone = null; - } - - public LinphoneNumberOrAddress(String v, String normalizedV) { - mValue = v; - mNormalizedPhone = normalizedV != null ? normalizedV : v; - mIsSIPAddress = false; - mOldValueForUpdatePurpose = null; - } - - public LinphoneNumberOrAddress(String v, boolean isSip, String old) { - this(v, isSip); - mOldValueForUpdatePurpose = old; - } - - @Override - public int compareTo(LinphoneNumberOrAddress noa) { - if (mValue != null) { - if (noa.isSIPAddress() && isSIPAddress()) { - return mValue.compareTo(noa.getValue()); - } else if (!noa.isSIPAddress() && !isSIPAddress()) { - return getNormalizedPhone().compareTo(noa.getNormalizedPhone()); - } - } - return -1; - } - - @Override - public boolean equals(Object obj) { - if (obj.getClass() != LinphoneNumberOrAddress.class) return false; - LinphoneNumberOrAddress noa = (LinphoneNumberOrAddress) obj; - return this.compareTo(noa) == 0; - } - - public boolean isSIPAddress() { - return mIsSIPAddress; - } - - public String getOldValue() { - return mOldValueForUpdatePurpose; - } - - public void setOldValue(String v) { - mOldValueForUpdatePurpose = v; - } - - public String getValue() { - return mValue; - } - - public void setValue(String v) { - mValue = v; - } - - public String getNormalizedPhone() { - return mNormalizedPhone != null ? mNormalizedPhone : mValue; - } - - public String toString() { - return (isSIPAddress() ? "sip:" : "tel:") + getNormalizedPhone(); - } -} diff --git a/app/src/main/java/org/linphone/contacts/SearchContactViewHolder.java b/app/src/main/java/org/linphone/contacts/SearchContactViewHolder.java deleted file mode 100644 index 8085860e6..000000000 --- a/app/src/main/java/org/linphone/contacts/SearchContactViewHolder.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import android.view.View; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; -import androidx.recyclerview.widget.RecyclerView; -import org.linphone.R; - -public class SearchContactViewHolder extends RecyclerView.ViewHolder - implements View.OnClickListener { - public final TextView name; - public final TextView address; - public final ImageView linphoneContact; - public final ImageView isSelect; - public final RelativeLayout avatarLayout; - public final View disabled; - - private final ClickListener mListener; - - public SearchContactViewHolder(View view, ClickListener listener) { - super(view); - - name = view.findViewById(R.id.contact_name); - address = view.findViewById(R.id.contact_address); - linphoneContact = view.findViewById(R.id.contact_linphone); - isSelect = view.findViewById(R.id.contact_is_select); - avatarLayout = view.findViewById(R.id.avatar_layout); - disabled = view.findViewById(R.id.disabled); - - mListener = listener; - view.setOnClickListener(this); - } - - @Override - public void onClick(View view) { - if (mListener != null) { - if (disabled.getVisibility() == View.GONE) { - mListener.onItemClicked(getAdapterPosition()); - } - } - } - - public interface ClickListener { - void onItemClicked(int position); - } -} diff --git a/app/src/main/java/org/linphone/contacts/SearchContactsAdapter.java b/app/src/main/java/org/linphone/contacts/SearchContactsAdapter.java deleted file mode 100644 index c3537b855..000000000 --- a/app/src/main/java/org/linphone/contacts/SearchContactsAdapter.java +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.core.Address; -import org.linphone.core.Core; -import org.linphone.core.FriendCapability; -import org.linphone.core.PresenceBasicStatus; -import org.linphone.core.PresenceModel; -import org.linphone.core.ProxyConfig; -import org.linphone.core.SearchResult; - -public class SearchContactsAdapter extends RecyclerView.Adapter { - private List mContacts; - private ArrayList mContactsSelected; - private boolean mOnlySipContact = false; - private final SearchContactViewHolder.ClickListener mListener; - private final boolean mIsOnlyOnePersonSelection; - private String mPreviousSearch; - private boolean mSecurityEnabled; - - public SearchContactsAdapter( - SearchContactViewHolder.ClickListener clickListener, - boolean hideSelectionMark, - boolean isSecurityEnabled) { - mIsOnlyOnePersonSelection = hideSelectionMark; - mListener = clickListener; - setContactsSelectedList(null); - mPreviousSearch = null; - mSecurityEnabled = isSecurityEnabled; - mContacts = new ArrayList<>(); - } - - public List getContacts() { - return mContacts; - } - - public void setOnlySipContact(boolean enable) { - mOnlySipContact = enable; - } - - public void setSecurityEnabled(boolean enable) { - mSecurityEnabled = enable; - notifyDataSetChanged(); - } - - @NonNull - @Override - public SearchContactViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = - LayoutInflater.from(parent.getContext()) - .inflate(R.layout.search_contact_cell, parent, false); - return new SearchContactViewHolder(v, mListener); - } - - @Override - public void onBindViewHolder(@NonNull SearchContactViewHolder holder, int position) { - SearchResult searchResult = getItem(position); - - LinphoneContact contact; - if (searchResult.getFriend() != null && searchResult.getFriend().getUserData() != null) { - contact = (LinphoneContact) searchResult.getFriend().getUserData(); - } else { - if (searchResult.getAddress() == null) { - contact = - ContactsManager.getInstance() - .findContactFromPhoneNumber(searchResult.getPhoneNumber()); - } else { - contact = - ContactsManager.getInstance() - .findContactFromAddress(searchResult.getAddress()); - } - } - - final String numberOrAddress = - (searchResult.getPhoneNumber() != null) - ? searchResult.getPhoneNumber() - : searchResult.getAddress().asStringUriOnly(); - - holder.name.setVisibility(View.GONE); - if (contact != null && contact.getFullName() != null) { - holder.name.setVisibility(View.VISIBLE); - holder.name.setText(contact.getFullName()); - } else if (searchResult.getAddress() != null) { - if (searchResult.getAddress().getUsername() != null) { - holder.name.setVisibility(View.VISIBLE); - holder.name.setText(searchResult.getAddress().getUsername()); - } else if (searchResult.getAddress().getDisplayName() != null) { - holder.name.setVisibility(View.VISIBLE); - holder.name.setText(searchResult.getAddress().getDisplayName()); - } - } - holder.disabled.setVisibility(View.GONE); - - if (mSecurityEnabled || !mIsOnlyOnePersonSelection) { - Core core = LinphoneManager.getCore(); - ProxyConfig defaultProxyConfig = core.getDefaultProxyConfig(); - if (defaultProxyConfig != null && searchResult.getAddress() != null) { - // SDK won't accept ourselves in the list of participants - if (defaultProxyConfig.getIdentityAddress().weakEqual(searchResult.getAddress())) { - // Disable row, we can't use our own address in a group chat room - holder.disabled.setVisibility(View.VISIBLE); - } - } - } - - if (contact != null) { - if (contact.getFullName() == null - && contact.getFirstName() == null - && contact.getLastName() == null) { - contact.setFullName(holder.name.getText().toString()); - } - ContactAvatar.displayAvatar( - contact, - contact.hasFriendCapability(FriendCapability.LimeX3Dh), - holder.avatarLayout); - - if ((!mIsOnlyOnePersonSelection - && !searchResult.hasCapability(FriendCapability.GroupChat)) - || (mSecurityEnabled - && !searchResult.hasCapability(FriendCapability.LimeX3Dh))) { - // Disable row, contact doesn't have the required capabilities - holder.disabled.setVisibility(View.VISIBLE); - } - } else { - ContactAvatar.displayAvatar(holder.name.getText().toString(), holder.avatarLayout); - } - - holder.address.setText(numberOrAddress); - if (holder.linphoneContact != null) { - holder.linphoneContact.setVisibility(View.GONE); - if (searchResult.getFriend() != null - && contact != null - && contact.getBasicStatusFromPresenceModelForUriOrTel(numberOrAddress) - == PresenceBasicStatus.Open) { - holder.linphoneContact.setVisibility(View.VISIBLE); - } - } - if (holder.isSelect != null) { - if (isContactSelected(searchResult)) { - holder.isSelect.setVisibility(View.VISIBLE); - } else { - holder.isSelect.setVisibility(View.INVISIBLE); - } - if (mIsOnlyOnePersonSelection) { - holder.isSelect.setVisibility(View.GONE); - } - } - } - - public long getItemId(int position) { - return position; - } - - private synchronized boolean isContactSelected(SearchResult sr) { - for (ContactAddress c : mContactsSelected) { - Address addr = c.getAddress(); - if (addr != null && sr.getAddress() != null) { - if (addr.weakEqual(sr.getAddress())) { - return true; - } - } else { - if (c.getPhoneNumber() != null && sr.getPhoneNumber() != null) { - if (c.getPhoneNumber().compareTo(sr.getPhoneNumber()) == 0) return true; - } - } - } - return false; - } - - public synchronized ArrayList getContactsSelectedList() { - return mContactsSelected; - } - - public synchronized void setContactsSelectedList(ArrayList contactsList) { - if (contactsList == null) { - mContactsSelected = new ArrayList<>(); - } else { - mContactsSelected = contactsList; - } - } - - public synchronized boolean toggleContactSelection(ContactAddress ca) { - if (mContactsSelected.contains(ca)) { - mContactsSelected.remove(ca); - return false; - } else { - mContactsSelected.add(ca); - return true; - } - } - - private SearchResult getItem(int position) { - return mContacts.get(position); - } - - @Override - public int getItemCount() { - return mContacts.size(); - } - - public void searchContacts(String search) { - List result = new ArrayList<>(); - - if (mPreviousSearch != null) { - if (mPreviousSearch.length() > search.length()) { - ContactsManager.getInstance().getMagicSearch().resetSearchCache(); - } - } - mPreviousSearch = search; - - String domain = ""; - ProxyConfig prx = Objects.requireNonNull(LinphoneManager.getCore()).getDefaultProxyConfig(); - if (prx != null) domain = prx.getDomain(); - SearchResult[] searchResults = - ContactsManager.getInstance() - .getMagicSearch() - .getContactListFromFilter(search, mOnlySipContact ? domain : ""); - - for (SearchResult sr : searchResults) { - if (LinphoneContext.instance() - .getApplicationContext() - .getResources() - .getBoolean(R.bool.hide_sip_contacts_without_presence)) { - if (sr.getFriend() != null) { - PresenceModel pm = - sr.getFriend() - .getPresenceModelForUriOrTel(sr.getAddress().asStringUriOnly()); - if (pm != null && pm.getBasicStatus().equals(PresenceBasicStatus.Open)) { - result.add(sr); - } else { - pm = sr.getFriend().getPresenceModelForUriOrTel(sr.getPhoneNumber()); - if (pm != null && pm.getBasicStatus().equals(PresenceBasicStatus.Open)) { - result.add(sr); - } - } - } - } else { - result.add(sr); - } - } - - mContacts = result; - notifyDataSetChanged(); - } -} diff --git a/app/src/main/java/org/linphone/contacts/views/ContactAvatar.java b/app/src/main/java/org/linphone/contacts/views/ContactAvatar.java deleted file mode 100644 index d2fd5fbc6..000000000 --- a/app/src/main/java/org/linphone/contacts/views/ContactAvatar.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts.views; - -import android.graphics.Bitmap; -import android.view.View; -import org.linphone.R; -import org.linphone.contacts.LinphoneContact; -import org.linphone.core.ChatRoomSecurityLevel; -import org.linphone.utils.ImageUtils; - -public class ContactAvatar { - - private static String generateAvatar(String displayName) { - String[] names = displayName.split(" "); - StringBuilder generatedAvatarText = new StringBuilder(); - int count = 0; - for (String name : names) { - if (name != null && name.length() > 0 && count < 2) { - generatedAvatarText.append(name.charAt(0)); - count += 1; - } - } - return generatedAvatarText.toString().toUpperCase(); - } - - private static void setSecurityLevel(ChatRoomSecurityLevel level, View v) { - ContactAvatarHolder holder = new ContactAvatarHolder(v); - if (holder.securityLevel != null) { - holder.securityLevel.setVisibility(View.VISIBLE); - switch (level) { - case Safe: - holder.securityLevel.setImageResource(R.drawable.security_2_indicator); - break; - case Encrypted: - holder.securityLevel.setImageResource(R.drawable.security_1_indicator); - break; - case ClearText: - case Unsafe: - default: - holder.securityLevel.setImageResource(R.drawable.security_alert_indicator); - break; - } - } else { - holder.securityLevel.setVisibility(View.GONE); - } - } - - private static void showHasLimeX3dhCapability(View v) { - ContactAvatarHolder holder = new ContactAvatarHolder(v); - if (holder.securityLevel != null) { - holder.securityLevel.setVisibility(View.VISIBLE); - holder.securityLevel.setImageResource(R.drawable.security_toogle_icon_green); - } else { - holder.securityLevel.setVisibility(View.GONE); - } - } - - public static void displayAvatar(String displayName, View v, boolean showBorder) { - if (displayName == null || v == null) return; - - ContactAvatarHolder holder = new ContactAvatarHolder(v); - holder.init(); - - boolean generated_avatars = - v.getContext().getResources().getBoolean(R.bool.generate_text_avatar); - if (displayName.startsWith("+") || !generated_avatars) { - // If display name is a phone number, use default avatar because generated one will be - // +... - holder.generatedAvatar.setVisibility(View.GONE); - holder.generatedAvatarBackground.setVisibility(View.GONE); - } else { - String generatedAvatar = generateAvatar(displayName); - if (generatedAvatar != null && generatedAvatar.length() > 0) { - holder.generatedAvatar.setText(generatedAvatar); - holder.generatedAvatar.setVisibility(View.VISIBLE); - holder.generatedAvatarBackground.setVisibility(View.VISIBLE); - } else { - holder.generatedAvatar.setVisibility(View.GONE); - holder.generatedAvatarBackground.setVisibility(View.GONE); - } - } - holder.securityLevel.setVisibility(View.GONE); - - if (showBorder) { - holder.avatarBorder.setVisibility(View.VISIBLE); - } - } - - public static void displayAvatar(String displayName, View v) { - displayAvatar(displayName, v, false); - } - - public static void displayAvatar( - String displayName, ChatRoomSecurityLevel securityLevel, View v) { - displayAvatar(displayName, v); - setSecurityLevel(securityLevel, v); - } - - public static void displayAvatar(LinphoneContact contact, View v, boolean showBorder) { - if (contact == null || v == null) return; - - ContactAvatarHolder holder = new ContactAvatarHolder(v); - holder.init(); - - boolean generated_avatars = - v.getContext().getResources().getBoolean(R.bool.generate_text_avatar); - - // Kepp the generated avatar ready in case of failure while loading picture - holder.generatedAvatar.setText( - generateAvatar( - contact.getFullName() == null - ? contact.getFirstName() + " " + contact.getLastName() - : contact.getFullName())); - - holder.generatedAvatar.setVisibility(View.GONE); - holder.generatedAvatarBackground.setVisibility(View.GONE); - holder.contactPicture.setVisibility(View.VISIBLE); - holder.securityLevel.setVisibility(View.GONE); - - Bitmap bm = ImageUtils.getRoundBitmapFromUri(v.getContext(), contact.getThumbnailUri()); - if (bm != null) { - displayAvatar(bm, holder); - } else if (generated_avatars) { - holder.generatedAvatar.setVisibility(View.VISIBLE); - holder.generatedAvatarBackground.setVisibility(View.VISIBLE); - } - - if (showBorder) { - holder.avatarBorder.setVisibility(View.VISIBLE); - } - } - - private static void displayAvatar(Bitmap bm, ContactAvatarHolder holder) { - holder.contactPicture.setImageBitmap(bm); - holder.contactPicture.setVisibility(View.VISIBLE); - holder.generatedAvatar.setVisibility(View.GONE); - holder.generatedAvatarBackground.setVisibility(View.GONE); - } - - public static void displayAvatar(Bitmap bm, View v) { - if (bm == null || v == null) return; - - ContactAvatarHolder holder = new ContactAvatarHolder(v); - holder.init(); - - holder.generatedAvatar.setVisibility(View.GONE); - holder.generatedAvatarBackground.setVisibility(View.GONE); - holder.contactPicture.setVisibility(View.VISIBLE); - holder.securityLevel.setVisibility(View.GONE); - - displayAvatar(bm, holder); - } - - public static void displayAvatar(LinphoneContact contact, View v) { - displayAvatar(contact, v, false); - } - - public static void displayAvatar( - LinphoneContact contact, boolean hasLimeX3dhCapability, View v) { - displayAvatar(contact, v); - if (hasLimeX3dhCapability) { - showHasLimeX3dhCapability(v); - } - } - - public static void displayAvatar( - LinphoneContact contact, ChatRoomSecurityLevel securityLevel, View v) { - displayAvatar(contact, v); - setSecurityLevel(securityLevel, v); - } - - public static void displayGroupChatAvatar(View v) { - ContactAvatarHolder holder = new ContactAvatarHolder(v); - holder.contactPicture.setImageResource(R.drawable.chat_group_avatar); - holder.generatedAvatar.setVisibility(View.GONE); - holder.generatedAvatarBackground.setVisibility(View.GONE); - holder.securityLevel.setVisibility(View.GONE); - holder.avatarBorder.setVisibility(View.GONE); - } - - public static void displayGroupChatAvatar(ChatRoomSecurityLevel level, View v) { - displayGroupChatAvatar(v); - setSecurityLevel(level, v); - } -} diff --git a/app/src/main/java/org/linphone/contacts/views/ContactAvatarHolder.java b/app/src/main/java/org/linphone/contacts/views/ContactAvatarHolder.java deleted file mode 100644 index 3ae8bce5c..000000000 --- a/app/src/main/java/org/linphone/contacts/views/ContactAvatarHolder.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts.views; - -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; -import org.linphone.R; - -class ContactAvatarHolder { - public final ImageView contactPicture; - public final ImageView avatarBorder; - public final ImageView securityLevel; - public final TextView generatedAvatar; - public final ImageView generatedAvatarBackground; - - public ContactAvatarHolder(View v) { - contactPicture = v.findViewById(R.id.contact_picture); - securityLevel = v.findViewById(R.id.security_level); - generatedAvatar = v.findViewById(R.id.generated_avatar); - generatedAvatarBackground = v.findViewById(R.id.generated_avatar_background); - avatarBorder = v.findViewById(R.id.border); - } - - public void init() { - contactPicture.setVisibility(View.VISIBLE); - generatedAvatar.setVisibility(View.VISIBLE); - generatedAvatarBackground.setVisibility(View.VISIBLE); - securityLevel.setVisibility(View.GONE); - avatarBorder.setVisibility(View.GONE); - } -} diff --git a/app/src/main/java/org/linphone/contacts/views/ContactSelectView.java b/app/src/main/java/org/linphone/contacts/views/ContactSelectView.java deleted file mode 100644 index 60ba826d2..000000000 --- a/app/src/main/java/org/linphone/contacts/views/ContactSelectView.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.contacts.views; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.contacts.ContactAddress; - -public class ContactSelectView extends View { - private final TextView mContactName; - private final ImageView mDeleteContact; - - public ContactSelectView(Context context) { - super(context); - - LayoutInflater inflater = - (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - View view = inflater.inflate(R.layout.contact_selected, null); - - mContactName = view.findViewById(R.id.sipUri); - mDeleteContact = view.findViewById(R.id.contactChatDelete); - } - - public void setContactName(ContactAddress ca) { - if (ca.getContact() != null) { - mContactName.setText(ca.getContact().getFirstName()); - } else { - LinphoneManager.getCore() - .createFriendWithAddress(ca.getAddressAsDisplayableString()) - .getName(); - mContactName.setText(ca.getAddressAsDisplayableString()); - } - } - - public void setListener(OnClickListener listener) { - mDeleteContact.setOnClickListener(listener); - } -} diff --git a/app/src/main/java/org/linphone/core/BootReceiver.kt b/app/src/main/java/org/linphone/core/BootReceiver.kt new file mode 100644 index 000000000..34c09968c --- /dev/null +++ b/app/src/main/java/org/linphone/core/BootReceiver.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.core + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.core.content.ContextCompat +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.core.tools.Log + +class BootReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val serviceIntent = Intent(Intent.ACTION_MAIN).setClass(context, CoreService::class.java) + if (intent.action.equals(Intent.ACTION_SHUTDOWN, ignoreCase = true)) { + android.util.Log.d( + context.getString(R.string.app_name), + "[Boot Receiver] Device is shutting down, destroying Core to unregister" + ) + context.stopService(serviceIntent) + } else if (intent.action.equals(Intent.ACTION_BOOT_COMPLETED, ignoreCase = true)) { + val autoStart = corePreferences.autoStart + Log.i("[Boot Receiver] Device is starting, autoStart is $autoStart") + if (autoStart) { + serviceIntent.putExtra("StartForeground", true) + ContextCompat.startForegroundService(context, serviceIntent) + } + } + } +} diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt new file mode 100644 index 000000000..ca879cc04 --- /dev/null +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -0,0 +1,439 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.core + +import android.content.Context +import android.content.Intent +import android.graphics.PixelFormat +import android.media.AudioManager +import android.os.Handler +import android.os.Looper +import android.os.Vibrator +import android.telephony.PhoneStateListener +import android.telephony.TelephonyManager +import android.view.* +import java.io.File +import java.util.* +import kotlin.math.abs +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.activities.call.CallActivity +import org.linphone.activities.call.IncomingCallActivity +import org.linphone.activities.call.OutgoingCallActivity +import org.linphone.compatibility.Compatibility +import org.linphone.contact.Contact +import org.linphone.contact.ContactsManager +import org.linphone.core.tools.Log +import org.linphone.mediastream.Version +import org.linphone.notifications.NotificationsManager +import org.linphone.utils.AppUtils +import org.linphone.utils.LinphoneUtils + +class CoreContext(val context: Context, coreConfig: Config) { + val core: Core + val handler: Handler = Handler(Looper.getMainLooper()) + + val appVersion: String by lazy { + "${org.linphone.BuildConfig.VERSION_NAME} (${org.linphone.BuildConfig.BUILD_TYPE})" + } + + val sdkVersion: String by lazy { + val sdkVersion = context.getString(org.linphone.R.string.linphone_sdk_version) + val sdkBranch = context.getString(org.linphone.R.string.linphone_sdk_branch) + "$sdkVersion ($sdkBranch)" + } + + val contactsManager: ContactsManager by lazy { + ContactsManager(context) + } + val notificationsManager: NotificationsManager by lazy { + NotificationsManager(context) + } + + private var gsmCallActive = false + private val phoneStateListener = object : PhoneStateListener() { + override fun onCallStateChanged(state: Int, phoneNumber: String?) { + gsmCallActive = when (state) { + TelephonyManager.CALL_STATE_OFFHOOK -> { + Log.i("[Context] Phone state is off hook") + true + } + TelephonyManager.CALL_STATE_RINGING -> { + Log.i("[Context] Phone state is ringing") + true + } + TelephonyManager.CALL_STATE_IDLE -> { + Log.i("[Context] Phone state is idle") + false + } + else -> { + Log.w("[Context] Phone state is unexpected: $state") + false + } + } + } + } + + private var overlayX = 0f + private var overlayY = 0f + private var callOverlay: View? = null + private var isVibrating = false + + private val listener: CoreListenerStub = object : CoreListenerStub() { + override fun onGlobalStateChanged(core: Core, state: GlobalState, message: String) { + Log.i("[Context] Global state changed [$state]") + if (state == GlobalState.On) { + contactsManager.fetchContactsAsync() + } + } + + override fun onCallStateChanged( + core: Core, + call: Call, + state: Call.State, + message: String + ) { + Log.i("[Context] Call state changed [$state]") + if (state == Call.State.IncomingReceived || state == Call.State.IncomingEarlyMedia) { + if (gsmCallActive) { + Log.w("[Context] Refusing the call with reason busy because a GSM call is active") + call.decline(Reason.Busy) + return + } + + if (core.callsNb == 1 && corePreferences.vibrateWhileIncomingCall) { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + if ((audioManager.ringerMode == AudioManager.RINGER_MODE_VIBRATE || + audioManager.ringerMode == AudioManager.RINGER_MODE_NORMAL)) { + val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + if (vibrator.hasVibrator()) { + Log.i("[Context] Starting incoming call vibration") + Compatibility.vibrate(vibrator) + isVibrating = true + } + } + } + + // Starting SDK 24 (Android 7.0) we rely on the fullscreen intent of the call incoming notification + if (Version.sdkStrictlyBelow(Version.API24_NOUGAT_70)) { + onIncomingReceived() + } + + if (corePreferences.autoAnswerEnabled) { + val autoAnswerDelay = corePreferences.autoAnswerDelay + if (autoAnswerDelay == 0) { + Log.w("[Context] Auto answering call immediately") + answerCall(call) + } else { + val timer = Timer("Auto answer scheduler") + Log.i("[Context] Scheduling auto answering in $autoAnswerDelay milliseconds") + timer.schedule(object : TimerTask() { + override fun run() { + Log.w("[Context] Auto answering call") + answerCall(call) + } + }, autoAnswerDelay.toLong()) + } + } + } else if (state == Call.State.OutgoingInit) { + onOutgoingStarted() + } else if (state == Call.State.Connected) { + if (isVibrating) { + Log.i("[Context] Stopping vibration") + val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + vibrator.cancel() + isVibrating = false + } + + onCallStarted() + } else if (state == Call.State.End || state == Call.State.Error || state == Call.State.Released) { + if (core.callsNb == 0) { + if (isVibrating) { + Log.i("[Context] Stopping vibration") + val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + vibrator.cancel() + isVibrating = false + } + + removeCallOverlay() + } + } + } + } + + init { + core = Factory.instance().createCoreWithConfig(coreConfig, context) + Log.i("[Context] Ready") + } + + fun start(isPush: Boolean = false) { + Log.i("[Context] Starting") + + notificationsManager.onCoreReady() + + core.addListener(listener) + if (isPush) { + Log.i("[Context] Push received, assume in background") + core.enterBackground() + } + core.config.setBool("net", "use_legacy_push_notification_params", true) + core.start() + + configureCore() + + val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + Log.i("[Context] Registering phone state listener") + telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE) + } + + fun stop() { + Log.i("[Context] Stopping") + + val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + Log.i("[Context] Unregistering phone state listener") + telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE) + + notificationsManager.destroy() + contactsManager.destroy() + + core.stop() + core.removeListener(listener) + } + + private fun configureCore() { + Log.i("[Context] Configuring Core") + + core.zrtpSecretsFile = context.filesDir.absolutePath + "/zrtp_secrets" + core.callLogsDatabasePath = context.filesDir.absolutePath + "/linphone-log-history.db" + + initUserCertificates() + + computeUserAgent() + + for (lpc in core.proxyConfigList) { + if (lpc.identityAddress.domain == corePreferences.defaultDomain) { + // Ensure conference URI is set on sip.linphone.org proxy configs + if (lpc.conferenceFactoryUri == null) { + lpc.edit() + val uri = corePreferences.conferenceServerUri + Log.i("[Context] Setting conference factory on proxy config ${lpc.identityAddress.asString()} to default value: $uri") + lpc.conferenceFactoryUri = uri + lpc.done() + } + + // Ensure LIME server URL is set if at least one sip.linphone.org proxy + if (core.limeX3DhAvailable()) { + var url: String? = core.limeX3DhServerUrl + if (url == null || url.isEmpty()) { + url = corePreferences.limeX3dhServerUrl + Log.i("[Context] Setting LIME X3Dh server url to default value: $url") + core.limeX3DhServerUrl = url + } + } + } + } + + Log.i("[Context] Core configured") + } + + private fun computeUserAgent() { + val deviceName: String = corePreferences.deviceName + val appName: String = context.resources.getString(org.linphone.R.string.app_name) + val androidVersion = org.linphone.BuildConfig.VERSION_NAME + val userAgent = "$appName/$androidVersion ($deviceName) LinphoneSDK" + val sdkVersion = context.getString(org.linphone.R.string.linphone_sdk_version) + val sdkBranch = context.getString(org.linphone.R.string.linphone_sdk_branch) + val sdkUserAgent = "$sdkVersion ($sdkBranch)" + core.setUserAgent(userAgent, sdkUserAgent) + } + + private fun initUserCertificates() { + val userCertsPath = context.filesDir.absolutePath + "/user-certs" + val f = File(userCertsPath) + if (!f.exists()) { + if (!f.mkdir()) { + Log.e("[Context] $userCertsPath can't be created.") + } + } + core.userCertificatesPath = userCertsPath + } + + /* Call related functions */ + + fun answerCall(call: Call) { + Log.i("[Context] Answering call $call") + val params = core.createCallParams(call) + params.recordFile = LinphoneUtils.getRecordingFilePathForAddress(call.remoteAddress) + call.acceptWithParams(params) + } + + fun declineCall(call: Call) { + Log.i("[Context] Declining call $call") + call.decline(Reason.Declined) + } + + fun terminateCall(call: Call) { + Log.i("[Context] Terminating call $call") + call.terminate() + } + + fun transferCallTo(addressToCall: String) { + val currentCall = core.currentCall ?: core.calls.first() + if (currentCall == null) { + Log.e("[Context] Couldn't find a call to transfer") + } else { + Log.i("[Context] Transferring current call to $addressToCall") + currentCall.transfer(addressToCall) + } + } + + fun startCall(to: String) { + var stringAddress = to + if (android.util.Patterns.PHONE.matcher(to).matches()) { + val contact: Contact? = contactsManager.findContactByPhoneNumber(to) + val alias = contact?.getContactForPhoneNumberOrAddress(to) + if (alias != null) { + Log.i("[Context] Found matching alias $alias for phone number $to, using it") + stringAddress = alias + } + } + + val address: Address? = core.interpretUrl(stringAddress) + if (address == null) { + Log.e("[Context] Failed to parse $stringAddress, abort outgoing call") + return + } + + startCall(address) + } + + fun startCall(address: Address, forceZRTP: Boolean = false) { + if (!core.isNetworkReachable) { + Log.e("[Context] Network unreachable, abort outgoing call") + return + } + + val params = core.createCallParams(null) + if (forceZRTP) { + params.mediaEncryption = MediaEncryption.ZRTP + } + params.recordFile = LinphoneUtils.getRecordingFilePathForAddress(address) + + val call = core.inviteAddressWithParams(address, params) + Log.i("[Context] Starting call $call") + } + + fun switchCamera() { + val currentDevice = core.videoDevice + Log.i("[Context] Current camera device is $currentDevice") + + val devices = core.videoDevicesList + for (camera in devices) { + if (camera != currentDevice && camera != "StaticImage: Static picture") { + Log.i("[Context] New camera device will be $camera") + core.videoDevice = camera + break + } + } + + val call = core.currentCall + if (call == null) { + Log.w("[Context] Switching camera while not in call") + return + } + call.update(null) + } + + fun createCallOverlay() { + if (!corePreferences.showCallOverlay || callOverlay != null) { + return + } + + if (overlayY == 0f) overlayY = AppUtils.pixelsToDp(40f) + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val params: WindowManager.LayoutParams = WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, + Compatibility.getOverlayType(), WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT) + params.x = overlayX.toInt() + params.y = overlayY.toInt() + params.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL + val overlay = LayoutInflater.from(context).inflate(org.linphone.R.layout.call_overlay, null) + + var initX = overlayX + var initY = overlayY + overlay.setOnTouchListener { _, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + initX = params.x - event.rawX + initY = params.y - event.rawY + } + MotionEvent.ACTION_MOVE -> { + val x = (event.rawX + initX).toInt() + val y = (event.rawY + initY).toInt() + + params.x = x + params.y = y + windowManager.updateViewLayout(overlay, params) + } + MotionEvent.ACTION_UP -> { + if (abs(overlayX - params.x) < 5 && abs(overlayY - params.y) < 5) { + onCallStarted() + } + overlayX = params.x.toFloat() + overlayY = params.y.toFloat() + } + else -> false + } + true + } + + callOverlay = overlay + windowManager.addView(overlay, params) + } + + fun removeCallOverlay() { + if (callOverlay != null) { + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + windowManager.removeView(callOverlay) + callOverlay = null + } + } + + /* Start call related activities */ + + private fun onIncomingReceived() { + val intent = Intent(context, IncomingCallActivity::class.java) + // This flag is required to start an Activity from a Service context + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } + + private fun onOutgoingStarted() { + val intent = Intent(context, OutgoingCallActivity::class.java) + // This flag is required to start an Activity from a Service context + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } + + private fun onCallStarted() { + val intent = Intent(context, CallActivity::class.java) + // This flag is required to start an Activity from a Service context + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + context.startActivity(intent) + } +} diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt new file mode 100644 index 000000000..67cdfa847 --- /dev/null +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.core + +import android.content.Context +import java.io.File +import java.io.FileOutputStream +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.compatibility.Compatibility +import org.linphone.core.tools.Log + +class CorePreferences constructor(private val context: Context) { + private var _config: Config? = null + var config: Config + get() = _config ?: coreContext.core.config + set(value) { + _config = value + } + + /* App settings */ + + var debugLogs: Boolean + get() = config.getBool("app", "debug", true) + set(value) { + config.setBool("app", "debug", value) + } + + var autoStart: Boolean + get() = config.getBool("app", "auto_start", true) + set(value) { + config.setBool("app", "auto_start", value) + } + + var keepServiceAlive: Boolean + get() = config.getBool("app", "keep_service_alive", false) + set(value) { + config.setBool("app", "keep_service_alive", value) + } + + /* UI */ + + var forcePortrait: Boolean + get() = config.getBool("app", "force_portrait_orientation", false) + set(value) { + config.setBool("app", "force_portrait_orientation", value) + } + + /** -1 means auto, 0 no, 1 yes */ + var darkMode: Int + get() { + if (!darkModeAllowed) return 0 + return config.getInt("app", "dark_mode", -1) + } + set(value) { + config.setInt("app", "dark_mode", value) + } + + /* Audio */ + + val echoCancellerCalibration: Int + get() = config.getInt("sound", "ec_delay", -1) + + /* Video */ + + // TODO: use it to show video preview on dialer for tablets + var videoPreview: Boolean + get() = config.getBool("app", "video_preview", false) + set(value) = config.setBool("app", "video_preview", value) + + /* Chat */ + + var makePublicDownloadedImages: Boolean + get() = config.getBool("app", "make_downloaded_images_public_in_gallery", true) + set(value) { + config.setBool("app", "make_downloaded_images_public_in_gallery", value) + } + + var hideEmptyRooms: Boolean + get() = config.getBool("app", "hide_empty_chat_rooms", true) + set(value) { + config.setBool("app", "hide_empty_chat_rooms", value) + } + + var hideRoomsFromRemovedProxies: Boolean + get() = config.getBool("app", "hide_chat_rooms_from_removed_proxies", true) + set(value) { + config.setBool("app", "hide_chat_rooms_from_removed_proxies", value) + } + + var deviceName: String + get() = config.getString("app", "device_name", Compatibility.getDeviceName(context)) + set(value) = config.setString("app", "device_name", value) + + /* Contacts */ + + // TODO: use it + var storePresenceInNativeContact: Boolean + get() = config.getBool("app", "store_presence_in_native_contact", false) + set(value) { + config.setBool("app", "store_presence_in_native_contact", value) + } + + var displayOrganization: Boolean + get() = config.getBool("app", "display_contact_organization", contactOrganizationVisible) + set(value) { + config.setBool("app", "display_contact_organization", value) + } + + var contactsShortcuts: Boolean + get() = config.getBool("app", "shortcuts", true) + set(value) { + config.setBool("app", "shortcuts", value) + } + + /* Call */ + + var vibrateWhileIncomingCall: Boolean + get() = config.getBool("app", "incoming_call_vibration", true) + set(value) { + config.setBool("app", "incoming_call_vibration", value) + } + + var autoAnswerEnabled: Boolean + get() = config.getBool("app", "auto_answer", false) + set(value) { + config.setBool("app", "auto_answer", value) + } + + var autoAnswerDelay: Int + get() = config.getInt("app", "auto_answer_delay", 0) + set(value) { + config.setInt("app", "auto_answer_delay", value) + } + + var showCallOverlay: Boolean + get() = config.getBool("app", "call_overlay", false) + set(value) { + config.setBool("app", "call_overlay", value) + } + + /* Assistant */ + + var firstStart: Boolean + get() = config.getBool("app", "first_start", true) + set(value) { + config.setBool("app", "first_start", value) + } + + var xmlRpcServerUrl: String? + get() = config.getString("assistant", "xmlrpc_url", null) + set(value) { + config.setString("assistant", "xmlrpc_url", value) + } + + /* Dialog related */ + + var limeSecurityPopupEnabled: Boolean + get() = config.getBool("app", "lime_security_popup_enabled", true) + set(value) { + config.setBool("app", "lime_security_popup_enabled", value) + } + + /* Other */ + + var voiceMailUri: String? + get() = config.getString("app", "voice_mail", null) + set(value) { + config.setString("app", "voice_mail", value) + } + + /* App settings previously in non_localizable_custom */ + + val defaultDomain: String + get() = config.getString("app", "default_domain", "sip.linphone.org") + + val fetchContactsFromDefaultDirectory: Boolean + get() = config.getBool("app", "fetch_contacts_from_default_directory", true) + + val hideContactsWithoutPresence: Boolean + get() = config.getBool("app", "hide_contacts_without_presence", false) + + val rlsUri: String + get() = config.getString("app", "rls_uri", "sip:rls@sip.linphone.org") + + val conferenceServerUri: String + get() = config.getString("app", "default_conference_factory_uri", "sip:conference-factory@sip.linphone.org") + + val limeX3dhServerUrl: String + get() = config.getString("app", "default_lime_x3dh_server_url", "https://lime.linphone.org/lime-server/lime-server.php") + + val allowMultipleFilesAndTextInSameMessage: Boolean + get() = config.getBool("app", "allow_multiple_files_and_text_in_same_message", true) + + val contactOrganizationVisible: Boolean + get() = config.getBool("app", "display_contact_organization", true) + + // If enabled, SIP addresses will be stored in a different raw id than the contact and with a custom MIME type + // If disabled, account won't be created + val useLinphoneSyncAccount: Boolean + get() = config.getBool("app", "use_linphone_tag", true) + + private val darkModeAllowed: Boolean + get() = config.getBool("app", "dark_mode_allowed", true) + + /* Assets stuff */ + + val configPath: String + get() = context.filesDir.absolutePath + "/.linphonerc" + + val factoryConfigPath: String + get() = context.filesDir.absolutePath + "/linphonerc" + + val linphoneDefaultValuesPath: String + get() = context.filesDir.absolutePath + "/assistant_linphone_default_values" + + val defaultValuesPath: String + get() = context.filesDir.absolutePath + "/assistant_default_values" + + val ringtonePath: String + get() = context.filesDir.absolutePath + "/share/sounds/linphone/rings/notes_of_the_optimistic.mkv" + + fun copyAssetsFromPackage() { + copy("linphonerc_default", configPath) + copy("linphonerc_factory", factoryConfigPath, true) + copy("assistant_linphone_default_values", linphoneDefaultValuesPath, true) + copy("assistant_default_values", defaultValuesPath, true) + } + + fun getString(resource: Int): String { + return context.getString(resource) + } + + private fun copy(from: String, to: String, overrideIfExists: Boolean = false) { + val outFile = File(to) + if (outFile.exists()) { + if (!overrideIfExists) { + Log.i("[Preferences] File $to already exists") + return + } + } + Log.i("[Preferences] Overriding $to by $from asset") + + val outStream = FileOutputStream(outFile) + val inFile = context.assets.open(from) + val buffer = ByteArray(1024) + var length: Int = inFile.read(buffer) + + while (length > 0) { + outStream.write(buffer, 0, length) + length = inFile.read(buffer) + } + + inFile.close() + outStream.flush() + outStream.close() + } +} diff --git a/app/src/main/java/org/linphone/menu/SideMenuItem.java b/app/src/main/java/org/linphone/core/CorePushReceiver.kt similarity index 65% rename from app/src/main/java/org/linphone/menu/SideMenuItem.java rename to app/src/main/java/org/linphone/core/CorePushReceiver.kt index 74e5a22ca..b3dbd15f6 100644 --- a/app/src/main/java/org/linphone/menu/SideMenuItem.java +++ b/app/src/main/java/org/linphone/core/CorePushReceiver.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2019 Belledonne Communications SARL. + * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,18 +17,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.menu; +package org.linphone.core -public class SideMenuItem { - final String name; - final int icon; +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import org.linphone.core.tools.Log - SideMenuItem(String name, int icon) { - this.name = name; - this.icon = icon; - } - - public String toString() { - return name; +class CorePushReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + Log.f("[Core Push Receiver] Push received") } } diff --git a/app/src/main/java/org/linphone/core/CoreService.kt b/app/src/main/java/org/linphone/core/CoreService.kt new file mode 100644 index 000000000..94c16eb5d --- /dev/null +++ b/app/src/main/java/org/linphone/core/CoreService.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.core + +import android.content.Intent +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.tools.Log +import org.linphone.core.tools.service.CoreService + +class CoreService : CoreService() { + override fun onCreate() { + super.onCreate() + + coreContext.notificationsManager.service = this + Log.i("[Service] Created") + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + if (intent.extras?.get("StartForeground") == true) { + Log.i("[Service] Starting as foreground") + coreContext.notificationsManager.startForeground(this, true) + } + return super.onStartCommand(intent, flags, startId) + } + + override fun createServiceNotificationChannel() { + // Done elsewhere + } + + override fun showForegroundServiceNotification() { + coreContext.notificationsManager.startCallForeground(this) + } + + override fun hideForegroundServiceNotification() { + coreContext.notificationsManager.stopCallForeground() + } + + override fun onDestroy() { + Log.i("[Service] Stopping") + coreContext.notificationsManager.service = null + + super.onDestroy() + } +} diff --git a/app/src/main/java/org/linphone/dialer/DialerActivity.java b/app/src/main/java/org/linphone/dialer/DialerActivity.java deleted file mode 100644 index 42736cf72..000000000 --- a/app/src/main/java/org/linphone/dialer/DialerActivity.java +++ /dev/null @@ -1,448 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.dialer; - -import android.Manifest; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.TextureView; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.asynclayoutinflater.view.AsyncLayoutInflater; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Collection; -import org.linphone.BuildConfig; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.activities.MainActivity; -import org.linphone.call.views.CallButton; -import org.linphone.contacts.ContactsActivity; -import org.linphone.contacts.ContactsManager; -import org.linphone.core.Call; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.VersionUpdateCheckResult; -import org.linphone.core.tools.Log; -import org.linphone.dialer.views.AddressText; -import org.linphone.dialer.views.Digit; -import org.linphone.dialer.views.EraseButton; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.LinphoneUtils; - -public class DialerActivity extends MainActivity implements AddressText.AddressChangedListener { - private static final String ACTION_CALL_LINPHONE = "org.linphone.intent.action.CallLaunched"; - - private AddressText mAddress; - private CallButton mStartCall, mAddCall, mTransferCall; - private ImageView mAddContact, mBackToCall; - - private boolean mIsTransfer; - private CoreListenerStub mListener; - private boolean mInterfaceLoaded; - private String mAddressToCallOnLayoutReady; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mInterfaceLoaded = false; - // Uses the fragment container layout to inflate the dialer view instead of using a fragment - new AsyncLayoutInflater(this) - .inflate( - R.layout.dialer, - null, - new AsyncLayoutInflater.OnInflateFinishedListener() { - @Override - public void onInflateFinished( - @NonNull View view, int resid, @Nullable ViewGroup parent) { - LinearLayout fragmentContainer = - findViewById(R.id.fragmentContainer); - LinearLayout.LayoutParams params = - new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT); - fragmentContainer.addView(view, params); - initUI(view); - mInterfaceLoaded = true; - if (mAddressToCallOnLayoutReady != null) { - mAddress.setText(mAddressToCallOnLayoutReady); - mAddressToCallOnLayoutReady = null; - } - } - }); - - if (isTablet()) { - findViewById(R.id.fragmentContainer2).setVisibility(View.GONE); - } - - mListener = - new CoreListenerStub() { - @Override - public void onCallStateChanged( - Core core, Call call, Call.State state, String message) { - if (state == Call.State.OutgoingInit) { - if (mAddress != null) mAddress.setText(""); - } - updateLayout(); - } - - @Override - public void onVersionUpdateCheckResultReceived( - Core core, - VersionUpdateCheckResult result, - String version, - String url) { - if (result == VersionUpdateCheckResult.NewVersionAvailable) { - final String urlToUse = url; - final String versionAv = version; - LinphoneUtils.dispatchOnUIThreadAfter( - new Runnable() { - @Override - public void run() { - AlertDialog.Builder builder = - new AlertDialog.Builder(DialerActivity.this); - builder.setMessage( - getString(R.string.update_available) - + ": " - + versionAv); - builder.setCancelable(false); - builder.setNeutralButton( - getString(R.string.ok), - new DialogInterface.OnClickListener() { - @Override - public void onClick( - DialogInterface dialogInterface, - int i) { - if (urlToUse != null) { - Intent urlIntent = - new Intent( - Intent.ACTION_VIEW); - urlIntent.setData( - Uri.parse(urlToUse)); - startActivity(urlIntent); - } - } - }); - builder.show(); - } - }, - 1000); - } - } - }; - - // On dialer we ask for all permissions - mPermissionsToHave = - new String[] { - // This one is to allow floating notifications - Manifest.permission.SYSTEM_ALERT_WINDOW, - // Required starting Android 9 to be able to start a foreground service - "android.permission.FOREGROUND_SERVICE", - Manifest.permission.WRITE_CONTACTS, - Manifest.permission.READ_CONTACTS - }; - - mIsTransfer = false; - handleIntentParams(getIntent()); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - - handleIntentParams(intent); - } - - @Override - protected void onResume() { - super.onResume(); - - mDialerSelected.setVisibility(View.VISIBLE); - - Core core = LinphoneManager.getCore(); - if (core != null) { - core.addListener(mListener); - } - - if (mInterfaceLoaded) { - updateLayout(); - enableVideoPreviewIfTablet(true); - } - } - - @Override - protected void onPause() { - enableVideoPreviewIfTablet(false); - Core core = LinphoneManager.getCore(); - if (core != null) { - core.removeListener(mListener); - } - - super.onPause(); - } - - @Override - protected void onDestroy() { - if (mInterfaceLoaded) { - if (mAddress != null) mAddress.setText(""); - mAddress = null; - mStartCall = null; - mAddCall = null; - mTransferCall = null; - mAddContact = null; - mBackToCall = null; - } - if (mListener != null) mListener = null; - - super.onDestroy(); - } - - @Override - protected void onStart() { - super.onStart(); - - if (getResources().getBoolean(R.bool.check_for_update_when_app_starts)) { - checkForUpdate(); - } - } - - private void checkForUpdate() { - String url = LinphonePreferences.instance().getCheckReleaseUrl(); - if (url != null && !url.isEmpty()) { - int lastTimestamp = LinphonePreferences.instance().getLastCheckReleaseTimestamp(); - int currentTimeStamp = (int) System.currentTimeMillis(); - int interval = getResources().getInteger(R.integer.time_between_update_check); // 24h - if (lastTimestamp == 0 || currentTimeStamp - lastTimestamp >= interval) { - LinphoneManager.getCore().checkForUpdate(BuildConfig.VERSION_NAME); - LinphonePreferences.instance().setLastCheckReleaseTimestamp(currentTimeStamp); - } - } - } - - private void initUI(View view) { - mAddress = view.findViewById(R.id.address); - mAddress.setAddressListener(this); - - EraseButton erase = view.findViewById(R.id.erase); - erase.setAddressWidget(mAddress); - - mStartCall = view.findViewById(R.id.start_call); - mStartCall.setAddressWidget(mAddress); - - mAddCall = view.findViewById(R.id.add_call); - mAddCall.setAddressWidget(mAddress); - - mTransferCall = view.findViewById(R.id.transfer_call); - mTransferCall.setAddressWidget(mAddress); - mTransferCall.setIsTransfer(true); - - mAddContact = view.findViewById(R.id.add_contact); - mAddContact.setEnabled(false); - mAddContact.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(DialerActivity.this, ContactsActivity.class); - intent.putExtra("EditOnClick", true); - intent.putExtra("SipAddress", mAddress.getText().toString()); - startActivity(intent); - if (mAddress != null) mAddress.setText(""); - } - }); - - mBackToCall = view.findViewById(R.id.back_to_call); - mBackToCall.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - goBackToCall(); - if (mAddress != null) mAddress.setText(""); - } - }); - - if (getIntent() != null) { - mAddress.setText(getIntent().getStringExtra("SipUri")); - } - - setUpNumpad(view); - updateLayout(); - enableVideoPreviewIfTablet(true); - } - - private void enableVideoPreviewIfTablet(boolean enable) { - Core core = LinphoneManager.getCore(); - TextureView preview = findViewById(R.id.video_preview); - ImageView changeCamera = findViewById(R.id.video_preview_change_camera); - - if (preview != null && changeCamera != null && core != null) { - if (enable && isTablet() && LinphonePreferences.instance().isVideoPreviewEnabled()) { - preview.setVisibility(View.VISIBLE); - core.setNativePreviewWindowId(preview); - core.enableVideoPreview(true); - - if (core.getVideoDevicesList().length > 1) { - changeCamera.setVisibility(View.VISIBLE); - changeCamera.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - LinphoneManager.getCallManager().switchCamera(); - } - }); - } - } else { - preview.setVisibility(View.GONE); - changeCamera.setVisibility(View.GONE); - core.setNativePreviewWindowId(null); - core.enableVideoPreview(false); - } - } - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putSerializable("address", mAddress.getText().toString()); - outState.putSerializable("isTransfer", mIsTransfer); - } - - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - mIsTransfer = savedInstanceState.getBoolean("isTransfer"); - if (mAddress != null) mAddress.setText(savedInstanceState.getString("address")); - } - - @Override - public void onAddressChanged() { - mAddContact.setEnabled(!mAddress.getText().toString().isEmpty()); - } - - private void updateLayout() { - Core core = LinphoneManager.getCore(); - if (core == null) { - return; - } - - boolean atLeastOneCall = core.getCallsNb() > 0; - mStartCall.setVisibility(atLeastOneCall ? View.GONE : View.VISIBLE); - mAddContact.setVisibility(atLeastOneCall ? View.GONE : View.VISIBLE); - mAddContact.setEnabled(!mAddress.getText().toString().isEmpty()); - - if (!atLeastOneCall) { - if (core.getVideoActivationPolicy().getAutomaticallyInitiate()) { - mStartCall.setImageResource(R.drawable.call_video_start); - } else { - mStartCall.setImageResource(R.drawable.call_audio_start); - } - } - - mBackToCall.setVisibility(atLeastOneCall ? View.VISIBLE : View.GONE); - mAddCall.setVisibility(atLeastOneCall && !mIsTransfer ? View.VISIBLE : View.GONE); - mTransferCall.setVisibility(atLeastOneCall && mIsTransfer ? View.VISIBLE : View.GONE); - } - - private void handleIntentParams(Intent intent) { - if (intent == null) return; - - mIsTransfer = intent.getBooleanExtra("isTransfer", mIsTransfer); - - String action = intent.getAction(); - String addressToCall = null; - if (ACTION_CALL_LINPHONE.equals(action) - && (intent.getStringExtra("NumberToCall") != null)) { - String numberToCall = intent.getStringExtra("NumberToCall"); - Log.i("[Dialer] ACTION_CALL_LINPHONE with number: " + numberToCall); - LinphoneManager.getCallManager().newOutgoingCall(numberToCall, null); - } else { - Uri uri = intent.getData(); - if (uri != null) { - Log.i("[Dialer] Intent data is: " + uri.toString()); - if (Intent.ACTION_CALL.equals(action)) { - String dataString = intent.getDataString(); - - try { - addressToCall = URLDecoder.decode(dataString, "UTF-8"); - } catch (UnsupportedEncodingException e) { - Log.e("[Dialer] Unable to decode URI " + dataString); - addressToCall = dataString; - } - - if (addressToCall.startsWith("sip:")) { - addressToCall = addressToCall.substring("sip:".length()); - } else if (addressToCall.startsWith("tel:")) { - addressToCall = addressToCall.substring("tel:".length()); - } - Log.i("[Dialer] ACTION_CALL with number: " + addressToCall); - } else { - addressToCall = - ContactsManager.getInstance() - .getAddressOrNumberForAndroidContact(getContentResolver(), uri); - Log.i("[Dialer] " + action + " with number: " + addressToCall); - } - } else { - String sipUri = intent.getStringExtra("SipUri"); - if (sipUri != null) { - Log.i("[Dialer] Found extra SIP URI: " + sipUri); - addressToCall = sipUri; - } else { - Log.w("[Dialer] Intent data is null for action " + action); - } - } - } - - if (addressToCall != null) { - if (mAddress != null) { - mAddress.setText(addressToCall); - } else { - mAddressToCallOnLayoutReady = addressToCall; - } - } - } - - private void setUpNumpad(View view) { - if (view == null) return; - for (Digit v : retrieveChildren((ViewGroup) view, Digit.class)) { - v.setAddressWidget(mAddress); - } - } - - private Collection retrieveChildren(ViewGroup viewGroup, Class clazz) { - final Collection views = new ArrayList<>(); - for (int i = 0; i < viewGroup.getChildCount(); i++) { - View v = viewGroup.getChildAt(i); - if (v instanceof ViewGroup) { - views.addAll(retrieveChildren((ViewGroup) v, clazz)); - } else { - if (clazz.isInstance(v)) views.add(clazz.cast(v)); - } - } - return views; - } -} diff --git a/app/src/main/java/org/linphone/dialer/views/AddressText.java b/app/src/main/java/org/linphone/dialer/views/AddressText.java deleted file mode 100644 index ea199cafc..000000000 --- a/app/src/main/java/org/linphone/dialer/views/AddressText.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.dialer.views; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.graphics.Paint; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.widget.EditText; -import org.linphone.R; - -@SuppressLint("AppCompatCustomView") -public class AddressText extends EditText implements AddressType { - private String mDisplayedName; - private final Paint mTestPaint; - private AddressChangedListener mAddressListener; - - public AddressText(Context context, AttributeSet attrs) { - super(context, attrs); - - mTestPaint = new Paint(); - mTestPaint.set(this.getPaint()); - mAddressListener = null; - } - - private void clearDisplayedName() { - mDisplayedName = null; - } - - public String getDisplayedName() { - return mDisplayedName; - } - - public void setDisplayedName(String displayedName) { - this.mDisplayedName = displayedName; - } - - private String getHintText() { - String resizedText = getContext().getString(R.string.address_bar_hint); - if (getHint() != null) { - resizedText = getHint().toString(); - } - return resizedText; - } - - @Override - protected void onTextChanged(CharSequence text, int start, int before, int after) { - clearDisplayedName(); - - refitText(getWidth(), getHeight()); - - if (mAddressListener != null) { - mAddressListener.onAddressChanged(); - } - - super.onTextChanged(text, start, before, after); - } - - @Override - protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { - if (width != oldWidth) { - refitText(getWidth(), getHeight()); - } - } - - private float getOptimizedTextSize(String text, int textWidth, int textHeight) { - int targetWidth = textWidth - getPaddingLeft() - getPaddingRight(); - int targetHeight = textHeight - getPaddingTop() - getPaddingBottom(); - float hi = 90; - float lo = 2; - final float threshold = 0.5f; - - mTestPaint.set(getPaint()); - - while ((hi - lo) > threshold) { - float size = (hi + lo) / 2; - mTestPaint.setTextSize(size); - if (mTestPaint.measureText(text) >= targetWidth || size >= targetHeight) { - hi = size; - } else { - lo = size; - } - } - - return lo; - } - - private void refitText(int textWidth, int textHeight) { - if (textWidth <= 0) { - return; - } - - float size = getOptimizedTextSize(getHintText(), textWidth, textHeight); - float entrySize = getOptimizedTextSize(getText().toString(), textWidth, textHeight); - if (entrySize < size) size = entrySize; - setTextSize(TypedValue.COMPLEX_UNIT_PX, size); - } - - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - int parentWidth = MeasureSpec.getSize(widthMeasureSpec); - int height = getMeasuredHeight(); - - refitText(parentWidth, height); - setMeasuredDimension(parentWidth, height); - } - - public void setAddressListener(AddressChangedListener listener) { - mAddressListener = listener; - } - - public interface AddressChangedListener { - void onAddressChanged(); - } -} diff --git a/app/src/main/java/org/linphone/dialer/views/Digit.java b/app/src/main/java/org/linphone/dialer/views/Digit.java deleted file mode 100644 index 951b71210..000000000 --- a/app/src/main/java/org/linphone/dialer/views/Digit.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.dialer.views; - -import android.annotation.SuppressLint; -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.res.TypedArray; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.widget.Button; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Call; -import org.linphone.core.Core; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; - -@SuppressLint("AppCompatCustomView") -public class Digit extends Button implements AddressAware { - private boolean mPlayDtmf; - private AddressText mAddress; - - public Digit(Context context, AttributeSet attrs, int style) { - super(context, attrs, style); - init(context, attrs); - } - - public Digit(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs); - } - - public Digit(Context context) { - super(context); - setLongClickable(true); - } - - private void init(Context context, AttributeSet attrs) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Numpad); - mPlayDtmf = 1 == a.getInt(R.styleable.Numpad_play_dtmf, 1); - a.recycle(); - - setLongClickable(true); - } - - @Override - protected void onTextChanged(CharSequence text, int start, int before, int after) { - super.onTextChanged(text, start, before, after); - - if (text == null || text.length() < 1) { - return; - } - - DialKeyListener lListener = new DialKeyListener(); - setOnClickListener(lListener); - setOnTouchListener(lListener); - - if ("0+".equals(text.toString())) { - setOnLongClickListener(lListener); - } - - if ("1".equals(text.toString())) { - setOnLongClickListener(lListener); - } - } - - @Override - public void setAddressWidget(AddressText address) { - mAddress = address; - } - - private class DialKeyListener implements OnClickListener, OnTouchListener, OnLongClickListener { - final char mKeyCode; - boolean mIsDtmfStarted; - - DialKeyListener() { - mKeyCode = Digit.this.getText().subSequence(0, 1).charAt(0); - } - - private boolean linphoneServiceReady() { - if (!LinphoneContext.isReady()) { - Log.e("[Numpad] Service is not ready while pressing digit"); - return false; - } - return true; - } - - public void onClick(View v) { - if (mPlayDtmf) { - if (!linphoneServiceReady()) return; - Core core = LinphoneManager.getCore(); - core.stopDtmf(); - mIsDtmfStarted = false; - Call call = core.getCurrentCall(); - if (call != null) { - call.sendDtmf(mKeyCode); - } - } - - if (mAddress != null) { - int begin = mAddress.getSelectionStart(); - if (begin == -1) { - begin = mAddress.length(); - } - if (begin >= 0) { - mAddress.getEditableText().insert(begin, String.valueOf(mKeyCode)); - } - - if (LinphonePreferences.instance().getDebugPopupAddress() != null - && mAddress.getText() - .toString() - .equals(LinphonePreferences.instance().getDebugPopupAddress())) { - displayDebugPopup(); - } - } - } - - void displayDebugPopup() { - AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext()); - alertDialog.setTitle(getContext().getString(R.string.debug_popup_title)); - if (LinphonePreferences.instance().isDebugEnabled()) { - alertDialog.setItems( - getContext().getResources().getStringArray(R.array.popup_send_log), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - if (which == 0) { - LinphonePreferences.instance().setDebugEnabled(false); - } - if (which == 1) { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.uploadLogCollection(); - } - } - } - }); - - } else { - alertDialog.setItems( - getContext().getResources().getStringArray(R.array.popup_enable_log), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - if (which == 0) { - LinphonePreferences.instance().setDebugEnabled(true); - } - } - }); - } - alertDialog.show(); - mAddress.getEditableText().clear(); - } - - public boolean onTouch(View v, MotionEvent event) { - if (!mPlayDtmf) return false; - if (!linphoneServiceReady()) return true; - - LinphoneManager.getCallManager().resetCallControlsHidingTimer(); - - Core core = LinphoneManager.getCore(); - if (event.getAction() == MotionEvent.ACTION_DOWN && !mIsDtmfStarted) { - LinphoneManager.getCallManager() - .playDtmf(getContext().getContentResolver(), mKeyCode); - mIsDtmfStarted = true; - } else { - if (event.getAction() == MotionEvent.ACTION_UP) { - core.stopDtmf(); - mIsDtmfStarted = false; - } - } - return false; - } - - public boolean onLongClick(View v) { - int id = v.getId(); - Core core = LinphoneManager.getCore(); - - if (mPlayDtmf) { - if (!linphoneServiceReady()) return true; - // Called if "0+" dtmf - core.stopDtmf(); - } - - if (id == R.id.Digit1 && core.getCalls().length == 0) { - String voiceMail = LinphonePreferences.instance().getVoiceMailUri(); - mAddress.getEditableText().clear(); - if (voiceMail != null) { - mAddress.getEditableText().append(voiceMail); - LinphoneManager.getCallManager().newOutgoingCall(mAddress); - } - return true; - } - - if (mAddress == null) return true; - - int begin = mAddress.getSelectionStart(); - if (begin == -1) { - begin = mAddress.getEditableText().length(); - } - if (begin >= 0) { - mAddress.getEditableText().insert(begin, "+"); - } - return true; - } - } -} diff --git a/app/src/main/java/org/linphone/dialer/views/EraseButton.java b/app/src/main/java/org/linphone/dialer/views/EraseButton.java deleted file mode 100644 index 0fc2645af..000000000 --- a/app/src/main/java/org/linphone/dialer/views/EraseButton.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.dialer.views; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.AttributeSet; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.View.OnLongClickListener; -import android.widget.ImageView; - -@SuppressLint("AppCompatCustomView") -public class EraseButton extends ImageView - implements AddressAware, OnClickListener, OnLongClickListener, TextWatcher { - - private AddressText mAddress; - - public EraseButton(Context context, AttributeSet attrs) { - super(context, attrs); - setEnabled(false); - setOnClickListener(this); - setOnLongClickListener(this); - } - - public void onClick(View v) { - if (mAddress.getText().length() > 0) { - int lBegin = mAddress.getSelectionStart(); - if (lBegin == -1) { - lBegin = mAddress.getEditableText().length() - 1; - } - if (lBegin > 0) { - mAddress.getEditableText().delete(lBegin - 1, lBegin); - } - } - setEnabled(mAddress.getText().length() > 0); - } - - public boolean onLongClick(View v) { - mAddress.getEditableText().clear(); - return true; - } - - public void setAddressWidget(AddressText view) { - mAddress = view; - view.addTextChangedListener(this); - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - - @Override - public void afterTextChanged(Editable s) { - setEnabled(s.length() > 0); - } -} diff --git a/app/src/main/java/org/linphone/firebase/FirebaseMessaging.java b/app/src/main/java/org/linphone/firebase/FirebaseMessaging.java deleted file mode 100644 index 959fca4d7..000000000 --- a/app/src/main/java/org/linphone/firebase/FirebaseMessaging.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.firebase; - -import com.google.firebase.messaging.FirebaseMessagingService; -import com.google.firebase.messaging.RemoteMessage; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.core.Core; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.LinphoneUtils; - -public class FirebaseMessaging extends FirebaseMessagingService { - private Runnable mPushReceivedRunnable = - new Runnable() { - @Override - public void run() { - if (!LinphoneContext.isReady()) { - android.util.Log.i( - "FirebaseMessaging", "[Push Notification] Starting context"); - new LinphoneContext(getApplicationContext()); - LinphoneContext.instance().start(true); - } else { - Log.i("[Push Notification] Notifying Core"); - if (LinphoneManager.getInstance() != null) { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.ensureRegistered(); - } - } - } - } - }; - - public FirebaseMessaging() {} - - @Override - public void onNewToken(final String token) { - android.util.Log.i("FirebaseIdService", "[Push Notification] Refreshed token: " + token); - - LinphoneUtils.dispatchOnUIThread( - new Runnable() { - @Override - public void run() { - LinphonePreferences.instance().setPushNotificationRegistrationID(token); - } - }); - } - - @Override - public void onMessageReceived(RemoteMessage remoteMessage) { - android.util.Log.i("FirebaseMessaging", "[Push Notification] Received"); - LinphoneUtils.dispatchOnUIThread(mPushReceivedRunnable); - } -} diff --git a/app/src/main/java/org/linphone/firebase/FirebasePushHelper.java b/app/src/main/java/org/linphone/firebase/FirebasePushHelper.java deleted file mode 100644 index 6041ae8ba..000000000 --- a/app/src/main/java/org/linphone/firebase/FirebasePushHelper.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.firebase; - -import android.content.Context; -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GoogleApiAvailability; -import com.google.android.gms.tasks.OnCompleteListener; -import com.google.android.gms.tasks.Task; -import com.google.firebase.iid.FirebaseInstanceId; -import com.google.firebase.iid.InstanceIdResult; -import org.linphone.R; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.PushNotificationUtils; - -@Keep -public class FirebasePushHelper implements PushNotificationUtils.PushHelperInterface { - public FirebasePushHelper() {} - - @Override - public void init(Context context) { - Log.i( - "[Push Notification] firebase push sender id " - + context.getString(R.string.gcm_defaultSenderId)); - try { - FirebaseInstanceId.getInstance() - .getInstanceId() - .addOnCompleteListener( - new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task task) { - if (!task.isSuccessful()) { - Log.e( - "[Push Notification] firebase getInstanceId failed: " - + task.getException()); - return; - } - String token = task.getResult().getToken(); - Log.i("[Push Notification] firebase token is: " + token); - LinphonePreferences.instance() - .setPushNotificationRegistrationID(token); - } - }); - } catch (Exception e) { - Log.e("[Push Notification] firebase not available."); - } - } - - @Override - public boolean isAvailable(Context context) { - GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance(); - int resultCode = googleApiAvailability.isGooglePlayServicesAvailable(context); - return resultCode == ConnectionResult.SUCCESS; - } -} diff --git a/app/src/main/java/org/linphone/fragments/StatusBarFragment.java b/app/src/main/java/org/linphone/fragments/StatusBarFragment.java deleted file mode 100644 index 8b498b4a7..000000000 --- a/app/src/main/java/org/linphone/fragments/StatusBarFragment.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.fragments; - -import android.app.Fragment; -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Content; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.Event; -import org.linphone.core.ProxyConfig; -import org.linphone.core.RegistrationState; -import org.linphone.core.tools.Log; - -public class StatusBarFragment extends Fragment { - private TextView mStatusText, mVoicemailCount; - private ImageView mStatusLed; - private ImageView mVoicemail; - private CoreListenerStub mListener; - private MenuClikedListener mMenuListener; - - @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.status_bar, container, false); - - mStatusText = view.findViewById(R.id.status_text); - mStatusLed = view.findViewById(R.id.status_led); - ImageView menu = view.findViewById(R.id.side_menu_button); - mVoicemail = view.findViewById(R.id.voicemail); - mVoicemailCount = view.findViewById(R.id.voicemail_count); - - mMenuListener = null; - menu.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - if (mMenuListener != null) { - mMenuListener.onMenuCliked(); - } - } - }); - - // We create it once to not delay the first display - populateSliderContent(); - - mListener = - new CoreListenerStub() { - @Override - public void onRegistrationStateChanged( - final Core core, - final ProxyConfig proxy, - final RegistrationState state, - String smessage) { - if (core.getProxyConfigList() == null) { - showNoAccountConfigured(); - return; - } - - if ((core.getDefaultProxyConfig() != null - && core.getDefaultProxyConfig().equals(proxy)) - || core.getDefaultProxyConfig() == null) { - mStatusLed.setImageResource(getStatusIconResource(state)); - mStatusText.setText(getStatusIconText(state)); - } - - try { - mStatusText.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.refreshRegisters(); - } - } - }); - } catch (IllegalStateException ise) { - Log.e(ise); - } - } - - @Override - public void onNotifyReceived( - Core core, Event ev, String eventName, Content content) { - - if (!content.getType().equals("application")) return; - if (!content.getSubtype().equals("simple-message-summary")) return; - - if (content.getSize() == 0) return; - - int unreadCount = 0; - String data = content.getStringBuffer().toLowerCase(); - String[] voiceMail = data.split("voice-message: "); - if (voiceMail.length >= 2) { - final String[] intToParse = voiceMail[1].split("/", 0); - try { - unreadCount = Integer.parseInt(intToParse[0]); - } catch (NumberFormatException nfe) { - Log.e("[Status Fragment] " + nfe); - } - if (unreadCount > 0) { - mVoicemailCount.setText(String.valueOf(unreadCount)); - mVoicemail.setVisibility(View.VISIBLE); - mVoicemailCount.setVisibility(View.VISIBLE); - } else { - mVoicemail.setVisibility(View.GONE); - mVoicemailCount.setVisibility(View.GONE); - } - } - } - }; - - return view; - } - - @Override - public void onResume() { - super.onResume(); - - Core core = LinphoneManager.getCore(); - if (core != null) { - core.addListener(mListener); - ProxyConfig lpc = core.getDefaultProxyConfig(); - if (lpc != null) { - mListener.onRegistrationStateChanged(core, lpc, lpc.getState(), null); - } else { - showNoAccountConfigured(); - } - } else { - mStatusText.setVisibility(View.VISIBLE); - } - } - - @Override - public void onPause() { - super.onPause(); - - if (LinphoneContext.isReady()) { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.removeListener(mListener); - } - } - } - - public void setMenuListener(MenuClikedListener listener) { - mMenuListener = listener; - } - - private void populateSliderContent() { - Core core = LinphoneManager.getCore(); - if (core != null) { - mVoicemailCount.setVisibility(View.VISIBLE); - - if (core.getProxyConfigList().length == 0) { - showNoAccountConfigured(); - } - } - } - - private void showNoAccountConfigured() { - mStatusLed.setImageResource(R.drawable.led_disconnected); - mStatusText.setText(getString(R.string.no_account)); - } - - private int getStatusIconResource(RegistrationState state) { - try { - if (state == RegistrationState.Ok) { - return R.drawable.led_connected; - } else if (state == RegistrationState.Progress) { - return R.drawable.led_inprogress; - } else if (state == RegistrationState.Failed) { - return R.drawable.led_error; - } else { - return R.drawable.led_disconnected; - } - } catch (Exception e) { - Log.e(e); - } - - return R.drawable.led_disconnected; - } - - private String getStatusIconText(RegistrationState state) { - Context context = getActivity(); - try { - if (state == RegistrationState.Ok) { - return context.getString(R.string.status_connected); - } else if (state == RegistrationState.Progress) { - return context.getString(R.string.status_in_progress); - } else if (state == RegistrationState.Failed) { - return context.getString(R.string.status_error); - } else { - return context.getString(R.string.status_not_connected); - } - } catch (Exception e) { - Log.e(e); - } - - return context.getString(R.string.status_not_connected); - } - - public interface MenuClikedListener { - void onMenuCliked(); - } -} diff --git a/app/src/main/java/org/linphone/history/HistoryActivity.java b/app/src/main/java/org/linphone/history/HistoryActivity.java deleted file mode 100644 index 052550a38..000000000 --- a/app/src/main/java/org/linphone/history/HistoryActivity.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.history; - -import android.app.Fragment; -import android.content.Intent; -import android.os.Bundle; -import android.view.View; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.activities.MainActivity; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.core.Address; -import org.linphone.utils.LinphoneUtils; - -public class HistoryActivity extends MainActivity { - public static final String NAME = "History"; - - @Override - protected void onCreate(Bundle savedInstanceState) { - getIntent().putExtra("Activity", NAME); - super.onCreate(savedInstanceState); - } - - @Override - protected void onStart() { - super.onStart(); - - Fragment currentFragment = getFragmentManager().findFragmentById(R.id.fragmentContainer); - if (currentFragment == null) { - HistoryFragment fragment = new HistoryFragment(); - changeFragment(fragment, "History", false); - if (isTablet()) { - fragment.displayFirstLog(); - } - } - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - - // Clean fragments stack upon return - while (getFragmentManager().getBackStackEntryCount() > 0) { - getFragmentManager().popBackStackImmediate(); - } - } - - @Override - protected void onResume() { - super.onResume(); - - mHistorySelected.setVisibility(View.VISIBLE); - LinphoneManager.getCore().resetMissedCallsCount(); - displayMissedCalls(); - LinphoneContext.instance().getNotificationManager().dismissMissedCallNotification(); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - } - - @Override - public void goBack() { - // 1 is for the empty fragment on tablets - if (!isTablet() || getFragmentManager().getBackStackEntryCount() > 1) { - if (popBackStack()) { - return; - } - } - super.goBack(); - } - - public void showHistoryDetails(Address address) { - Bundle extras = new Bundle(); - if (address != null) { - LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(address); - String displayName = - contact != null - ? contact.getFullName() - : LinphoneUtils.getAddressDisplayName(address); - String pictureUri = - contact != null && contact.getPhotoUri() != null - ? contact.getPhotoUri().toString() - : null; - - extras.putString("SipUri", address.asStringUriOnly()); - extras.putString("DisplayName", displayName); - extras.putString("PictureUri", pictureUri); - } - HistoryDetailFragment fragment = new HistoryDetailFragment(); - fragment.setArguments(extras); - changeFragment(fragment, "History detail", true); - } -} diff --git a/app/src/main/java/org/linphone/history/HistoryAdapter.java b/app/src/main/java/org/linphone/history/HistoryAdapter.java deleted file mode 100644 index e61d3596d..000000000 --- a/app/src/main/java/org/linphone/history/HistoryAdapter.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.history; - -import android.annotation.SuppressLint; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.List; -import org.linphone.R; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.core.Address; -import org.linphone.core.Call; -import org.linphone.core.CallLog; -import org.linphone.utils.LinphoneUtils; -import org.linphone.utils.SelectableAdapter; -import org.linphone.utils.SelectableHelper; - -public class HistoryAdapter extends SelectableAdapter { - private final List mLogs; - private final HistoryActivity mActivity; - private final HistoryViewHolder.ClickListener mClickListener; - - public HistoryAdapter( - HistoryActivity activity, - List logs, - HistoryViewHolder.ClickListener listener, - SelectableHelper helper) { - super(helper); - mLogs = logs; - mActivity = activity; - mClickListener = listener; - } - - public Object getItem(int position) { - return mLogs.get(position); - } - - @NonNull - @Override - public HistoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = - LayoutInflater.from(parent.getContext()) - .inflate(R.layout.history_cell, parent, false); - return new HistoryViewHolder(v, mClickListener); - } - - @Override - public void onBindViewHolder(@NonNull final HistoryViewHolder holder, final int position) { - CallLog log = mLogs.get(position); - long timestamp = log.getStartDate() * 1000; - final Address address; - - holder.contact.setSelected(true); // For automated horizontal scrolling of long texts - Calendar logTime = Calendar.getInstance(); - logTime.setTimeInMillis(timestamp); - holder.separatorText.setText(timestampToHumanDate(logTime)); - holder.select.setVisibility(isEditionEnabled() ? View.VISIBLE : View.GONE); - holder.select.setChecked(isSelected(position)); - - if (position > 0) { - CallLog previousLog = mLogs.get(position - 1); - long previousTimestamp = previousLog.getStartDate() * 1000; - Calendar previousLogTime = Calendar.getInstance(); - previousLogTime.setTimeInMillis(previousTimestamp); - - if (isSameDay(previousLogTime, logTime)) { - holder.separator.setVisibility(View.GONE); - } else { - holder.separator.setVisibility(View.VISIBLE); - } - } else { - holder.separator.setVisibility(View.VISIBLE); - } - - if (log.getDir() == Call.Dir.Incoming) { - address = log.getFromAddress(); - if (log.getStatus() == Call.Status.Missed) { - holder.callDirection.setImageResource(R.drawable.call_status_missed); - } else { - holder.callDirection.setImageResource(R.drawable.call_status_incoming); - } - } else { - address = log.getToAddress(); - holder.callDirection.setImageResource(R.drawable.call_status_outgoing); - } - - LinphoneContact c = ContactsManager.getInstance().findContactFromAddress(address); - String displayName = null; - String sipUri = (address != null) ? address.asString() : ""; - - if (c != null) { - displayName = c.getFullName(); - } - if (displayName == null) { - holder.contact.setText(LinphoneUtils.getAddressDisplayName(sipUri)); - } else { - holder.contact.setText(displayName); - } - - if (c != null) { - ContactAvatar.displayAvatar(c, holder.avatarLayout); - } else { - ContactAvatar.displayAvatar(holder.contact.getText().toString(), holder.avatarLayout); - } - - holder.detail.setVisibility(isEditionEnabled() ? View.INVISIBLE : View.VISIBLE); - holder.detail.setOnClickListener( - !isEditionEnabled() - ? new View.OnClickListener() { - @Override - public void onClick(View v) { - mActivity.showHistoryDetails(address); - } - } - : null); - } - - @Override - public int getItemCount() { - return mLogs.size(); - } - - @SuppressLint("SimpleDateFormat") - private String timestampToHumanDate(Calendar cal) { - SimpleDateFormat dateFormat; - if (isToday(cal)) { - return mActivity.getString(R.string.today); - } else if (isYesterday(cal)) { - return mActivity.getString(R.string.yesterday); - } else { - dateFormat = - new SimpleDateFormat( - mActivity.getResources().getString(R.string.history_date_format)); - } - - return dateFormat.format(cal.getTime()); - } - - private boolean isSameDay(Calendar cal1, Calendar cal2) { - if (cal1 == null || cal2 == null) { - return false; - } - - return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) - && cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) - && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)); - } - - private boolean isToday(Calendar cal) { - return isSameDay(cal, Calendar.getInstance()); - } - - private boolean isYesterday(Calendar cal) { - Calendar yesterday = Calendar.getInstance(); - yesterday.roll(Calendar.DAY_OF_MONTH, -1); - return isSameDay(cal, yesterday); - } -} diff --git a/app/src/main/java/org/linphone/history/HistoryDetailFragment.java b/app/src/main/java/org/linphone/history/HistoryDetailFragment.java deleted file mode 100644 index a616f98d9..000000000 --- a/app/src/main/java/org/linphone/history/HistoryDetailFragment.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.history; - -import android.app.Fragment; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.RelativeLayout; -import android.widget.TextView; -import java.util.Arrays; -import java.util.List; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.activities.MainActivity; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.contacts.views.ContactAvatar; -import org.linphone.core.Address; -import org.linphone.core.Call; -import org.linphone.core.CallLog; -import org.linphone.core.ChatRoom; -import org.linphone.core.ChatRoomBackend; -import org.linphone.core.ChatRoomListenerStub; -import org.linphone.core.ChatRoomParams; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.Factory; -import org.linphone.core.FriendCapability; -import org.linphone.core.ProxyConfig; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.LinphoneUtils; - -public class HistoryDetailFragment extends Fragment { - private ImageView mAddToContacts; - private ImageView mGoToContact; - private TextView mContactName, mContactAddress; - private String mSipUri, mDisplayName; - private RelativeLayout mWaitLayout, mAvatarLayout, mChatSecured; - private LinphoneContact mContact; - private ChatRoom mChatRoom; - private ChatRoomListenerStub mChatRoomCreationListener; - private ListView mLogsList; - private CoreListenerStub mListener; - - @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - mSipUri = getArguments().getString("SipUri"); - mDisplayName = getArguments().getString("DisplayName"); - - View view = inflater.inflate(R.layout.history_detail, container, false); - - mWaitLayout = view.findViewById(R.id.waitScreen); - mWaitLayout.setVisibility(View.GONE); - - ImageView dialBack = view.findViewById(R.id.call); - dialBack.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - ((MainActivity) getActivity()).newOutgoingCall(mSipUri); - } - }); - - ImageView back = view.findViewById(R.id.back); - back.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - ((HistoryActivity) getActivity()).goBack(); - } - }); - back.setVisibility( - getResources().getBoolean(R.bool.isTablet) ? View.INVISIBLE : View.VISIBLE); - - ImageView chat = view.findViewById(R.id.chat); - chat.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - goToChat(false); - } - }); - - mChatSecured = view.findViewById(R.id.chat_secured); - mChatSecured.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - goToChat(true); - } - }); - - if (getResources().getBoolean(R.bool.force_end_to_end_encryption_in_chat)) { - chat.setVisibility(View.GONE); - } - if (getResources().getBoolean(R.bool.disable_chat)) { - chat.setVisibility(View.GONE); - mChatSecured.setVisibility(View.GONE); - } - - mAddToContacts = view.findViewById(R.id.add_contact); - mAddToContacts.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - Address addr = Factory.instance().createAddress(mSipUri); - if (addr != null) { - addr.clean(); - ((HistoryActivity) getActivity()) - .showContactsListForCreationOrEdition(addr); - } - } - }); - - mGoToContact = view.findViewById(R.id.goto_contact); - mGoToContact.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - ((HistoryActivity) getActivity()).showContactDetails(mContact); - } - }); - - mAvatarLayout = view.findViewById(R.id.avatar_layout); - mContactName = view.findViewById(R.id.contact_name); - mContactAddress = view.findViewById(R.id.contact_address); - - mChatRoomCreationListener = - new ChatRoomListenerStub() { - @Override - public void onStateChanged(ChatRoom cr, ChatRoom.State newState) { - if (newState == ChatRoom.State.Created) { - mWaitLayout.setVisibility(View.GONE); - ((HistoryActivity) getActivity()) - .showChatRoom(cr.getLocalAddress(), cr.getPeerAddress()); - } else if (newState == ChatRoom.State.CreationFailed) { - mWaitLayout.setVisibility(View.GONE); - ((HistoryActivity) getActivity()).displayChatRoomError(); - Log.e( - "[History Detail Fragment] Group mChat room for address " - + cr.getPeerAddress() - + " has failed !"); - } - } - }; - - mLogsList = view.findViewById(R.id.logs_list); - - mListener = - new CoreListenerStub() { - @Override - public void onCallStateChanged( - Core core, Call call, Call.State state, String message) { - if (state == Call.State.End || state == Call.State.Error) { - displayHistory(); - } - } - }; - - return view; - } - - @Override - public void onResume() { - super.onResume(); - - LinphoneManager.getCore().addListener(mListener); - displayHistory(); - } - - @Override - public void onPause() { - if (mChatRoom != null) { - mChatRoom.removeListener(mChatRoomCreationListener); - } - LinphoneManager.getCore().removeListener(mListener); - - super.onPause(); - } - - private void displayHistory() { - if (mSipUri != null) { - Address address = Factory.instance().createAddress(mSipUri); - mChatSecured.setVisibility(View.GONE); - - Core core = LinphoneManager.getCore(); - if (address != null && core != null) { - address.clean(); - CallLog[] logs; - logs = core.getCallHistoryForAddress(address); - List logsList = Arrays.asList(logs); - mLogsList.setAdapter( - new HistoryLogAdapter( - getActivity(), R.layout.history_detail_cell, logsList)); - - mContactAddress.setText(LinphoneUtils.getDisplayableAddress(address)); - mContact = ContactsManager.getInstance().findContactFromAddress(address); - - if (mDisplayName == null) { - mDisplayName = LinphoneUtils.getAddressDisplayName(address); - } - - if (mContact != null) { - mContactName.setText(mContact.getFullName()); - ContactAvatar.displayAvatar(mContact, mAvatarLayout); - mAddToContacts.setVisibility(View.GONE); - mGoToContact.setVisibility(View.VISIBLE); - - if (!getResources().getBoolean(R.bool.disable_chat) - && mContact.hasPresenceModelForUriOrTelCapability( - address.asStringUriOnly(), FriendCapability.LimeX3Dh)) { - mChatSecured.setVisibility(View.VISIBLE); - } - } else { - mContactName.setText(mDisplayName); - ContactAvatar.displayAvatar(mDisplayName, mAvatarLayout); - mAddToContacts.setVisibility(View.VISIBLE); - mGoToContact.setVisibility(View.GONE); - } - } else { - mContactAddress.setText(mSipUri); - mContactName.setText( - mDisplayName == null - ? LinphoneUtils.getAddressDisplayName(mSipUri) - : mDisplayName); - } - } - } - - private void goToChat(boolean isSecured) { - Core core = LinphoneManager.getCore(); - if (core == null) return; - - Address participant = Factory.instance().createAddress(mSipUri); - if (participant == null) { - Log.e("[History Detail] Couldn't parse ", mSipUri); - return; - } - ProxyConfig defaultProxyConfig = core.getDefaultProxyConfig(); - - if (defaultProxyConfig != null) { - ChatRoom room = - core.findOneToOneChatRoom( - defaultProxyConfig.getIdentityAddress(), participant, isSecured); - if (room != null) { - ((HistoryActivity) getActivity()) - .showChatRoom(room.getLocalAddress(), room.getPeerAddress()); - } else { - if (defaultProxyConfig.getConferenceFactoryUri() != null - && (isSecured - || !LinphonePreferences.instance().useBasicChatRoomFor1To1())) { - mWaitLayout.setVisibility(View.VISIBLE); - - ChatRoomParams params = core.createDefaultChatRoomParams(); - params.enableEncryption(isSecured); - params.enableGroup(false); - // We don't want a basic chat room, - // so if isSecured is false we have to set this manually - params.setBackend(ChatRoomBackend.FlexisipChat); - - Address[] participants = new Address[1]; - participants[0] = participant; - - mChatRoom = - core.createChatRoom( - params, - getString(R.string.dummy_group_chat_subject), - participants); - if (mChatRoom != null) { - mChatRoom.addListener(mChatRoomCreationListener); - } else { - Log.w("[History Detail Fragment] createChatRoom returned null..."); - mWaitLayout.setVisibility(View.GONE); - } - } else { - room = core.getChatRoom(participant); - if (room != null) { - ((HistoryActivity) getActivity()) - .showChatRoom(room.getLocalAddress(), room.getPeerAddress()); - } - } - } - } else { - if (isSecured) { - Log.e( - "[History Detail Fragment] Can't create a secured chat room without proxy config"); - return; - } - - ChatRoom room = core.getChatRoom(participant); - if (room != null) { - ((HistoryActivity) getActivity()) - .showChatRoom(room.getLocalAddress(), room.getPeerAddress()); - } - } - } -} diff --git a/app/src/main/java/org/linphone/history/HistoryFragment.java b/app/src/main/java/org/linphone/history/HistoryFragment.java deleted file mode 100644 index f31f0e55f..000000000 --- a/app/src/main/java/org/linphone/history/HistoryFragment.java +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.history; - -import android.app.Fragment; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ImageView; -import android.widget.TextView; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.activities.MainActivity; -import org.linphone.call.views.LinphoneLinearLayoutManager; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.ContactsUpdatedListener; -import org.linphone.core.Address; -import org.linphone.core.Call; -import org.linphone.core.CallLog; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.utils.SelectableHelper; - -public class HistoryFragment extends Fragment - implements OnClickListener, - OnItemClickListener, - HistoryViewHolder.ClickListener, - ContactsUpdatedListener, - SelectableHelper.DeleteListener, - LinphoneContext.CoreStartedListener { - private RecyclerView mHistoryList; - private TextView mNoCallHistory, mNoMissedCallHistory; - private ImageView mMissedCalls, mAllCalls; - private View mAllCallsSelected, mMissedCallsSelected; - private boolean mOnlyDisplayMissedCalls; - private List mLogs; - private HistoryAdapter mHistoryAdapter; - private SelectableHelper mSelectionHelper; - private CoreListenerStub mListener; - - @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.history, container, false); - mSelectionHelper = new SelectableHelper(view, this); - - mNoCallHistory = view.findViewById(R.id.no_call_history); - mNoMissedCallHistory = view.findViewById(R.id.no_missed_call_history); - - mHistoryList = view.findViewById(R.id.history_list); - - LinearLayoutManager layoutManager = new LinphoneLinearLayoutManager(getActivity()); - mHistoryList.setLayoutManager(layoutManager); - // Divider between items - DividerItemDecoration dividerItemDecoration = - new DividerItemDecoration( - mHistoryList.getContext(), layoutManager.getOrientation()); - dividerItemDecoration.setDrawable(getResources().getDrawable(R.drawable.divider)); - mHistoryList.addItemDecoration(dividerItemDecoration); - - mAllCalls = view.findViewById(R.id.all_calls); - mAllCalls.setOnClickListener(this); - - mAllCallsSelected = view.findViewById(R.id.all_calls_select); - - mMissedCalls = view.findViewById(R.id.missed_calls); - mMissedCalls.setOnClickListener(this); - - mMissedCallsSelected = view.findViewById(R.id.missed_calls_select); - - mAllCalls.setEnabled(false); - mOnlyDisplayMissedCalls = false; - - mListener = - new CoreListenerStub() { - @Override - public void onCallStateChanged( - Core core, Call call, Call.State state, String message) { - if (state == Call.State.End || state == Call.State.Error) { - reloadData(); - } - } - }; - - return view; - } - - @Override - public void onResume() { - super.onResume(); - - ContactsManager.getInstance().addContactsListener(this); - LinphoneContext.instance().addCoreStartedListener(this); - LinphoneManager.getCore().addListener(mListener); - - reloadData(); - } - - @Override - public void onPause() { - ContactsManager.getInstance().removeContactsListener(this); - LinphoneContext.instance().removeCoreStartedListener(this); - LinphoneManager.getCore().removeListener(mListener); - - super.onPause(); - } - - @Override - public void onContactsUpdated() { - HistoryAdapter adapter = (HistoryAdapter) mHistoryList.getAdapter(); - if (adapter != null) { - adapter.notifyDataSetChanged(); - } - } - - @Override - public void onCoreStarted() { - reloadData(); - } - - @Override - public void onClick(View v) { - int id = v.getId(); - - if (id == R.id.all_calls) { - mAllCalls.setEnabled(false); - mAllCallsSelected.setVisibility(View.VISIBLE); - mMissedCallsSelected.setVisibility(View.INVISIBLE); - mMissedCalls.setEnabled(true); - mOnlyDisplayMissedCalls = false; - refresh(); - } - if (id == R.id.missed_calls) { - mAllCalls.setEnabled(true); - mAllCallsSelected.setVisibility(View.INVISIBLE); - mMissedCallsSelected.setVisibility(View.VISIBLE); - mMissedCalls.setEnabled(false); - mOnlyDisplayMissedCalls = true; - } - hideHistoryListAndDisplayMessageIfEmpty(); - mHistoryAdapter = - new HistoryAdapter((HistoryActivity) getActivity(), mLogs, this, mSelectionHelper); - mHistoryList.setAdapter(mHistoryAdapter); - mSelectionHelper.setAdapter(mHistoryAdapter); - mSelectionHelper.setDialogMessage(R.string.chat_room_delete_dialog); - } - - @Override - public void onItemClick(AdapterView adapter, View view, int position, long id) { - if (mHistoryAdapter.isEditionEnabled()) { - CallLog log = mLogs.get(position); - Core core = LinphoneManager.getCore(); - core.removeCallLog(log); - mLogs = Arrays.asList(core.getCallLogs()); - } - } - - @Override - public void onDeleteSelection(Object[] objectsToDelete) { - int size = mHistoryAdapter.getSelectedItemCount(); - for (int i = 0; i < size; i++) { - CallLog log = (CallLog) objectsToDelete[i]; - LinphoneManager.getCore().removeCallLog(log); - onResume(); - } - } - - @Override - public void onItemClicked(int position) { - if (mHistoryAdapter.isEditionEnabled()) { - mHistoryAdapter.toggleSelection(position); - } else { - if (position >= 0 && position < mLogs.size()) { - CallLog log = mLogs.get(position); - Address address; - if (log.getDir() == Call.Dir.Incoming) { - address = log.getFromAddress(); - } else { - address = log.getToAddress(); - } - if (address != null) { - ((MainActivity) getActivity()).newOutgoingCall(address.asStringUriOnly()); - } - } - } - } - - @Override - public boolean onItemLongClicked(int position) { - if (!mHistoryAdapter.isEditionEnabled()) { - mSelectionHelper.enterEditionMode(); - } - mHistoryAdapter.toggleSelection(position); - return true; - } - - private void refresh() { - mLogs = Arrays.asList(LinphoneManager.getCore().getCallLogs()); - } - - public void displayFirstLog() { - Address addr; - if (mLogs != null && mLogs.size() > 0) { - CallLog log = mLogs.get(0); // More recent one is 0 - if (log.getDir() == Call.Dir.Incoming) { - addr = log.getFromAddress(); - } else { - addr = log.getToAddress(); - } - ((HistoryActivity) getActivity()).showHistoryDetails(addr); - } else { - ((HistoryActivity) getActivity()).showEmptyChildFragment(); - } - } - - private void reloadData() { - mLogs = Arrays.asList(LinphoneManager.getCore().getCallLogs()); - hideHistoryListAndDisplayMessageIfEmpty(); - mHistoryAdapter = - new HistoryAdapter((HistoryActivity) getActivity(), mLogs, this, mSelectionHelper); - mHistoryList.setAdapter(mHistoryAdapter); - mSelectionHelper.setAdapter(mHistoryAdapter); - mSelectionHelper.setDialogMessage(R.string.call_log_delete_dialog); - } - - private void removeNotMissedCallsFromLogs() { - if (mOnlyDisplayMissedCalls) { - List missedCalls = new ArrayList<>(); - for (CallLog log : mLogs) { - if (log.getStatus() == Call.Status.Missed) { - missedCalls.add(log); - } - } - mLogs = missedCalls; - } - } - - private void hideHistoryListAndDisplayMessageIfEmpty() { - removeNotMissedCallsFromLogs(); - mNoCallHistory.setVisibility(View.GONE); - mNoMissedCallHistory.setVisibility(View.GONE); - - if (mLogs.isEmpty()) { - if (mOnlyDisplayMissedCalls) { - mNoMissedCallHistory.setVisibility(View.VISIBLE); - } else { - mNoCallHistory.setVisibility(View.VISIBLE); - } - mHistoryList.setVisibility(View.GONE); - } else { - mNoCallHistory.setVisibility(View.GONE); - mNoMissedCallHistory.setVisibility(View.GONE); - mHistoryList.setVisibility(View.VISIBLE); - } - } -} diff --git a/app/src/main/java/org/linphone/history/HistoryLogAdapter.java b/app/src/main/java/org/linphone/history/HistoryLogAdapter.java deleted file mode 100644 index 0cf203ccd..000000000 --- a/app/src/main/java/org/linphone/history/HistoryLogAdapter.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.history; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.List; -import org.linphone.R; -import org.linphone.core.Call; -import org.linphone.core.CallLog; -import org.linphone.utils.LinphoneUtils; - -class HistoryLogAdapter extends ArrayAdapter { - private final Context mContext; - private final List mItems; - private final int mResource; - - HistoryLogAdapter(@NonNull Context context, int resource, @NonNull List objects) { - super(context, resource, objects); - mContext = context; - mResource = resource; - mItems = objects; - } - - @Nullable - @Override - public CallLog getItem(int position) { - return mItems.get(position); - } - - @Override - public int getCount() { - return mItems.size(); - } - - @SuppressLint("SimpleDateFormat") - private String secondsToDisplayableString(int secs) { - SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); - Calendar cal = Calendar.getInstance(); - cal.set(0, 0, 0, 0, 0, secs); - return dateFormat.format(cal.getTime()); - } - - @NonNull - @Override - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - LayoutInflater inflater = - (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - View rowView = convertView; - if (rowView == null) { - rowView = inflater.inflate(mResource, parent, false); - } - CallLog callLog = getItem(position); - - String callTime = secondsToDisplayableString(callLog.getDuration()); - String callDate = String.valueOf(callLog.getStartDate()); - String status; - if (callLog.getDir() == Call.Dir.Outgoing) { - status = mContext.getString(R.string.outgoing); - } else { - if (callLog.getStatus() == Call.Status.Missed) { - status = mContext.getString(R.string.missed); - } else { - status = mContext.getString(R.string.incoming); - } - } - - TextView date = rowView.findViewById(R.id.date); - TextView time = rowView.findViewById(R.id.time); - ImageView callDirection = rowView.findViewById(R.id.direction); - - if (status.equals(mContext.getResources().getString(R.string.missed))) { - callDirection.setImageResource(R.drawable.call_missed); - } else if (status.equals(mContext.getResources().getString(R.string.incoming))) { - callDirection.setImageResource(R.drawable.call_incoming); - } else if (status.equals(mContext.getResources().getString(R.string.outgoing))) { - callDirection.setImageResource(R.drawable.call_outgoing); - } - - time.setText(callTime == null ? "" : callTime); - long longDate = Long.parseLong(callDate); - date.setText( - LinphoneUtils.timestampToHumanDate( - mContext, - longDate, - mContext.getString(R.string.history_detail_date_format))); - - return rowView; - } -} diff --git a/app/src/main/java/org/linphone/history/HistoryViewHolder.java b/app/src/main/java/org/linphone/history/HistoryViewHolder.java deleted file mode 100644 index a2efc8f1c..000000000 --- a/app/src/main/java/org/linphone/history/HistoryViewHolder.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.history; - -import android.view.View; -import android.widget.CheckBox; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; -import androidx.recyclerview.widget.RecyclerView; -import org.linphone.R; - -public class HistoryViewHolder extends RecyclerView.ViewHolder - implements View.OnClickListener, View.OnLongClickListener { - public final TextView contact; - public final ImageView detail; - public final CheckBox select; - public final ImageView callDirection; - public final RelativeLayout avatarLayout; - public final LinearLayout separator; - public final TextView separatorText; - - private final ClickListener mListener; - - public HistoryViewHolder(View view, ClickListener listener) { - super(view); - contact = view.findViewById(R.id.sip_uri); - detail = view.findViewById(R.id.detail); - select = view.findViewById(R.id.delete); - callDirection = view.findViewById(R.id.icon); - avatarLayout = view.findViewById(R.id.avatar_layout); - separator = view.findViewById(R.id.separator); - separatorText = view.findViewById(R.id.separator_text); - mListener = listener; - view.setOnClickListener(this); - view.setOnLongClickListener(this); - } - - @Override - public void onClick(View view) { - if (mListener != null) { - mListener.onItemClicked(getAdapterPosition()); - } - } - - @Override - public boolean onLongClick(View view) { - if (mListener != null) { - return mListener.onItemLongClicked(getAdapterPosition()); - } - return false; - } - - public interface ClickListener { - void onItemClicked(int position); - - boolean onItemLongClicked(int position); - } -} diff --git a/app/src/main/java/org/linphone/menu/SideMenuAccountsListAdapter.java b/app/src/main/java/org/linphone/menu/SideMenuAccountsListAdapter.java deleted file mode 100644 index 05fe1ebc7..000000000 --- a/app/src/main/java/org/linphone/menu/SideMenuAccountsListAdapter.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.menu; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ImageView; -import android.widget.TextView; -import java.util.ArrayList; -import java.util.List; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Core; -import org.linphone.core.ProxyConfig; -import org.linphone.core.RegistrationState; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; - -class SideMenuAccountsListAdapter extends BaseAdapter { - private final Context mContext; - private List proxy_list; - - SideMenuAccountsListAdapter(Context context) { - mContext = context; - proxy_list = new ArrayList<>(); - refresh(); - } - - private void refresh() { - proxy_list = new ArrayList<>(); - Core core = LinphoneManager.getCore(); - for (ProxyConfig proxyConfig : core.getProxyConfigList()) { - if (proxyConfig != core.getDefaultProxyConfig()) { - proxy_list.add(proxyConfig); - } - } - } - - public int getCount() { - if (proxy_list != null) { - return proxy_list.size(); - } else { - return 0; - } - } - - public Object getItem(int position) { - return proxy_list.get(position); - } - - public long getItemId(int position) { - return position; - } - - public View getView(final int position, View convertView, ViewGroup parent) { - View view; - ProxyConfig lpc = (ProxyConfig) getItem(position); - if (convertView != null) { - view = convertView; - } else { - view = - LayoutInflater.from(mContext) - .inflate(R.layout.side_menu_account_cell, parent, false); - } - - ImageView status = view.findViewById(R.id.account_status); - TextView address = view.findViewById(R.id.account_address); - String sipAddress = lpc.getIdentityAddress().asStringUriOnly(); - - address.setText(sipAddress); - - int nbAccounts = LinphonePreferences.instance().getAccountCount(); - int accountIndex; - - for (int i = 0; i < nbAccounts; i++) { - String username = LinphonePreferences.instance().getAccountUsername(i); - String domain = LinphonePreferences.instance().getAccountDomain(i); - String id = "sip:" + username + "@" + domain; - if (id.equals(sipAddress)) { - accountIndex = i; - view.setTag(accountIndex); - break; - } - } - status.setImageResource(getStatusIconResource(lpc.getState())); - return view; - } - - private int getStatusIconResource(RegistrationState state) { - try { - if (state == RegistrationState.Ok) { - return R.drawable.led_connected; - } else if (state == RegistrationState.Progress) { - return R.drawable.led_inprogress; - } else if (state == RegistrationState.Failed) { - return R.drawable.led_error; - } else { - return R.drawable.led_disconnected; - } - } catch (Exception e) { - Log.e(e); - } - - return R.drawable.led_disconnected; - } -} diff --git a/app/src/main/java/org/linphone/menu/SideMenuAdapter.java b/app/src/main/java/org/linphone/menu/SideMenuAdapter.java deleted file mode 100644 index 52ef184df..000000000 --- a/app/src/main/java/org/linphone/menu/SideMenuAdapter.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.menu; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import java.util.List; -import org.linphone.R; - -class SideMenuAdapter extends ArrayAdapter { - private final List mItems; - private final int mResource; - - SideMenuAdapter(@NonNull Context context, int resource, @NonNull List objects) { - super(context, resource, objects); - mResource = resource; - mItems = objects; - } - - @Nullable - @Override - public SideMenuItem getItem(int position) { - return mItems.get(position); - } - - @Override - public int getCount() { - return mItems.size(); - } - - @NonNull - @Override - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - LayoutInflater inflater = - (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - View rowView = convertView; - if (rowView == null) { - rowView = inflater.inflate(mResource, parent, false); - } - - TextView textView = rowView.findViewById(R.id.item_name); - ImageView imageView = rowView.findViewById(R.id.item_icon); - - SideMenuItem item = getItem(position); - textView.setText(item.name); - imageView.setImageResource(item.icon); - - return rowView; - } -} diff --git a/app/src/main/java/org/linphone/menu/SideMenuFragment.java b/app/src/main/java/org/linphone/menu/SideMenuFragment.java deleted file mode 100644 index 059ce4087..000000000 --- a/app/src/main/java/org/linphone/menu/SideMenuFragment.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.menu; - -import android.content.Intent; -import android.os.Bundle; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.RelativeLayout; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.drawerlayout.widget.DrawerLayout; -import androidx.fragment.app.Fragment; -import java.util.ArrayList; -import java.util.List; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.activities.AboutActivity; -import org.linphone.activities.MainActivity; -import org.linphone.assistant.MenuAssistantActivity; -import org.linphone.core.Core; -import org.linphone.core.ProxyConfig; -import org.linphone.core.RegistrationState; -import org.linphone.core.tools.Log; -import org.linphone.recording.RecordingsActivity; -import org.linphone.settings.LinphonePreferences; -import org.linphone.settings.SettingsActivity; -import org.linphone.utils.LinphoneUtils; - -public class SideMenuFragment extends Fragment { - private DrawerLayout mSideMenu; - private RelativeLayout mSideMenuContent; - private RelativeLayout mDefaultAccount; - private ListView mAccountsList, mSideMenuItemList; - private QuitClikedListener mQuitListener; - - @Nullable - @Override - public View onCreateView( - @NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.side_menu, container, false); - - List sideMenuItems = new ArrayList<>(); - if (getResources().getBoolean(R.bool.show_log_out_in_side_menu)) { - sideMenuItems.add( - new SideMenuItem( - getResources().getString(R.string.menu_logout), - R.drawable.quit_default)); - } - if (!getResources().getBoolean(R.bool.hide_assistant_from_side_menu)) { - sideMenuItems.add( - new SideMenuItem( - getResources().getString(R.string.menu_assistant), - R.drawable.menu_assistant)); - } - if (!getResources().getBoolean(R.bool.hide_settings_from_side_menu)) { - sideMenuItems.add( - new SideMenuItem( - getResources().getString(R.string.menu_settings), - R.drawable.menu_options)); - } - if (getResources().getBoolean(R.bool.enable_in_app_purchase)) { - sideMenuItems.add( - new SideMenuItem( - getResources().getString(R.string.inapp), R.drawable.menu_options)); - } - if (!getResources().getBoolean(R.bool.hide_recordings_from_side_menu)) { - sideMenuItems.add( - new SideMenuItem( - getResources().getString(R.string.menu_recordings), - R.drawable.menu_recordings)); - } - sideMenuItems.add( - new SideMenuItem( - getResources().getString(R.string.menu_about), R.drawable.menu_about)); - mSideMenuItemList = view.findViewById(R.id.item_list); - - mSideMenuItemList.setAdapter( - new SideMenuAdapter(getActivity(), R.layout.side_menu_item_cell, sideMenuItems)); - mSideMenuItemList.setOnItemClickListener( - new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int i, long l) { - String selectedItem = mSideMenuItemList.getAdapter().getItem(i).toString(); - if (selectedItem.equals(getString(R.string.menu_logout))) { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.setDefaultProxyConfig(null); - core.clearAllAuthInfo(); - core.clearProxyConfig(); - startActivity( - new Intent() - .setClass( - getActivity(), - MenuAssistantActivity.class)); - getActivity().finish(); - } - } else if (selectedItem.equals(getString(R.string.menu_settings))) { - startActivity(new Intent(getActivity(), SettingsActivity.class)); - } else if (selectedItem.equals(getString(R.string.menu_about))) { - startActivity(new Intent(getActivity(), AboutActivity.class)); - } else if (selectedItem.equals(getString(R.string.menu_assistant))) { - startActivity(new Intent(getActivity(), MenuAssistantActivity.class)); - } else if (selectedItem.equals(getString(R.string.menu_recordings))) { - startActivity(new Intent(getActivity(), RecordingsActivity.class)); - } - } - }); - - mAccountsList = view.findViewById(R.id.accounts_list); - mDefaultAccount = view.findViewById(R.id.default_account); - - RelativeLayout quitLayout = view.findViewById(R.id.side_menu_quit); - quitLayout.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - if (mQuitListener != null) { - mQuitListener.onQuitClicked(); - } - } - }); - - return view; - } - - public void setQuitListener(QuitClikedListener listener) { - mQuitListener = listener; - } - - public void setDrawer(DrawerLayout drawer, RelativeLayout content) { - mSideMenu = drawer; - mSideMenuContent = content; - } - - public boolean isOpened() { - return mSideMenu != null && mSideMenu.isDrawerVisible(Gravity.LEFT); - } - - public void closeDrawer() { - openOrCloseSideMenu(false, false); - } - - public void openOrCloseSideMenu(boolean open, boolean animate) { - if (mSideMenu == null || mSideMenuContent == null) return; - - if (open) { - mSideMenu.openDrawer(mSideMenuContent, animate); - } else { - mSideMenu.closeDrawer(mSideMenuContent, animate); - } - } - - private void displayMainAccount() { - mDefaultAccount.setVisibility(View.VISIBLE); - ImageView status = mDefaultAccount.findViewById(R.id.main_account_status); - TextView address = mDefaultAccount.findViewById(R.id.main_account_address); - TextView displayName = mDefaultAccount.findViewById(R.id.main_account_display_name); - - if (!LinphoneContext.isReady() || LinphoneManager.getCore() == null) return; - - ProxyConfig proxy = LinphoneManager.getCore().getDefaultProxyConfig(); - if (proxy == null) { - displayName.setText(getString(R.string.no_account)); - status.setVisibility(View.GONE); - address.setText(""); - mDefaultAccount.setOnClickListener(null); - } else { - address.setText(proxy.getIdentityAddress().asStringUriOnly()); - displayName.setText(LinphoneUtils.getAddressDisplayName(proxy.getIdentityAddress())); - status.setImageResource(getStatusIconResource(proxy.getState())); - status.setVisibility(View.VISIBLE); - - if (!getResources().getBoolean(R.bool.disable_accounts_settings_from_side_menu)) { - mDefaultAccount.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - ((MainActivity) getActivity()) - .showAccountSettings( - LinphonePreferences.instance() - .getDefaultAccountIndex()); - } - }); - } - } - } - - private int getStatusIconResource(RegistrationState state) { - try { - if (state == RegistrationState.Ok) { - return R.drawable.led_connected; - } else if (state == RegistrationState.Progress) { - return R.drawable.led_inprogress; - } else if (state == RegistrationState.Failed) { - return R.drawable.led_error; - } else { - return R.drawable.led_disconnected; - } - } catch (Exception e) { - Log.e(e); - } - - return R.drawable.led_disconnected; - } - - public void displayAccountsInSideMenu() { - Core core = LinphoneManager.getCore(); - if (core != null - && core.getProxyConfigList() != null - && core.getProxyConfigList().length > 1) { - mAccountsList.setVisibility(View.VISIBLE); - mAccountsList.setAdapter(new SideMenuAccountsListAdapter(getActivity())); - mAccountsList.setOnItemClickListener( - new AdapterView.OnItemClickListener() { - @Override - public void onItemClick( - AdapterView adapterView, View view, int i, long l) { - if (view != null && view.getTag() != null) { - int position = Integer.parseInt(view.getTag().toString()); - ((MainActivity) getActivity()).showAccountSettings(position); - } - } - }); - } else { - mAccountsList.setVisibility(View.GONE); - } - displayMainAccount(); - } - - public interface QuitClikedListener { - void onQuitClicked(); - } -} diff --git a/app/src/main/java/org/linphone/notifications/Notifiable.java b/app/src/main/java/org/linphone/notifications/Notifiable.java deleted file mode 100644 index 025be3613..000000000 --- a/app/src/main/java/org/linphone/notifications/Notifiable.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.notifications; - -import java.util.ArrayList; -import java.util.List; - -public class Notifiable { - private final int mNotificationId; - private List mMessages; - private boolean mIsGroup; - private String mGroupTitle; - private String mLocalIdentity; - private String mRemoteIdentity; - private String mMyself; - private int mIconId; - private int mTextId; - - public Notifiable(int id) { - mNotificationId = id; - mMessages = new ArrayList<>(); - mIsGroup = false; - mIconId = 0; - mTextId = 0; - } - - public int getNotificationId() { - return mNotificationId; - } - - public void resetMessages() { - mMessages = new ArrayList<>(); - } - - public void addMessage(NotifiableMessage notifMessage) { - mMessages.add(notifMessage); - } - - public List getMessages() { - return mMessages; - } - - public boolean isGroup() { - return mIsGroup; - } - - public void setIsGroup(boolean isGroup) { - mIsGroup = isGroup; - } - - public String getGroupTitle() { - return mGroupTitle; - } - - public void setGroupTitle(String title) { - mGroupTitle = title; - } - - public String getMyself() { - return mMyself; - } - - public void setMyself(String myself) { - mMyself = myself; - } - - public String getLocalIdentity() { - return mLocalIdentity; - } - - public String getRemoteIdentity() { - return mRemoteIdentity; - } - - public void setLocalIdentity(String localIdentity) { - mLocalIdentity = localIdentity; - } - - public void setRemoteIdentity(String remoteIdentity) { - mRemoteIdentity = remoteIdentity; - } - - public int getIconResourceId() { - return mIconId; - } - - public void setIconResourceId(int id) { - mIconId = id; - } - - public int getTextResourceId() { - return mTextId; - } - - public void setTextResourceId(int id) { - mTextId = id; - } - - public String toString() { - return "Id: " - + mNotificationId - + ", local identity: " - + mLocalIdentity - + ", myself: " - + mMyself - + ", isGrouped: " - + mIsGroup; - } -} diff --git a/app/src/main/java/org/linphone/notifications/NotifiableMessage.java b/app/src/main/java/org/linphone/notifications/NotifiableMessage.java deleted file mode 100644 index a036266b1..000000000 --- a/app/src/main/java/org/linphone/notifications/NotifiableMessage.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.notifications; - -import android.graphics.Bitmap; -import android.net.Uri; - -public class NotifiableMessage { - private final String mMessage; - private final String mSender; - private final long mTime; - private Bitmap mSenderBitmap; - private final Uri mFilePath; - private final String mFileMime; - - public NotifiableMessage( - String message, String sender, long time, Uri filePath, String fileMime) { - mMessage = message; - mSender = sender; - mTime = time; - mFilePath = filePath; - mFileMime = fileMime; - } - - public String getMessage() { - return mMessage; - } - - public String getSender() { - return mSender; - } - - public long getTime() { - return mTime; - } - - public Bitmap getSenderBitmap() { - return mSenderBitmap; - } - - public void setSenderBitmap(Bitmap bm) { - mSenderBitmap = bm; - } - - public Uri getFilePath() { - return mFilePath; - } - - public String getFileMime() { - return mFileMime; - } -} diff --git a/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.java b/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.java deleted file mode 100644 index 82d939918..000000000 --- a/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.notifications; - -import static android.content.Context.NOTIFICATION_SERVICE; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.RemoteInput; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.compatibility.Compatibility; -import org.linphone.core.Address; -import org.linphone.core.Call; -import org.linphone.core.ChatMessage; -import org.linphone.core.ChatRoom; -import org.linphone.core.Core; -import org.linphone.core.Factory; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; - -public class NotificationBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(final Context context, Intent intent) { - final int notifId = intent.getIntExtra(Compatibility.INTENT_NOTIF_ID, 0); - final String localyIdentity = intent.getStringExtra(Compatibility.INTENT_LOCAL_IDENTITY); - final String remoteIdentity = intent.getStringExtra(Compatibility.INTENT_REMOTE_IDENTITY); - - if (!LinphoneContext.isReady()) { - Log.e("[Notification Broadcast Receiver] Context not ready..."); - } - - if (intent.getAction().equals(Compatibility.INTENT_REPLY_NOTIF_ACTION) - || intent.getAction().equals(Compatibility.INTENT_MARK_AS_READ_ACTION)) { - String remoteSipAddr = remoteIdentity; - - Core core; - boolean stopCoreWhenFinished = false; - if (!LinphoneContext.isReady()) { - String basePath = context.getFilesDir().getAbsolutePath(); - core = - Factory.instance() - .createCore( - basePath + LinphonePreferences.LINPHONE_DEFAULT_RC, - basePath + LinphonePreferences.LINPHONE_FACTORY_RC, - context); - stopCoreWhenFinished = true; - Log.e("[Notification Broadcast Receiver] Created temporary Core"); - core.start(); - } else core = LinphoneManager.getCore(); - - if (core == null) { - Log.e("[Notification Broadcast Receiver] Couldn't get Core instance"); - onError(context, notifId); - return; - } - - Address remoteAddr = core.interpretUrl(remoteSipAddr); - if (remoteAddr == null) { - Log.e( - "[Notification Broadcast Receiver] Couldn't interpret remote address " - + remoteSipAddr); - onError(context, notifId); - return; - } - - Address localAddr = core.interpretUrl(localyIdentity); - if (localAddr == null) { - Log.e( - "[Notification Broadcast Receiver] Couldn't interpret local address " - + localyIdentity); - onError(context, notifId); - return; - } - - ChatRoom room = core.getChatRoom(remoteAddr, localAddr); - if (room == null) { - Log.e( - "[Notification Broadcast Receiver] Couldn't find chat room for remote address " - + remoteSipAddr - + " and local address " - + localyIdentity); - onError(context, notifId); - return; - } - - room.markAsRead(); - - if (intent.getAction().equals(Compatibility.INTENT_REPLY_NOTIF_ACTION)) { - final String reply = getMessageText(intent).toString(); - if (reply == null) { - Log.e("[Notification Broadcast Receiver] Couldn't get reply text"); - onError(context, notifId); - return; - } - - ChatMessage msg = room.createMessage(reply); - msg.setUserData(notifId); - if (!stopCoreWhenFinished) { - msg.addListener( - LinphoneContext.instance() - .getNotificationManager() - .getMessageListener()); - } - msg.send(); - Log.i("[Notification Broadcast Receiver] Reply sent for notif id " + notifId); - } else { - if (!stopCoreWhenFinished) { - LinphoneContext.instance() - .getNotificationManager() - .dismissNotification(notifId); - } else { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); - notificationManager.cancel(notifId); - } - } - - if (stopCoreWhenFinished) { - core.stopAsync(); - } - } else if (intent.getAction().equals(Compatibility.INTENT_ANSWER_CALL_NOTIF_ACTION) - || intent.getAction().equals(Compatibility.INTENT_HANGUP_CALL_NOTIF_ACTION)) { - if (!LinphoneContext.isReady()) return; - - String remoteAddr = - LinphoneContext.instance() - .getNotificationManager() - .getSipUriForCallNotificationId(notifId); - - Core core = LinphoneManager.getCore(); - if (core == null) { - Log.e("[Notification Broadcast Receiver] Couldn't get Core instance"); - return; - } - Call call = core.findCallFromUri(remoteAddr); - if (call == null) { - Log.e( - "[Notification Broadcast Receiver] Couldn't find call from remote address " - + remoteAddr); - return; - } - - if (intent.getAction().equals(Compatibility.INTENT_ANSWER_CALL_NOTIF_ACTION)) { - LinphoneManager.getCallManager().acceptCall(call); - } else { - call.terminate(); - } - } - } - - private void onError(Context context, int notifId) { - Notification replyError = - Compatibility.createRepliedNotification(context, context.getString(R.string.error)); - LinphoneContext.instance().getNotificationManager().sendNotification(notifId, replyError); - } - - private CharSequence getMessageText(Intent intent) { - Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); - if (remoteInput != null) { - return remoteInput.getCharSequence(Compatibility.KEY_TEXT_REPLY); - } - return null; - } -} diff --git a/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt b/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt new file mode 100644 index 000000000..44b2a5454 --- /dev/null +++ b/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.notifications + +import android.app.RemoteInput +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Call +import org.linphone.core.Core +import org.linphone.core.tools.Log + +class NotificationBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val notificationId = intent.getIntExtra(NotificationsManager.INTENT_NOTIF_ID, 0) + val localIdentity = intent.getStringExtra(NotificationsManager.INTENT_LOCAL_IDENTITY) + + if (intent.action == NotificationsManager.INTENT_REPLY_NOTIF_ACTION || intent.action == NotificationsManager.INTENT_MARK_AS_READ_ACTION) { + val remoteSipAddress: String? = coreContext.notificationsManager.getSipUriForChatNotificationId(notificationId) + val core: Core = coreContext.core + + val remoteAddress = core.interpretUrl(remoteSipAddress) + if (remoteAddress == null) { + Log.e("[Notification Broadcast Receiver] Couldn't interpret remote address $remoteSipAddress") + return + } + + val localAddress = core.interpretUrl(localIdentity) + if (localAddress == null) { + Log.e("[Notification Broadcast Receiver] Couldn't interpret local address $localIdentity") + return + } + + val room = core.getChatRoom(remoteAddress, localAddress) + if (room == null) { + Log.e("[Notification Broadcast Receiver] Couldn't find chat room for remote address $remoteSipAddress and local address $localIdentity") + return + } + + room.markAsRead() + if (intent.action == NotificationsManager.INTENT_REPLY_NOTIF_ACTION) { + val reply = getMessageText(intent)?.toString() + if (reply == null) { + Log.e("[Notification Broadcast Receiver] Couldn't get reply text") + return + } + + val msg = room.createMessage(reply) + msg.userData = notificationId + msg.addListener(coreContext.notificationsManager.chatListener) + msg.send() + Log.i("[Notification Broadcast Receiver] Reply sent for notif id $notificationId") + } else { + coreContext.notificationsManager.cancel(notificationId) + } + } else if (intent.action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION || intent.action == NotificationsManager.INTENT_HANGUP_CALL_NOTIF_ACTION) { + val remoteAddress: String? = coreContext.notificationsManager.getSipUriForCallNotificationId(notificationId) + val core: Core = coreContext.core + + val call = core.findCallFromUri(remoteAddress) + if (call == null) { + Log.e("[Notification Broadcast Receiver] Couldn't find call from remote address $remoteAddress") + return + } + + if (intent.action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION) { + coreContext.answerCall(call) + } else { + if (call.state == Call.State.IncomingReceived || call.state == Call.State.IncomingEarlyMedia) coreContext.declineCall(call) else coreContext.terminateCall(call) + } + } + } + + private fun getMessageText(intent: Intent): CharSequence? { + val remoteInput = RemoteInput.getResultsFromIntent(intent) + return remoteInput?.getCharSequence(NotificationsManager.KEY_TEXT_REPLY) + } +} diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.java b/app/src/main/java/org/linphone/notifications/NotificationsManager.java deleted file mode 100644 index fc87d6219..000000000 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.java +++ /dev/null @@ -1,761 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.notifications; - -import static android.content.Context.NOTIFICATION_SERVICE; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.service.notification.StatusBarNotification; -import java.io.File; -import java.util.HashMap; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.call.CallActivity; -import org.linphone.call.CallIncomingActivity; -import org.linphone.call.CallOutgoingActivity; -import org.linphone.chat.ChatActivity; -import org.linphone.compatibility.Compatibility; -import org.linphone.contacts.ContactsManager; -import org.linphone.contacts.LinphoneContact; -import org.linphone.core.Address; -import org.linphone.core.Call; -import org.linphone.core.ChatMessage; -import org.linphone.core.ChatMessageListenerStub; -import org.linphone.core.ChatRoom; -import org.linphone.core.ChatRoomCapabilities; -import org.linphone.core.Content; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.Reason; -import org.linphone.core.tools.Log; -import org.linphone.dialer.DialerActivity; -import org.linphone.history.HistoryActivity; -import org.linphone.service.LinphoneService; -import org.linphone.settings.LinphonePreferences; -import org.linphone.utils.DeviceUtils; -import org.linphone.utils.FileUtils; -import org.linphone.utils.ImageUtils; -import org.linphone.utils.LinphoneUtils; -import org.linphone.utils.MediaScannerListener; - -public class NotificationsManager { - private static final int SERVICE_NOTIF_ID = 1; - private static final int MISSED_CALLS_NOTIF_ID = 2; - - private final Context mContext; - private final NotificationManager mNM; - private final HashMap mChatNotifMap; - private final HashMap mCallNotifMap; - private int mLastNotificationId; - private final Notification mServiceNotification; - private int mCurrentForegroundServiceNotification; - private String mCurrentChatRoomAddress; - private CoreListenerStub mListener; - private ChatMessageListenerStub mMessageListener; - - public NotificationsManager(Context context) { - mContext = context; - mChatNotifMap = new HashMap<>(); - mCallNotifMap = new HashMap<>(); - mCurrentForegroundServiceNotification = 0; - mCurrentChatRoomAddress = null; - - mNM = (NotificationManager) mContext.getSystemService(NOTIFICATION_SERVICE); - - if (mContext.getResources().getBoolean(R.bool.keep_missed_call_notification_upon_restart)) { - StatusBarNotification[] notifs = Compatibility.getActiveNotifications(mNM); - if (notifs != null && notifs.length > 1) { - for (StatusBarNotification notif : notifs) { - if (notif.getId() != MISSED_CALLS_NOTIF_ID) { - dismissNotification(notif.getId()); - } - } - } - } else { - mNM.cancelAll(); - } - - mLastNotificationId = 5; // Do not conflict with hardcoded notifications ids ! - - Compatibility.createNotificationChannels(mContext); - - Bitmap bm = null; - try { - bm = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.ic_launcher); - } catch (Exception e) { - Log.e(e); - } - - Intent notifIntent = new Intent(mContext, DialerActivity.class); - notifIntent.putExtra("Notification", true); - addFlagsToIntent(notifIntent); - - PendingIntent pendingIntent = - PendingIntent.getActivity( - mContext, SERVICE_NOTIF_ID, notifIntent, PendingIntent.FLAG_UPDATE_CURRENT); - mServiceNotification = - Compatibility.createNotification( - mContext, - mContext.getString(R.string.service_name), - "", - R.drawable.linphone_notification_icon, - R.mipmap.ic_launcher, - bm, - pendingIntent, - Notification.PRIORITY_MIN, - true); - - mListener = - new CoreListenerStub() { - @Override - public void onMessageSent(Core core, ChatRoom room, ChatMessage message) { - if (room.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { - Compatibility.createChatShortcuts(mContext); - } - } - - @Override - public void onMessageReceived( - Core core, final ChatRoom cr, final ChatMessage message) { - if (message.isOutgoing() - || mContext.getResources().getBoolean(R.bool.disable_chat) - || mContext.getResources() - .getBoolean(R.bool.disable_chat_message_notification)) { - return; - } - - if (mCurrentChatRoomAddress != null - && mCurrentChatRoomAddress.equals( - cr.getPeerAddress().asStringUriOnly())) { - Log.i( - "[Notifications Manager] Message received for currently displayed chat room, do not make a notification"); - return; - } - - if (message.getErrorInfo() != null - && message.getErrorInfo().getReason() - == Reason.UnsupportedContent) { - Log.w( - "[Notifications Manager] Message received but content is unsupported, do not notify it"); - return; - } - - if (!message.hasTextContent() - && message.getFileTransferInformation() == null) { - Log.w( - "[Notifications Manager] Message has no text or file transfer information to display, ignoring it..."); - return; - } - - final Address from = message.getFromAddress(); - final LinphoneContact contact = - ContactsManager.getInstance().findContactFromAddress(from); - final String textMessage = - (message.hasTextContent()) - ? message.getTextContent() - : mContext.getString( - R.string.content_description_incoming_file); - - String file = null; - for (Content c : message.getContents()) { - if (c.isFile()) { - file = c.getFilePath(); - LinphoneManager.getInstance() - .getMediaScanner() - .scanFile( - new File(file), - new MediaScannerListener() { - @Override - public void onMediaScanned( - String path, Uri uri) { - createNotification( - cr, - contact, - from, - textMessage, - message.getTime(), - uri, - FileUtils.getMimeFromFile(path)); - } - }); - break; - } - } - - if (file == null) { - createNotification( - cr, contact, from, textMessage, message.getTime(), null, null); - } - - if (cr.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { - Compatibility.createChatShortcuts(mContext); - } - } - }; - - mMessageListener = - new ChatMessageListenerStub() { - @Override - public void onMsgStateChanged(ChatMessage msg, ChatMessage.State state) { - if (msg.getUserData() == null) return; - int notifId = (int) msg.getUserData(); - Log.i( - "[Notifications Manager] Reply message state changed (" - + state.name() - + ") for notif id " - + notifId); - - if (state != ChatMessage.State.InProgress) { - // There is no need to be called here twice - msg.removeListener(this); - } - - if (state == ChatMessage.State.Delivered - || state == ChatMessage.State.Displayed) { - Notifiable notif = - mChatNotifMap.get( - msg.getChatRoom().getPeerAddress().asStringUriOnly()); - if (notif == null) { - Log.e( - "[Notifications Manager] Couldn't find message notification for SIP URI " - + msg.getChatRoom() - .getPeerAddress() - .asStringUriOnly()); - dismissNotification(notifId); - return; - } else if (notif.getNotificationId() != notifId) { - Log.w( - "[Notifications Manager] Notif ID doesn't match: " - + notifId - + " != " - + notif.getNotificationId()); - } - - displayReplyMessageNotification(msg, notif); - } else if (state == ChatMessage.State.NotDelivered) { - Log.e( - "[Notifications Manager] Couldn't send reply, message is not delivered"); - dismissNotification(notifId); - } - } - }; - } - - public void onCoreReady() { - Core core = LinphoneManager.getCore(); - if (core != null) { - core.addListener(mListener); - } - } - - public void destroy() { - // mNM.cancelAll(); - // Don't use cancelAll to keep message notifications ! - // When a message is received by a push, it will create a LinphoneService - // but it might be getting killed quite quickly after that - // causing the notification to be missed by the user... - Log.i("[Notifications Manager] Getting destroyed, clearing Service & Call notifications"); - - if (mCurrentForegroundServiceNotification > 0) { - mNM.cancel(mCurrentForegroundServiceNotification); - } - - for (Notifiable notifiable : mCallNotifMap.values()) { - mNM.cancel(notifiable.getNotificationId()); - } - - Core core = LinphoneManager.getCore(); - if (core != null) { - core.removeListener(mListener); - } - } - - private void addFlagsToIntent(Intent intent) { - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - } - - public void startForeground() { - if (LinphoneService.isReady()) { - Log.i("[Notifications Manager] Starting Service as foreground"); - LinphoneService.instance().startForeground(SERVICE_NOTIF_ID, mServiceNotification); - mCurrentForegroundServiceNotification = SERVICE_NOTIF_ID; - } - } - - private void startForeground(Notification notification, int id) { - if (LinphoneService.isReady()) { - Log.i("[Notifications Manager] Starting Service as foreground while in call"); - LinphoneService.instance().startForeground(id, notification); - mCurrentForegroundServiceNotification = id; - } - } - - public void stopForeground() { - if (LinphoneService.isReady()) { - Log.i("[Notifications Manager] Stopping Service as foreground"); - LinphoneService.instance().stopForeground(true); - mCurrentForegroundServiceNotification = 0; - } - } - - public void removeForegroundServiceNotificationIfPossible() { - if (LinphoneService.isReady()) { - if (mCurrentForegroundServiceNotification == SERVICE_NOTIF_ID - && !isServiceNotificationDisplayed()) { - Log.i( - "[Notifications Manager] Linphone has started after device boot, stopping Service as foreground"); - stopForeground(); - } - } - } - - public void setCurrentlyDisplayedChatRoom(String address) { - mCurrentChatRoomAddress = address; - if (address != null) { - resetMessageNotifCount(address); - } - } - - public void dismissMissedCallNotification() { - dismissNotification(MISSED_CALLS_NOTIF_ID); - } - - public void sendNotification(int id, Notification notif) { - Log.i("[Notifications Manager] Notifying " + id); - mNM.notify(id, notif); - } - - public void dismissNotification(int notifId) { - Log.i("[Notifications Manager] Dismissing " + notifId); - mNM.cancel(notifId); - } - - public void resetMessageNotifCount(String address) { - Notifiable notif = mChatNotifMap.get(address); - if (notif != null) { - notif.resetMessages(); - mNM.cancel(notif.getNotificationId()); - } - } - - public ChatMessageListenerStub getMessageListener() { - return mMessageListener; - } - - private boolean isServiceNotificationDisplayed() { - return LinphonePreferences.instance().getServiceNotificationVisibility(); - } - - public String getSipUriForNotificationId(int notificationId) { - for (String addr : mChatNotifMap.keySet()) { - if (mChatNotifMap.get(addr).getNotificationId() == notificationId) { - return addr; - } - } - return null; - } - - private void displayMessageNotificationFromNotifiable( - Notifiable notif, String remoteSipUri, String localSipUri) { - Intent notifIntent = new Intent(mContext, ChatActivity.class); - notifIntent.putExtra("RemoteSipUri", remoteSipUri); - notifIntent.putExtra("LocalSipUri", localSipUri); - addFlagsToIntent(notifIntent); - - PendingIntent pendingIntent = - PendingIntent.getActivity( - mContext, - notif.getNotificationId(), - notifIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - - NotifiableMessage lastNotifiable = notif.getMessages().get(notif.getMessages().size() - 1); - String from = lastNotifiable.getSender(); - String message = lastNotifiable.getMessage(); - Bitmap bm = lastNotifiable.getSenderBitmap(); - if (notif.isGroup()) { - message = - mContext.getString(R.string.group_chat_notif) - .replace("%1", from) - .replace("%2", message); - from = notif.getGroupTitle(); - } - - Notification notification = - Compatibility.createMessageNotification( - mContext, notif, from, message, bm, pendingIntent); - sendNotification(notif.getNotificationId(), notification); - } - - private void displayReplyMessageNotification(ChatMessage msg, Notifiable notif) { - if (msg == null || notif == null) return; - Log.i( - "[Notifications Manager] Updating message notification with reply for notif " - + notif.getNotificationId()); - - NotifiableMessage notifMessage = - new NotifiableMessage( - msg.getTextContent(), - notif.getMyself(), - System.currentTimeMillis(), - null, - null); - notif.addMessage(notifMessage); - - ChatRoom cr = msg.getChatRoom(); - - displayMessageNotificationFromNotifiable( - notif, - cr.getPeerAddress().asStringUriOnly(), - cr.getLocalAddress().asStringUriOnly()); - } - - public void displayGroupChatMessageNotification( - String subject, - String conferenceAddress, - String fromName, - Uri fromPictureUri, - String message, - Address localIdentity, - long timestamp, - Uri filePath, - String fileMime) { - - Bitmap bm = ImageUtils.getRoundBitmapFromUri(mContext, fromPictureUri); - Notifiable notif = mChatNotifMap.get(conferenceAddress); - NotifiableMessage notifMessage = - new NotifiableMessage(message, fromName, timestamp, filePath, fileMime); - if (notif == null) { - notif = new Notifiable(mLastNotificationId); - mLastNotificationId += 1; - mChatNotifMap.put(conferenceAddress, notif); - } - Log.i("[Notifications Manager] Creating group chat message notifiable " + notif); - - notifMessage.setSenderBitmap(bm); - notif.addMessage(notifMessage); - notif.setIsGroup(true); - notif.setGroupTitle(subject); - notif.setMyself(LinphoneUtils.getAddressDisplayName(localIdentity)); - notif.setLocalIdentity(localIdentity.asString()); - notif.setRemoteIdentity(conferenceAddress); - - displayMessageNotificationFromNotifiable( - notif, conferenceAddress, localIdentity.asStringUriOnly()); - } - - public void displayMessageNotification( - String fromSipUri, - String fromName, - Uri fromPictureUri, - String message, - Address localIdentity, - long timestamp, - Uri filePath, - String fileMime) { - if (fromName == null) { - fromName = fromSipUri; - } - - Bitmap bm = ImageUtils.getRoundBitmapFromUri(mContext, fromPictureUri); - Notifiable notif = mChatNotifMap.get(fromSipUri); - NotifiableMessage notifMessage = - new NotifiableMessage(message, fromName, timestamp, filePath, fileMime); - if (notif == null) { - notif = new Notifiable(mLastNotificationId); - mLastNotificationId += 1; - mChatNotifMap.put(fromSipUri, notif); - } - Log.i("[Notifications Manager] Creating chat message notifiable " + notif); - - notifMessage.setSenderBitmap(bm); - notif.addMessage(notifMessage); - notif.setIsGroup(false); - notif.setMyself(LinphoneUtils.getAddressDisplayName(localIdentity)); - notif.setLocalIdentity(localIdentity.asString()); - notif.setRemoteIdentity(fromSipUri); - - displayMessageNotificationFromNotifiable( - notif, fromSipUri, localIdentity.asStringUriOnly()); - } - - public void displayMissedCallNotification(Call call) { - Intent missedCallNotifIntent = new Intent(mContext, HistoryActivity.class); - addFlagsToIntent(missedCallNotifIntent); - - PendingIntent pendingIntent = - PendingIntent.getActivity( - mContext, - MISSED_CALLS_NOTIF_ID, - missedCallNotifIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - - int missedCallCount = LinphoneManager.getCore().getMissedCallsCount(); - String body; - if (missedCallCount > 1) { - body = - mContext.getString(R.string.missed_calls_notif_body) - .replace("%i", String.valueOf(missedCallCount)); - Log.i("[Notifications Manager] Creating missed calls notification"); - } else { - Address address = call.getRemoteAddress(); - LinphoneContact c = ContactsManager.getInstance().findContactFromAddress(address); - if (c != null) { - body = c.getFullName(); - } else { - body = address.getDisplayName(); - if (body == null) { - body = address.asStringUriOnly(); - } - } - Log.i("[Notifications Manager] Creating missed call notification"); - } - - Notification notif = - Compatibility.createMissedCallNotification( - mContext, - mContext.getString(R.string.missed_calls_notif_title), - body, - pendingIntent, - missedCallCount); - sendNotification(MISSED_CALLS_NOTIF_ID, notif); - } - - public void displayCallNotification(Call call) { - if (call == null) return; - - Class callNotifIntentClass = CallActivity.class; - if (call.getState() == Call.State.IncomingReceived - || call.getState() == Call.State.IncomingEarlyMedia) { - callNotifIntentClass = CallIncomingActivity.class; - } else if (call.getState() == Call.State.OutgoingInit - || call.getState() == Call.State.OutgoingProgress - || call.getState() == Call.State.OutgoingRinging - || call.getState() == Call.State.OutgoingEarlyMedia) { - callNotifIntentClass = CallOutgoingActivity.class; - } - Intent callNotifIntent = new Intent(mContext, callNotifIntentClass); - callNotifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - PendingIntent pendingIntent = - PendingIntent.getActivity( - mContext, 0, callNotifIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - Address address = call.getRemoteAddress(); - String addressAsString = address.asStringUriOnly(); - Notifiable notif = mCallNotifMap.get(addressAsString); - if (notif == null) { - notif = new Notifiable(mLastNotificationId); - mLastNotificationId += 1; - mCallNotifMap.put(addressAsString, notif); - } - - int notificationTextId; - int iconId; - switch (call.getState()) { - case Released: - case End: - if (mCurrentForegroundServiceNotification == notif.getNotificationId()) { - Log.i( - "[Notifications Manager] Call ended, stopping notification used to keep service alive"); - // Call is released, remove service notification to allow for an other call to - // be service notification - stopForeground(); - } - mNM.cancel(notif.getNotificationId()); - mCallNotifMap.remove(addressAsString); - return; - case Paused: - case PausedByRemote: - case Pausing: - iconId = R.drawable.topbar_call_notification; - notificationTextId = R.string.incall_notif_paused; - break; - case IncomingEarlyMedia: - case IncomingReceived: - iconId = R.drawable.topbar_call_notification; - notificationTextId = R.string.incall_notif_incoming; - break; - case OutgoingEarlyMedia: - case OutgoingInit: - case OutgoingProgress: - case OutgoingRinging: - iconId = R.drawable.topbar_call_notification; - notificationTextId = R.string.incall_notif_outgoing; - break; - default: - if (call.getCurrentParams().videoEnabled()) { - iconId = R.drawable.topbar_videocall_notification; - notificationTextId = R.string.incall_notif_video; - } else { - iconId = R.drawable.topbar_call_notification; - notificationTextId = R.string.incall_notif_active; - } - break; - } - - if (notif.getIconResourceId() == iconId - && notif.getTextResourceId() == notificationTextId) { - // Notification hasn't changed, do not "update" it to avoid blinking - return; - } else if (notif.getTextResourceId() == R.string.incall_notif_incoming) { - // If previous notif was incoming call, as we will switch channels, dismiss it first - dismissNotification(notif.getNotificationId()); - } - - notif.setIconResourceId(iconId); - notif.setTextResourceId(notificationTextId); - Log.i( - "[Notifications Manager] Call notification notifiable is " - + notif - + ", pending intent " - + callNotifIntentClass); - - LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(address); - Uri pictureUri = contact != null ? contact.getThumbnailUri() : null; - Bitmap bm = ImageUtils.getRoundBitmapFromUri(mContext, pictureUri); - String name = - contact != null - ? contact.getFullName() - : LinphoneUtils.getAddressDisplayName(address); - boolean isIncoming = callNotifIntentClass == CallIncomingActivity.class; - - Notification notification; - if (isIncoming) { - notification = - Compatibility.createIncomingCallNotification( - mContext, - notif.getNotificationId(), - bm, - name, - addressAsString, - pendingIntent); - } else { - notification = - Compatibility.createInCallNotification( - mContext, - notif.getNotificationId(), - mContext.getString(notificationTextId), - iconId, - bm, - name, - pendingIntent); - } - - // Don't use incoming call notification as foreground service notif ! - if (!isServiceNotificationDisplayed() && !isIncoming) { - if (call.getCore().getCallsNb() == 0) { - Log.i( - "[Notifications Manager] Foreground service mode is disabled, stopping call notification used to keep it alive"); - stopForeground(); - } else { - if (mCurrentForegroundServiceNotification == 0) { - if (DeviceUtils.isAppUserRestricted(mContext)) { - Log.w( - "[Notifications Manager] App has been restricted, can't use call notification to keep service alive !"); - sendNotification(notif.getNotificationId(), notification); - } else { - Log.i( - "[Notifications Manager] Foreground service mode is disabled, using call notification to keep it alive"); - startForeground(notification, notif.getNotificationId()); - } - } else { - sendNotification(notif.getNotificationId(), notification); - } - } - } else { - sendNotification(notif.getNotificationId(), notification); - } - } - - public String getSipUriForCallNotificationId(int notificationId) { - for (String addr : mCallNotifMap.keySet()) { - if (mCallNotifMap.get(addr).getNotificationId() == notificationId) { - return addr; - } - } - return null; - } - - private void createNotification( - ChatRoom cr, - LinphoneContact contact, - Address from, - String textMessage, - long time, - Uri file, - String mime) { - if (cr.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { - if (contact != null) { - displayMessageNotification( - cr.getPeerAddress().asStringUriOnly(), - contact.getFullName(), - contact.getThumbnailUri(), - textMessage, - cr.getLocalAddress(), - time, - file, - mime); - } else { - displayMessageNotification( - cr.getPeerAddress().asStringUriOnly(), - from.getUsername(), - null, - textMessage, - cr.getLocalAddress(), - time, - file, - mime); - } - } else { - String subject = cr.getSubject(); - if (contact != null) { - displayGroupChatMessageNotification( - subject, - cr.getPeerAddress().asStringUriOnly(), - contact.getFullName(), - contact.getThumbnailUri(), - textMessage, - cr.getLocalAddress(), - time, - file, - mime); - } else { - displayGroupChatMessageNotification( - subject, - cr.getPeerAddress().asStringUriOnly(), - from.getUsername(), - null, - textMessage, - cr.getLocalAddress(), - time, - file, - mime); - } - } - } -} diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt new file mode 100644 index 000000000..6ee21328f --- /dev/null +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -0,0 +1,700 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.notifications + +import android.app.Notification +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.os.Bundle +import android.widget.RemoteViews +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.Person +import androidx.core.app.RemoteInput +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.IconCompat +import androidx.navigation.NavDeepLinkBuilder +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.activities.call.CallActivity +import org.linphone.activities.call.IncomingCallActivity +import org.linphone.activities.call.OutgoingCallActivity +import org.linphone.activities.main.MainActivity +import org.linphone.compatibility.Compatibility +import org.linphone.contact.Contact +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.ImageUtils +import org.linphone.utils.LinphoneUtils + +private class Notifiable(val notificationId: Int) { + val messages: ArrayList = arrayListOf() + + var isGroup: Boolean = false + var groupTitle: String? = null + var localIdentity: String? = null + var myself: String? = null +} + +private class NotifiableMessage( + var message: String, + val contact: Contact?, + val sender: String, + val time: Long, + val senderAvatar: Bitmap? = null, + var filePath: Uri? = null, + var fileMime: String? = null +) + +class NotificationsManager(private val context: Context) { + companion object { + const val CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP" + const val KEY_TEXT_REPLY = "key_text_reply" + const val INTENT_NOTIF_ID = "NOTIFICATION_ID" + const val INTENT_REPLY_NOTIF_ACTION = "org.linphone.REPLY_ACTION" + const val INTENT_HANGUP_CALL_NOTIF_ACTION = "org.linphone.HANGUP_CALL_ACTION" + const val INTENT_ANSWER_CALL_NOTIF_ACTION = "org.linphone.ANSWER_CALL_ACTION" + const val INTENT_LOCAL_IDENTITY = "LOCAL_IDENTITY" + const val INTENT_MARK_AS_READ_ACTION = "org.linphone.MARK_AS_READ_ACTION" + + private const val SERVICE_NOTIF_ID = 1 + private const val MISSED_CALLS_NOTIF_ID = 2 + } + + var currentlyDisplayedChatRoomAddress: String? = null + set(value) { + field = value + if (value != null) { + // When a chat room becomes visible, cancel unread chat notification if any + val notifiable: Notifiable? = chatNotificationsMap[value] + if (notifiable != null) { + cancel(notifiable.notificationId) + notifiable.messages.clear() + } + } + } + + private val notificationManager: NotificationManagerCompat by lazy { + NotificationManagerCompat.from(context) + } + private val chatNotificationsMap: HashMap = HashMap() + private val callNotificationsMap: HashMap = HashMap() + + private var lastNotificationId: Int = 5 + private var currentForegroundServiceNotificationId: Int = 0 + private var serviceNotification: Notification? = null + var service: CoreService? = null + + private val listener: CoreListenerStub = object : CoreListenerStub() { + override fun onCallStateChanged( + core: Core?, + call: Call?, + state: Call.State?, + message: String? + ) { + if (call == null) return + Log.i("[Notifications Manager] Call state changed [$state]") + + when (state) { + Call.State.IncomingEarlyMedia, Call.State.IncomingReceived -> displayIncomingCallNotification(call) + Call.State.End, Call.State.Error -> dismissCallNotification(call) + Call.State.Released -> { + if (call.callLog?.status == Call.Status.Missed) { + displayMissedCallNotification(call) + } + } + else -> displayCallNotification(call) + } + } + + override fun onMessageReceived(core: Core?, room: ChatRoom?, message: ChatMessage?) { + if (message == null || room == null || message.isOutgoing) return + + if (currentlyDisplayedChatRoomAddress == room.peerAddress?.asStringUriOnly()) { + Log.i("[Notifications Manager] Chat room is currently displayed, do not notify received message") + return + } + + if (message.errorInfo?.reason == Reason.UnsupportedContent) { + Log.w("[Notifications Manager] Received message with unsupported content, do not notify") + return + } + + if (!message.hasTextContent() && message.fileTransferInformation == null) { + Log.w("[Notifications Manager] Received message with neither text or attachment, do not notify") + return + } + + displayIncomingChatNotification(room, message) + } + } + + val chatListener: ChatMessageListener = object : ChatMessageListenerStub() { + override fun onMsgStateChanged(message: ChatMessage?, state: ChatMessage.State?) { + if (message == null || message.userData == null) return + val id = message.userData as Int + Log.i("[Notifications Manager] Reply message state changed [$state] for id $id") + + if (state != ChatMessage.State.InProgress) { + // No need to be called here twice + message.removeListener(this) + } + + if (state == ChatMessage.State.Delivered || state == ChatMessage.State.Displayed) { + val address = message.chatRoom.peerAddress.asStringUriOnly() + val notifiable = chatNotificationsMap[address] + if (notifiable != null) { + if (notifiable.notificationId != id) { + Log.w("[Notifications Manager] ID doesn't match: ${notifiable.notificationId} != $id") + } + displayReplyMessageNotification(message, notifiable) + } else { + Log.e("[Notifications Manager] Couldn't find notification for chat room $address") + cancel(id) + } + } else if (state == ChatMessage.State.NotDelivered) { + Log.e("[Notifications Manager] Reply wasn't delivered") + cancel(id) + } + } + } + + init { + notificationManager.cancelAll() + + Compatibility.createNotificationChannels(context, notificationManager) + } + + fun onCoreReady() { + coreContext.core.addListener(listener) + } + + fun destroy() { + // Don't use cancelAll to keep message notifications ! + // When a message is received by a push, it will create a CoreService + // but it might be getting killed quite quickly after that + // causing the notification to be missed by the user... + Log.i("[Notifications Manager] Getting destroyed, clearing foreground Service & call notifications") + + if (currentForegroundServiceNotificationId > 0) { + notificationManager.cancel(currentForegroundServiceNotificationId) + } + + for (notifiable in callNotificationsMap.values) { + notificationManager.cancel(notifiable.notificationId) + } + + coreContext.core.removeListener(listener) + } + + private fun notify(id: Int, notification: Notification) { + Log.i("[Notifications Manager] Notifying $id") + notificationManager.notify(id, notification) + } + + fun cancel(id: Int) { + Log.i("[Notifications Manager] Canceling $id") + notificationManager.cancel(id) + } + + fun getSipUriForChatNotificationId(notificationId: Int): String? { + for (address in chatNotificationsMap.keys) { + if (chatNotificationsMap[address]?.notificationId == notificationId) { + return address + } + } + return null + } + + fun getSipUriForCallNotificationId(notificationId: Int): String? { + for (address in callNotificationsMap.keys) { + if (callNotificationsMap[address]?.notificationId == notificationId) { + return address + } + } + return null + } + + /* Service related */ + + fun startForeground() { + val coreService = service + if (coreService != null) { + startForeground(coreService, useAutoStartDescription = false) + } else { + Log.e("[Notifications Manager] Can't start service as foreground, no service!") + } + } + + fun startCallForeground(coreService: CoreService) { + service = coreService + when { + currentForegroundServiceNotificationId != 0 -> { + Log.e("[Notifications Manager] There is already a foreground service notification") + } + coreContext.core.callsNb > 0 -> { + // When this method will be called, we won't have any notification yet + val call = coreContext.core.currentCall ?: coreContext.core.calls[0] + when (call.state) { + Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> { + displayIncomingCallNotification(call, true) + } + else -> displayCallNotification(call, true) + } + } + } + } + + fun startForeground(coreService: CoreService, useAutoStartDescription: Boolean = true) { + Log.i("[Notifications Manager] Starting Service as foreground") + if (serviceNotification == null) { + createServiceNotification(useAutoStartDescription) + } + currentForegroundServiceNotificationId = SERVICE_NOTIF_ID + coreService.startForeground(currentForegroundServiceNotificationId, serviceNotification) + service = coreService + } + + private fun startForeground(notificationId: Int, callNotification: Notification) { + if (currentForegroundServiceNotificationId == 0 && service != null) { + Log.i("[Notifications Manager] Starting Service as foreground using call notification") + currentForegroundServiceNotificationId = notificationId + service?.startForeground(currentForegroundServiceNotificationId, callNotification) + } + } + + private fun stopForegroundNotification() { + if (service != null) { + Log.i("[Notifications Manager] Stopping Service as foreground") + service?.stopForeground(true) + currentForegroundServiceNotificationId = 0 + } + } + + fun stopForegroundNotificationIfPossible() { + if (service != null && currentForegroundServiceNotificationId == SERVICE_NOTIF_ID && !corePreferences.keepServiceAlive) { + stopForegroundNotification() + } + } + + fun stopCallForeground() { + if (service != null && currentForegroundServiceNotificationId != SERVICE_NOTIF_ID && !corePreferences.keepServiceAlive) { + stopForegroundNotification() + } + } + + private fun createServiceNotification(useAutoStartDescription: Boolean = false) { + val pendingIntent = NavDeepLinkBuilder(context) + .setComponentName(MainActivity::class.java) + .setGraph(R.navigation.main_nav_graph) + .setDestination(R.id.dialerFragment) + .createPendingIntent() + + serviceNotification = NotificationCompat.Builder(context, context.getString(R.string.notification_channel_service_id)) + .setContentTitle(context.getString(R.string.service_name)) + .setContentText(if (useAutoStartDescription) context.getString(R.string.service_auto_start_description) else context.getString(R.string.service_description)) + .setSmallIcon(R.drawable.topbar_service_notification) + .setContentIntent(pendingIntent) + .setCategory(Notification.CATEGORY_SERVICE) + .setVisibility(NotificationCompat.VISIBILITY_SECRET) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setOngoing(true) + .setColor(ContextCompat.getColor(context, R.color.primary_color)) + .build() + } + + /* Call related */ + + private fun getNotifiableForCall(call: Call): Notifiable { + val address = call.remoteAddress.asStringUriOnly() + var notifiable: Notifiable? = callNotificationsMap[address] + if (notifiable == null) { + notifiable = Notifiable(lastNotificationId) + lastNotificationId += 1 + callNotificationsMap[address] = notifiable + } + return notifiable + } + + private fun displayIncomingCallNotification(call: Call, useAsForeground: Boolean = false) { + val address = call.remoteAddress.asStringUriOnly() + val notifiable = getNotifiableForCall(call) + + val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress) + val pictureUri = contact?.getContactThumbnailPictureUri() + val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri) + val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress) + + val incomingCallNotificationIntent = Intent(context, IncomingCallActivity::class.java) + incomingCallNotificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val pendingIntent = PendingIntent.getActivity(context, 0, incomingCallNotificationIntent, PendingIntent.FLAG_UPDATE_CURRENT) + + val notificationLayoutHeadsUp = RemoteViews(context.packageName, R.layout.call_incoming_notification_heads_up) + notificationLayoutHeadsUp.setTextViewText(R.id.caller, displayName) + notificationLayoutHeadsUp.setTextViewText(R.id.sip_uri, address) + notificationLayoutHeadsUp.setTextViewText(R.id.incoming_call_info, context.getString(R.string.incoming_call_notification_title)) + + if (roundPicture != null) { + notificationLayoutHeadsUp.setImageViewBitmap(R.id.caller_picture, roundPicture) + } + + val notification = NotificationCompat.Builder(context, context.getString(R.string.notification_channel_incoming_call_id)) + .setStyle(NotificationCompat.DecoratedCustomViewStyle()) + .setSmallIcon(R.drawable.topbar_call_notification) + .setContentTitle(displayName) + .setContentText(context.getString(R.string.incoming_call_notification_title)) + .setContentIntent(pendingIntent) + .setCategory(NotificationCompat.CATEGORY_CALL) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setWhen(System.currentTimeMillis()) + .setAutoCancel(false) + .setShowWhen(true) + .setOngoing(true) + .setColor(ContextCompat.getColor(context, R.color.primary_color)) + .setFullScreenIntent(pendingIntent, true) + .addAction(getCallDeclineAction(notifiable.notificationId)) + .addAction(getCallAnswerAction(notifiable.notificationId)) + .setCustomHeadsUpContentView(notificationLayoutHeadsUp) + .build() + notify(notifiable.notificationId, notification) + + if (useAsForeground) { + startForeground(notifiable.notificationId, notification) + } + } + + fun displayMissedCallNotification(call: Call) { + val missedCallCount: Int = call.core.missedCallsCount + val body: String + if (missedCallCount > 1) { + body = context.getString(R.string.missed_calls_notification_body) + .format(missedCallCount) + Log.i("[Notifications Manager] Updating missed calls notification count to $missedCallCount") + } else { + val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress) + body = context.getString(R.string.missed_call_notification_body) + .format(contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)) + Log.i("[Notifications Manager] Creating missed call notification") + } + + val pendingIntent = NavDeepLinkBuilder(context) + .setComponentName(MainActivity::class.java) + .setGraph(R.navigation.main_nav_graph) + .setDestination(R.id.masterCallLogsFragment) + .createPendingIntent() + + val notification = NotificationCompat.Builder( + context, context.getString(R.string.notification_channel_incoming_call_id)) + .setContentTitle(context.getString(R.string.missed_call_notification_title)) + .setContentText(body) + .setSmallIcon(R.drawable.call_status_missed) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .setCategory(Notification.CATEGORY_EVENT) + .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setNumber(missedCallCount) + .setColor(ContextCompat.getColor(context, R.color.notification_led_color)) + .build() + notify(MISSED_CALLS_NOTIF_ID, notification) + } + + fun dismissMissedCallNotification() { + cancel(MISSED_CALLS_NOTIF_ID) + } + + fun displayCallNotification(call: Call, useAsForeground: Boolean = false) { + val notifiable = getNotifiableForCall(call) + + val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress) + val pictureUri = contact?.getContactThumbnailPictureUri() + val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri) + val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress) + + val stringResourceId: Int + val iconResourceId: Int + val callActivity: Class<*> + when (call.state) { + Call.State.Paused, Call.State.Pausing, Call.State.PausedByRemote -> { + callActivity = CallActivity::class.java + stringResourceId = R.string.call_notification_paused + iconResourceId = R.drawable.topbar_call_notification + } + Call.State.OutgoingRinging, Call.State.OutgoingProgress, Call.State.OutgoingInit, Call.State.OutgoingEarlyMedia -> { + callActivity = OutgoingCallActivity::class.java + stringResourceId = R.string.call_notification_outgoing + iconResourceId = R.drawable.topbar_call_notification + } + else -> { + callActivity = CallActivity::class.java + stringResourceId = R.string.call_notification_active + iconResourceId = if (call.currentParams.videoEnabled()) { + R.drawable.topbar_videocall_notification + } else { + R.drawable.topbar_call_notification + } + } + } + + val callNotificationIntent = Intent(context, callActivity) + callNotificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val pendingIntent = PendingIntent.getActivity(context, 0, callNotificationIntent, PendingIntent.FLAG_UPDATE_CURRENT) + + val notification = NotificationCompat.Builder( + context, context.getString(R.string.notification_channel_service_id)) + .setContentTitle(contact?.fullName ?: displayName) + .setContentText(context.getString(stringResourceId)) + .setSmallIcon(iconResourceId) + .setLargeIcon(roundPicture) + .setAutoCancel(false) + .setContentIntent(pendingIntent) + .setCategory(Notification.CATEGORY_CALL) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setOngoing(true) + .setColor(ContextCompat.getColor(context, R.color.notification_led_color)) + .addAction(getCallDeclineAction(notifiable.notificationId)) + .build() + notify(notifiable.notificationId, notification) + + if (useAsForeground) { + startForeground(notifiable.notificationId, notification) + } + } + + private fun dismissCallNotification(call: Call) { + val address = call.remoteAddress?.asStringUriOnly() + val notifiable: Notifiable? = callNotificationsMap[address] + if (notifiable != null) { + cancel(notifiable.notificationId) + callNotificationsMap.remove(address) + } + } + + /* Chat related */ + + private fun displayChatNotifiable(room: ChatRoom, notifiable: Notifiable) { + val args = Bundle() + args.putString("RemoteSipUri", room.peerAddress.asStringUriOnly()) + args.putString("LocalSipUri", room.localAddress.asStringUriOnly()) + + val pendingIntent = NavDeepLinkBuilder(context) + .setComponentName(MainActivity::class.java) + .setGraph(R.navigation.main_nav_graph) + .setDestination(R.id.masterChatRoomsFragment) + .setArguments(args) + .createPendingIntent() + + val notification = createMessageNotification(notifiable, pendingIntent) + if (notification != null) notify(notifiable.notificationId, notification) + } + + private fun displayIncomingChatNotification(room: ChatRoom, message: ChatMessage) { + val contact: Contact? = coreContext.contactsManager.findContactByAddress(message.fromAddress) + val pictureUri = contact?.getContactThumbnailPictureUri() + val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri) + val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(message.fromAddress) + + val notifiable = getNotifiableForRoom(room) + var text = "" + if (message.hasTextContent()) text = message.textContent + else { + for (content in message.contents) { + text = content.name + } + } + val notifiableMessage = NotifiableMessage(text, contact, displayName, message.time, senderAvatar = roundPicture) + notifiable.messages.add(notifiableMessage) + + if (room.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { + notifiable.isGroup = false + } else { + notifiable.isGroup = true + notifiable.groupTitle = room.subject + } + + notifiable.myself = LinphoneUtils.getDisplayName(room.localAddress) + notifiable.localIdentity = room.localAddress.asStringUriOnly() + + displayChatNotifiable(room, notifiable) + } + + private fun getNotifiableForRoom(room: ChatRoom): Notifiable { + val address = room.peerAddress.asStringUriOnly() + var notifiable: Notifiable? = chatNotificationsMap[address] + if (notifiable == null) { + notifiable = Notifiable(lastNotificationId) + lastNotificationId += 1 + chatNotificationsMap[address] = notifiable + } + return notifiable + } + + private fun displayReplyMessageNotification(message: ChatMessage, notifiable: Notifiable) { + Log.i("[Notifications Manager] Updating message notification with reply for notification ${notifiable.notificationId}") + + val reply = NotifiableMessage( + message.textContent, + null, + notifiable.myself ?: LinphoneUtils.getDisplayName(message.fromAddress), + System.currentTimeMillis() + ) + notifiable.messages.add(reply) + + displayChatNotifiable(message.chatRoom, notifiable) + } + + /* Notifications */ + + private fun createMessageNotification(notifiable: Notifiable, pendingIntent: PendingIntent): Notification? { + val me = Person.Builder().setName(notifiable.myself).build() + val style = NotificationCompat.MessagingStyle(me) + val largeIcon: Bitmap? = notifiable.messages.last().senderAvatar + + for (message in notifiable.messages) { + val contact = message.contact + val person = if (contact != null) { + contact.getPerson() + } else { + val builder = Person.Builder().setName(message.sender) + val userIcon = if (message.senderAvatar != null) IconCompat.createWithBitmap(message.senderAvatar) else IconCompat.createWithResource(context, R.drawable.avatar) + if (userIcon != null) builder.setIcon(userIcon) + builder.build() + } + + val msg = NotificationCompat.MessagingStyle.Message(message.message, message.time, person) + if (message.filePath != null) msg.setData(message.fileMime, message.filePath) + style.addMessage(msg) + } + + if (notifiable.isGroup) { + style.conversationTitle = notifiable.groupTitle + } + style.isGroupConversation = notifiable.isGroup + + return NotificationCompat.Builder(context, context.getString(R.string.notification_channel_chat_id)) + .setSmallIcon(R.drawable.topbar_chat_notification) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .setLargeIcon(largeIcon) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .setGroup(CHAT_NOTIFICATIONS_GROUP) + .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) + .setNumber(notifiable.messages.size) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setStyle(style) + .setColor(ContextCompat.getColor(context, R.color.primary_color)) + .addAction(getReplyMessageAction(notifiable)) + .addAction(getMarkMessageAsReadAction(notifiable)) + .build() + } + + /* Notifications actions */ + + private fun getCallAnswerAction(callId: Int): NotificationCompat.Action { + val answerIntent = Intent(context, NotificationBroadcastReceiver::class.java) + answerIntent.action = INTENT_ANSWER_CALL_NOTIF_ACTION + answerIntent.putExtra(INTENT_NOTIF_ID, callId) + + val answerPendingIntent = PendingIntent.getBroadcast( + context, callId, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT + ) + + return NotificationCompat.Action.Builder( + R.drawable.call_audio_start, + context.getString(R.string.incoming_call_notification_answer_action_label), + answerPendingIntent + ).build() + } + + private fun getCallDeclineAction(callId: Int): NotificationCompat.Action { + val hangupIntent = Intent(context, NotificationBroadcastReceiver::class.java) + hangupIntent.action = INTENT_HANGUP_CALL_NOTIF_ACTION + hangupIntent.putExtra(INTENT_NOTIF_ID, callId) + + val hangupPendingIntent = PendingIntent.getBroadcast( + context, callId, hangupIntent, PendingIntent.FLAG_UPDATE_CURRENT + ) + + return NotificationCompat.Action.Builder( + R.drawable.call_hangup, + context.getString(R.string.incoming_call_notification_hangup_action_label), + hangupPendingIntent + ).build() + } + + private fun getReplyMessageAction(notifiable: Notifiable): NotificationCompat.Action { + val replyLabel = + context.resources.getString(R.string.received_chat_notification_reply_label) + val remoteInput = + RemoteInput.Builder(KEY_TEXT_REPLY).setLabel(replyLabel).build() + + val replyIntent = Intent(context, NotificationBroadcastReceiver::class.java) + replyIntent.action = INTENT_REPLY_NOTIF_ACTION + replyIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) + replyIntent.putExtra(INTENT_LOCAL_IDENTITY, notifiable.localIdentity) + + val replyPendingIntent = PendingIntent.getBroadcast( + context, + notifiable.notificationId, + replyIntent, + PendingIntent.FLAG_UPDATE_CURRENT + ) + return NotificationCompat.Action.Builder( + R.drawable.chat_send_over, + context.getString(R.string.received_chat_notification_reply_label), + replyPendingIntent + ) + .addRemoteInput(remoteInput) + .setAllowGeneratedReplies(true) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) + .build() + } + + private fun getMarkMessageAsReadAction(notifiable: Notifiable): NotificationCompat.Action { + val markAsReadIntent = Intent(context, NotificationBroadcastReceiver::class.java) + markAsReadIntent.action = INTENT_MARK_AS_READ_ACTION + markAsReadIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) + markAsReadIntent.putExtra(INTENT_LOCAL_IDENTITY, notifiable.localIdentity) + + val markAsReadPendingIntent = PendingIntent.getBroadcast( + context, + notifiable.notificationId, + markAsReadIntent, + PendingIntent.FLAG_UPDATE_CURRENT + ) + return NotificationCompat.Action.Builder( + R.drawable.chat_send_over, + context.getString(R.string.received_chat_notification_mark_as_read_label), + markAsReadPendingIntent + ) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) + .build() + } +} diff --git a/app/src/main/java/org/linphone/receivers/AccountEnableReceiver.java b/app/src/main/java/org/linphone/receivers/AccountEnableReceiver.java deleted file mode 100644 index 987b93892..000000000 --- a/app/src/main/java/org/linphone/receivers/AccountEnableReceiver.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.receivers; - -/* -See Linphone (Tasker Plugin) -https://github.com/GrahamJB1/linphone-plugin -*/ - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; -import org.linphone.settings.LinphonePreferences; - -public class AccountEnableReceiver extends BroadcastReceiver { - private static final String TAG = "AccountEnableReceiver"; - private static final String FIELD_ID = "id"; - private static final String FIELD_ACTIVE = "active"; - - @Override - public void onReceive(Context context, Intent intent) { - int prefsAccountIndex = (int) (long) intent.getLongExtra(FIELD_ID, -1); - boolean enable = intent.getBooleanExtra(FIELD_ACTIVE, true); - Log.i(TAG, "Received broadcast for index=" + prefsAccountIndex + ",enable=" + enable); - if (prefsAccountIndex < 0 - || prefsAccountIndex >= LinphonePreferences.instance().getAccountCount()) return; - LinphonePreferences.instance().setAccountEnabled(prefsAccountIndex, enable); - } -} diff --git a/app/src/main/java/org/linphone/receivers/BootReceiver.java b/app/src/main/java/org/linphone/receivers/BootReceiver.java deleted file mode 100644 index a990d871b..000000000 --- a/app/src/main/java/org/linphone/receivers/BootReceiver.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.receivers; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import org.linphone.compatibility.Compatibility; -import org.linphone.service.LinphoneService; -import org.linphone.settings.LinphonePreferences; - -public class BootReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equalsIgnoreCase(Intent.ACTION_SHUTDOWN)) { - android.util.Log.d( - "Linphone", - "[Boot Receiver] Device is shutting down, destroying Core to unregister"); - context.stopService( - new Intent(Intent.ACTION_MAIN).setClass(context, LinphoneService.class)); - } else if (intent.getAction().equalsIgnoreCase(Intent.ACTION_BOOT_COMPLETED)) { - LinphonePreferences.instance().setContext(context); - boolean autostart = LinphonePreferences.instance().isAutoStartEnabled(); - android.util.Log.i( - "Linphone", "[Boot Receiver] Device is starting, auto_start is " + autostart); - - if (autostart && !LinphoneService.isReady()) { - startService(context); - } - } else if (intent.getAction().equalsIgnoreCase(Intent.ACTION_MY_PACKAGE_REPLACED)) { - LinphonePreferences.instance().setContext(context); - boolean foregroundService = - LinphonePreferences.instance().getServiceNotificationVisibility(); - android.util.Log.i( - "Linphone", - "[Boot Receiver] App has been updated, foreground service is " - + foregroundService); - - if (foregroundService && !LinphoneService.isReady()) { - startService(context); - } - } - } - - private void startService(Context context) { - Intent serviceIntent = new Intent(Intent.ACTION_MAIN); - serviceIntent.setClass(context, LinphoneService.class); - serviceIntent.putExtra("ForceStartForeground", true); - Compatibility.startService(context, serviceIntent); - } -} diff --git a/app/src/main/java/org/linphone/recording/Recording.java b/app/src/main/java/org/linphone/recording/Recording.java deleted file mode 100644 index 993e082ed..000000000 --- a/app/src/main/java/org/linphone/recording/Recording.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.recording; - -import android.annotation.SuppressLint; -import android.content.Context; -import androidx.annotation.NonNull; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.linphone.LinphoneManager; -import org.linphone.core.Player; -import org.linphone.core.PlayerListener; -import org.linphone.core.tools.Log; -import org.linphone.utils.LinphoneUtils; - -class Recording implements PlayerListener, Comparable { - public static final Pattern RECORD_PATTERN = - Pattern.compile(".*/(.*)_(\\d{2}-\\d{2}-\\d{4}-\\d{2}-\\d{2}-\\d{2})\\..*"); - - private final String mRecordPath; - private String mName; - private Date mRecordDate; - private final Player mPlayer; - private RecordingListener mListener; - private Runnable mUpdateCurrentPositionTimer; - - @SuppressLint("SimpleDateFormat") - public Recording(Context context, String recordPath) { - this.mRecordPath = recordPath; - - Matcher m = RECORD_PATTERN.matcher(recordPath); - if (m.matches()) { - mName = m.group(1); - - try { - mRecordDate = new SimpleDateFormat("dd-MM-yyyy-HH-mm-ss").parse(m.group(2)); - } catch (ParseException e) { - Log.e(e); - } - } - - mUpdateCurrentPositionTimer = - new Runnable() { - @Override - public void run() { - if (mListener != null) - mListener.currentPositionChanged(getCurrentPosition()); - if (isPlaying()) - LinphoneUtils.dispatchOnUIThreadAfter(mUpdateCurrentPositionTimer, 20); - } - }; - - mPlayer = LinphoneManager.getCore().createLocalPlayer(null, null, null); - mPlayer.addListener(this); - } - - public String getRecordPath() { - return mRecordPath; - } - - public String getName() { - return mName; - } - - public Date getRecordDate() { - return mRecordDate; - } - - public boolean isClosed() { - return mPlayer.getState() == Player.State.Closed; - } - - public void play() { - if (isClosed()) { - mPlayer.open(mRecordPath); - } - - mPlayer.start(); - LinphoneUtils.dispatchOnUIThread(mUpdateCurrentPositionTimer); - } - - public boolean isPlaying() { - return mPlayer.getState() == Player.State.Playing; - } - - public void pause() { - if (!isClosed()) { - mPlayer.pause(); - } - } - - public boolean isPaused() { - return mPlayer.getState() == Player.State.Paused; - } - - public void seek(int i) { - if (!isClosed()) mPlayer.seek(i); - } - - public int getCurrentPosition() { - if (isClosed()) { - mPlayer.open(mRecordPath); - } - - return mPlayer.getCurrentPosition(); - } - - public int getDuration() { - if (isClosed()) { - mPlayer.open(mRecordPath); - } - - return mPlayer.getDuration(); - } - - public void close() { - mPlayer.removeListener(this); - mPlayer.close(); - } - - public void setRecordingListener(RecordingListener listener) { - this.mListener = listener; - } - - @Override - public void onEofReached(Player player) { - if (mListener != null) mListener.endOfRecordReached(); - } - - @Override - public boolean equals(Object o) { - if (o instanceof Recording) { - Recording r = (Recording) o; - return mRecordPath.equals(r.getRecordPath()); - } - return false; - } - - @Override - public int compareTo(@NonNull Recording o) { - return -mRecordDate.compareTo(o.getRecordDate()); - } -} diff --git a/app/src/main/java/org/linphone/recording/RecordingListener.java b/app/src/main/java/org/linphone/recording/RecordingListener.java deleted file mode 100644 index b9ac62a74..000000000 --- a/app/src/main/java/org/linphone/recording/RecordingListener.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.recording; - -interface RecordingListener { - void currentPositionChanged(int currentPosition); - - void endOfRecordReached(); -} diff --git a/app/src/main/java/org/linphone/recording/RecordingViewHolder.java b/app/src/main/java/org/linphone/recording/RecordingViewHolder.java deleted file mode 100644 index 2a909c7b8..000000000 --- a/app/src/main/java/org/linphone/recording/RecordingViewHolder.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.recording; - -import android.view.View; -import android.widget.CheckBox; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.SeekBar; -import android.widget.TextView; -import androidx.recyclerview.widget.RecyclerView; -import org.linphone.R; - -public class RecordingViewHolder extends RecyclerView.ViewHolder - implements View.OnClickListener, View.OnLongClickListener { - public final ImageView playButton; - public final TextView name; - public final TextView date; - public final TextView currentPosition; - public final TextView duration; - public final SeekBar progressionBar; - public final CheckBox select; - public final LinearLayout separator; - public final TextView separatorText; - - private final RecordingViewHolder.ClickListener mListener; - - public RecordingViewHolder(View view, RecordingViewHolder.ClickListener listener) { - super(view); - - playButton = view.findViewById(R.id.record_play); - name = view.findViewById(R.id.record_name); - date = view.findViewById(R.id.record_date); - currentPosition = view.findViewById(R.id.record_current_time); - duration = view.findViewById(R.id.record_duration); - progressionBar = view.findViewById(R.id.record_progression_bar); - select = view.findViewById(R.id.delete); - separator = view.findViewById(R.id.separator); - separatorText = view.findViewById(R.id.separator_text); - - mListener = listener; - view.setOnClickListener(this); - view.setOnLongClickListener(this); - } - - @Override - public void onClick(View view) { - if (mListener != null) { - mListener.onItemClicked(getAdapterPosition()); - } - } - - @Override - public boolean onLongClick(View view) { - if (mListener != null) { - return mListener.onItemLongClicked(getAdapterPosition()); - } - return false; - } - - public interface ClickListener { - void onItemClicked(int position); - - boolean onItemLongClicked(int position); - } -} diff --git a/app/src/main/java/org/linphone/recording/RecordingsActivity.java b/app/src/main/java/org/linphone/recording/RecordingsActivity.java deleted file mode 100644 index 00f4b0e13..000000000 --- a/app/src/main/java/org/linphone/recording/RecordingsActivity.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.recording; - -import android.Manifest; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.activities.MainActivity; -import org.linphone.call.views.LinphoneLinearLayoutManager; -import org.linphone.utils.FileUtils; -import org.linphone.utils.SelectableHelper; - -public class RecordingsActivity extends MainActivity - implements SelectableHelper.DeleteListener, RecordingViewHolder.ClickListener { - private RecyclerView mRecordingList; - private List mRecordings; - private TextView mNoRecordings; - private RecordingsAdapter mRecordingsAdapter; - private SelectableHelper mSelectableHelper; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mOnBackPressGoHome = false; - mAlwaysHideTabBar = true; - - // Uses the fragment container layout to inflate the about view instead of using a fragment - View recordingsView = - LayoutInflater.from(this).inflate(R.layout.recordings_list, null, false); - LinearLayout fragmentContainer = findViewById(R.id.fragmentContainer); - LinearLayout.LayoutParams params = - new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - fragmentContainer.addView(recordingsView, params); - - if (isTablet()) { - findViewById(R.id.fragmentContainer2).setVisibility(View.GONE); - } - - ImageView backButton = findViewById(R.id.back); - backButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - goBack(); - } - }); - - mSelectableHelper = new SelectableHelper(findViewById(R.id.root_layout), this); - - mRecordingList = findViewById(R.id.recording_list); - mNoRecordings = findViewById(R.id.no_recordings); - - LinearLayoutManager mLayoutManager = new LinphoneLinearLayoutManager(this); - mRecordingList.setLayoutManager(mLayoutManager); - - // Divider between items - DividerItemDecoration dividerItemDecoration = - new DividerItemDecoration(this, mLayoutManager.getOrientation()); - dividerItemDecoration.setDrawable(getResources().getDrawable(R.drawable.divider)); - mRecordingList.addItemDecoration(dividerItemDecoration); - - mRecordings = new ArrayList<>(); - - mPermissionsToHave = - new String[] { - Manifest.permission.READ_EXTERNAL_STORAGE, - }; - } - - @Override - protected void onResume() { - super.onResume(); - - hideTopBar(); - hideTabBar(); - - LinphoneManager.getAudioManager().routeAudioToSpeaker(); - - removeDeletedRecordings(); - searchForRecordings(); - - hideRecordingListAndDisplayMessageIfEmpty(); - mRecordingsAdapter = new RecordingsAdapter(this, mRecordings, this, mSelectableHelper); - mRecordingList.setAdapter(mRecordingsAdapter); - mSelectableHelper.setAdapter(mRecordingsAdapter); - mSelectableHelper.setDialogMessage(R.string.recordings_delete_dialog); - } - - @Override - protected void onPause() { - super.onPause(); - - LinphoneManager.getAudioManager().routeAudioToEarPiece(); - - // Close all opened mRecordings - for (Recording r : mRecordings) { - if (!r.isClosed()) { - if (r.isPlaying()) r.pause(); - r.close(); - } - } - } - - @Override - protected void onDestroy() { - mRecordingList = null; - mRecordings = null; - mNoRecordings = null; - mRecordingsAdapter = null; - mSelectableHelper = null; - - super.onDestroy(); - } - - @Override - public void onDeleteSelection(Object[] objectsToDelete) { - int size = mRecordingsAdapter.getSelectedItemCount(); - for (int i = 0; i < size; i++) { - Recording record = (Recording) objectsToDelete[i]; - - if (record.isPlaying()) record.pause(); - record.close(); - - File recordingFile = new File(record.getRecordPath()); - if (recordingFile.delete()) { - mRecordings.remove(record); - } - } - hideRecordingListAndDisplayMessageIfEmpty(); - } - - @Override - public void onItemClicked(int position) { - if (mRecordingsAdapter.isEditionEnabled()) { - mRecordingsAdapter.toggleSelection(position); - } - } - - @Override - public boolean onItemLongClicked(int position) { - if (!mRecordingsAdapter.isEditionEnabled()) { - mSelectableHelper.enterEditionMode(); - } - mRecordingsAdapter.toggleSelection(position); - return true; - } - - private void hideRecordingListAndDisplayMessageIfEmpty() { - if (mRecordings == null || mRecordings.isEmpty()) { - mNoRecordings.setVisibility(View.VISIBLE); - mRecordingList.setVisibility(View.GONE); - } else { - mNoRecordings.setVisibility(View.GONE); - mRecordingList.setVisibility(View.VISIBLE); - } - } - - private void removeDeletedRecordings() { - String recordingsDirectory = FileUtils.getRecordingsDirectory(this); - File directory = new File(recordingsDirectory); - - if (directory.exists() && directory.isDirectory()) { - File[] existingRecordings = directory.listFiles(); - - for (Recording r : mRecordings) { - boolean exists = false; - for (File f : existingRecordings) { - if (f.getPath().equals(r.getRecordPath())) { - exists = true; - break; - } - } - - if (!exists) mRecordings.remove(r); - } - - Collections.sort(mRecordings); - } - hideRecordingListAndDisplayMessageIfEmpty(); - } - - private void searchForRecordings() { - String recordingsDirectory = FileUtils.getRecordingsDirectory(this); - File directory = new File(recordingsDirectory); - - if (directory.exists() && directory.isDirectory()) { - File[] existingRecordings = directory.listFiles(); - if (existingRecordings == null) return; - - for (File f : existingRecordings) { - boolean exists = false; - for (Recording r : mRecordings) { - if (r.getRecordPath().equals(f.getPath())) { - exists = true; - break; - } - } - - if (!exists) { - if (Recording.RECORD_PATTERN.matcher(f.getPath()).matches()) { - mRecordings.add(new Recording(this, f.getPath())); - } - } - } - - Collections.sort(mRecordings); - } - } -} diff --git a/app/src/main/java/org/linphone/recording/RecordingsAdapter.java b/app/src/main/java/org/linphone/recording/RecordingsAdapter.java deleted file mode 100644 index f9c20aefe..000000000 --- a/app/src/main/java/org/linphone/recording/RecordingsAdapter.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.recording; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.SeekBar; -import androidx.annotation.NonNull; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.TimeUnit; -import org.linphone.R; -import org.linphone.utils.SelectableAdapter; -import org.linphone.utils.SelectableHelper; - -public class RecordingsAdapter extends SelectableAdapter { - private final List mRecordings; - private final Context mContext; - private final RecordingViewHolder.ClickListener mClickListener; - - public RecordingsAdapter( - Context context, - List recordings, - RecordingViewHolder.ClickListener listener, - SelectableHelper helper) { - super(helper); - - mRecordings = recordings; - mContext = context; - mClickListener = listener; - } - - @Override - public Object getItem(int position) { - return mRecordings.get(position); - } - - @NonNull - @Override - public RecordingViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { - View v = - LayoutInflater.from(viewGroup.getContext()) - .inflate(R.layout.recording_cell, viewGroup, false); - return new RecordingViewHolder(v, mClickListener); - } - - @SuppressLint("SimpleDateFormat") - @Override - public void onBindViewHolder(@NonNull final RecordingViewHolder viewHolder, int i) { - final Recording record = mRecordings.get(i); - - viewHolder.name.setSelected(true); // For automated horizontal scrolling of long texts - - Calendar recordTime = Calendar.getInstance(); - recordTime.setTime(record.getRecordDate()); - viewHolder.separatorText.setText(DateToHumanDate(recordTime)); - viewHolder.select.setVisibility(isEditionEnabled() ? View.VISIBLE : View.GONE); - viewHolder.select.setChecked(isSelected(i)); - - if (i > 0) { - Recording previousRecord = mRecordings.get(i - 1); - Date previousRecordDate = previousRecord.getRecordDate(); - Calendar previousRecordTime = Calendar.getInstance(); - previousRecordTime.setTime(previousRecordDate); - - if (isSameDay(previousRecordTime, recordTime)) { - viewHolder.separator.setVisibility(View.GONE); - } else { - viewHolder.separator.setVisibility(View.VISIBLE); - } - } else { - viewHolder.separator.setVisibility(View.VISIBLE); - } - - if (record.isPlaying()) { - viewHolder.playButton.setImageResource(R.drawable.record_pause); - } else { - viewHolder.playButton.setImageResource(R.drawable.record_play); - } - viewHolder.playButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - if (record.isPaused()) { - record.play(); - viewHolder.playButton.setImageResource(R.drawable.record_pause); - } else { - record.pause(); - viewHolder.playButton.setImageResource(R.drawable.record_play); - } - } - }); - - viewHolder.name.setText(record.getName()); - viewHolder.date.setText(new SimpleDateFormat("HH:mm").format(record.getRecordDate())); - - int position = record.getCurrentPosition(); - viewHolder.currentPosition.setText( - String.format( - Locale.getDefault(), - "%02d:%02d", - TimeUnit.MILLISECONDS.toMinutes(position), - TimeUnit.MILLISECONDS.toSeconds(position) - - TimeUnit.MINUTES.toSeconds( - TimeUnit.MILLISECONDS.toMinutes(position)))); - - int duration = record.getDuration(); - viewHolder.duration.setText( - String.format( - Locale.getDefault(), - "%02d:%02d", - TimeUnit.MILLISECONDS.toMinutes(duration), - TimeUnit.MILLISECONDS.toSeconds(duration) - - TimeUnit.MINUTES.toSeconds( - TimeUnit.MILLISECONDS.toMinutes(duration)))); - - viewHolder.progressionBar.setMax(record.getDuration()); - viewHolder.progressionBar.setProgress(0); - viewHolder.progressionBar.setOnSeekBarChangeListener( - new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (fromUser) { - int progressToSet = - progress > 0 && progress < seekBar.getMax() ? progress : 0; - - if (progress == seekBar.getMax()) { - if (record.isPlaying()) record.pause(); - } - - record.seek(progressToSet); - seekBar.setProgress(progressToSet); - - int currentPosition = record.getCurrentPosition(); - viewHolder.currentPosition.setText( - String.format( - Locale.getDefault(), - "%02d:%02d", - TimeUnit.MILLISECONDS.toMinutes(currentPosition), - TimeUnit.MILLISECONDS.toSeconds(currentPosition) - - TimeUnit.MINUTES.toSeconds( - TimeUnit.MILLISECONDS.toMinutes( - currentPosition)))); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) {} - - @Override - public void onStopTrackingTouch(SeekBar seekBar) {} - }); - - record.setRecordingListener( - new RecordingListener() { - @Override - public void currentPositionChanged(int currentPosition) { - viewHolder.currentPosition.setText( - String.format( - Locale.getDefault(), - "%02d:%02d", - TimeUnit.MILLISECONDS.toMinutes(currentPosition), - TimeUnit.MILLISECONDS.toSeconds(currentPosition) - - TimeUnit.MINUTES.toSeconds( - TimeUnit.MILLISECONDS.toMinutes( - currentPosition)))); - viewHolder.progressionBar.setProgress(currentPosition); - } - - @Override - public void endOfRecordReached() { - record.pause(); - record.seek(0); - viewHolder.progressionBar.setProgress(0); - viewHolder.currentPosition.setText("00:00"); - viewHolder.playButton.setImageResource(R.drawable.record_play); - } - }); - } - - @Override - public int getItemCount() { - return mRecordings.size(); - } - - @SuppressLint("SimpleDateFormat") - private String DateToHumanDate(Calendar cal) { - SimpleDateFormat dateFormat; - if (isToday(cal)) { - return mContext.getString(R.string.today); - } else if (isYesterday(cal)) { - return mContext.getString(R.string.yesterday); - } else { - dateFormat = - new SimpleDateFormat( - mContext.getResources().getString(R.string.history_date_format)); - } - - return dateFormat.format(cal.getTime()); - } - - private boolean isSameDay(Calendar cal1, Calendar cal2) { - if (cal1 == null || cal2 == null) { - return false; - } - - return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) - && cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) - && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)); - } - - private boolean isToday(Calendar cal) { - return isSameDay(cal, Calendar.getInstance()); - } - - private boolean isYesterday(Calendar cal) { - Calendar yesterday = Calendar.getInstance(); - yesterday.roll(Calendar.DAY_OF_MONTH, -1); - return isSameDay(cal, yesterday); - } -} diff --git a/app/src/main/java/org/linphone/service/ActivityMonitor.java b/app/src/main/java/org/linphone/service/ActivityMonitor.java deleted file mode 100644 index a5eef5e21..000000000 --- a/app/src/main/java/org/linphone/service/ActivityMonitor.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.service; - -import android.app.Activity; -import android.app.Application; -import android.os.Bundle; -import java.util.ArrayList; -import org.linphone.LinphoneManager; -import org.linphone.core.tools.Log; -import org.linphone.utils.LinphoneUtils; - -/** - * Believe me or not, but knowing the application visibility state on Android is a nightmare. After - * two days of hard work I ended with the following class, that does the job more or less reliabily. - */ -public class ActivityMonitor implements Application.ActivityLifecycleCallbacks { - private final ArrayList activities = new ArrayList<>(); - private boolean mActive = false; - private int mRunningActivities = 0; - private InactivityChecker mLastChecker; - - @Override - public synchronized void onActivityCreated(Activity activity, Bundle savedInstanceState) { - Log.i("[Activity Monitor] Activity created:" + activity); - if (!activities.contains(activity)) activities.add(activity); - } - - @Override - public void onActivityStarted(Activity activity) { - Log.i("Activity started:" + activity); - } - - @Override - public synchronized void onActivityResumed(Activity activity) { - Log.i("[Activity Monitor] Activity resumed:" + activity); - if (activities.contains(activity)) { - mRunningActivities++; - Log.i("[Activity Monitor] runningActivities=" + mRunningActivities); - checkActivity(); - } - } - - @Override - public synchronized void onActivityPaused(Activity activity) { - Log.i("[Activity Monitor] Activity paused:" + activity); - if (activities.contains(activity)) { - mRunningActivities--; - Log.i("[Activity Monitor] runningActivities=" + mRunningActivities); - checkActivity(); - } - } - - @Override - public void onActivityStopped(Activity activity) { - Log.i("[Activity Monitor] Activity stopped:" + activity); - } - - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} - - @Override - public synchronized void onActivityDestroyed(Activity activity) { - Log.i("[Activity Monitor] Activity destroyed:" + activity); - activities.remove(activity); - } - - void startInactivityChecker() { - if (mLastChecker != null) mLastChecker.cancel(); - LinphoneUtils.dispatchOnUIThreadAfter((mLastChecker = new InactivityChecker()), 2000); - } - - void checkActivity() { - if (mRunningActivities == 0) { - if (mActive) startInactivityChecker(); - } else if (mRunningActivities > 0) { - if (!mActive) { - mActive = true; - onForegroundMode(); - } - if (mLastChecker != null) { - mLastChecker.cancel(); - mLastChecker = null; - } - } - } - - private void onBackgroundMode() { - Log.i("[Activity Monitor] App has entered background mode"); - if (LinphoneManager.getCore() != null) { - LinphoneManager.getCore().enterBackground(); - } - } - - private void onForegroundMode() { - Log.i("[Activity Monitor] App has left background mode"); - if (LinphoneManager.getCore() != null) { - LinphoneManager.getCore().enterForeground(); - } - } - - class InactivityChecker implements Runnable { - private boolean isCanceled; - - void cancel() { - isCanceled = true; - } - - @Override - public void run() { - if (LinphoneService.isReady()) { - synchronized (LinphoneService.instance()) { - if (!isCanceled) { - if (ActivityMonitor.this.mRunningActivities == 0 && mActive) { - mActive = false; - onBackgroundMode(); - } - } - } - } - } - } -} diff --git a/app/src/main/java/org/linphone/service/LinphoneService.java b/app/src/main/java/org/linphone/service/LinphoneService.java deleted file mode 100644 index 47ba8365f..000000000 --- a/app/src/main/java/org/linphone/service/LinphoneService.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.service; - -import android.app.Application; -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; -import android.view.WindowManager; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.call.views.LinphoneGL2JNIViewOverlay; -import org.linphone.call.views.LinphoneOverlay; -import org.linphone.call.views.LinphoneTextureViewOverlay; -import org.linphone.core.Call; -import org.linphone.core.Core; -import org.linphone.core.tools.Log; -import org.linphone.mediastream.Version; -import org.linphone.settings.LinphonePreferences; - -public final class LinphoneService extends Service { - private static LinphoneService sInstance; - - private LinphoneOverlay mOverlay; - private WindowManager mWindowManager; - private Application.ActivityLifecycleCallbacks mActivityCallbacks; - private boolean misLinphoneContextOwned; - - @SuppressWarnings("unchecked") - @Override - public void onCreate() { - super.onCreate(); - - setupActivityMonitor(); - - misLinphoneContextOwned = false; - if (!LinphoneContext.isReady()) { - new LinphoneContext(getApplicationContext()); - misLinphoneContextOwned = true; - } - Log.i("[Service] Created"); - - mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - super.onStartCommand(intent, flags, startId); - - boolean isPush = false; - if (intent != null && intent.getBooleanExtra("PushNotification", false)) { - Log.i("[Service] [Push Notification] LinphoneService started because of a push"); - isPush = true; - } - - if (sInstance != null) { - Log.w("[Service] Attempt to start the LinphoneService but it is already running !"); - return START_STICKY; - } - sInstance = this; - - if (LinphonePreferences.instance().getServiceNotificationVisibility() - || (Version.sdkAboveOrEqual(Version.API26_O_80) - && intent != null - && intent.getBooleanExtra("ForceStartForeground", false))) { - Log.i("[Service] Background service mode enabled, displaying notification"); - // We need to call this asap after the Service can be accessed through it's singleton - LinphoneContext.instance().getNotificationManager().startForeground(); - } - - if (misLinphoneContextOwned) { - LinphoneContext.instance().start(isPush); - } else { - LinphoneContext.instance().updateContext(this); - } - - Log.i("[Service] Started"); - return START_STICKY; - } - - @Override - public void onTaskRemoved(Intent rootIntent) { - boolean serviceNotif = LinphonePreferences.instance().getServiceNotificationVisibility(); - if (serviceNotif) { - Log.i("[Service] Service is running in foreground, don't stop it"); - } else if (getResources().getBoolean(R.bool.kill_service_with_task_manager)) { - Log.i("[Service] Task removed, stop service"); - Core core = LinphoneManager.getCore(); - if (core != null) { - core.terminateAllCalls(); - } - stopSelf(); - } - super.onTaskRemoved(rootIntent); - } - - @SuppressWarnings("UnusedAssignment") - @Override - public synchronized void onDestroy() { - Log.i("[Service] Destroying"); - - if (mActivityCallbacks != null) { - getApplication().unregisterActivityLifecycleCallbacks(mActivityCallbacks); - mActivityCallbacks = null; - } - destroyOverlay(); - - LinphoneContext.instance().destroy(); - sInstance = null; - - super.onDestroy(); - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - public static boolean isReady() { - return sInstance != null; - } - - public static LinphoneService instance() { - if (isReady()) return sInstance; - - throw new RuntimeException("LinphoneService not instantiated yet"); - } - - /* Managers accessors */ - - public void createOverlay() { - Log.i("[Service] Creating video overlay"); - if (mOverlay != null) destroyOverlay(); - - Core core = LinphoneManager.getCore(); - Call call = core.getCurrentCall(); - if (call == null || !call.getCurrentParams().videoEnabled()) return; - - if ("MSAndroidOpenGLDisplay".equals(core.getVideoDisplayFilter())) { - mOverlay = new LinphoneGL2JNIViewOverlay(this); - } else { - mOverlay = new LinphoneTextureViewOverlay(this); - } - WindowManager.LayoutParams params = mOverlay.getWindowManagerLayoutParams(); - params.x = 0; - params.y = 0; - mOverlay.addToWindowManager(mWindowManager, params); - } - - public void destroyOverlay() { - Log.i("[Service] Destroying video overlay"); - if (mOverlay != null) { - mOverlay.removeFromWindowManager(mWindowManager); - mOverlay.destroy(); - } - mOverlay = null; - } - - private void setupActivityMonitor() { - if (mActivityCallbacks != null) return; - getApplication() - .registerActivityLifecycleCallbacks(mActivityCallbacks = new ActivityMonitor()); - } -} diff --git a/app/src/main/java/org/linphone/service/ServiceWaitThread.java b/app/src/main/java/org/linphone/service/ServiceWaitThread.java deleted file mode 100644 index 60e767e83..000000000 --- a/app/src/main/java/org/linphone/service/ServiceWaitThread.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.service; - -import org.linphone.utils.LinphoneUtils; - -public class ServiceWaitThread extends Thread { - private ServiceWaitThreadListener mListener; - - public ServiceWaitThread(ServiceWaitThreadListener listener) { - mListener = listener; - } - - @Override - public void run() { - while (!LinphoneService.isReady()) { - try { - sleep(30); - } catch (InterruptedException e) { - throw new RuntimeException("waiting thread sleep() has been interrupted"); - } - } - - if (mListener != null) { - LinphoneUtils.dispatchOnUIThread( - new Runnable() { - @Override - public void run() { - mListener.onServiceReady(); - } - }); - } - } -} diff --git a/app/src/main/java/org/linphone/service/ServiceWaitThreadListener.java b/app/src/main/java/org/linphone/service/ServiceWaitThreadListener.java deleted file mode 100644 index 3f4f19552..000000000 --- a/app/src/main/java/org/linphone/service/ServiceWaitThreadListener.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.service; - -public interface ServiceWaitThreadListener { - void onServiceReady(); -} diff --git a/app/src/main/java/org/linphone/settings/AccountSettingsFragment.java b/app/src/main/java/org/linphone/settings/AccountSettingsFragment.java deleted file mode 100644 index 9be93c880..000000000 --- a/app/src/main/java/org/linphone/settings/AccountSettingsFragment.java +++ /dev/null @@ -1,700 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings; - -import android.content.Intent; -import android.os.Bundle; -import android.text.InputType; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.assistant.PhoneAccountLinkingAssistantActivity; -import org.linphone.core.AVPFMode; -import org.linphone.core.Address; -import org.linphone.core.AuthInfo; -import org.linphone.core.Core; -import org.linphone.core.Factory; -import org.linphone.core.NatPolicy; -import org.linphone.core.ProxyConfig; -import org.linphone.core.TransportType; -import org.linphone.core.tools.Log; -import org.linphone.settings.widget.BasicSetting; -import org.linphone.settings.widget.ListSetting; -import org.linphone.settings.widget.SettingListenerBase; -import org.linphone.settings.widget.SwitchSetting; -import org.linphone.settings.widget.TextSetting; -import org.linphone.utils.PushNotificationUtils; - -public class AccountSettingsFragment extends SettingsFragment { - private View mRootView; - private int mAccountIndex; - private ProxyConfig mProxyConfig; - private AuthInfo mAuthInfo; - private boolean mIsNewlyCreatedAccount; - - private TextSetting mUsername, - mUserId, - mPassword, - mDomain, - mDisplayName, - mProxy, - mStun, - mExpire, - mPrefix, - mAvpfInterval; - private SwitchSetting mDisable, - mUseAsDefault, - mOutboundProxy, - mIce, - mAvpf, - mReplacePlusBy00, - mPush; - private BasicSetting mChangePassword, mDeleteAccount, mLinkAccount; - private ListSetting mTransport; - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - mRootView = inflater.inflate(R.layout.settings_account, container, false); - - loadSettings(); - - mIsNewlyCreatedAccount = true; - mAccountIndex = getArguments().getInt("Account", -1); - if (mAccountIndex == -1 && savedInstanceState != null) { - mAccountIndex = savedInstanceState.getInt("Account", -1); - } - - mProxyConfig = null; - Core core = LinphoneManager.getCore(); - if (mAccountIndex >= 0 && core != null) { - ProxyConfig[] proxyConfigs = core.getProxyConfigList(); - if (proxyConfigs.length > mAccountIndex) { - mProxyConfig = proxyConfigs[mAccountIndex]; - mIsNewlyCreatedAccount = false; - } else { - Log.e("[Account Settings] Proxy config not found !"); - } - } - - return mRootView; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putInt("Account", mAccountIndex); - } - - @Override - public void onResume() { - super.onResume(); - - updateValues(); - } - - @Override - public void onPause() { - super.onPause(); - if (mIsNewlyCreatedAccount) { - Core core = LinphoneManager.getCore(); - if (core != null && mProxyConfig != null && mAuthInfo != null) { - core.addAuthInfo(mAuthInfo); - core.addProxyConfig(mProxyConfig); - if (mUseAsDefault.isChecked()) { - core.setDefaultProxyConfig(mProxyConfig); - } - } - } - } - - private void loadSettings() { - mUsername = mRootView.findViewById(R.id.pref_username); - - mUserId = mRootView.findViewById(R.id.pref_auth_userid); - - mPassword = mRootView.findViewById(R.id.pref_passwd); - mPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - - mDomain = mRootView.findViewById(R.id.pref_domain); - - mDisplayName = mRootView.findViewById(R.id.pref_display_name); - mDisplayName.setInputType( - InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PERSON_NAME); - - mProxy = mRootView.findViewById(R.id.pref_proxy); - mProxy.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); - - mStun = mRootView.findViewById(R.id.pref_stun_server); - mStun.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); - - mExpire = mRootView.findViewById(R.id.pref_expire); - mExpire.setInputType(InputType.TYPE_CLASS_NUMBER); - - mPrefix = mRootView.findViewById(R.id.pref_prefix); - mPrefix.setInputType(InputType.TYPE_CLASS_NUMBER); - - mAvpfInterval = mRootView.findViewById(R.id.pref_avpf_rr_interval); - mAvpfInterval.setInputType(InputType.TYPE_CLASS_NUMBER); - - mDisable = mRootView.findViewById(R.id.pref_disable_account); - - mUseAsDefault = mRootView.findViewById(R.id.pref_default_account); - - mOutboundProxy = mRootView.findViewById(R.id.pref_enable_outbound_proxy); - - mIce = mRootView.findViewById(R.id.pref_ice_enable); - - mAvpf = mRootView.findViewById(R.id.pref_avpf); - - mReplacePlusBy00 = mRootView.findViewById(R.id.pref_escape_plus); - - mPush = mRootView.findViewById(R.id.pref_push_notification); - mPush.setVisibility( - PushNotificationUtils.isAvailable(getActivity()) ? View.VISIBLE : View.GONE); - - mChangePassword = mRootView.findViewById(R.id.pref_change_password); - mChangePassword.setVisibility(View.GONE); // TODO add feature - - mDeleteAccount = mRootView.findViewById(R.id.pref_delete_account); - - mLinkAccount = mRootView.findViewById(R.id.pref_link_account); - - mTransport = mRootView.findViewById(R.id.pref_transport); - initTransportList(); - } - - private void setListeners() { - mUsername.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - if (newValue.isEmpty()) { - return; - } - - if (mAuthInfo != null) { - mAuthInfo.setUsername(newValue); - } else { - Log.e("[Account Settings] No auth info !"); - } - - if (mProxyConfig != null) { - mProxyConfig.edit(); - Address identity = mProxyConfig.getIdentityAddress(); - if (identity != null) { - identity.setUsername(newValue); - } - mProxyConfig.setIdentityAddress(identity); - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mUserId.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - if (mAuthInfo != null) { - mAuthInfo.setUserid(newValue); - - Core core = LinphoneManager.getCore(); - if (core != null) { - core.refreshRegisters(); - } - } else { - Log.e("[Account Settings] No auth info !"); - } - } - }); - - mPassword.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - if (mAuthInfo != null) { - mAuthInfo.setHa1(null); - mAuthInfo.setPassword(newValue); - // Reset algorithm to generate correct hash depending on - // algorithm set in next to come 401 - mAuthInfo.setAlgorithm(null); - Core core = LinphoneManager.getCore(); - if (core != null) { - core.addAuthInfo(mAuthInfo); - core.refreshRegisters(); - } - } else { - Log.e("[Account Settings] No auth info !"); - } - } - }); - - mDomain.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - if (newValue.isEmpty()) { - return; - } - if (newValue.contains(":")) { - Log.e( - "[Account Settings] Do not specify port information inside domain field !"); - return; - } - - if (mAuthInfo != null) { - mAuthInfo.setDomain(newValue); - } else { - Log.e("[Account Settings] No auth info !"); - } - - if (mProxyConfig != null) { - mProxyConfig.edit(); - Address identity = mProxyConfig.getIdentityAddress(); - if (identity != null) { - identity.setDomain(newValue); - } - mProxyConfig.setIdentityAddress(identity); - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mDisplayName.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - if (mProxyConfig != null) { - mProxyConfig.edit(); - Address identity = mProxyConfig.getIdentityAddress(); - if (identity != null) { - identity.setDisplayName(newValue); - } - mProxyConfig.setIdentityAddress(identity); - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mProxy.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - if (mProxyConfig != null) { - mProxyConfig.edit(); - Address proxy = Factory.instance().createAddress(newValue); - if (proxy != null) { - mProxyConfig.setServerAddr(proxy.asString()); - if (mOutboundProxy.isChecked()) { - mProxyConfig.setRoute(proxy.asString()); - } - mTransport.setValue(proxy.getTransport().toInt()); - } - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mStun.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - if (mProxyConfig != null) { - mProxyConfig.edit(); - NatPolicy natPolicy = mProxyConfig.getNatPolicy(); - if (natPolicy == null) { - Core core = LinphoneManager.getCore(); - if (core != null) { - natPolicy = core.createNatPolicy(); - mProxyConfig.setNatPolicy(natPolicy); - } - } - if (natPolicy != null) { - natPolicy.setStunServer(newValue); - } - if (newValue == null || newValue.isEmpty()) { - mIce.setChecked(false); - } - mIce.setEnabled(newValue != null && !newValue.isEmpty()); - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mExpire.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - if (mProxyConfig != null) { - mProxyConfig.edit(); - try { - mProxyConfig.setExpires(Integer.parseInt(newValue)); - } catch (NumberFormatException nfe) { - Log.e(nfe); - } - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mPrefix.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - if (mProxyConfig != null) { - mProxyConfig.edit(); - mProxyConfig.setDialPrefix(newValue); - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mAvpfInterval.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - if (mProxyConfig != null) { - mProxyConfig.edit(); - try { - mProxyConfig.setAvpfRrInterval(Integer.parseInt(newValue)); - } catch (NumberFormatException nfe) { - Log.e(nfe); - } - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mDisable.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - if (mProxyConfig != null) { - mProxyConfig.edit(); - mProxyConfig.enableRegister(!newValue); - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mUseAsDefault.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - if (mProxyConfig != null) { - Core core = LinphoneManager.getCore(); - if (core != null && newValue) { - core.setDefaultProxyConfig(mProxyConfig); - mUseAsDefault.setEnabled(false); - } - ((SettingsActivity) getActivity()) - .getSideMenuFragment() - .displayAccountsInSideMenu(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mOutboundProxy.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - if (mProxyConfig != null) { - mProxyConfig.edit(); - if (newValue) { - mProxyConfig.setRoute(mProxy.getValue()); - } else { - mProxyConfig.setRoute(null); - } - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mIce.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - if (mProxyConfig != null) { - mProxyConfig.edit(); - - NatPolicy natPolicy = mProxyConfig.getNatPolicy(); - if (natPolicy == null) { - Core core = LinphoneManager.getCore(); - if (core != null) { - natPolicy = core.createNatPolicy(); - mProxyConfig.setNatPolicy(natPolicy); - } - } - - if (natPolicy != null) { - natPolicy.enableIce(newValue); - } - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mAvpf.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - if (mProxyConfig != null) { - mProxyConfig.edit(); - mProxyConfig.setAvpfMode( - newValue ? AVPFMode.Enabled : AVPFMode.Disabled); - mAvpfInterval.setEnabled(mProxyConfig.avpfEnabled()); - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mReplacePlusBy00.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - if (mProxyConfig != null) { - mProxyConfig.edit(); - mProxyConfig.setDialEscapePlus(newValue); - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mPush.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - if (mProxyConfig != null) { - mProxyConfig.edit(); - mProxyConfig.setPushNotificationAllowed(newValue); - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - - mChangePassword.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - // TODO add feature - } - }); - - mDeleteAccount.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - Core core = LinphoneManager.getCore(); - if (core != null) { - if (mProxyConfig != null) { - core.removeProxyConfig(mProxyConfig); - } - if (mAuthInfo != null) { - core.removeAuthInfo(mAuthInfo); - } - } - - // Set a new default proxy config if the current one has been removed - if (core != null && core.getDefaultProxyConfig() == null) { - ProxyConfig[] proxyConfigs = core.getProxyConfigList(); - if (proxyConfigs.length > 0) { - core.setDefaultProxyConfig(proxyConfigs[0]); - } - } - - ((SettingsActivity) getActivity()) - .getSideMenuFragment() - .displayAccountsInSideMenu(); - ((SettingsActivity) getActivity()).goBack(); - } - }); - - mLinkAccount.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - Intent assistant = new Intent(); - assistant.setClass( - getActivity(), PhoneAccountLinkingAssistantActivity.class); - assistant.putExtra("AccountNumber", mAccountIndex); - startActivity(assistant); - } - }); - - mTransport.setListener( - new SettingListenerBase() { - @Override - public void onListValueChanged(int position, String newLabel, String newValue) { - if (mProxyConfig != null) { - mProxyConfig.edit(); - String server = mProxyConfig.getServerAddr(); - Address serverAddr = Factory.instance().createAddress(server); - if (serverAddr != null) { - try { - serverAddr.setTransport( - TransportType.fromInt(Integer.parseInt(newValue))); - server = serverAddr.asString(); - mProxyConfig.setServerAddr(server); - if (mOutboundProxy.isChecked()) { - mProxyConfig.setRoute(server); - } - mProxy.setValue(server); - } catch (NumberFormatException nfe) { - Log.e(nfe); - } - } - mProxyConfig.done(); - } else { - Log.e("[Account Settings] No proxy config !"); - } - } - }); - } - - private void updateValues() { - Core core = LinphoneManager.getCore(); - if (core == null) return; - - // Create a proxy config if there is none - if (mProxyConfig == null) { - // Ensure the default configuration is loaded first - String defaultConfig = LinphonePreferences.instance().getDefaultDynamicConfigFile(); - core.loadConfigFromXml(defaultConfig); - mProxyConfig = core.createProxyConfig(); - mAuthInfo = Factory.instance().createAuthInfo(null, null, null, null, null, null); - mIsNewlyCreatedAccount = true; - } - - if (mProxyConfig != null) { - Address identityAddress = mProxyConfig.getIdentityAddress(); - mAuthInfo = mProxyConfig.findAuthInfo(); - - NatPolicy natPolicy = mProxyConfig.getNatPolicy(); - if (natPolicy == null) { - natPolicy = core.createNatPolicy(); - core.setNatPolicy(natPolicy); - } - - if (mAuthInfo != null) { - mUserId.setValue(mAuthInfo.getUserid()); - // If password is hashed we can't display it - mPassword.setValue(mAuthInfo.getPassword()); - } - - mUsername.setValue(identityAddress.getUsername()); - - mDomain.setValue(identityAddress.getDomain()); - - mDisplayName.setValue(identityAddress.getDisplayName()); - - mProxy.setValue(mProxyConfig.getServerAddr()); - - mStun.setValue(natPolicy.getStunServer()); - - mExpire.setValue(mProxyConfig.getExpires()); - - mPrefix.setValue(mProxyConfig.getDialPrefix()); - - mAvpfInterval.setValue(mProxyConfig.getAvpfRrInterval()); - mAvpfInterval.setEnabled(mProxyConfig.avpfEnabled()); - - mDisable.setChecked(!mProxyConfig.registerEnabled()); - - mUseAsDefault.setChecked(mProxyConfig.equals(core.getDefaultProxyConfig())); - mUseAsDefault.setEnabled(!mUseAsDefault.isChecked()); - - String[] routes = mProxyConfig.getRoutes(); - mOutboundProxy.setChecked(routes != null && routes.length > 0); - - mIce.setChecked(natPolicy.iceEnabled()); - mIce.setEnabled( - natPolicy.getStunServer() != null && !natPolicy.getStunServer().isEmpty()); - - mAvpf.setChecked(mProxyConfig.avpfEnabled()); - - mReplacePlusBy00.setChecked(mProxyConfig.getDialEscapePlus()); - - mPush.setChecked(mProxyConfig.isPushNotificationAllowed()); - - Address proxy = Factory.instance().createAddress(mProxyConfig.getServerAddr()); - if (proxy != null) { - mTransport.setValue(proxy.getTransport().toInt()); - } - - mLinkAccount.setEnabled( - mProxyConfig.getDomain().equals(getString(R.string.default_domain))); - } - - setListeners(); - } - - private void initTransportList() { - List entries = new ArrayList<>(); - List values = new ArrayList<>(); - - entries.add(getString(R.string.pref_transport_udp)); - values.add(String.valueOf(TransportType.Udp.toInt())); - entries.add(getString(R.string.pref_transport_tcp)); - values.add(String.valueOf(TransportType.Tcp.toInt())); - - if (!getResources().getBoolean(R.bool.disable_all_security_features_for_markets)) { - entries.add(getString(R.string.pref_transport_tls)); - values.add(String.valueOf(TransportType.Tls.toInt())); - } - - mTransport.setItems(entries, values); - } -} diff --git a/app/src/main/java/org/linphone/settings/AdvancedSettingsFragment.java b/app/src/main/java/org/linphone/settings/AdvancedSettingsFragment.java deleted file mode 100644 index cc27bcb31..000000000 --- a/app/src/main/java/org/linphone/settings/AdvancedSettingsFragment.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.provider.Settings; -import android.text.InputType; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatDelegate; -import org.linphone.LinphoneContext; -import org.linphone.R; -import org.linphone.compatibility.Compatibility; -import org.linphone.settings.widget.BasicSetting; -import org.linphone.settings.widget.SettingListenerBase; -import org.linphone.settings.widget.SwitchSetting; -import org.linphone.settings.widget.TextSetting; - -public class AdvancedSettingsFragment extends SettingsFragment { - private View mRootView; - private LinphonePreferences mPrefs; - - private SwitchSetting mDebug, mJavaLogger, mBackgroundMode, mStartAtBoot, mDarkMode; - private TextSetting mRemoteProvisioningUrl, mDisplayName, mUsername, mDeviceName, mLogUploadUrl; - private BasicSetting mAndroidAppSettings; - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - mRootView = inflater.inflate(R.layout.settings_advanced, container, false); - - loadSettings(); - - return mRootView; - } - - @Override - public void onResume() { - super.onResume(); - - mPrefs = LinphonePreferences.instance(); - - updateValues(); - } - - private void loadSettings() { - mDebug = mRootView.findViewById(R.id.pref_debug); - - mJavaLogger = mRootView.findViewById(R.id.pref_java_debug); - // This is only required for blackberry users for all we know - mJavaLogger.setVisibility( - Build.MANUFACTURER.equals("BlackBerry") ? View.VISIBLE : View.GONE); - - mLogUploadUrl = mRootView.findViewById(R.id.pref_log_collection_upload_server_url); - mLogUploadUrl.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); - - mBackgroundMode = mRootView.findViewById(R.id.pref_background_mode); - - mStartAtBoot = mRootView.findViewById(R.id.pref_autostart); - - mDarkMode = mRootView.findViewById(R.id.pref_dark_mode); - mDarkMode.setVisibility( - getResources().getBoolean(R.bool.allow_dark_mode) ? View.VISIBLE : View.GONE); - - mRemoteProvisioningUrl = mRootView.findViewById(R.id.pref_remote_provisioning); - mRemoteProvisioningUrl.setInputType( - InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); - - mDisplayName = mRootView.findViewById(R.id.pref_display_name); - - mUsername = mRootView.findViewById(R.id.pref_user_name); - - mAndroidAppSettings = mRootView.findViewById(R.id.pref_android_app_settings); - - mDeviceName = mRootView.findViewById(R.id.pref_device_name); - } - - private void setListeners() { - mDebug.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.setDebugEnabled(newValue); - } - }); - - mJavaLogger.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.setJavaLogger(newValue); - } - }); - - mLogUploadUrl.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - mPrefs.setLogCollectionUploadServerUrl(newValue); - } - }); - - mBackgroundMode.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.setServiceNotificationVisibility(newValue); - if (newValue) { - LinphoneContext.instance().getNotificationManager().startForeground(); - } else { - LinphoneContext.instance().getNotificationManager().stopForeground(); - } - } - }); - - mStartAtBoot.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.setAutoStart(newValue); - } - }); - - mDarkMode.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.enableDarkMode(newValue); - AppCompatDelegate.setDefaultNightMode( - newValue - ? AppCompatDelegate.MODE_NIGHT_YES - : AppCompatDelegate.MODE_NIGHT_NO); - } - }); - - mRemoteProvisioningUrl.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - mPrefs.setRemoteProvisioningUrl(newValue); - } - }); - - mDisplayName.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - mPrefs.setDefaultDisplayName(newValue); - } - }); - - mUsername.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - mPrefs.setDefaultUsername(newValue); - } - }); - - mAndroidAppSettings.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - Context context = getActivity(); - Intent i = new Intent(); - i.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - i.addCategory(Intent.CATEGORY_DEFAULT); - i.setData(Uri.parse("package:" + context.getPackageName())); - i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - startActivity(i); - } - }); - - mDeviceName.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - mPrefs.setDeviceName(newValue); - } - }); - } - - private void updateValues() { - mDebug.setChecked(mPrefs.isDebugEnabled()); - - mJavaLogger.setChecked(mPrefs.useJavaLogger()); - - mLogUploadUrl.setValue(mPrefs.getLogCollectionUploadServerUrl()); - - mBackgroundMode.setChecked(mPrefs.getServiceNotificationVisibility()); - if (Compatibility.isAppUserRestricted(getActivity())) { - mBackgroundMode.setChecked(false); - mBackgroundMode.setEnabled(false); - mBackgroundMode.setSubtitle(getString(R.string.pref_background_mode_warning_desc)); - } - - mStartAtBoot.setChecked(mPrefs.isAutoStartEnabled()); - - mDarkMode.setChecked(mPrefs.isDarkModeEnabled()); - - mRemoteProvisioningUrl.setValue(mPrefs.getRemoteProvisioningUrl()); - - mDisplayName.setValue(mPrefs.getDefaultDisplayName()); - - mUsername.setValue(mPrefs.getDefaultUsername()); - - mDeviceName.setValue(mPrefs.getDeviceName(getActivity())); - - setListeners(); - } -} diff --git a/app/src/main/java/org/linphone/settings/AudioSettingsFragment.java b/app/src/main/java/org/linphone/settings/AudioSettingsFragment.java deleted file mode 100644 index 64b24608d..000000000 --- a/app/src/main/java/org/linphone/settings/AudioSettingsFragment.java +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings; - -import android.Manifest; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.text.InputType; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import androidx.annotation.Nullable; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Core; -import org.linphone.core.CoreListenerStub; -import org.linphone.core.EcCalibratorStatus; -import org.linphone.core.PayloadType; -import org.linphone.core.tools.Log; -import org.linphone.settings.widget.BasicSetting; -import org.linphone.settings.widget.ListSetting; -import org.linphone.settings.widget.SettingListenerBase; -import org.linphone.settings.widget.SwitchSetting; -import org.linphone.settings.widget.TextSetting; - -public class AudioSettingsFragment extends SettingsFragment { - private View mRootView; - private LinphonePreferences mPrefs; - - private SwitchSetting mEchoCanceller, mAdaptiveRateControl; - private TextSetting mMicGain, mSpeakerGain; - private ListSetting mCodecBitrateLimit; - private BasicSetting mEchoCalibration, mEchoTester; - private LinearLayout mAudioCodecs; - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - mRootView = inflater.inflate(R.layout.settings_audio, container, false); - - loadSettings(); - - return mRootView; - } - - @Override - public void onResume() { - super.onResume(); - - mPrefs = LinphonePreferences.instance(); - - updateValues(); - } - - private void loadSettings() { - mEchoCanceller = mRootView.findViewById(R.id.pref_echo_cancellation); - - mAdaptiveRateControl = mRootView.findViewById(R.id.pref_adaptive_rate_control); - - mMicGain = mRootView.findViewById(R.id.pref_mic_gain_db); - mMicGain.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); - - mSpeakerGain = mRootView.findViewById(R.id.pref_playback_gain_db); - mSpeakerGain.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); - - mCodecBitrateLimit = mRootView.findViewById(R.id.pref_codec_bitrate_limit); - - mEchoCalibration = mRootView.findViewById(R.id.pref_echo_canceller_calibration); - - mEchoTester = mRootView.findViewById(R.id.pref_echo_tester); - - mAudioCodecs = mRootView.findViewById(R.id.pref_audio_codecs); - } - - private void setListeners() { - mEchoCanceller.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.setEchoCancellation(newValue); - } - }); - - mAdaptiveRateControl.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.enableAdaptiveRateControl(newValue); - } - }); - - mMicGain.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - try { - mPrefs.setMicGainDb(Float.valueOf(newValue)); - } catch (NumberFormatException nfe) { - Log.e("Can't set mic gain, number format exception: " + nfe); - } - } - }); - - mSpeakerGain.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - try { - mPrefs.setPlaybackGainDb(Float.valueOf(newValue)); - } catch (NumberFormatException nfe) { - Log.e("Can't set speaker gain, number format exception: " + nfe); - } - } - }); - - mCodecBitrateLimit.setListener( - new SettingListenerBase() { - @Override - public void onListValueChanged(int position, String newLabel, String newValue) { - try { - int bitrate = Integer.valueOf(newValue); - mPrefs.setCodecBitrateLimit(bitrate); - - Core core = LinphoneManager.getCore(); - for (final PayloadType pt : core.getAudioPayloadTypes()) { - if (pt.isVbr()) { - pt.setNormalBitrate(bitrate); - } - } - } catch (NumberFormatException nfe) { - Log.e("Can't set codec bitrate limit, number format exception: " + nfe); - } - } - }); - - mEchoCalibration.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - mEchoCalibration.setSubtitle(getString(R.string.ec_calibrating)); - - int recordAudio = - getActivity() - .getPackageManager() - .checkPermission( - Manifest.permission.RECORD_AUDIO, - getActivity().getPackageName()); - if (recordAudio == PackageManager.PERMISSION_GRANTED) { - startEchoCancellerCalibration(); - } else { - ((SettingsActivity) getActivity()) - .requestPermissionIfNotGranted( - Manifest.permission.RECORD_AUDIO); - } - } - }); - - mEchoTester.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - int recordAudio = - getActivity() - .getPackageManager() - .checkPermission( - Manifest.permission.RECORD_AUDIO, - getActivity().getPackageName()); - if (recordAudio == PackageManager.PERMISSION_GRANTED) { - if (LinphoneManager.getAudioManager().getEchoTesterStatus()) { - stopEchoTester(); - } else { - startEchoTester(); - } - } else { - ((SettingsActivity) getActivity()) - .requestPermissionIfNotGranted( - Manifest.permission.RECORD_AUDIO); - } - } - }); - } - - private void updateValues() { - mEchoCanceller.setChecked(mPrefs.echoCancellationEnabled()); - - mAdaptiveRateControl.setChecked(mPrefs.adaptiveRateControlEnabled()); - - mMicGain.setValue(mPrefs.getMicGainDb()); - - mSpeakerGain.setValue(mPrefs.getPlaybackGainDb()); - - mCodecBitrateLimit.setValue(mPrefs.getCodecBitrateLimit()); - - if (mPrefs.echoCancellationEnabled()) { - mEchoCalibration.setSubtitle( - String.format( - getString(R.string.ec_calibrated), - String.valueOf(mPrefs.getEchoCalibration()))); - } - - populateAudioCodecs(); - - setListeners(); - } - - private void populateAudioCodecs() { - mAudioCodecs.removeAllViews(); - Core core = LinphoneManager.getCore(); - if (core != null) { - for (final PayloadType pt : core.getAudioPayloadTypes()) { - final SwitchSetting codec = new SwitchSetting(getActivity()); - codec.setTitle(pt.getMimeType()); - /* Special case */ - if (pt.getMimeType().equals("mpeg4-generic")) { - codec.setTitle("AAC-ELD"); - } - - codec.setSubtitle(pt.getClockRate() + " Hz"); - if (pt.enabled()) { - // Never use codec.setChecked(pt.enabled) ! - codec.setChecked(true); - } - codec.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - pt.enable(newValue); - } - }); - - mAudioCodecs.addView(codec); - } - } - } - - private void startEchoTester() { - LinphoneManager.getAudioManager().startEchoTester(); - mEchoTester.setSubtitle("Is running"); - } - - private void stopEchoTester() { - LinphoneManager.getAudioManager().stopEchoTester(); - mEchoTester.setSubtitle("Is stopped"); - } - - private void startEchoCancellerCalibration() { - if (LinphoneManager.getAudioManager().getEchoTesterStatus()) stopEchoTester(); - LinphoneManager.getCore() - .addListener( - new CoreListenerStub() { - @Override - public void onEcCalibrationResult( - Core core, EcCalibratorStatus status, int delayMs) { - if (status == EcCalibratorStatus.InProgress) return; - core.removeListener(this); - - if (status == EcCalibratorStatus.DoneNoEcho) { - mEchoCalibration.setSubtitle(getString(R.string.no_echo)); - } else if (status == EcCalibratorStatus.Done) { - mEchoCalibration.setSubtitle( - String.format( - getString(R.string.ec_calibrated), - String.valueOf(delayMs))); - } else if (status == EcCalibratorStatus.Failed) { - mEchoCalibration.setSubtitle(getString(R.string.failed)); - } - mEchoCanceller.setChecked(status != EcCalibratorStatus.DoneNoEcho); - } - }); - LinphoneManager.getCore().startEchoCancellerCalibration(); - } -} diff --git a/app/src/main/java/org/linphone/settings/CallSettingsFragment.java b/app/src/main/java/org/linphone/settings/CallSettingsFragment.java deleted file mode 100644 index dc1f9e3de..000000000 --- a/app/src/main/java/org/linphone/settings/CallSettingsFragment.java +++ /dev/null @@ -1,338 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings; - -import android.Manifest; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.Bundle; -import android.provider.Settings; -import android.text.InputType; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Core; -import org.linphone.core.MediaEncryption; -import org.linphone.core.tools.Log; -import org.linphone.mediastream.Version; -import org.linphone.settings.widget.BasicSetting; -import org.linphone.settings.widget.ListSetting; -import org.linphone.settings.widget.SettingListenerBase; -import org.linphone.settings.widget.SwitchSetting; -import org.linphone.settings.widget.TextSetting; - -public class CallSettingsFragment extends SettingsFragment { - private View mRootView; - private LinphonePreferences mPrefs; - - private SwitchSetting mDeviceRingtone, - mMediaEncryptionMandatory, - mVibrateIncomingCall, - mDtmfSipInfo, - mDtmfRfc2833, - mAutoAnswer; - private ListSetting mMediaEncryption; - private TextSetting mAutoAnswerTime, mIncomingCallTimeout, mVoiceMailUri; - private BasicSetting mDndPermissionSettings, mAndroidNotificationSettings; - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - mRootView = inflater.inflate(R.layout.settings_call, container, false); - - loadSettings(); - - return mRootView; - } - - @Override - public void onResume() { - super.onResume(); - - mPrefs = LinphonePreferences.instance(); - - updateValues(); - } - - private void loadSettings() { - mDeviceRingtone = mRootView.findViewById(R.id.pref_device_ringtone); - - mVibrateIncomingCall = mRootView.findViewById(R.id.pref_vibrate_on_incoming_calls); - - mDtmfSipInfo = mRootView.findViewById(R.id.pref_sipinfo_dtmf); - - mDtmfRfc2833 = mRootView.findViewById(R.id.pref_rfc2833_dtmf); - - mAutoAnswer = mRootView.findViewById(R.id.pref_auto_answer); - - mMediaEncryption = mRootView.findViewById(R.id.pref_media_encryption); - initMediaEncryptionList(); - - mAndroidNotificationSettings = mRootView.findViewById(R.id.pref_android_app_notif_settings); - - mAutoAnswerTime = mRootView.findViewById(R.id.pref_auto_answer_time); - mAutoAnswerTime.setInputType(InputType.TYPE_CLASS_NUMBER); - - mIncomingCallTimeout = mRootView.findViewById(R.id.pref_incoming_call_timeout); - mAutoAnswerTime.setInputType(InputType.TYPE_CLASS_NUMBER); - - mVoiceMailUri = mRootView.findViewById(R.id.pref_voice_mail); - mAutoAnswerTime.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); - - mDndPermissionSettings = - mRootView.findViewById(R.id.pref_grant_read_dnd_settings_permission); - - mMediaEncryptionMandatory = mRootView.findViewById(R.id.pref_media_encryption_mandatory); - } - - private void setListeners() { - mDeviceRingtone.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - int readExternalStorage = - getActivity() - .getPackageManager() - .checkPermission( - Manifest.permission.READ_EXTERNAL_STORAGE, - getActivity().getPackageName()); - if (readExternalStorage == PackageManager.PERMISSION_GRANTED) { - mPrefs.enableDeviceRingtone(newValue); - } else { - ((SettingsActivity) getActivity()) - .requestPermissionIfNotGranted( - Manifest.permission.READ_EXTERNAL_STORAGE); - } - } - }); - - mVibrateIncomingCall.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.enableIncomingCallVibration(newValue); - } - }); - - mDtmfSipInfo.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.sendDTMFsAsSipInfo(newValue); - if (!newValue && !mDtmfRfc2833.isChecked()) { - mDtmfRfc2833.setChecked(true); - } - } - }); - - mDtmfRfc2833.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.sendDtmfsAsRfc2833(newValue); - if (!newValue && !mDtmfSipInfo.isChecked()) { - mDtmfRfc2833.setChecked(true); - } - } - }); - - mAutoAnswer.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.enableAutoAnswer(newValue); - mAutoAnswerTime.setVisibility( - mPrefs.isAutoAnswerEnabled() ? View.VISIBLE : View.GONE); - } - }); - - mMediaEncryption.setListener( - new SettingListenerBase() { - @Override - public void onListValueChanged(int position, String newLabel, String newValue) { - try { - MediaEncryption encryption = - MediaEncryption.fromInt(Integer.parseInt(newValue)); - mPrefs.setMediaEncryption(encryption); - - if (encryption == MediaEncryption.None) { - mMediaEncryptionMandatory.setChecked(false); - } - mMediaEncryptionMandatory.setEnabled( - encryption != MediaEncryption.None); - } catch (NumberFormatException nfe) { - Log.e(nfe); - } - } - }); - - mAutoAnswerTime.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - try { - mPrefs.setAutoAnswerTime(Integer.parseInt(newValue)); - } catch (NumberFormatException nfe) { - Log.e(nfe); - } - } - }); - - mIncomingCallTimeout.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - try { - mPrefs.setIncTimeout(Integer.parseInt(newValue)); - } catch (NumberFormatException nfe) { - Log.e(nfe); - } - } - }); - - mVoiceMailUri.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - mPrefs.setVoiceMailUri(newValue); - } - }); - - mDndPermissionSettings.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - try { - startActivity( - new Intent( - "android.settings.NOTIFICATION_POLICY_ACCESS_SETTINGS")); - } catch (ActivityNotFoundException anfe) { - Log.e("[Call Settings] Activity not found: ", anfe); - } - } - }); - - mMediaEncryptionMandatory.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.setMediaEncryptionMandatory(newValue); - } - }); - - mAndroidNotificationSettings.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - if (Build.VERSION.SDK_INT >= Version.API26_O_80) { - Context context = getActivity(); - Intent i = new Intent(); - i.setAction(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); - i.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); - i.putExtra( - Settings.EXTRA_CHANNEL_ID, - context.getString(R.string.notification_service_channel_id)); - i.addCategory(Intent.CATEGORY_DEFAULT); - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - startActivity(i); - } - } - }); - } - - private void updateValues() { - mDeviceRingtone.setChecked(mPrefs.isDeviceRingtoneEnabled()); - - mVibrateIncomingCall.setChecked(mPrefs.isIncomingCallVibrationEnabled()); - - mDtmfSipInfo.setChecked(mPrefs.useSipInfoDtmfs()); - - mDtmfRfc2833.setChecked(mPrefs.useRfc2833Dtmfs()); - - mAutoAnswer.setChecked(mPrefs.isAutoAnswerEnabled()); - - mMediaEncryption.setValue(mPrefs.getMediaEncryption().toInt()); - - mAutoAnswerTime.setValue(mPrefs.getAutoAnswerTime()); - mAutoAnswerTime.setVisibility(mPrefs.isAutoAnswerEnabled() ? View.VISIBLE : View.GONE); - - mIncomingCallTimeout.setValue(mPrefs.getIncTimeout()); - - mVoiceMailUri.setValue(mPrefs.getVoiceMailUri()); - - mDndPermissionSettings.setVisibility( - Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60) ? View.VISIBLE : View.GONE); - - mMediaEncryptionMandatory.setChecked(mPrefs.isMediaEncryptionMandatory()); - mMediaEncryptionMandatory.setEnabled(mPrefs.getMediaEncryption() != MediaEncryption.None); - - if (Version.sdkStrictlyBelow(Version.API26_O_80)) { - mAndroidNotificationSettings.setVisibility(View.GONE); - } - - setListeners(); - } - - private void initMediaEncryptionList() { - List entries = new ArrayList<>(); - List values = new ArrayList<>(); - - entries.add(getString(R.string.pref_none)); - values.add(String.valueOf(MediaEncryption.None.toInt())); - - Core core = LinphoneManager.getCore(); - if (core != null - && !getResources().getBoolean(R.bool.disable_all_security_features_for_markets)) { - boolean hasZrtp = core.mediaEncryptionSupported(MediaEncryption.ZRTP); - boolean hasSrtp = core.mediaEncryptionSupported(MediaEncryption.SRTP); - boolean hasDtls = core.mediaEncryptionSupported(MediaEncryption.DTLS); - - if (!hasSrtp && !hasZrtp && !hasDtls) { - mMediaEncryption.setEnabled(false); - } else { - if (hasSrtp) { - entries.add("SRTP"); - values.add(String.valueOf(MediaEncryption.SRTP.toInt())); - } - if (hasZrtp) { - entries.add("ZRTP"); - values.add(String.valueOf(MediaEncryption.ZRTP.toInt())); - } - if (hasDtls) { - entries.add("DTLS"); - values.add(String.valueOf(MediaEncryption.DTLS.toInt())); - } - } - } - - mMediaEncryption.setItems(entries, values); - } -} diff --git a/app/src/main/java/org/linphone/settings/ChatSettingsFragment.java b/app/src/main/java/org/linphone/settings/ChatSettingsFragment.java deleted file mode 100644 index fc45c0b44..000000000 --- a/app/src/main/java/org/linphone/settings/ChatSettingsFragment.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings; - -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.provider.Settings; -import android.text.InputType; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.Nullable; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.ChatRoom; -import org.linphone.core.tools.Log; -import org.linphone.mediastream.Version; -import org.linphone.settings.widget.BasicSetting; -import org.linphone.settings.widget.ListSetting; -import org.linphone.settings.widget.SettingListenerBase; -import org.linphone.settings.widget.SwitchSetting; -import org.linphone.settings.widget.TextSetting; - -public class ChatSettingsFragment extends SettingsFragment { - private View mRootView; - private LinphonePreferences mPrefs; - private TextSetting mSharingServer, mMaxSizeForAutoDownloadIncomingFiles; - private BasicSetting mAndroidNotificationSettings; - private ListSetting mAutoDownloadIncomingFilesPolicy; - private SwitchSetting mHideEmptyRooms, mHideRemovedProxiesRooms, mMakeDownloadedImagesPublic; - private SwitchSetting mEnableEphemeralBeta; - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - mRootView = inflater.inflate(R.layout.settings_chat, container, false); - - loadSettings(); - - return mRootView; - } - - @Override - public void onResume() { - super.onResume(); - - mPrefs = LinphonePreferences.instance(); - - updateValues(); - } - - private void loadSettings() { - mSharingServer = mRootView.findViewById(R.id.pref_image_sharing_server); - mSharingServer.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); - - mMaxSizeForAutoDownloadIncomingFiles = - mRootView.findViewById(R.id.pref_auto_download_max_size); - - mAutoDownloadIncomingFilesPolicy = mRootView.findViewById(R.id.pref_auto_download_policy); - - mMakeDownloadedImagesPublic = - mRootView.findViewById( - R.id.pref_android_app_make_downloaded_images_visible_in_native_gallery); - - mAndroidNotificationSettings = mRootView.findViewById(R.id.pref_android_app_notif_settings); - - mHideEmptyRooms = mRootView.findViewById(R.id.pref_android_app_hide_empty_chat_rooms); - - mHideRemovedProxiesRooms = - mRootView.findViewById(R.id.pref_android_app_hide_chat_rooms_from_removed_proxies); - - mEnableEphemeralBeta = - mRootView.findViewById(R.id.pref_android_app_enable_ephemeral_messages_beta); - } - - private void setListeners() { - mSharingServer.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - mPrefs.setSharingPictureServerUrl(newValue); - } - }); - - mAutoDownloadIncomingFilesPolicy.setListener( - new SettingListenerBase() { - @Override - public void onListValueChanged(int position, String newLabel, String newValue) { - try { - int max_size = Integer.valueOf(newValue); - mPrefs.setAutoDownloadFileMaxSize(max_size); - updateAutoDownloadSettingsFromValue(max_size); - } catch (NumberFormatException nfe) { - Log.e(nfe); - } - } - }); - - mMaxSizeForAutoDownloadIncomingFiles.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - try { - mPrefs.setAutoDownloadFileMaxSize(Integer.valueOf(newValue)); - } catch (NumberFormatException nfe) { - Log.e(nfe); - } - } - }); - - mMakeDownloadedImagesPublic.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.setDownloadedImagesVisibleInNativeGallery(newValue); - } - }); - - mAndroidNotificationSettings.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - if (Build.VERSION.SDK_INT >= Version.API26_O_80) { - Context context = getActivity(); - Intent i = new Intent(); - i.setAction(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); - i.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); - i.putExtra( - Settings.EXTRA_CHANNEL_ID, - context.getString(R.string.notification_channel_id)); - i.addCategory(Intent.CATEGORY_DEFAULT); - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - startActivity(i); - } - } - }); - - mHideEmptyRooms.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - LinphonePreferences.instance().setHideEmptyChatRooms(newValue); - } - }); - - mHideRemovedProxiesRooms.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - LinphonePreferences.instance().setHideRemovedProxiesChatRooms(newValue); - } - }); - - mEnableEphemeralBeta.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - LinphonePreferences.instance().enableEphemeralMessages(newValue); - if (!newValue) { - for (ChatRoom room : LinphoneManager.getCore().getChatRooms()) { - room.enableEphemeral(false); - } - } - } - }); - } - - private void updateValues() { - mSharingServer.setValue(mPrefs.getSharingPictureServerUrl()); - - updateAutoDownloadSettingsFromValue(mPrefs.getAutoDownloadFileMaxSize()); - - if (Version.sdkStrictlyBelow(Version.API26_O_80)) { - mAndroidNotificationSettings.setVisibility(View.GONE); - } - - mMakeDownloadedImagesPublic.setChecked(mPrefs.makeDownloadedImagesVisibleInNativeGallery()); - - mHideEmptyRooms.setChecked(LinphonePreferences.instance().hideEmptyChatRooms()); - - mHideRemovedProxiesRooms.setChecked( - LinphonePreferences.instance().hideRemovedProxiesChatRooms()); - - mEnableEphemeralBeta.setChecked( - LinphonePreferences.instance().isEphemeralMessagesEnabled()); - - setListeners(); - } - - private void updateAutoDownloadSettingsFromValue(int max_size) { - if (max_size == -1) { - mAutoDownloadIncomingFilesPolicy.setValue( - getString(R.string.pref_auto_download_policy_disabled_key)); - } else if (max_size == 0) { - mAutoDownloadIncomingFilesPolicy.setValue( - getString(R.string.pref_auto_download_policy_always_key)); - } else { - mAutoDownloadIncomingFilesPolicy.setValue( - getString(R.string.pref_auto_download_policy_size_key)); - } - mMaxSizeForAutoDownloadIncomingFiles.setValue(max_size); - mMaxSizeForAutoDownloadIncomingFiles.setVisibility(max_size > 0 ? View.VISIBLE : View.GONE); - } -} diff --git a/app/src/main/java/org/linphone/settings/ContactSettingsFragment.java b/app/src/main/java/org/linphone/settings/ContactSettingsFragment.java deleted file mode 100644 index 81c4c3dcb..000000000 --- a/app/src/main/java/org/linphone/settings/ContactSettingsFragment.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.Nullable; -import org.linphone.R; -import org.linphone.compatibility.Compatibility; -import org.linphone.mediastream.Version; -import org.linphone.settings.widget.SettingListenerBase; -import org.linphone.settings.widget.SwitchSetting; - -public class ContactSettingsFragment extends SettingsFragment { - private View mContactView; - private SwitchSetting mContactPresenceNativeContact, - mFriendListSubscribe, - mDisplayDetailContact, - mCreateShortcuts; - private LinphonePreferences mPrefs; - - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - mContactView = inflater.inflate(R.layout.settings_contact, container, false); - - loadSettings(); - - return mContactView; - } - - @Override - public void onResume() { - super.onResume(); - - mPrefs = LinphonePreferences.instance(); - - updateValues(); - } - - private void loadSettings() { - mFriendListSubscribe = mContactView.findViewById(R.id.pref_friendlist_subscribe); - - mContactPresenceNativeContact = - mContactView.findViewById(R.id.pref_contact_presence_native_contact); - - mDisplayDetailContact = mContactView.findViewById(R.id.pref_contact_organization); - - mCreateShortcuts = mContactView.findViewById(R.id.pref_contact_shortcuts); - } - - private void updateValues() { - setListeners(); - - mFriendListSubscribe.setChecked(mPrefs.isFriendlistsubscriptionEnabled()); - - mContactPresenceNativeContact.setChecked( - mPrefs.isPresenceStorageInNativeAndroidContactEnabled()); - - if (getResources().getBoolean(R.bool.display_contact_organization)) { - mDisplayDetailContact.setChecked(mPrefs.isDisplayContactOrganization()); - } else { - mDisplayDetailContact.setVisibility(View.INVISIBLE); - } - - if (Version.sdkAboveOrEqual(Version.API25_NOUGAT_71) - && getResources().getBoolean(R.bool.create_shortcuts)) { - mCreateShortcuts.setChecked(mPrefs.shortcutsCreationEnabled()); - } else { - mCreateShortcuts.setVisibility(View.GONE); - } - } - - private void setListeners() { - mContactPresenceNativeContact.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.enabledPresenceStorageInNativeAndroidContact(newValue); - } - }); - - mFriendListSubscribe.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.enabledFriendlistSubscription(newValue); - // Synchronization of the buttons between them, possibility to click on : - // "presence information"... only if "is friends subscript on enabled" is - // active - mContactPresenceNativeContact.setEnabled( - mPrefs.isFriendlistsubscriptionEnabled()); - - if (!newValue) { - mContactPresenceNativeContact.setChecked(false); - } - } - }); - - mDisplayDetailContact.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.enabledDisplayContactOrganization(newValue); - } - }); - - mCreateShortcuts.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.enableChatRoomsShortcuts(newValue); - if (newValue) { - Compatibility.createChatShortcuts(getActivity()); - } else { - Compatibility.removeChatShortcuts(getActivity()); - } - } - }); - } -} diff --git a/app/src/main/java/org/linphone/settings/LinphonePreferences.java b/app/src/main/java/org/linphone/settings/LinphonePreferences.java deleted file mode 100644 index 48fb666e9..000000000 --- a/app/src/main/java/org/linphone/settings/LinphonePreferences.java +++ /dev/null @@ -1,1334 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings; - -import android.Manifest; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import androidx.appcompat.app.AppCompatDelegate; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.compatibility.Compatibility; -import org.linphone.core.Address; -import org.linphone.core.AuthInfo; -import org.linphone.core.Config; -import org.linphone.core.Core; -import org.linphone.core.Factory; -import org.linphone.core.MediaEncryption; -import org.linphone.core.NatPolicy; -import org.linphone.core.ProxyConfig; -import org.linphone.core.Transports; -import org.linphone.core.Tunnel; -import org.linphone.core.TunnelConfig; -import org.linphone.core.VideoActivationPolicy; -import org.linphone.core.VideoDefinition; -import org.linphone.core.tools.Log; -import org.linphone.mediastream.Version; -import org.linphone.utils.LinphoneUtils; - -public class LinphonePreferences { - private static final int LINPHONE_CORE_RANDOM_PORT = -1; - public static final String LINPHONE_DEFAULT_RC = "/.linphonerc"; - public static final String LINPHONE_FACTORY_RC = "/linphonerc"; - private static final String LINPHONE_LPCONFIG_XSD = "/lpconfig.xsd"; - private static final String DEFAULT_ASSISTANT_RC = "/default_assistant_create.rc"; - private static final String LINPHONE_ASSISTANT_RC = "/linphone_assistant_create.rc"; - - private static LinphonePreferences sInstance; - - private Context mContext; - private String mBasePath; - // Tunnel settings - private TunnelConfig mTunnelConfig = null; - - private LinphonePreferences() {} - - public static synchronized LinphonePreferences instance() { - if (sInstance == null) { - sInstance = new LinphonePreferences(); - } - return sInstance; - } - - public void destroy() { - mContext = null; - sInstance = null; - } - - public void setContext(Context c) { - mContext = c; - mBasePath = mContext.getFilesDir().getAbsolutePath(); - try { - copyAssetsFromPackage(); - } catch (IOException ioe) { - - } - } - - /* Assets stuff */ - - private void copyAssetsFromPackage() throws IOException { - copyIfNotExist(R.raw.linphonerc_default, getLinphoneDefaultConfig()); - copyFromPackage(R.raw.linphonerc_factory, new File(getLinphoneFactoryConfig()).getName()); - copyIfNotExist(R.raw.lpconfig, mBasePath + LINPHONE_LPCONFIG_XSD); - copyFromPackage( - R.raw.default_assistant_create, - new File(mBasePath + DEFAULT_ASSISTANT_RC).getName()); - copyFromPackage( - R.raw.linphone_assistant_create, - new File(mBasePath + LINPHONE_ASSISTANT_RC).getName()); - } - - private void copyIfNotExist(int ressourceId, String target) throws IOException { - File lFileToCopy = new File(target); - if (!lFileToCopy.exists()) { - copyFromPackage(ressourceId, lFileToCopy.getName()); - } - } - - private void copyFromPackage(int ressourceId, String target) throws IOException { - FileOutputStream lOutputStream = mContext.openFileOutput(target, 0); - InputStream lInputStream = mContext.getResources().openRawResource(ressourceId); - int readByte; - byte[] buff = new byte[8048]; - while ((readByte = lInputStream.read(buff)) != -1) { - lOutputStream.write(buff, 0, readByte); - } - lOutputStream.flush(); - lOutputStream.close(); - lInputStream.close(); - } - - public String getLinphoneDefaultConfig() { - return mBasePath + LINPHONE_DEFAULT_RC; - } - - public String getLinphoneFactoryConfig() { - return mBasePath + LINPHONE_FACTORY_RC; - } - - public String getDefaultDynamicConfigFile() { - return mBasePath + DEFAULT_ASSISTANT_RC; - } - - public String getLinphoneDynamicConfigFile() { - return mBasePath + LINPHONE_ASSISTANT_RC; - } - - private String getString(int key) { - if (mContext == null && LinphoneContext.isReady()) { - mContext = LinphoneContext.instance().getApplicationContext(); - } - - return mContext.getString(key); - } - - private Core getLc() { - if (!LinphoneContext.isReady()) return null; - - return LinphoneManager.getCore(); - } - - public Config getConfig() { - Core core = getLc(); - if (core != null) { - return core.getConfig(); - } - - if (!LinphoneContext.isReady()) { - File linphonerc = new File(mBasePath + "/.linphonerc"); - if (linphonerc.exists()) { - return Factory.instance().createConfig(linphonerc.getAbsolutePath()); - } else if (mContext != null) { - InputStream inputStream = - mContext.getResources().openRawResource(R.raw.linphonerc_default); - InputStreamReader inputreader = new InputStreamReader(inputStream); - BufferedReader buffreader = new BufferedReader(inputreader); - StringBuilder text = new StringBuilder(); - String line; - try { - while ((line = buffreader.readLine()) != null) { - text.append(line); - text.append('\n'); - } - } catch (IOException ioe) { - Log.e(ioe); - } - return Factory.instance().createConfigFromString(text.toString()); - } - } else { - return Factory.instance().createConfig(getLinphoneDefaultConfig()); - } - return null; - } - - // App settings - public boolean isFirstLaunch() { - if (getConfig() == null) return true; - return getConfig().getBool("app", "first_launch", true); - } - - public void firstLaunchSuccessful() { - getConfig().setBool("app", "first_launch", false); - } - - public String getRingtone(String defaultRingtone) { - String ringtone = getConfig().getString("app", "ringtone", defaultRingtone); - if (ringtone == null || ringtone.isEmpty()) ringtone = defaultRingtone; - return ringtone; - } - - public boolean getReadAndAgreeTermsAndPrivacy() { - if (getConfig() == null) return false; - return getConfig().getBool("app", "read_and_agree_terms_and_privacy", false); - } - - public void setReadAndAgreeTermsAndPrivacy(boolean value) { - getConfig().setBool("app", "read_and_agree_terms_and_privacy", value); - } - - // Accounts settings - private ProxyConfig getProxyConfig(int n) { - if (getLc() == null) return null; - ProxyConfig[] prxCfgs = getLc().getProxyConfigList(); - if (n < 0 || n >= prxCfgs.length) return null; - return prxCfgs[n]; - } - - private AuthInfo getAuthInfo(int n) { - ProxyConfig prxCfg = getProxyConfig(n); - if (prxCfg == null) return null; - Address addr = prxCfg.getIdentityAddress(); - return getLc().findAuthInfo(null, addr.getUsername(), addr.getDomain()); - } - - public String getAccountUsername(int n) { - AuthInfo authInfo = getAuthInfo(n); - return authInfo == null ? null : authInfo.getUsername(); - } - - public String getAccountHa1(int n) { - AuthInfo authInfo = getAuthInfo(n); - return authInfo == null ? null : authInfo.getHa1(); - } - - public String getAccountDomain(int n) { - ProxyConfig proxyConf = getProxyConfig(n); - return (proxyConf != null) ? proxyConf.getDomain() : ""; - } - - public int getDefaultAccountIndex() { - if (getLc() == null) return -1; - ProxyConfig defaultPrxCfg = getLc().getDefaultProxyConfig(); - if (defaultPrxCfg == null) return -1; - - ProxyConfig[] prxCfgs = getLc().getProxyConfigList(); - for (int i = 0; i < prxCfgs.length; i++) { - if (defaultPrxCfg.getIdentityAddress().equals(prxCfgs[i].getIdentityAddress())) { - return i; - } - } - return -1; - } - - public int getAccountCount() { - if (getLc() == null || getLc().getProxyConfigList() == null) return 0; - - return getLc().getProxyConfigList().length; - } - - public void setAccountEnabled(int n, boolean enabled) { - if (getLc() == null) return; - ProxyConfig prxCfg = getProxyConfig(n); - if (prxCfg == null) { - LinphoneUtils.displayErrorAlert(getString(R.string.error), mContext); - return; - } - prxCfg.edit(); - prxCfg.enableRegister(enabled); - prxCfg.done(); - - // If default proxy config is disabled, try to set another one as default proxy - if (!enabled - && getLc().getDefaultProxyConfig() - .getIdentityAddress() - .equals(prxCfg.getIdentityAddress())) { - int count = getLc().getProxyConfigList().length; - if (count > 1) { - for (int i = 0; i < count; i++) { - if (isAccountEnabled(i)) { - getLc().setDefaultProxyConfig(getProxyConfig(i)); - break; - } - } - } - } - } - - private boolean isAccountEnabled(int n) { - return getProxyConfig(n).registerEnabled(); - } - // End of accounts settings - - // Audio settings - public void setEchoCancellation(boolean enable) { - if (getLc() == null) return; - getLc().enableEchoCancellation(enable); - } - - public boolean echoCancellationEnabled() { - if (getLc() == null) return false; - return getLc().echoCancellationEnabled(); - } - - public int getEchoCalibration() { - return getConfig().getInt("sound", "ec_delay", -1); - } - - public float getMicGainDb() { - return getLc().getMicGainDb(); - } - - public void setMicGainDb(float gain) { - getLc().setMicGainDb(gain); - } - - public float getPlaybackGainDb() { - return getLc().getPlaybackGainDb(); - } - - public void setPlaybackGainDb(float gain) { - getLc().setPlaybackGainDb(gain); - } - - // End of audio settings - - // Video settings - public boolean useFrontCam() { - if (getConfig() == null) return false; - return getConfig().getBool("app", "front_camera_default", true); - } - - public void setFrontCamAsDefault(boolean frontcam) { - if (getConfig() == null) return; - getConfig().setBool("app", "front_camera_default", frontcam); - } - - public String getCameraDevice() { - return getLc().getVideoDevice(); - } - - public void setCameraDevice(String device) { - getLc().setVideoDevice(device); - } - - public boolean isVideoEnabled() { - if (getLc() == null) return false; - return getLc().videoSupported() && getLc().videoEnabled(); - } - - public void enableVideo(boolean enable) { - if (getLc() == null) return; - getLc().enableVideoCapture(enable); - getLc().enableVideoDisplay(enable); - } - - public boolean shouldInitiateVideoCall() { - if (getLc() == null) return false; - return getLc().getVideoActivationPolicy().getAutomaticallyInitiate(); - } - - public void setInitiateVideoCall(boolean initiate) { - if (getLc() == null) return; - VideoActivationPolicy vap = getLc().getVideoActivationPolicy(); - vap.setAutomaticallyInitiate(initiate); - getLc().setVideoActivationPolicy(vap); - } - - public boolean shouldAutomaticallyAcceptVideoRequests() { - if (getLc() == null) return false; - VideoActivationPolicy vap = getLc().getVideoActivationPolicy(); - return vap.getAutomaticallyAccept(); - } - - public void setAutomaticallyAcceptVideoRequests(boolean accept) { - if (getLc() == null) return; - VideoActivationPolicy vap = getLc().getVideoActivationPolicy(); - vap.setAutomaticallyAccept(accept); - getLc().setVideoActivationPolicy(vap); - } - - public String getVideoPreset() { - if (getLc() == null) return null; - String preset = getLc().getVideoPreset(); - if (preset == null) preset = "default"; - return preset; - } - - public void setVideoPreset(String preset) { - if (getLc() == null) return; - if (preset.equals("default")) preset = null; - getLc().setVideoPreset(preset); - preset = getVideoPreset(); - if (!preset.equals("custom")) { - getLc().setPreferredFramerate(0); - } - setPreferredVideoSize(getPreferredVideoSize()); // Apply the bandwidth limit - } - - public String getPreferredVideoSize() { - // Core can only return video size (width and height), not the name - return getConfig().getString("video", "size", "qvga"); - } - - public void setPreferredVideoSize(String preferredVideoSize) { - if (getLc() == null) return; - VideoDefinition preferredVideoDefinition = - Factory.instance().createVideoDefinitionFromName(preferredVideoSize); - getLc().setPreferredVideoDefinition(preferredVideoDefinition); - } - - public int getPreferredVideoFps() { - if (getLc() == null) return 0; - return (int) getLc().getPreferredFramerate(); - } - - public void setPreferredVideoFps(int fps) { - if (getLc() == null) return; - getLc().setPreferredFramerate(fps); - } - - public int getBandwidthLimit() { - if (getLc() == null) return 0; - return getLc().getDownloadBandwidth(); - } - - public void setBandwidthLimit(int bandwidth) { - if (getLc() == null) return; - getLc().setUploadBandwidth(bandwidth); - getLc().setDownloadBandwidth(bandwidth); - } - // End of video settings - - // Contact settings - public boolean isFriendlistsubscriptionEnabled() { - if (getConfig() == null) return false; - if (getConfig().getBool("app", "friendlist_subscription_enabled", false)) { - // Old setting, do migration - getConfig().setBool("app", "friendlist_subscription_enabled", false); - enabledFriendlistSubscription(true); - } - return getLc().isFriendListSubscriptionEnabled(); - } - - public void enabledFriendlistSubscription(boolean enabled) { - if (getLc() == null) return; - getLc().enableFriendListSubscription(enabled); - } - - public boolean isPresenceStorageInNativeAndroidContactEnabled() { - if (getConfig() == null) return false; - return getConfig().getBool("app", "store_presence_in_native_contact", false); - } - - public void enabledPresenceStorageInNativeAndroidContact(boolean enabled) { - if (getConfig() == null) return; - getConfig().setBool("app", "store_presence_in_native_contact", enabled); - } - - public boolean isDisplayContactOrganization() { - if (getConfig() == null) return false; - return getConfig() - .getBool( - "app", - "display_contact_organization", - mContext.getResources().getBoolean(R.bool.display_contact_organization)); - } - - public void enabledDisplayContactOrganization(boolean enabled) { - if (getConfig() == null) return; - getConfig().setBool("app", "display_contact_organization", enabled); - } - // End of contact settings - - // Call settings - public boolean isMediaEncryptionMandatory() { - if (getLc() == null) return false; - return getLc().isMediaEncryptionMandatory(); - } - - public void setMediaEncryptionMandatory(boolean accept) { - if (getLc() == null) return; - getLc().setMediaEncryptionMandatory(accept); - } - - public boolean acceptIncomingEarlyMedia() { - if (getConfig() == null) return false; - return getConfig().getBool("sip", "incoming_calls_early_media", false); - } - - public void setAcceptIncomingEarlyMedia(boolean accept) { - if (getConfig() == null) return; - getConfig().setBool("sip", "incoming_calls_early_media", accept); - } - - public boolean useRfc2833Dtmfs() { - if (getLc() == null) return false; - return getLc().getUseRfc2833ForDtmf(); - } - - public void sendDtmfsAsRfc2833(boolean use) { - if (getLc() == null) return; - getLc().setUseRfc2833ForDtmf(use); - } - - public boolean useSipInfoDtmfs() { - if (getLc() == null) return false; - return getLc().getUseInfoForDtmf(); - } - - public void sendDTMFsAsSipInfo(boolean use) { - if (getLc() == null) return; - getLc().setUseInfoForDtmf(use); - } - - public int getIncTimeout() { - if (getLc() == null) return 0; - return getLc().getIncTimeout(); - } - - public void setIncTimeout(int timeout) { - if (getLc() == null) return; - getLc().setIncTimeout(timeout); - } - - public String getVoiceMailUri() { - if (getConfig() == null) return null; - return getConfig().getString("app", "voice_mail", null); - } - - public void setVoiceMailUri(String uri) { - if (getConfig() == null) return; - getConfig().setString("app", "voice_mail", uri); - } - - public boolean getNativeDialerCall() { - if (getConfig() == null) return false; - return getConfig().getBool("app", "native_dialer_call", false); - } - - public void setNativeDialerCall(boolean use) { - if (getConfig() == null) return; - getConfig().setBool("app", "native_dialer_call", use); - } - // End of call settings - - public boolean isWifiOnlyEnabled() { - if (getLc() == null) return false; - return getLc().wifiOnlyEnabled(); - } - - // Network settings - public void setWifiOnlyEnabled(Boolean enable) { - if (getLc() == null) return; - getLc().enableWifiOnly(enable); - } - - public void useRandomPort(boolean enabled) { - useRandomPort(enabled, true); - } - - private void useRandomPort(boolean enabled, boolean apply) { - if (getConfig() == null) return; - getConfig().setBool("app", "random_port", enabled); - if (apply) { - if (enabled) { - setSipPort(LINPHONE_CORE_RANDOM_PORT); - } else { - setSipPort(5060); - } - } - } - - public boolean isUsingRandomPort() { - if (getConfig() == null) return true; - return getConfig().getBool("app", "random_port", true); - } - - public String getSipPort() { - if (getLc() == null) return null; - Transports transports = getLc().getTransports(); - int port; - if (transports.getUdpPort() > 0) port = transports.getUdpPort(); - else port = transports.getTcpPort(); - return String.valueOf(port); - } - - public void setSipPort(int port) { - if (getLc() == null) return; - Transports transports = getLc().getTransports(); - transports.setUdpPort(port); - transports.setTcpPort(port); - transports.setTlsPort(LINPHONE_CORE_RANDOM_PORT); - getLc().setTransports(transports); - } - - private NatPolicy getOrCreateNatPolicy() { - if (getLc() == null) return null; - NatPolicy nat = getLc().getNatPolicy(); - if (nat == null) { - nat = getLc().createNatPolicy(); - } - return nat; - } - - public String getStunServer() { - NatPolicy nat = getOrCreateNatPolicy(); - if (nat == null) return null; - return nat.getStunServer(); - } - - public void setStunServer(String stun) { - if (getLc() == null) return; - NatPolicy nat = getOrCreateNatPolicy(); - if (nat == null) return; - nat.setStunServer(stun); - - getLc().setNatPolicy(nat); - } - - public boolean isIceEnabled() { - NatPolicy nat = getOrCreateNatPolicy(); - if (nat == null) return false; - return nat.iceEnabled(); - } - - public void setIceEnabled(boolean enabled) { - if (getLc() == null) return; - NatPolicy nat = getOrCreateNatPolicy(); - if (nat == null) return; - nat.enableIce(enabled); - if (enabled) nat.enableStun(true); - getLc().setNatPolicy(nat); - } - - public boolean isTurnEnabled() { - NatPolicy nat = getOrCreateNatPolicy(); - if (nat == null) return false; - return nat.turnEnabled(); - } - - public void setTurnEnabled(boolean enabled) { - if (getLc() == null) return; - NatPolicy nat = getOrCreateNatPolicy(); - if (nat == null) return; - nat.enableTurn(enabled); - getLc().setNatPolicy(nat); - } - - public String getTurnUsername() { - NatPolicy nat = getOrCreateNatPolicy(); - if (nat == null) return null; - return nat.getStunServerUsername(); - } - - public void setTurnUsername(String username) { - if (getLc() == null) return; - NatPolicy nat = getOrCreateNatPolicy(); - if (nat == null) return; - AuthInfo authInfo = getLc().findAuthInfo(null, nat.getStunServerUsername(), null); - - if (authInfo != null) { - AuthInfo cloneAuthInfo = authInfo.clone(); - getLc().removeAuthInfo(authInfo); - cloneAuthInfo.setUsername(username); - cloneAuthInfo.setUserid(username); - getLc().addAuthInfo(cloneAuthInfo); - } else { - authInfo = - Factory.instance().createAuthInfo(username, username, null, null, null, null); - getLc().addAuthInfo(authInfo); - } - nat.setStunServerUsername(username); - getLc().setNatPolicy(nat); - } - - public void setTurnPassword(String password) { - if (getLc() == null) return; - NatPolicy nat = getOrCreateNatPolicy(); - if (nat == null) return; - AuthInfo authInfo = getLc().findAuthInfo(null, nat.getStunServerUsername(), null); - - if (authInfo != null) { - AuthInfo cloneAuthInfo = authInfo.clone(); - getLc().removeAuthInfo(authInfo); - cloneAuthInfo.setPassword(password); - getLc().addAuthInfo(cloneAuthInfo); - } else { - authInfo = - Factory.instance() - .createAuthInfo( - nat.getStunServerUsername(), - nat.getStunServerUsername(), - password, - null, - null, - null); - getLc().addAuthInfo(authInfo); - } - } - - public MediaEncryption getMediaEncryption() { - if (getLc() == null) return null; - return getLc().getMediaEncryption(); - } - - public void setMediaEncryption(MediaEncryption menc) { - if (getLc() == null) return; - if (menc == null) return; - - getLc().setMediaEncryption(menc); - } - - public boolean isPushNotificationEnabled() { - if (getConfig() == null) return true; - return getConfig().getBool("app", "push_notification", true); - } - - public void setPushNotificationEnabled(boolean enable) { - if (getConfig() == null) return; - getConfig().setBool("app", "push_notification", enable); - - Core core = getLc(); - if (core == null) { - return; - } - - if (enable) { - // Add push infos to exisiting proxy configs - String regId = getPushNotificationRegistrationID(); - String appId = getString(R.string.gcm_defaultSenderId); - if (regId != null && core.getProxyConfigList().length > 0) { - for (ProxyConfig lpc : core.getProxyConfigList()) { - if (lpc == null) continue; - if (!lpc.isPushNotificationAllowed()) { - lpc.edit(); - lpc.setContactUriParameters(null); - lpc.done(); - if (lpc.getIdentityAddress() != null) - Log.d( - "[Push Notification] infos removed from proxy config " - + lpc.getIdentityAddress().asStringUriOnly()); - } else { - String contactInfos = - "app-id=" - + appId - + ";pn-type=" - + getString(R.string.push_type) - + ";pn-timeout=0" - + ";pn-tok=" - + regId - + ";pn-silent=1"; - String prevContactParams = lpc.getContactParameters(); - if (prevContactParams == null - || prevContactParams.compareTo(contactInfos) != 0) { - lpc.edit(); - lpc.setContactUriParameters(contactInfos); - lpc.done(); - if (lpc.getIdentityAddress() != null) - Log.d( - "[Push Notification] infos added to proxy config " - + lpc.getIdentityAddress().asStringUriOnly()); - } - } - } - Log.i( - "[Push Notification] Refreshing registers to ensure token is up to date: " - + regId); - core.refreshRegisters(); - } - } else { - if (core.getProxyConfigList().length > 0) { - for (ProxyConfig lpc : core.getProxyConfigList()) { - lpc.edit(); - lpc.setContactUriParameters(null); - lpc.done(); - if (lpc.getIdentityAddress() != null) - Log.d( - "[Push Notification] infos removed from proxy config " - + lpc.getIdentityAddress().asStringUriOnly()); - } - core.refreshRegisters(); - } - } - } - - private String getPushNotificationRegistrationID() { - if (getConfig() == null) return null; - return getConfig().getString("app", "push_notification_regid", null); - } - - public void setPushNotificationRegistrationID(String regId) { - if (getConfig() == null) return; - Log.i("[Push Notification] New token received: " + regId); - getConfig().setString("app", "push_notification_regid", (regId != null) ? regId : ""); - setPushNotificationEnabled(isPushNotificationEnabled()); - } - - public void useIpv6(Boolean enable) { - if (getLc() == null) return; - getLc().enableIpv6(enable); - } - - public boolean isUsingIpv6() { - if (getLc() == null) return false; - return getLc().ipv6Enabled(); - } - // End of network settings - - public boolean isDebugEnabled() { - if (getConfig() == null) return false; - return getConfig().getBool("app", "debug", false); - } - - // Advanced settings - public void setDebugEnabled(boolean enabled) { - if (getConfig() == null) return; - getConfig().setBool("app", "debug", enabled); - LinphoneUtils.configureLoggingService(enabled, mContext.getString(R.string.app_name)); - } - - public void setJavaLogger(boolean enabled) { - if (getConfig() == null) return; - getConfig().setBool("app", "java_logger", enabled); - LinphoneUtils.configureLoggingService( - isDebugEnabled(), mContext.getString(R.string.app_name)); - } - - public boolean useJavaLogger() { - if (getConfig() == null) return false; - return getConfig().getBool("app", "java_logger", false); - } - - public boolean isAutoStartEnabled() { - if (getConfig() == null) return false; - return getConfig().getBool("app", "auto_start", false); - } - - public void setAutoStart(boolean autoStartEnabled) { - if (getConfig() == null) return; - getConfig().setBool("app", "auto_start", autoStartEnabled); - } - - public String getSharingPictureServerUrl() { - if (getLc() == null) return null; - return getLc().getFileTransferServer(); - } - - public void setSharingPictureServerUrl(String url) { - if (getLc() == null) return; - getLc().setFileTransferServer(url); - } - - public String getLogCollectionUploadServerUrl() { - if (getLc() == null) return null; - return getLc().getLogCollectionUploadServerUrl(); - } - - public void setLogCollectionUploadServerUrl(String url) { - if (getLc() == null) return; - getLc().setLogCollectionUploadServerUrl(url); - } - - public String getRemoteProvisioningUrl() { - if (getLc() == null) return null; - return getLc().getProvisioningUri(); - } - - public void setRemoteProvisioningUrl(String url) { - if (getLc() == null) return; - if (url != null && url.isEmpty()) { - url = null; - } - getLc().setProvisioningUri(url); - } - - public String getDefaultDisplayName() { - if (getLc() == null) return null; - return getLc().createPrimaryContactParsed().getDisplayName(); - } - - public void setDefaultDisplayName(String displayName) { - if (getLc() == null) return; - Address primary = getLc().createPrimaryContactParsed(); - primary.setDisplayName(displayName); - getLc().setPrimaryContact(primary.asString()); - } - - public String getDefaultUsername() { - if (getLc() == null) return null; - return getLc().createPrimaryContactParsed().getUsername(); - } - - public void setDefaultUsername(String username) { - if (getLc() == null) return; - Address primary = getLc().createPrimaryContactParsed(); - primary.setUsername(username); - getLc().setPrimaryContact(primary.asString()); - } - // End of advanced settings - - public TunnelConfig getTunnelConfig() { - if (getLc() == null) return null; - if (getLc().tunnelAvailable()) { - Tunnel tunnel = getLc().getTunnel(); - if (mTunnelConfig == null) { - TunnelConfig[] servers = tunnel.getServers(); - if (servers.length > 0) { - mTunnelConfig = servers[0]; - } else { - mTunnelConfig = Factory.instance().createTunnelConfig(); - } - } - return mTunnelConfig; - } else { - return null; - } - } - - public String getTunnelHost() { - TunnelConfig config = getTunnelConfig(); - if (config != null) { - return config.getHost(); - } else { - return null; - } - } - - public void setTunnelHost(String host) { - TunnelConfig config = getTunnelConfig(); - if (config != null) { - config.setHost(host); - LinphoneManager.getInstance().initTunnelFromConf(); - } - } - - public int getTunnelPort() { - TunnelConfig config = getTunnelConfig(); - if (config != null) { - return config.getPort(); - } else { - return -1; - } - } - - public void setTunnelPort(int port) { - TunnelConfig config = getTunnelConfig(); - if (config != null) { - config.setPort(port); - LinphoneManager.getInstance().initTunnelFromConf(); - } - } - - public String getTunnelHost2() { - TunnelConfig config = getTunnelConfig(); - if (config != null) { - return config.getHost2(); - } else { - return null; - } - } - - public void setTunnelHost2(String host) { - TunnelConfig config = getTunnelConfig(); - if (config != null) { - config.setHost2(host); - LinphoneManager.getInstance().initTunnelFromConf(); - } - } - - public int getTunnelPort2() { - TunnelConfig config = getTunnelConfig(); - if (config != null) { - return config.getPort2(); - } else { - return -1; - } - } - - public void setTunnelPort2(int port) { - TunnelConfig config = getTunnelConfig(); - if (config != null) { - config.setPort2(port); - LinphoneManager.getInstance().initTunnelFromConf(); - } - } - - public void enableTunnelDualMode(boolean enable) { - LinphoneManager.getInstance().initTunnelFromConf(); - getLc().getTunnel().enableDualMode(enable); - } - - public boolean isTunnelDualModeEnabled() { - Tunnel tunnel = getLc().getTunnel(); - if (tunnel != null) { - return tunnel.dualModeEnabled(); - } - return false; - } - - public String getTunnelMode() { - return getConfig().getString("app", "tunnel", null); - } - - public void setTunnelMode(String mode) { - getConfig().setString("app", "tunnel", mode); - LinphoneManager.getInstance().initTunnelFromConf(); - } - - // End of tunnel settings - public boolean adaptiveRateControlEnabled() { - if (getLc() == null) return false; - return getLc().adaptiveRateControlEnabled(); - } - - public void enableAdaptiveRateControl(boolean enabled) { - if (getLc() == null) return; - getLc().enableAdaptiveRateControl(enabled); - } - - public int getCodecBitrateLimit() { - if (getConfig() == null) return 36; - return getConfig().getInt("audio", "codec_bitrate_limit", 36); - } - - public void setCodecBitrateLimit(int bitrate) { - if (getConfig() == null) return; - getConfig().setInt("audio", "codec_bitrate_limit", bitrate); - } - - public String getXmlrpcUrl() { - if (getConfig() == null) return null; - return getConfig().getString("assistant", "xmlrpc_url", null); - } - - public String getLinkPopupTime() { - if (getConfig() == null) return null; - return getConfig().getString("app", "link_popup_time", null); - } - - public void setLinkPopupTime(String date) { - if (getConfig() == null) return; - getConfig().setString("app", "link_popup_time", date); - } - - public boolean isLinkPopupEnabled() { - if (getConfig() == null) return true; - return getConfig().getBool("app", "link_popup_enabled", true); - } - - public void enableLinkPopup(boolean enable) { - if (getConfig() == null) return; - getConfig().setBool("app", "link_popup_enabled", enable); - } - - public boolean isDNDSettingsPopupEnabled() { - if (getConfig() == null) return true; - return getConfig().getBool("app", "dnd_settings_popup_enabled", true); - } - - public void enableDNDSettingsPopup(boolean enable) { - if (getConfig() == null) return; - getConfig().setBool("app", "dnd_settings_popup_enabled", enable); - } - - public boolean isLimeSecurityPopupEnabled() { - if (getConfig() == null) return true; - return getConfig().getBool("app", "lime_security_popup_enabled", true); - } - - public void enableLimeSecurityPopup(boolean enable) { - if (getConfig() == null) return; - getConfig().setBool("app", "lime_security_popup_enabled", enable); - } - - public String getDebugPopupAddress() { - if (getConfig() == null) return null; - return getConfig().getString("app", "debug_popup_magic", null); - } - - public String getActivityToLaunchOnIncomingReceived() { - if (getConfig() == null) return "org.linphone.call.CallIncomingActivity"; - return getConfig() - .getString( - "app", "incoming_call_activity", "org.linphone.call.CallIncomingActivity"); - } - - public void setActivityToLaunchOnIncomingReceived(String name) { - if (getConfig() == null) return; - getConfig().setString("app", "incoming_call_activity", name); - } - - public boolean getServiceNotificationVisibility() { - if (getConfig() == null) return false; - return getConfig().getBool("app", "show_service_notification", false); - } - - public void setServiceNotificationVisibility(boolean enable) { - if (getConfig() == null) return; - getConfig().setBool("app", "show_service_notification", enable); - } - - public String getCheckReleaseUrl() { - if (getConfig() == null) return null; - return getConfig().getString("misc", "version_check_url_root", null); - } - - public int getLastCheckReleaseTimestamp() { - if (getConfig() == null) return 0; - return getConfig().getInt("app", "version_check_url_last_timestamp", 0); - } - - public void setLastCheckReleaseTimestamp(int timestamp) { - if (getConfig() == null) return; - getConfig().setInt("app", "version_check_url_last_timestamp", timestamp); - } - - public boolean isOverlayEnabled() { - if (Version.sdkAboveOrEqual(Version.API26_O_80) - && mContext.getResources().getBoolean(R.bool.allow_pip_while_video_call)) { - // Disable overlay and use PIP feature - return false; - } - if (getConfig() == null) return false; - return getConfig().getBool("app", "display_overlay", false); - } - - public void enableOverlay(boolean enable) { - if (getConfig() == null) return; - getConfig().setBool("app", "display_overlay", enable); - } - - public boolean isDeviceRingtoneEnabled() { - int readExternalStorage = - mContext.getPackageManager() - .checkPermission( - Manifest.permission.READ_EXTERNAL_STORAGE, - mContext.getPackageName()); - if (getConfig() == null) return readExternalStorage == PackageManager.PERMISSION_GRANTED; - return getConfig().getBool("app", "device_ringtone", true) - && readExternalStorage == PackageManager.PERMISSION_GRANTED; - } - - public void enableDeviceRingtone(boolean enable) { - if (getConfig() == null) return; - getConfig().setBool("app", "device_ringtone", enable); - LinphoneManager.getInstance().enableDeviceRingtone(enable); - } - - public boolean isIncomingCallVibrationEnabled() { - if (getConfig() == null) return true; - return getConfig().getBool("app", "incoming_call_vibration", true); - } - - public void enableIncomingCallVibration(boolean enable) { - if (getConfig() == null) return; - getConfig().setBool("app", "incoming_call_vibration", enable); - } - - public boolean isBisFeatureEnabled() { - if (getConfig() == null) return true; - return getConfig().getBool("app", "bis_feature", true); - } - - public boolean isAutoAnswerEnabled() { - if (getConfig() == null) return false; - return getConfig().getBool("app", "auto_answer", false); - } - - public void enableAutoAnswer(boolean enable) { - if (getConfig() == null) return; - getConfig().setBool("app", "auto_answer", enable); - } - - public int getAutoAnswerTime() { - if (getConfig() == null) return 0; - return getConfig().getInt("app", "auto_answer_delay", 0); - } - - public void setAutoAnswerTime(int time) { - if (getConfig() == null) return; - getConfig().setInt("app", "auto_answer_delay", time); - } - - public void disableFriendsStorage() { - if (getConfig() == null) return; - getConfig().setBool("misc", "store_friends", false); - } - - public boolean useBasicChatRoomFor1To1() { - if (getConfig() == null) return false; - return getConfig().getBool("app", "prefer_basic_chat_room", false); - } - - // 0 is download all, -1 is disable feature, else size is bytes - public int getAutoDownloadFileMaxSize() { - if (getLc() == null) return -1; - return getLc().getMaxSizeForAutoDownloadIncomingFiles(); - } - - // 0 is download all, -1 is disable feature, else size is bytes - public void setAutoDownloadFileMaxSize(int size) { - if (getLc() == null) return; - getLc().setMaxSizeForAutoDownloadIncomingFiles(size); - } - - public void setDownloadedImagesVisibleInNativeGallery(boolean visible) { - if (getConfig() == null) return; - getConfig().setBool("app", "make_downloaded_images_public_in_gallery", visible); - } - - public boolean makeDownloadedImagesVisibleInNativeGallery() { - if (getConfig() == null) return false; - return getConfig().getBool("app", "make_downloaded_images_public_in_gallery", true); - } - - public boolean hasPowerSaverDialogBeenPrompted() { - if (getConfig() == null) return false; - return getConfig().getBool("app", "android_power_saver_dialog", false); - } - - public void powerSaverDialogPrompted(boolean b) { - if (getConfig() == null) return; - getConfig().setBool("app", "android_power_saver_dialog", b); - } - - public boolean isDarkModeEnabled() { - if (getConfig() == null) return false; - if (!mContext.getResources().getBoolean(R.bool.allow_dark_mode)) return false; - - boolean useNightModeDefault = - AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES; - if (mContext != null) { - int nightMode = - mContext.getResources().getConfiguration().uiMode - & Configuration.UI_MODE_NIGHT_MASK; - if (nightMode == Configuration.UI_MODE_NIGHT_YES) { - useNightModeDefault = true; - } - } - - return getConfig().getBool("app", "dark_mode", useNightModeDefault); - } - - public void enableDarkMode(boolean enable) { - if (getConfig() == null) return; - getConfig().setBool("app", "dark_mode", enable); - } - - public String getDeviceName(Context context) { - String defaultValue = Compatibility.getDeviceName(context); - if (getConfig() == null) return defaultValue; - return getConfig().getString("app", "device_name", defaultValue); - } - - public void setDeviceName(String name) { - if (getConfig() == null) return; - getConfig().setString("app", "device_name", name); - } - - public boolean isEchoCancellationCalibrationDone() { - if (getConfig() == null) return false; - return getConfig().getBool("app", "echo_cancellation_calibration_done", false); - } - - public void setEchoCancellationCalibrationDone(boolean done) { - if (getConfig() == null) return; - getConfig().setBool("app", "echo_cancellation_calibration_done", done); - } - - public boolean isOpenH264CodecDownloadEnabled() { - if (getConfig() == null) return true; - return getConfig().getBool("app", "open_h264_download_enabled", true); - } - - public void setOpenH264CodecDownloadEnabled(boolean enable) { - if (getConfig() == null) return; - getConfig().setBool("app", "open_h264_download_enabled", enable); - } - - public boolean isVideoPreviewEnabled() { - if (getConfig() == null) return false; - return isVideoEnabled() && getConfig().getBool("app", "video_preview", false); - } - - public void setVideoPreviewEnabled(boolean enabled) { - if (getConfig() == null) return; - getConfig().setBool("app", "video_preview", enabled); - } - - public boolean shortcutsCreationEnabled() { - if (getConfig() == null) return false; - return getConfig().getBool("app", "shortcuts", false); - } - - public void enableChatRoomsShortcuts(boolean enable) { - if (getConfig() == null) return; - getConfig().setBool("app", "shortcuts", enable); - } - - public boolean hideEmptyChatRooms() { - if (getConfig() == null) return true; - return getConfig().getBool("misc", "hide_empty_chat_rooms", true); - } - - public void setHideEmptyChatRooms(boolean hide) { - if (getConfig() == null) return; - getConfig().setBool("misc", "hide_empty_chat_rooms", hide); - } - - public boolean hideRemovedProxiesChatRooms() { - if (getConfig() == null) return true; - return getConfig().getBool("misc", "hide_chat_rooms_from_removed_proxies", true); - } - - public void setHideRemovedProxiesChatRooms(boolean hide) { - if (getConfig() == null) return; - getConfig().setBool("misc", "hide_chat_rooms_from_removed_proxies", hide); - } - - public void enableEphemeralMessages(boolean enable) { - if (getConfig() == null) return; - getConfig().setBool("app", "ephemeral", enable); - } - - public boolean isEphemeralMessagesEnabled() { - if (getConfig() == null) return true; - return getConfig().getBool("app", "ephemeral", false); - } -} diff --git a/app/src/main/java/org/linphone/settings/MenuSettingsFragment.java b/app/src/main/java/org/linphone/settings/MenuSettingsFragment.java deleted file mode 100644 index fc7d82f55..000000000 --- a/app/src/main/java/org/linphone/settings/MenuSettingsFragment.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.TextView; -import androidx.annotation.Nullable; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Core; -import org.linphone.core.ProxyConfig; -import org.linphone.settings.widget.BasicSetting; -import org.linphone.settings.widget.LedSetting; -import org.linphone.settings.widget.SettingListenerBase; -import org.linphone.utils.LinphoneUtils; - -public class MenuSettingsFragment extends SettingsFragment { - private View mRootView; - private BasicSetting mTunnel, mAudio, mVideo, mCall, mChat, mNetwork, mAdvanced, mContact; - private LinearLayout mAccounts; - private TextView mAccountsHeader; - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - mRootView = inflater.inflate(R.layout.settings, container, false); - - loadSettings(); - setListeners(); - - return mRootView; - } - - @Override - public void onResume() { - super.onResume(); - - updateValues(); - } - - private void loadSettings() { - mAccounts = mRootView.findViewById(R.id.accounts_settings_list); - mAccountsHeader = mRootView.findViewById(R.id.accounts_settings_list_header); - - mTunnel = mRootView.findViewById(R.id.pref_tunnel); - - mAudio = mRootView.findViewById(R.id.pref_audio); - - mVideo = mRootView.findViewById(R.id.pref_video); - - mCall = mRootView.findViewById(R.id.pref_call); - - mChat = mRootView.findViewById(R.id.pref_chat); - - mNetwork = mRootView.findViewById(R.id.pref_network); - - mAdvanced = mRootView.findViewById(R.id.pref_advanced); - - mContact = mRootView.findViewById(R.id.pref_contact); - } - - private void setListeners() { - mTunnel.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - ((SettingsActivity) getActivity()) - .showSettings( - new TunnelSettingsFragment(), - getString(R.string.pref_tunnel_title)); - } - }); - - mAudio.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - ((SettingsActivity) getActivity()) - .showSettings( - new AudioSettingsFragment(), - getString(R.string.pref_audio_title)); - } - }); - - mVideo.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - ((SettingsActivity) getActivity()) - .showSettings( - new VideoSettingsFragment(), - getString(R.string.pref_video_title)); - } - }); - - mCall.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - ((SettingsActivity) getActivity()) - .showSettings( - new CallSettingsFragment(), - getString(R.string.pref_call_title)); - } - }); - - mChat.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - ((SettingsActivity) getActivity()) - .showSettings( - new ChatSettingsFragment(), - getString(R.string.pref_chat_title)); - } - }); - - mNetwork.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - ((SettingsActivity) getActivity()) - .showSettings( - new NetworkSettingsFragment(), - getString(R.string.pref_network_title)); - } - }); - - mAdvanced.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - ((SettingsActivity) getActivity()) - .showSettings( - new AdvancedSettingsFragment(), - getString(R.string.pref_advanced_title)); - } - }); - - mContact.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - ((SettingsActivity) getActivity()) - .showSettings( - new ContactSettingsFragment(), - getString(R.string.pref_contact_title)); - } - }); - } - - private void updateValues() { - Core core = LinphoneManager.getCore(); - if (core != null) { - mTunnel.setVisibility(core.tunnelAvailable() ? View.VISIBLE : View.GONE); - initAccounts(core); - } - - if (getResources().getBoolean(R.bool.hide_accounts)) { - mAccounts.setVisibility(View.GONE); - mAccountsHeader.setVisibility(View.GONE); - } - } - - private void initAccounts(Core core) { - mAccounts.removeAllViews(); - ProxyConfig[] proxyConfigs = core.getProxyConfigList(); - - if (proxyConfigs == null || proxyConfigs.length == 0) { - mAccountsHeader.setVisibility(View.GONE); - } else { - mAccountsHeader.setVisibility(View.VISIBLE); - int i = 0; - for (ProxyConfig proxyConfig : proxyConfigs) { - final LedSetting account = new LedSetting(getActivity()); - account.setTitle( - LinphoneUtils.getDisplayableAddress(proxyConfig.getIdentityAddress())); - - if (proxyConfig.equals(core.getDefaultProxyConfig())) { - account.setSubtitle(getString(R.string.default_account_flag)); - } - - switch (proxyConfig.getState()) { - case Ok: - account.setColor(LedSetting.Color.GREEN); - break; - case Failed: - account.setColor(LedSetting.Color.RED); - break; - case Progress: - account.setColor(LedSetting.Color.ORANGE); - break; - case None: - case Cleared: - account.setColor(LedSetting.Color.GRAY); - break; - } - - final int accountIndex = i; - account.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - ((SettingsActivity) getActivity()) - .showAccountSettings(accountIndex, true); - } - }); - - mAccounts.addView(account); - i += 1; - } - } - } -} diff --git a/app/src/main/java/org/linphone/settings/NetworkSettingsFragment.java b/app/src/main/java/org/linphone/settings/NetworkSettingsFragment.java deleted file mode 100644 index 1aeaca3fd..000000000 --- a/app/src/main/java/org/linphone/settings/NetworkSettingsFragment.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings; - -import android.content.Intent; -import android.os.Bundle; -import android.text.InputType; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.Nullable; -import org.linphone.R; -import org.linphone.core.tools.Log; -import org.linphone.settings.widget.BasicSetting; -import org.linphone.settings.widget.SettingListenerBase; -import org.linphone.settings.widget.SwitchSetting; -import org.linphone.settings.widget.TextSetting; -import org.linphone.utils.DeviceUtils; -import org.linphone.utils.PushNotificationUtils; - -public class NetworkSettingsFragment extends SettingsFragment { - private View mRootView; - private LinphonePreferences mPrefs; - - private SwitchSetting mWifiOnly, mIpv6, mPush, mRandomPorts, mIce, mTurn; - private TextSetting mSipPort, mStunServer, mTurnUsername, mTurnPassword; - private BasicSetting mAndroidBatterySaverSettings; - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - mRootView = inflater.inflate(R.layout.settings_network, container, false); - - loadSettings(); - - return mRootView; - } - - @Override - public void onResume() { - super.onResume(); - - mPrefs = LinphonePreferences.instance(); - - updateValues(); - } - - private void loadSettings() { - mWifiOnly = mRootView.findViewById(R.id.pref_wifi_only); - - mIpv6 = mRootView.findViewById(R.id.pref_ipv6); - - mPush = mRootView.findViewById(R.id.pref_push_notification); - - mRandomPorts = mRootView.findViewById(R.id.pref_transport_use_random_ports); - - mIce = mRootView.findViewById(R.id.pref_ice_enable); - - mTurn = mRootView.findViewById(R.id.pref_turn_enable); - - mSipPort = mRootView.findViewById(R.id.pref_sip_port); - mSipPort.setInputType(InputType.TYPE_CLASS_NUMBER); - - mStunServer = mRootView.findViewById(R.id.pref_stun_server); - mStunServer.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); - - mTurnUsername = mRootView.findViewById(R.id.pref_turn_username); - - mTurnPassword = mRootView.findViewById(R.id.pref_turn_passwd); - mTurnPassword.setInputType( - InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - - mAndroidBatterySaverSettings = - mRootView.findViewById(R.id.pref_android_battery_protected_settings); - } - - private void setListeners() { - mWifiOnly.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.setWifiOnlyEnabled(newValue); - } - }); - - mIpv6.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.useIpv6(newValue); - } - }); - - mPush.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.setPushNotificationEnabled(newValue); - } - }); - - mRandomPorts.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.useRandomPort(newValue); - mSipPort.setVisibility( - mPrefs.isUsingRandomPort() ? View.GONE : View.VISIBLE); - } - }); - - mIce.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.setIceEnabled(newValue); - } - }); - - mTurn.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.setTurnEnabled(newValue); - mTurnUsername.setEnabled(mPrefs.isTurnEnabled()); - mTurnPassword.setEnabled(mPrefs.isTurnEnabled()); - } - }); - - mSipPort.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - try { - mPrefs.setSipPort(Integer.valueOf(newValue)); - } catch (NumberFormatException nfe) { - Log.e(nfe); - } - } - }); - - mStunServer.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - mPrefs.setStunServer(newValue); - mIce.setEnabled( - mPrefs.getStunServer() != null - && !mPrefs.getStunServer().isEmpty()); - mTurn.setEnabled( - mPrefs.getStunServer() != null - && !mPrefs.getStunServer().isEmpty()); - if (newValue == null || newValue.isEmpty()) { - mIce.setChecked(false); - mTurn.setChecked(false); - } - } - }); - - mTurnUsername.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - mPrefs.setTurnUsername(newValue); - } - }); - - mTurnPassword.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - mPrefs.setTurnPassword(newValue); - } - }); - - mAndroidBatterySaverSettings.setListener( - new SettingListenerBase() { - @Override - public void onClicked() { - mPrefs.powerSaverDialogPrompted(true); - Intent intent = DeviceUtils.getDevicePowerManagerIntent(getActivity()); - if (intent != null) { - try { - startActivity(intent); - } catch (SecurityException se) { - Log.e("[Network Settings] Security exception: ", se); - } - } - } - }); - } - - private void updateValues() { - mWifiOnly.setChecked(mPrefs.isWifiOnlyEnabled()); - - mIpv6.setChecked(mPrefs.isUsingIpv6()); - - mPush.setChecked(mPrefs.isPushNotificationEnabled()); - mPush.setVisibility( - PushNotificationUtils.isAvailable(getActivity()) ? View.VISIBLE : View.GONE); - - mRandomPorts.setChecked(mPrefs.isUsingRandomPort()); - - mIce.setChecked(mPrefs.isIceEnabled()); - mIce.setEnabled(mPrefs.getStunServer() != null && !mPrefs.getStunServer().isEmpty()); - - mTurn.setChecked(mPrefs.isTurnEnabled()); - mTurn.setEnabled(mPrefs.getStunServer() != null && !mPrefs.getStunServer().isEmpty()); - - mSipPort.setValue(mPrefs.getSipPort()); - mSipPort.setVisibility(mPrefs.isUsingRandomPort() ? View.GONE : View.VISIBLE); - - mStunServer.setValue(mPrefs.getStunServer()); - - mTurnUsername.setValue(mPrefs.getTurnUsername()); - mTurnUsername.setEnabled(mPrefs.isTurnEnabled()); - mTurnPassword.setEnabled(mPrefs.isTurnEnabled()); - - mAndroidBatterySaverSettings.setVisibility( - DeviceUtils.hasDevicePowerManager(getActivity()) ? View.VISIBLE : View.GONE); - - setListeners(); - } -} diff --git a/app/src/main/java/org/linphone/settings/SettingsActivity.java b/app/src/main/java/org/linphone/settings/SettingsActivity.java deleted file mode 100644 index 0f5176b3c..000000000 --- a/app/src/main/java/org/linphone/settings/SettingsActivity.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings; - -import android.Manifest; -import android.app.Fragment; -import android.app.FragmentManager; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; -import android.provider.Settings; -import androidx.annotation.Nullable; -import org.linphone.R; -import org.linphone.activities.MainActivity; -import org.linphone.compatibility.Compatibility; -import org.linphone.core.tools.Log; -import org.linphone.utils.LinphoneUtils; - -public class SettingsActivity extends MainActivity { - private static final int PERMISSIONS_REQUEST_OVERLAY = 206; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mOnBackPressGoHome = false; - mAlwaysHideTabBar = true; - } - - @Override - protected void onStart() { - super.onStart(); - - Fragment currentFragment = getFragmentManager().findFragmentById(R.id.fragmentContainer); - if (currentFragment == null) { - if (getIntent() != null && getIntent().getExtras() != null) { - Bundle extras = getIntent().getExtras(); - if (isTablet() || !extras.containsKey("Account")) { - showSettingsMenu(); - } - handleIntentExtras(extras); - } else { - showSettingsMenu(); - } - } - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - - // Clean fragments stack upon return - while (getFragmentManager().getBackStackEntryCount() > 0) { - getFragmentManager().popBackStackImmediate(); - } - - handleIntentExtras(intent.getExtras()); - } - - @Override - protected void onResume() { - super.onResume(); - - hideTabBar(); - - int count = getFragmentManager().getBackStackEntryCount(); - if (count == 0) { - showTopBarWithTitle(getString(R.string.settings)); - } else { - FragmentManager.BackStackEntry entry = - getFragmentManager().getBackStackEntryAt(count - 1); - showTopBarWithTitle(entry.getName()); - } - } - - @Override - public void goBack() { - if (!isTablet()) { - if (popBackStack()) { - showTopBarWithTitle(getString(R.string.settings)); - return; - } - } - super.goBack(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - if (requestCode == PERMISSIONS_REQUEST_OVERLAY) { - if (Compatibility.canDrawOverlays(this)) { - LinphonePreferences.instance().enableOverlay(true); - } - } - super.onActivityResult(requestCode, resultCode, data); - } - - @Override - public void onRequestPermissionsResult( - int requestCode, String[] permissions, int[] grantResults) { - if (permissions.length <= 0) return; - if (requestCode == MainActivity.FRAGMENT_SPECIFIC_PERMISSION) { - for (int i = 0; i < permissions.length; i++) { - Log.i( - "[Permission] " - + permissions[i] - + " is " - + (grantResults[i] == PackageManager.PERMISSION_GRANTED - ? "granted" - : "denied")); - if (permissions[i].equals(Manifest.permission.CAMERA) - && grantResults[i] == PackageManager.PERMISSION_GRANTED) { - LinphoneUtils.reloadVideoDevices(); - LinphonePreferences.instance().setVideoPreviewEnabled(true); - } - } - } else { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - } - - private void handleIntentExtras(Bundle extras) { - if (extras != null && extras.containsKey("Account")) { - int accountIndex = extras.getInt("Account"); - showAccountSettings(accountIndex, isTablet()); - } - } - - private void showSettingsMenu() { - Bundle extras = new Bundle(); - MenuSettingsFragment menuSettingsFragment = new MenuSettingsFragment(); - menuSettingsFragment.setArguments(extras); - changeFragment(menuSettingsFragment, getString(R.string.settings), false); - showTopBarWithTitle(getString(R.string.settings)); - } - - public void showSettings(Fragment fragment, String name) { - changeFragment(fragment, name, true); - showTopBarWithTitle(name); - } - - public void showAccountSettings(int accountIndex, boolean isChild) { - Bundle extras = new Bundle(); - extras.putInt("Account", accountIndex); - AccountSettingsFragment accountSettingsFragment = new AccountSettingsFragment(); - accountSettingsFragment.setArguments(extras); - changeFragment(accountSettingsFragment, getString(R.string.pref_sipaccount), isChild); - showTopBarWithTitle(getString(R.string.pref_sipaccount)); - } - - public boolean checkAndRequestOverlayPermission() { - Log.i( - "[Permission] Draw overlays permission is " - + (Compatibility.canDrawOverlays(this) ? "granted" : "denied")); - if (!Compatibility.canDrawOverlays(this)) { - Log.i("[Permission] Asking for overlay"); - Intent intent = - new Intent( - Settings.ACTION_MANAGE_OVERLAY_PERMISSION, - Uri.parse("package:" + getPackageName())); - startActivityForResult(intent, PERMISSIONS_REQUEST_OVERLAY); - return false; - } - return true; - } -} diff --git a/app/src/main/java/org/linphone/settings/SettingsFragment.java b/app/src/main/java/org/linphone/settings/SettingsFragment.java deleted file mode 100644 index 924edf31c..000000000 --- a/app/src/main/java/org/linphone/settings/SettingsFragment.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings; - -import android.app.Fragment; - -public abstract class SettingsFragment extends Fragment {} diff --git a/app/src/main/java/org/linphone/settings/TunnelSettingsFragment.java b/app/src/main/java/org/linphone/settings/TunnelSettingsFragment.java deleted file mode 100644 index 03c592a16..000000000 --- a/app/src/main/java/org/linphone/settings/TunnelSettingsFragment.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings; - -import android.os.Bundle; -import android.text.InputType; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.Nullable; -import org.linphone.R; -import org.linphone.core.tools.Log; -import org.linphone.settings.widget.ListSetting; -import org.linphone.settings.widget.SettingListenerBase; -import org.linphone.settings.widget.SwitchSetting; -import org.linphone.settings.widget.TextSetting; - -public class TunnelSettingsFragment extends SettingsFragment { - private View mRootView; - private LinphonePreferences mPrefs; - - private TextSetting mHost, mPort, mHost2, mPort2; - private SwitchSetting mDualMode; - private ListSetting mMode; - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - mRootView = inflater.inflate(R.layout.settings_tunnel, container, false); - - loadSettings(); - - return mRootView; - } - - @Override - public void onResume() { - super.onResume(); - - mPrefs = LinphonePreferences.instance(); - - updateValues(); - } - - private void loadSettings() { - mHost = mRootView.findViewById(R.id.pref_tunnel_host); - - mPort = mRootView.findViewById(R.id.pref_tunnel_port); - mPort.setInputType(InputType.TYPE_CLASS_NUMBER); - - mHost2 = mRootView.findViewById(R.id.pref_tunnel_host_2); - - mPort2 = mRootView.findViewById(R.id.pref_tunnel_port_2); - mPort2.setInputType(InputType.TYPE_CLASS_NUMBER); - - mMode = mRootView.findViewById(R.id.pref_tunnel_mode); - - mDualMode = mRootView.findViewById(R.id.pref_tunnel_dual_mode); - } - - private void setListeners() { - mHost.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - mPrefs.setTunnelHost(newValue); - } - }); - - mPort.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - try { - mPrefs.setTunnelPort(Integer.valueOf(newValue)); - } catch (NumberFormatException nfe) { - Log.e(nfe); - } - } - }); - - mHost2.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - mPrefs.setTunnelHost2(newValue); - } - }); - - mPort2.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - try { - mPrefs.setTunnelPort2(Integer.valueOf(newValue)); - } catch (NumberFormatException nfe) { - Log.e(nfe); - } - } - }); - - mMode.setListener( - new SettingListenerBase() { - @Override - public void onListValueChanged(int position, String newLabel, String newValue) { - mPrefs.setTunnelMode(newValue); - } - }); - - mDualMode.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.enableTunnelDualMode(newValue); - } - }); - } - - private void updateValues() { - mHost.setValue(mPrefs.getTunnelHost()); - - mPort.setValue(mPrefs.getTunnelPort()); - - mHost2.setValue(mPrefs.getTunnelHost2()); - - mPort2.setValue(mPrefs.getTunnelPort2()); - - mMode.setValue(mPrefs.getTunnelMode()); - - mDualMode.setChecked(mPrefs.isTunnelDualModeEnabled()); - - setListeners(); - } -} diff --git a/app/src/main/java/org/linphone/settings/VideoSettingsFragment.java b/app/src/main/java/org/linphone/settings/VideoSettingsFragment.java deleted file mode 100644 index fa520575e..000000000 --- a/app/src/main/java/org/linphone/settings/VideoSettingsFragment.java +++ /dev/null @@ -1,350 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings; - -import android.Manifest; -import android.os.Bundle; -import android.text.InputType; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.TextView; -import androidx.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Core; -import org.linphone.core.Factory; -import org.linphone.core.PayloadType; -import org.linphone.core.VideoDefinition; -import org.linphone.core.tools.Log; -import org.linphone.mediastream.Version; -import org.linphone.settings.widget.ListSetting; -import org.linphone.settings.widget.SettingListenerBase; -import org.linphone.settings.widget.SwitchSetting; -import org.linphone.settings.widget.TextSetting; - -public class VideoSettingsFragment extends SettingsFragment { - private View mRootView; - private LinphonePreferences mPrefs; - - private SwitchSetting mEnable, mAutoInitiate, mAutoAccept, mOverlay, mVideoPreview; - private ListSetting mPreset, mSize, mFps; - private TextSetting mBandwidth; - private LinearLayout mVideoCodecs; - private TextView mVideoCodecsHeader; - private ListSetting mCameraDevices; - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - mRootView = inflater.inflate(R.layout.settings_video, container, false); - - loadSettings(); - - return mRootView; - } - - @Override - public void onResume() { - super.onResume(); - - mPrefs = LinphonePreferences.instance(); - - updateValues(); - } - - private void loadSettings() { - mEnable = mRootView.findViewById(R.id.pref_video_enable); - - mVideoPreview = mRootView.findViewById(R.id.pref_video_preview); - - mAutoInitiate = mRootView.findViewById(R.id.pref_video_initiate_call_with_video); - - mAutoAccept = mRootView.findViewById(R.id.pref_video_automatically_accept_video); - - mCameraDevices = mRootView.findViewById(R.id.pref_video_camera_device); - initCameraDevicesList(); - - mOverlay = mRootView.findViewById(R.id.pref_overlay); - - mPreset = mRootView.findViewById(R.id.pref_video_preset); - - mSize = mRootView.findViewById(R.id.pref_preferred_video_size); - initVideoSizeList(); - - mFps = mRootView.findViewById(R.id.pref_preferred_fps); - initFpsList(); - - mBandwidth = mRootView.findViewById(R.id.pref_bandwidth_limit); - mBandwidth.setInputType(InputType.TYPE_CLASS_NUMBER); - - mVideoCodecs = mRootView.findViewById(R.id.pref_video_codecs); - mVideoCodecsHeader = mRootView.findViewById(R.id.pref_video_codecs_header); - } - - private void setListeners() { - mEnable.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.enableVideo(newValue); - if (!newValue) { - mVideoPreview.setChecked(false); - mAutoAccept.setChecked(false); - mAutoInitiate.setChecked(false); - } - updateVideoSettingsVisibility(newValue); - } - }); - - mVideoPreview.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - if (newValue) { - if (!((SettingsActivity) getActivity()) - .checkPermission(Manifest.permission.CAMERA)) { - ((SettingsActivity) getActivity()) - .requestPermissionIfNotGranted(Manifest.permission.CAMERA); - } else { - mPrefs.setVideoPreviewEnabled(true); - } - } else { - mPrefs.setVideoPreviewEnabled(false); - } - } - }); - - mAutoInitiate.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.setInitiateVideoCall(newValue); - } - }); - - mAutoAccept.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.setAutomaticallyAcceptVideoRequests(newValue); - } - }); - - mCameraDevices.setListener( - new SettingListenerBase() { - @Override - public void onListValueChanged(int position, String newLabel, String newValue) { - mPrefs.setCameraDevice(newValue); - } - }); - - mOverlay.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - mPrefs.enableOverlay( - newValue - && ((SettingsActivity) getActivity()) - .checkAndRequestOverlayPermission()); - } - }); - - mPreset.setListener( - new SettingListenerBase() { - @Override - public void onListValueChanged(int position, String newLabel, String newValue) { - mPrefs.setVideoPreset(newValue); - mFps.setVisibility(newValue.equals("custom") ? View.VISIBLE : View.GONE); - mBandwidth.setVisibility( - newValue.equals("custom") ? View.VISIBLE : View.GONE); - } - }); - - mSize.setListener( - new SettingListenerBase() { - @Override - public void onListValueChanged(int position, String newLabel, String newValue) { - mPrefs.setPreferredVideoSize(newValue); - } - }); - - mFps.setListener( - new SettingListenerBase() { - @Override - public void onListValueChanged(int position, String newLabel, String newValue) { - try { - mPrefs.setPreferredVideoFps(Integer.valueOf(newValue)); - } catch (NumberFormatException nfe) { - Log.e(nfe); - } - } - }); - - mBandwidth.setListener( - new SettingListenerBase() { - @Override - public void onTextValueChanged(String newValue) { - try { - mPrefs.setBandwidthLimit(Integer.valueOf(newValue)); - } catch (NumberFormatException nfe) { - Log.e(nfe); - } - } - }); - } - - private void updateValues() { - mEnable.setChecked(mPrefs.isVideoEnabled()); - updateVideoSettingsVisibility(mPrefs.isVideoEnabled()); - - mVideoPreview.setChecked(mPrefs.isVideoPreviewEnabled()); - - mAutoInitiate.setChecked(mPrefs.shouldInitiateVideoCall()); - - mAutoAccept.setChecked(mPrefs.shouldAutomaticallyAcceptVideoRequests()); - - mCameraDevices.setValue(mPrefs.getCameraDevice()); - - mOverlay.setChecked(mPrefs.isOverlayEnabled()); - if (Version.sdkAboveOrEqual(Version.API26_O_80) - && getResources().getBoolean(R.bool.allow_pip_while_video_call)) { - // Disable overlay and use PIP feature - mOverlay.setVisibility(View.GONE); - } - - mBandwidth.setValue(mPrefs.getBandwidthLimit()); - mBandwidth.setVisibility( - mPrefs.getVideoPreset().equals("custom") ? View.VISIBLE : View.GONE); - - mPreset.setValue(mPrefs.getVideoPreset()); - - mSize.setValue(mPrefs.getPreferredVideoSize()); - - mFps.setValue(mPrefs.getPreferredVideoFps()); - mFps.setVisibility(mPrefs.getVideoPreset().equals("custom") ? View.VISIBLE : View.GONE); - - populateVideoCodecs(); - - setListeners(); - } - - private void initVideoSizeList() { - List entries = new ArrayList<>(); - List values = new ArrayList<>(); - - for (VideoDefinition vd : Factory.instance().getSupportedVideoDefinitions()) { - entries.add(vd.getName()); - values.add(vd.getName()); - } - - mSize.setItems(entries, values); - } - - private void initFpsList() { - List entries = new ArrayList<>(); - List values = new ArrayList<>(); - - entries.add(getString(R.string.pref_none)); - values.add("0"); - for (int i = 5; i <= 30; i += 5) { - String str = Integer.toString(i); - entries.add(str); - values.add(str); - } - - mFps.setItems(entries, values); - } - - private void populateVideoCodecs() { - mVideoCodecs.removeAllViews(); - Core core = LinphoneManager.getCore(); - if (core != null) { - for (final PayloadType pt : core.getVideoPayloadTypes()) { - final SwitchSetting codec = new SwitchSetting(getActivity()); - codec.setTitle(pt.getMimeType()); - - if (pt.enabled()) { - // Never use codec.setChecked(pt.enabled) ! - codec.setChecked(true); - } - codec.setListener( - new SettingListenerBase() { - @Override - public void onBoolValueChanged(boolean newValue) { - pt.enable(newValue); - } - }); - - mVideoCodecs.addView(codec); - } - } - } - - private void updateVideoSettingsVisibility(boolean show) { - mVideoPreview.setVisibility( - show - && getResources().getBoolean(R.bool.isTablet) - && getResources() - .getBoolean(R.bool.show_camera_preview_on_dialer_on_tablets) - ? View.VISIBLE - : View.GONE); - mAutoInitiate.setVisibility(show ? View.VISIBLE : View.GONE); - mAutoAccept.setVisibility(show ? View.VISIBLE : View.GONE); - mCameraDevices.setVisibility(show ? View.VISIBLE : View.GONE); - mOverlay.setVisibility(show ? View.VISIBLE : View.GONE); - mBandwidth.setVisibility(show ? View.VISIBLE : View.GONE); - mPreset.setVisibility(show ? View.VISIBLE : View.GONE); - mSize.setVisibility(show ? View.VISIBLE : View.GONE); - mFps.setVisibility(show ? View.VISIBLE : View.GONE); - mVideoCodecs.setVisibility(show ? View.VISIBLE : View.GONE); - mVideoCodecsHeader.setVisibility(show ? View.VISIBLE : View.GONE); - - if (show) { - if (Version.sdkAboveOrEqual(Version.API26_O_80) - && getResources().getBoolean(R.bool.allow_pip_while_video_call)) { - // Disable overlay and use PIP feature - mOverlay.setVisibility(View.GONE); - } - mBandwidth.setVisibility( - mPrefs.getVideoPreset().equals("custom") ? View.VISIBLE : View.GONE); - mFps.setVisibility(mPrefs.getVideoPreset().equals("custom") ? View.VISIBLE : View.GONE); - } - } - - private void initCameraDevicesList() { - List entries = new ArrayList<>(); - List values = new ArrayList<>(); - - Core core = LinphoneManager.getCore(); - if (core != null) { - for (String camera : core.getVideoDevicesList()) { - entries.add(camera); - values.add(camera); - } - } - - mCameraDevices.setItems(entries, values); - } -} diff --git a/app/src/main/java/org/linphone/settings/widget/BasicSetting.java b/app/src/main/java/org/linphone/settings/widget/BasicSetting.java deleted file mode 100644 index 86f0b4294..000000000 --- a/app/src/main/java/org/linphone/settings/widget/BasicSetting.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings.widget; - -import android.content.Context; -import android.content.res.TypedArray; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; -import androidx.annotation.Nullable; -import org.linphone.R; - -public class BasicSetting extends LinearLayout { - protected View mView; - protected SettingListener mListener; - - private TextView mTitle; - private TextView mSubtitle; - - public BasicSetting(Context context) { - super(context); - init(null, 0, 0); - } - - public BasicSetting(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - init(attrs, 0, 0); - } - - public BasicSetting(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(attrs, defStyleAttr, 0); - } - - BasicSetting(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - init(attrs, defStyleAttr, defStyleRes); - } - - void inflateView() { - mView = - LayoutInflater.from(getContext()) - .inflate(R.layout.settings_widget_basic, this, true); - } - - public void setListener(SettingListener listener) { - mListener = listener; - } - - void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { - inflateView(); - - mTitle = mView.findViewById(R.id.setting_title); - mSubtitle = mView.findViewById(R.id.setting_subtitle); - - RelativeLayout rlayout = mView.findViewById(R.id.setting_layout); - rlayout.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - if (mTitle.isEnabled() && mListener != null) { - mListener.onClicked(); - } - } - }); - - if (attrs != null) { - TypedArray a = - getContext() - .getTheme() - .obtainStyledAttributes( - attrs, R.styleable.Settings, defStyleAttr, defStyleRes); - try { - String title = a.getString(R.styleable.Settings_title); - if (title != null) { - mTitle.setText(title); - } else { - mTitle.setVisibility(GONE); - } - - String subtitle = a.getString(R.styleable.Settings_subtitle); - if (subtitle != null) { - mSubtitle.setText(subtitle); - } else { - mSubtitle.setVisibility(GONE); - } - } finally { - a.recycle(); - } - } - } - - public void setTitle(String title) { - mTitle.setText(title); - mTitle.setVisibility(title == null || title.isEmpty() ? GONE : VISIBLE); - } - - public void setSubtitle(String subtitle) { - mSubtitle.setText(subtitle); - mSubtitle.setVisibility(subtitle == null || subtitle.isEmpty() ? GONE : VISIBLE); - } - - public void setEnabled(boolean enabled) { - mTitle.setEnabled(enabled); - mSubtitle.setEnabled(enabled); - } -} diff --git a/app/src/main/java/org/linphone/settings/widget/CheckBoxSetting.java b/app/src/main/java/org/linphone/settings/widget/CheckBoxSetting.java deleted file mode 100644 index 9f8c533ad..000000000 --- a/app/src/main/java/org/linphone/settings/widget/CheckBoxSetting.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings.widget; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.RelativeLayout; -import androidx.annotation.Nullable; -import org.linphone.R; - -public class CheckBoxSetting extends BasicSetting { - private CheckBox mCheckBox; - - public CheckBoxSetting(Context context) { - super(context); - } - - public CheckBoxSetting(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public CheckBoxSetting(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public CheckBoxSetting(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - protected void inflateView() { - mView = - LayoutInflater.from(getContext()) - .inflate(R.layout.settings_widget_checkbox, this, true); - } - - protected void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super.init(attrs, defStyleAttr, defStyleRes); - - mCheckBox = mView.findViewById(R.id.setting_checkbox); - mCheckBox.setOnCheckedChangeListener( - new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (mListener != null) { - mListener.onBoolValueChanged(isChecked); - } - } - }); - - RelativeLayout rlayout = mView.findViewById(R.id.setting_layout); - rlayout.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - if (mCheckBox.isEnabled()) { - toggle(); - } - } - }); - } - - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - mCheckBox.setEnabled(enabled); - } - - public void setChecked(boolean checked) { - mCheckBox.setChecked(checked); - } - - public boolean isChecked() { - return mCheckBox.isChecked(); - } - - private void toggle() { - mCheckBox.toggle(); - } -} diff --git a/app/src/main/java/org/linphone/settings/widget/LedSetting.java b/app/src/main/java/org/linphone/settings/widget/LedSetting.java deleted file mode 100644 index dec33d157..000000000 --- a/app/src/main/java/org/linphone/settings/widget/LedSetting.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings.widget; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.widget.ImageView; -import androidx.annotation.Nullable; -import org.linphone.R; - -public class LedSetting extends BasicSetting { - public enum Color { - GRAY, - GREEN, - ORANGE, - RED - } - - private ImageView mLed; - - public LedSetting(Context context) { - super(context); - } - - public LedSetting(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public LedSetting(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public LedSetting(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - protected void inflateView() { - mView = LayoutInflater.from(getContext()).inflate(R.layout.settings_widget_led, this, true); - } - - protected void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super.init(attrs, defStyleAttr, defStyleRes); - - mLed = mView.findViewById(R.id.setting_led); - } - - public void setColor(Color color) { - switch (color) { - case GRAY: - mLed.setImageResource(R.drawable.led_disconnected); - break; - case GREEN: - mLed.setImageResource(R.drawable.led_connected); - break; - case ORANGE: - mLed.setImageResource(R.drawable.led_inprogress); - break; - case RED: - mLed.setImageResource(R.drawable.led_error); - break; - } - } -} diff --git a/app/src/main/java/org/linphone/settings/widget/ListSetting.java b/app/src/main/java/org/linphone/settings/widget/ListSetting.java deleted file mode 100644 index 3c64515cc..000000000 --- a/app/src/main/java/org/linphone/settings/widget/ListSetting.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings.widget; - -import android.content.Context; -import android.content.res.TypedArray; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Spinner; -import androidx.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; -import org.linphone.R; - -public class ListSetting extends BasicSetting implements AdapterView.OnItemSelectedListener { - private Spinner mSpinner; - private List mItems; - private List mItemsValues; - - public ListSetting(Context context) { - super(context); - } - - public ListSetting(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public ListSetting(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public ListSetting(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - protected void inflateView() { - mView = - LayoutInflater.from(getContext()) - .inflate(R.layout.settings_widget_list, this, true); - } - - protected void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super.init(attrs, defStyleAttr, defStyleRes); - mItems = new ArrayList<>(); - mItemsValues = new ArrayList<>(); - - mSpinner = mView.findViewById(R.id.setting_spinner); - mSpinner.setOnItemSelectedListener(this); - - if (attrs != null) { - TypedArray a = - getContext() - .getTheme() - .obtainStyledAttributes( - attrs, R.styleable.Settings, defStyleAttr, defStyleRes); - try { - CharSequence[] names = a.getTextArray(R.styleable.Settings_list_items_names); - CharSequence[] values = a.getTextArray(R.styleable.Settings_list_items_values); - if (values != null && names != null) { - for (CharSequence cs : names) { - mItems.add(cs.toString()); - } - for (CharSequence cs : values) { - mItemsValues.add(cs.toString()); - } - setItems(mItems, mItemsValues); - } - } finally { - a.recycle(); - } - } - } - - public void setItems(List list, List valuesList) { - mItems = list; - mItemsValues = valuesList; - ArrayAdapter dataAdapter = - new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, list); - dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - mSpinner.setAdapter(dataAdapter); - } - - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (mListener != null && position < mItems.size()) { - String itemValue = null; - if (mItemsValues != null && position < mItemsValues.size()) { - itemValue = mItemsValues.get(position); - } - mListener.onListValueChanged(position, mItems.get(position), itemValue); - } - } - - @Override - public void onNothingSelected(AdapterView parent) {} - - public void setValue(String value) { - int index = mItemsValues.indexOf(value); - if (index == -1) { - index = mItems.indexOf(value); - } - if (index != -1) { - mSpinner.setSelection(index); - } - } - - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - mSpinner.setEnabled(enabled); - } - - public void setValue(int value) { - setValue(String.valueOf(value)); - } - - public void setValue(float value) { - setValue(String.valueOf(value)); - } - - public void setValue(double value) { - setValue(String.valueOf(value)); - } -} diff --git a/app/src/main/java/org/linphone/settings/widget/SettingListenerBase.java b/app/src/main/java/org/linphone/settings/widget/SettingListenerBase.java deleted file mode 100644 index 48c16680f..000000000 --- a/app/src/main/java/org/linphone/settings/widget/SettingListenerBase.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings.widget; - -public class SettingListenerBase implements SettingListener { - public void onClicked() {} - - public void onTextValueChanged(String newValue) {} - - public void onBoolValueChanged(boolean newValue) {} - - public void onListValueChanged(int position, String newLabel, String newValue) {} -} diff --git a/app/src/main/java/org/linphone/settings/widget/SwitchSetting.java b/app/src/main/java/org/linphone/settings/widget/SwitchSetting.java deleted file mode 100644 index 2d598b234..000000000 --- a/app/src/main/java/org/linphone/settings/widget/SwitchSetting.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings.widget; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.CompoundButton; -import android.widget.RelativeLayout; -import android.widget.Switch; -import androidx.annotation.Nullable; -import org.linphone.R; - -public class SwitchSetting extends BasicSetting { - private Switch mSwitch; - - public SwitchSetting(Context context) { - super(context); - } - - public SwitchSetting(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public SwitchSetting(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public SwitchSetting(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - protected void inflateView() { - mView = - LayoutInflater.from(getContext()) - .inflate(R.layout.settings_widget_switch, this, true); - } - - protected void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super.init(attrs, defStyleAttr, defStyleRes); - - mSwitch = mView.findViewById(R.id.setting_switch); - mSwitch.setOnCheckedChangeListener( - new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (mListener != null) { - mListener.onBoolValueChanged(isChecked); - } - } - }); - - RelativeLayout rlayout = mView.findViewById(R.id.setting_layout); - rlayout.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - if (mSwitch.isEnabled()) { - toggle(); - } - } - }); - } - - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - mSwitch.setEnabled(enabled); - } - - public void setChecked(boolean checked) { - mSwitch.setChecked(checked); - } - - public boolean isChecked() { - return mSwitch.isChecked(); - } - - private void toggle() { - mSwitch.toggle(); - } -} diff --git a/app/src/main/java/org/linphone/settings/widget/TextSetting.java b/app/src/main/java/org/linphone/settings/widget/TextSetting.java deleted file mode 100644 index 8d73cf9fa..000000000 --- a/app/src/main/java/org/linphone/settings/widget/TextSetting.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.settings.widget; - -import android.content.Context; -import android.content.res.TypedArray; -import android.text.InputType; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.EditText; -import androidx.annotation.Nullable; -import org.linphone.R; - -public class TextSetting extends BasicSetting { - private EditText mInput; - - public TextSetting(Context context) { - super(context); - } - - public TextSetting(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public TextSetting(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public TextSetting(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - protected void inflateView() { - mView = - LayoutInflater.from(getContext()) - .inflate(R.layout.settings_widget_text, this, true); - } - - protected void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super.init(attrs, defStyleAttr, defStyleRes); - - mInput = mView.findViewById(R.id.setting_input); - mInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); - - if (attrs != null) { - TypedArray a = - getContext() - .getTheme() - .obtainStyledAttributes( - attrs, R.styleable.Settings, defStyleAttr, defStyleRes); - try { - String hint = a.getString(R.styleable.Settings_hint); - mInput.setHint(hint); - } finally { - a.recycle(); - } - } - - mInput.setOnFocusChangeListener( - new OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (!hasFocus) { - if (mListener != null) { - mListener.onTextValueChanged(mInput.getText().toString()); - } - } - } - }); - } - - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - mInput.setEnabled(enabled); - } - - public void setInputType(int inputType) { - mInput.setInputType(inputType); - } - - public void setValue(String value) { - mInput.setText(value); - } - - public void setValue(int value) { - setValue(String.valueOf(value)); - } - - public void setValue(float value) { - setValue(String.valueOf(value)); - } - - public void setValue(double value) { - setValue(String.valueOf(value)); - } - - public String getValue() { - return mInput.getText().toString(); - } -} diff --git a/app/src/main/java/org/linphone/sync/Authenticator.java b/app/src/main/java/org/linphone/sync/Authenticator.java deleted file mode 100644 index 0837ccdfb..000000000 --- a/app/src/main/java/org/linphone/sync/Authenticator.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.sync; - -import android.accounts.AbstractAccountAuthenticator; -import android.accounts.Account; -import android.accounts.AccountAuthenticatorResponse; -import android.content.Context; -import android.os.Bundle; - -class Authenticator extends AbstractAccountAuthenticator { - - public Authenticator(Context context) { - super(context); - } - - @Override - public Bundle editProperties(AccountAuthenticatorResponse r, String s) { - throw new UnsupportedOperationException(); - } - - @Override - public Bundle addAccount( - AccountAuthenticatorResponse r, String s, String s2, String[] strings, Bundle bundle) { - return null; - } - - @Override - public Bundle confirmCredentials( - AccountAuthenticatorResponse r, Account account, Bundle bundle) { - return null; - } - - @Override - public Bundle getAuthToken( - AccountAuthenticatorResponse r, Account account, String s, Bundle bundle) { - throw new UnsupportedOperationException(); - } - - @Override - public String getAuthTokenLabel(String s) { - throw new UnsupportedOperationException(); - } - - @Override - public Bundle updateCredentials( - AccountAuthenticatorResponse r, Account account, String s, Bundle bundle) { - throw new UnsupportedOperationException(); - } - - @Override - public Bundle hasFeatures(AccountAuthenticatorResponse r, Account account, String[] strings) { - throw new UnsupportedOperationException(); - } -} diff --git a/app/src/main/java/org/linphone/sync/SyncAdapter.java b/app/src/main/java/org/linphone/sync/SyncAdapter.java deleted file mode 100755 index f00e80fbc..000000000 --- a/app/src/main/java/org/linphone/sync/SyncAdapter.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.sync; - -import android.accounts.Account; -import android.content.AbstractThreadedSyncAdapter; -import android.content.ContentProviderClient; -import android.content.Context; -import android.content.SyncResult; -import android.os.Bundle; - -class SyncAdapter extends AbstractThreadedSyncAdapter { - - public SyncAdapter(Context context, boolean autoInitialize) { - super(context, autoInitialize); - } - - @Override - public void onPerformSync( - Account account, - Bundle extras, - String authority, - ContentProviderClient provider, - SyncResult syncResult) {} -} diff --git a/app/src/main/java/org/linphone/sync/SyncService.java b/app/src/main/java/org/linphone/sync/SyncService.java deleted file mode 100755 index 3e5e2c2a2..000000000 --- a/app/src/main/java/org/linphone/sync/SyncService.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.sync; - -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; - -public class SyncService extends Service { - private static final Object sSyncAdapterLock = new Object(); - private static SyncAdapter sSyncAdapter = null; - - @Override - public void onCreate() { - - synchronized (sSyncAdapterLock) { - if (sSyncAdapter == null) { - sSyncAdapter = new SyncAdapter(getApplicationContext(), true); - } - } - } - - @Override - public IBinder onBind(Intent intent) { - return sSyncAdapter.getSyncAdapterBinder(); - } -} diff --git a/app/src/main/java/org/linphone/utils/AppUtils.kt b/app/src/main/java/org/linphone/utils/AppUtils.kt new file mode 100644 index 000000000..10622a345 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/AppUtils.kt @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +import android.content.ContentUris +import android.content.ContentValues +import android.provider.ContactsContract +import android.text.Spanned +import android.util.TypedValue +import androidx.core.text.HtmlCompat +import java.util.* +import java.util.regex.Pattern +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences + +/** + * Various utility methods for application + */ +class AppUtils { + companion object { + fun getString(id: Int): String { + return coreContext.context.getString(id) + } + + fun getStringWithPlural(id: Int, count: Int): String { + return coreContext.context.resources.getQuantityString(id, count, count) + } + + fun getStringWithPlural(id: Int, count: Int, value: String): String { + return coreContext.context.resources.getQuantityString(id, count, value) + } + + fun getTextWithHttpLinks(input: String): Spanned { + var text = input + if (text.contains("<")) { + text = text.replace("<", "<") + } + if (text.contains(">")) { + text = text.replace(">", ">") + } + if (text.contains("\n")) { + text = text.replace("\n", "
") + } + if (text.contains("http://")) { + val indexHttp = text.indexOf("http://") + val indexFinHttp = + if (text.indexOf(" ", indexHttp) == -1) text.length else text.indexOf( + " ", + indexHttp + ) + val link = text.substring(indexHttp, indexFinHttp) + val linkWithoutScheme = link.replace("http://", "") + text = text.replaceFirst( + Pattern.quote(link).toRegex(), + "$linkWithoutScheme" + ) + } + if (text.contains("https://")) { + val indexHttp = text.indexOf("https://") + val indexFinHttp = + if (text.indexOf(" ", indexHttp) == -1) text.length else text.indexOf( + " ", + indexHttp + ) + val link = text.substring(indexHttp, indexFinHttp) + val linkWithoutScheme = link.replace("https://", "") + text = text.replaceFirst( + Pattern.quote(link).toRegex(), + "$linkWithoutScheme" + ) + } + return HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY) + } + + fun getInitials(displayName: String): String { + if (displayName.isEmpty()) return "" + + val split = displayName.toUpperCase(Locale.getDefault()).split(" ") + return when (split.size) { + 0 -> "" + 1 -> split[0][0].toString() + else -> split[0][0].toString() + split[1][0].toString() + } + } + + fun createAndroidContact(): Long { + var accountType: String? = null + var accountName: String? = null + + if (corePreferences.useLinphoneSyncAccount) { + accountType = getString(org.linphone.R.string.sync_account_type) + accountName = getString(org.linphone.R.string.sync_account_name) + } + + val values = ContentValues() + values.put(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) + values.put(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) + + val rawContactUri = coreContext.context.contentResolver + .insert(ContactsContract.RawContacts.CONTENT_URI, values) + return ContentUris.parseId(rawContactUri) + } + + fun pixelsToDp(pixels: Float): Float { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + pixels, + coreContext.context.resources.displayMetrics + ) + } + } +} diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt new file mode 100644 index 000000000..b045d6d06 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +import android.content.Context +import android.graphics.drawable.Drawable +import android.net.Uri +import android.text.Editable +import android.text.TextWatcher +import android.util.Patterns +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.* +import androidx.databinding.* +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import org.linphone.BR +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.activities.GenericActivity +import org.linphone.activities.main.settings.SettingListener +import org.linphone.contact.ContactAvatarView +import org.linphone.core.tools.Log + +/** + * This file contains all the data binding necessary for the app + */ + +@BindingAdapter("android:src") +fun ImageView.setSourceImageResource(resource: Int) { + this.setImageResource(resource) +} + +@BindingAdapter("android:contentDescription") +fun ImageView.setContentDescription(resource: Int) { + if (resource == 0) { + Log.w("Can't set content description with resource id 0") + return + } + this.contentDescription = context.getString(resource) +} + +@BindingAdapter("android:textStyle") +fun TextView.setTypeface(typeface: Int) { + this.setTypeface(null, typeface) +} + +@BindingAdapter("android:layout_height") +fun View.setLayoutHeight(dimension: Float) { + this.layoutParams.height = dimension.toInt() +} + +@BindingAdapter("android:background") +fun LinearLayout.setBackground(resource: Int) { + this.setBackgroundResource(resource) +} + +@BindingAdapter("style") +fun TextView.setStyle(resource: Int) { + this.setTextAppearance(context, resource) +} + +@BindingAdapter("android:layout_marginLeft") +fun setLeftMargin(view: View, margin: Float) { + val layoutParams = view.layoutParams as RelativeLayout.LayoutParams + layoutParams.leftMargin = margin.toInt() + view.layoutParams = layoutParams +} + +@BindingAdapter("android:layout_weight") +fun setLayoutWeight(view: View, weight: Float) { + val layoutParams = view.layoutParams as LinearLayout.LayoutParams + layoutParams.weight = weight + view.layoutParams = layoutParams +} + +@BindingAdapter("android:layout_alignLeft") +fun setLayoutLeftAlign(view: View, oldTargetId: Int, newTargetId: Int) { + val layoutParams = view.layoutParams as RelativeLayout.LayoutParams + if (oldTargetId != 0) layoutParams.removeRule(RelativeLayout.ALIGN_LEFT) + if (newTargetId != 0) layoutParams.addRule(RelativeLayout.ALIGN_LEFT, newTargetId) + view.layoutParams = layoutParams +} + +@BindingAdapter("android:layout_alignRight") +fun setLayoutRightAlign(view: View, oldTargetId: Int, newTargetId: Int) { + val layoutParams = view.layoutParams as RelativeLayout.LayoutParams + if (oldTargetId != 0) layoutParams.removeRule(RelativeLayout.ALIGN_RIGHT) + if (newTargetId != 0) layoutParams.addRule(RelativeLayout.ALIGN_RIGHT, newTargetId) + view.layoutParams = layoutParams +} + +@BindingAdapter("android:layout_toLeftOf") +fun setLayoutToLeftOf(view: View, oldTargetId: Int, newTargetId: Int) { + val layoutParams = view.layoutParams as RelativeLayout.LayoutParams + if (oldTargetId != 0) layoutParams.removeRule(RelativeLayout.LEFT_OF) + if (newTargetId != 0) layoutParams.addRule(RelativeLayout.LEFT_OF, newTargetId) + view.layoutParams = layoutParams +} + +@BindingAdapter("onClickToggleSwitch") +fun switchSetting(view: View, switchId: Int) { + val switch: Switch = view.findViewById(switchId) + view.setOnClickListener { switch.isChecked = !switch.isChecked } +} + +@BindingAdapter("onValueChanged") +fun editTextSetting(view: EditText, lambda: () -> Unit) { + view.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) lambda() + } + + view.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + lambda() + true + } + false + } + + view.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + if (s?.isEmpty() == true) { + lambda() + } + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { } + }) +} + +@BindingAdapter("selectedIndex", "settingListener") +fun spinnerSetting(spinner: Spinner, selectedIndex: Int, listener: SettingListener) { + spinner.setSelection(selectedIndex, true) + + spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(parent: AdapterView<*>?) {} + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + listener.onListValueChanged(position) + } + } +} + +@BindingAdapter("entries") +fun setEntries( + viewGroup: ViewGroup, + entries: List? +) { + viewGroup.removeAllViews() + if (entries != null) { + for (i in entries) { + viewGroup.addView(i.root) + } + } +} + +private fun setEntries( + viewGroup: ViewGroup, + entries: List?, + layoutId: Int, + onLongClick: View.OnLongClickListener?, + parent: Any? +) { + viewGroup.removeAllViews() + if (entries != null) { + val inflater = viewGroup.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + for (i in entries.indices) { + val entry = entries[i] + val binding = DataBindingUtil.inflate(inflater, layoutId, viewGroup, false) + binding.setVariable(BR.data, entry) + binding.setVariable(BR.longClickListener, onLongClick) + binding.setVariable(BR.parent, parent) + + // This is a bit hacky... + binding.lifecycleOwner = viewGroup.context as GenericActivity + + viewGroup.addView(binding.root) + } + } +} + +@BindingAdapter("entries", "layout") +fun setEntries( + viewGroup: ViewGroup, + entries: List?, + layoutId: Int +) { + setEntries(viewGroup, entries, layoutId, null, null) +} + +@BindingAdapter("entries", "layout", "onLongClick") +fun setEntries( + viewGroup: ViewGroup, + entries: List?, + layoutId: Int, + onLongClick: View.OnLongClickListener? +) { + setEntries(viewGroup, entries, layoutId, onLongClick, null) +} + +@BindingAdapter("entries", "layout", "parent") +fun setEntries( + viewGroup: ViewGroup, + entries: List?, + layoutId: Int, + parent: Any? +) { + setEntries(viewGroup, entries, layoutId, null, parent) +} + +@BindingAdapter("glidePath") +fun loadImageWithGlide(imageView: ImageView, path: String) { + if (path.isNotEmpty() && FileUtils.isExtensionImage(path)) { + Glide.with(imageView).load(path).into(imageView) + } else { + Log.w("[Data Binding] [Glide] Can't load $path") + } +} + +@BindingAdapter("glideAvatar") +fun loadAvatarWithGlide(imageView: ImageView, path: Uri?) { + loadAvatarWithGlide(imageView, path?.toString()) +} + +@BindingAdapter("glideAvatar") +fun loadAvatarWithGlide(imageView: ImageView, path: String?) { + if (path != null) { + Glide.with(imageView).load(path).apply(RequestOptions.circleCropTransform()).listener(object : + RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + Log.w("[Data Binding] [Glide] Can't load $path") + imageView.visibility = View.GONE + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + imageView.visibility = View.VISIBLE + return false + } + }).into(imageView) + } else { + imageView.visibility = View.GONE + } +} + +@BindingAdapter("showSecurityLevel") +fun ContactAvatarView.setShowAvatarSecurityLevel(visible: Boolean) { + this.binding.securityBadgeVisibility = visible +} + +@BindingAdapter("showLimeCapability") +fun ContactAvatarView.setShowLimeCapability(limeCapability: Boolean) { + this.binding.showLimeCapability = limeCapability +} + +@BindingAdapter("assistantPhoneNumberValidation") +fun addPhoneNumberEditTextValidation(editText: EditText, enabled: Boolean) { + if (!enabled) return + editText.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + when { + s?.matches(Regex("\\d+")) == false -> + editText.error = editText.context.getString(R.string.assistant_error_phone_number_invalid_characters) + } + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { } + }) +} + +@BindingAdapter("assistantPhoneNumberPrefixValidation") +fun addPrefixEditTextValidation(editText: EditText, enabled: Boolean) { + if (!enabled) return + editText.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (s == null || s.isEmpty() || !s.startsWith("+")) { + editText.setText("+$s") + } + } + }) +} + +@BindingAdapter("assistantUsernameValidation") +fun addUsernameEditTextValidation(editText: EditText, enabled: Boolean) { + if (!enabled) return + val usernameRegexp = corePreferences.config.getString("assistant", "username_regex", "^[a-z0-9+_.\\-]*\$") + val usernameMaxLength = corePreferences.config.getInt("assistant", "username_max_length", 64) + editText.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + when { + s?.matches(Regex(usernameRegexp)) == false -> + editText.error = editText.context.getString(R.string.assistant_error_username_invalid_characters) + s?.length ?: 0 > usernameMaxLength -> { + editText.error = editText.context.getString(R.string.assistant_error_username_too_long) + } + } + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { } + }) +} + +@BindingAdapter("emailConfirmationValidation") +fun addEmailEditTextValidation(editText: EditText, enabled: Boolean) { + if (!enabled) return + editText.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (!Patterns.EMAIL_ADDRESS.matcher(s).matches()) { + editText.error = editText.context.getString(R.string.assistant_error_invalid_email_address) + } + } + }) +} + +@BindingAdapter("passwordConfirmationValidation") +fun addPasswordConfirmationEditTextValidation(password: EditText, passwordConfirmation: EditText) { + password.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) {} + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (passwordConfirmation.text == null || s == null || passwordConfirmation.text.toString() != s.toString()) { + passwordConfirmation.error = passwordConfirmation.context.getString(R.string.assistant_error_passwords_dont_match) + } else { + passwordConfirmation.error = null // To clear other edit text field error + } + } + }) + + passwordConfirmation.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) {} + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (password.text == null || s == null || password.text.toString() != s.toString()) { + passwordConfirmation.error = passwordConfirmation.context.getString(R.string.assistant_error_passwords_dont_match) + } + } + }) +} + +@BindingAdapter("errorMessage") +fun setEditTextError(editText: EditText, error: String?) { + if (error != editText.error) { + editText.error = error + } +} + +@InverseBindingAdapter(attribute = "errorMessage") +fun getEditTextError(editText: EditText): String? { + return editText.error?.toString() +} + +@BindingAdapter("errorMessageAttrChanged") +fun setEditTextErrorListener(editText: EditText, attrChange: InverseBindingListener) { + editText.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + attrChange.onChange() + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + editText.error = null + attrChange.onChange() + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { } + }) +} diff --git a/app/src/main/java/org/linphone/utils/DeviceUtils.java b/app/src/main/java/org/linphone/utils/DeviceUtils.java deleted file mode 100644 index c5d271dd1..000000000 --- a/app/src/main/java/org/linphone/utils/DeviceUtils.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.utils; - -import android.app.Dialog; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.TextView; -import androidx.core.content.ContextCompat; -import java.util.List; -import org.linphone.R; -import org.linphone.compatibility.Compatibility; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; - -public class DeviceUtils { - private static final Intent[] POWERMANAGER_INTENTS = { - new Intent() - .setComponent( - new ComponentName( - "com.miui.securitycenter", - "com.miui.permcenter.autostart.AutoStartManagementActivity")), - new Intent() - .setComponent( - new ComponentName( - "com.letv.android.letvsafe", - "com.letv.android.letvsafe.AutobootManageActivity")), - new Intent() - .setComponent( - new ComponentName( - "com.huawei.systemmanager", - "com.huawei.systemmanager.appcontrol.activity.StartupAppControlActivity")), - new Intent() - .setComponent( - new ComponentName( - "com.huawei.systemmanager", - "com.huawei.systemmanager.optimize.process.ProtectActivity")), - new Intent() - .setComponent( - new ComponentName( - "com.coloros.safecenter", - "com.coloros.safecenter.permission.startup.StartupAppListActivity")), - new Intent() - .setComponent( - new ComponentName( - "com.coloros.safecenter", - "com.coloros.safecenter.startupapp.StartupAppListActivity")), - new Intent() - .setComponent( - new ComponentName( - "com.oppo.safe", - "com.oppo.safe.permission.startup.StartupAppListActivity")), - new Intent() - .setComponent( - new ComponentName( - "com.iqoo.secure", - "com.iqoo.secure.ui.phoneoptimize.AddWhiteListActivity")), - new Intent() - .setComponent( - new ComponentName( - "com.iqoo.secure", - "com.iqoo.secure.ui.phoneoptimize.BgStartUpManager")), - new Intent() - .setComponent( - new ComponentName( - "com.vivo.permissionmanager", - "com.vivo.permissionmanager.activity.BgStartUpManagerActivity")), - new Intent() - .setComponent( - new ComponentName( - "com.samsung.android.lool", - "com.samsung.android.sm.ui.battery.BatteryActivity")), - new Intent() - .setComponent( - new ComponentName( - "com.htc.pitroad", - "com.htc.pitroad.landingpage.activity.LandingPageActivity")), - new Intent() - .setComponent( - new ComponentName( - "com.asus.mobilemanager", "com.asus.mobilemanager.MainActivity")), - new Intent() - .setComponent( - new ComponentName( - "com.asus.mobilemanager", - "com.asus.mobilemanager.autostart.AutoStartActivity")), - new Intent() - .setComponent( - new ComponentName( - "com.asus.mobilemanager", - "com.asus.mobilemanager.entry.FunctionActivity")) - .setData(Uri.parse("mobilemanager://function/entry/AutoStart")), - new Intent() - .setComponent( - new ComponentName( - "com.dewav.dwappmanager", - "com.dewav.dwappmanager.memory.SmartClearupWhiteList")) - }; - - public static Intent getDevicePowerManagerIntent(Context context) { - for (Intent intent : POWERMANAGER_INTENTS) { - if (DeviceUtils.isIntentCallable(context, intent)) { - return intent; - } - } - return null; - } - - public static boolean hasDevicePowerManager(Context context) { - return getDevicePowerManagerIntent(context) != null; - } - - public static boolean isAppUserRestricted(Context context) { - return Compatibility.isAppUserRestricted(context); - } - - public static int getAppStandbyBucket(Context context) { - return Compatibility.getAppStandbyBucket(context); - } - - public static void displayDialogIfDeviceHasPowerManagerThatCouldPreventPushNotifications( - final Context context) { - for (final Intent intent : POWERMANAGER_INTENTS) { - if (DeviceUtils.isIntentCallable(context, intent)) { - Log.w( - "[Hacks] ", - android.os.Build.MANUFACTURER, - " device with power saver detected: ", - intent.getComponent().getClassName()); - if (!LinphonePreferences.instance().hasPowerSaverDialogBeenPrompted()) { - Log.w("[Hacks] Asking power saver for whitelist !"); - - final Dialog dialog = new Dialog(context); - dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); - Drawable d = - new ColorDrawable( - ContextCompat.getColor(context, R.color.dark_grey_color)); - d.setAlpha(200); - dialog.setContentView(R.layout.dialog); - dialog.getWindow() - .setLayout( - WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.MATCH_PARENT); - dialog.getWindow().setBackgroundDrawable(d); - - TextView customText = dialog.findViewById(R.id.dialog_message); - customText.setText(R.string.device_power_saver_dialog_message); - - TextView customTitle = dialog.findViewById(R.id.dialog_title); - customTitle.setText(R.string.device_power_saver_dialog_title); - - dialog.findViewById(R.id.dialog_do_not_ask_again_layout) - .setVisibility(View.VISIBLE); - final CheckBox doNotAskAgain = dialog.findViewById(R.id.doNotAskAgain); - dialog.findViewById(R.id.doNotAskAgainLabel) - .setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - doNotAskAgain.setChecked(!doNotAskAgain.isChecked()); - } - }); - - Button accept = dialog.findViewById(R.id.dialog_ok_button); - accept.setVisibility(View.VISIBLE); - accept.setText(R.string.device_power_saver_dialog_button_go_to_settings); - accept.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - Log.w( - "[Hacks] Power saver detected, user is going to settings :)"); - // If user is going into the settings, - // assume it will make the change so don't prompt again - LinphonePreferences.instance().powerSaverDialogPrompted(true); - - try { - context.startActivity(intent); - } catch (SecurityException se) { - Log.e( - "[Hacks] Couldn't start intent [", - intent.getComponent().getClassName(), - "], security exception was thrown: ", - se); - } - dialog.dismiss(); - } - }); - - Button cancel = dialog.findViewById(R.id.dialog_cancel_button); - cancel.setText(R.string.device_power_saver_dialog_button_later); - cancel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - Log.w( - "[Hacks] Power saver detected, user didn't go to settings :("); - if (doNotAskAgain.isChecked()) { - LinphonePreferences.instance() - .powerSaverDialogPrompted(true); - } - dialog.dismiss(); - } - }); - - Button delete = dialog.findViewById(R.id.dialog_delete_button); - delete.setVisibility(View.GONE); - - dialog.show(); - } - } - } - } - - private static boolean isIntentCallable(Context context, Intent intent) { - List list = - context.getPackageManager() - .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - return list.size() > 0; - } -} diff --git a/app/src/main/java/org/linphone/utils/DialogUtils.kt b/app/src/main/java/org/linphone/utils/DialogUtils.kt new file mode 100644 index 000000000..5f27b4b83 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/DialogUtils.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +import android.app.Dialog +import android.content.Context +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.view.LayoutInflater +import android.view.Window +import android.view.WindowManager +import androidx.core.content.ContextCompat +import androidx.databinding.DataBindingUtil +import org.linphone.R +import org.linphone.activities.main.viewmodels.DialogViewModel +import org.linphone.databinding.DialogBinding + +class DialogUtils { + companion object { + fun getDialog(context: Context, viewModel: DialogViewModel): Dialog { + val dialog = Dialog(context) + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) + + val binding: DialogBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.dialog, null, false) + binding.viewModel = viewModel + dialog.setContentView(binding.root) + + val d: Drawable = ColorDrawable(ContextCompat.getColor(dialog.context, R.color.dark_grey_color)) + d.alpha = 200 + dialog.window + ?.setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT + ) + dialog.window?.setBackgroundDrawable(d) + return dialog + } + } +} diff --git a/app/src/main/java/org/linphone/utils/Event.kt b/app/src/main/java/org/linphone/utils/Event.kt new file mode 100644 index 000000000..0e3a53f19 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/Event.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +import java.util.concurrent.atomic.AtomicBoolean + +/** + * This class allows to limit the number of notification for an event. + * The first one to consume the event will stop the dispatch. + */ +open class Event(private val content: T) { + private val handled = AtomicBoolean(false) + + fun consumed(): Boolean { + return handled.get() + } + + fun consume(handleContent: (T) -> Unit) { + if (!handled.get()) { + handled.set(true) + handleContent(content) + } + } +} diff --git a/app/src/main/java/org/linphone/utils/FileUtils.java b/app/src/main/java/org/linphone/utils/FileUtils.java deleted file mode 100644 index eb24ac6f6..000000000 --- a/app/src/main/java/org/linphone/utils/FileUtils.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.utils; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Environment; -import android.provider.MediaStore; -import android.provider.OpenableColumns; -import android.text.TextUtils; -import java.io.File; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import org.linphone.LinphoneManager; -import org.linphone.core.Address; -import org.linphone.core.Friend; -import org.linphone.core.FriendList; -import org.linphone.core.tools.Log; - -public class FileUtils { - public static String getNameFromFilePath(String filePath) { - if (filePath == null) return null; - - String name = filePath; - int i = filePath.lastIndexOf('/'); - if (i > 0) { - name = filePath.substring(i + 1); - } - return name; - } - - public static String getExtensionFromFileName(String fileName) { - if (fileName == null) return null; - - String extension = null; - int i = fileName.lastIndexOf('.'); - if (i > 0) { - extension = fileName.substring(i + 1); - } - return extension; - } - - public static Boolean isExtensionImage(String path) { - String extension = getExtensionFromFileName(path); - if (extension != null) extension = extension.toLowerCase(); - return (extension != null && extension.matches("(png|jpg|jpeg|bmp|gif)")); - } - - public static String getFilePath(final Context context, final Uri uri) { - if (uri == null) return null; - - String result = null; - String name = getNameFromUri(uri, context); - - try { - File localFile = createFile(context, name); - InputStream remoteFile = context.getContentResolver().openInputStream(uri); - Log.i( - "[File Utils] Trying to copy file from " - + uri.toString() - + " to local file " - + localFile.getAbsolutePath()); - - if (copyToFile(remoteFile, localFile)) { - Log.i("[File Utils] Copy successful"); - result = localFile.getAbsolutePath(); - } else { - Log.e("[File Utils] Copy failed"); - } - - remoteFile.close(); - } catch (IOException e) { - Log.e("[File Utils] getFilePath exception: ", e); - } - - return result; - } - - private static String getNameFromUri(Uri uri, Context context) { - String name = null; - if (uri != null) { - if (uri.getScheme().equals("content")) { - Cursor returnCursor = - context.getContentResolver().query(uri, null, null, null, null); - if (returnCursor != null) { - returnCursor.moveToFirst(); - int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); - name = returnCursor.getString(nameIndex); - returnCursor.close(); - } - } else if (uri.getScheme().equals("file")) { - name = uri.getLastPathSegment(); - } - } - return name; - } - - /** - * Copy data from a source stream to destFile. Return true if succeed, return false if failed. - */ - private static boolean copyToFile(InputStream inputStream, File destFile) { - if (inputStream == null || destFile == null) return false; - try { - try (OutputStream out = new FileOutputStream(destFile)) { - byte[] buffer = new byte[4096]; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) >= 0) { - out.write(buffer, 0, bytesRead); - } - } - return true; - } catch (IOException e) { - Log.e("[File Utils] copyToFile exception: " + e); - } - return false; - } - - private static File createFile(Context context, String fileName) { - if (fileName == null) return null; - if (TextUtils.isEmpty(fileName)) fileName = getStartDate(); - - if (!fileName.contains(".")) { - fileName = fileName + ".unknown"; - } - - final File root; - root = context.getExternalCacheDir(); - - if (root != null && !root.exists()) { - boolean result = root.mkdirs(); - if (!result) { - Log.e("[File Utils] Couldn't create directory " + root.getAbsolutePath()); - } - } - return new File(root, fileName); - } - - public static Uri getCVSPathFromLookupUri(String content) { - if (content == null) return null; - - String contactId = getNameFromFilePath(content); - FriendList[] friendList = LinphoneManager.getCore().getFriendsLists(); - for (FriendList list : friendList) { - for (Friend friend : list.getFriends()) { - if (friend.getRefKey().equals(contactId)) { - String contactVcard = friend.getVcard().asVcard4String(); - return createCvsFromString(contactVcard); - } - } - } - return null; - } - - public static String getRealPathFromURI(Context context, Uri contentUri) { - String[] proj = {MediaStore.Images.Media.DATA}; - Cursor cursor = context.getContentResolver().query(contentUri, proj, null, null, null); - if (cursor != null && cursor.moveToFirst()) { - int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); - String result = cursor.getString(column_index); - cursor.close(); - return result; - } - return null; - } - - public static void deleteFile(String filePath) { - if (filePath == null || filePath.isEmpty()) return; - File file = new File(filePath); - if (file.exists()) { - try { - if (file.delete()) { - Log.i("[File Utils] File deleted: ", filePath); - } else { - Log.e("[File Utils] Can't delete ", filePath); - } - } catch (Exception e) { - Log.e("[File Utils] Can't delete ", filePath, ", exception: ", e); - } - } else { - Log.e("[File Utils] File ", filePath, " doesn't exists"); - } - } - - public static String getStorageDirectory(Context mContext) { - File path = null; - if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - Log.w("[File Utils] External storage is mounted"); - String directory = Environment.DIRECTORY_DOWNLOADS; - path = mContext.getExternalFilesDir(directory); - } - - if (path == null) { - Log.w("[File Utils] Couldn't get external storage path, using internal"); - path = mContext.getFilesDir(); - } - - return path.getAbsolutePath(); - } - - public static String getRecordingsDirectory(Context mContext) { - return getStorageDirectory(mContext); - } - - @SuppressLint("SimpleDateFormat") - public static String getCallRecordingFilename(Context context, Address address) { - String fileName = getRecordingsDirectory(context) + "/"; - - String name = - address.getDisplayName() == null ? address.getUsername() : address.getDisplayName(); - fileName += name + "_"; - - DateFormat format = new SimpleDateFormat("dd-MM-yyyy-HH-mm-ss"); - fileName += format.format(new Date()) + ".mkv"; - - return fileName; - } - - private static Uri createCvsFromString(String vcardString) { - String contactName = getContactNameFromVcard(vcardString); - File vcfFile = new File(Environment.getExternalStorageDirectory(), contactName + ".cvs"); - try { - FileWriter fw = new FileWriter(vcfFile); - fw.write(vcardString); - fw.close(); - return Uri.fromFile(vcfFile); - } catch (IOException e) { - Log.e("[File Utils] createCVSFromString exception: " + e); - } - return null; - } - - private static String getContactNameFromVcard(String vcard) { - if (vcard != null) { - String contactName = vcard.substring(vcard.indexOf("FN:") + 3); - contactName = contactName.substring(0, contactName.indexOf("\n") - 1); - contactName = contactName.replace(";", ""); - contactName = contactName.replace(" ", ""); - return contactName; - } - return null; - } - - private static String getStartDate() { - try { - return new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ROOT).format(new Date()); - } catch (RuntimeException e) { - return new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); - } - } - - public static String getMimeFromFile(String path) { - if (isExtensionImage(path)) { - return "image/" + getExtensionFromFileName(path); - } - return "file/" + getExtensionFromFileName(path); - } -} diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt new file mode 100644 index 000000000..dac26cd0e --- /dev/null +++ b/app/src/main/java/org/linphone/utils/FileUtils.kt @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +import android.content.Context +import android.net.Uri +import android.os.Environment +import android.provider.OpenableColumns +import java.io.* +import java.text.SimpleDateFormat +import java.util.* +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.tools.Log + +class FileUtils { + companion object { + fun getMimeFromFile(path: String?): String? { + val filePath = path ?: "" + return if (isExtensionImage(filePath)) { + "image/" + getExtensionFromFileName(filePath) + } else "file/" + getExtensionFromFileName(filePath) + } + + fun getNameFromFilePath(filePath: String): String { + var name = filePath + val i = filePath.lastIndexOf('/') + if (i > 0) { + name = filePath.substring(i + 1) + } + return name + } + + fun getExtensionFromFileName(fileName: String): String { + var extension = "" + val i = fileName.lastIndexOf('.') + if (i > 0) { + extension = fileName.substring(i + 1) + } + return extension + } + + fun isExtensionImage(path: String): Boolean { + val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault()) + return extension.matches(Regex("(png|jpg|jpeg|bmp|gif)")) + } + + fun getFileStorageDir(isPicture: Boolean = false): File { + var path: File? = null + if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { + Log.w("[File Utils] External storage is mounted") + var directory = Environment.DIRECTORY_DOWNLOADS + if (isPicture) { + Log.w("[File Utils] Using pictures directory instead of downloads") + directory = Environment.DIRECTORY_PICTURES + } + path = coreContext.context.getExternalFilesDir(directory) + } + + val returnPath: File = path ?: coreContext.context.filesDir + if (path == null) Log.w("[File Utils] Couldn't get external storage path, using internal") + + return returnPath + } + + fun getFileStoragePath(fileName: String): File { + val path = getFileStorageDir(isExtensionImage(fileName)) + var file = File(path, fileName) + + var prefix = 1 + while (file.exists()) { + file = File(path, prefix.toString() + "_" + fileName) + Log.w("[File Utils] File with that name already exists, renamed to ${file.name}") + prefix += 1 + } + return file + } + + fun deleteFile(filePath: String) { + val file = File(filePath) + if (file.exists()) { + try { + if (file.delete()) { + Log.i("[File Utils] Deleted $filePath") + } else { + Log.e("[File Utils] Can't delete $filePath") + } + } catch (e: Exception) { + Log.e("[File Utils] Can't delete $filePath, exception: $e") + } + } else { + Log.e("[File Utils] File $filePath doesn't exists") + } + } + + fun getFilePath(context: Context, uri: Uri): String? { + var result: String? = null + val name: String = getNameFromUri(uri, context) + + try { + val localFile: File = createFile(name) + val remoteFile = + context.contentResolver.openInputStream(uri) + Log.i( + "[File Utils] Trying to copy file from " + + uri.toString() + + " to local file " + + localFile.absolutePath + ) + if (copyToFile(remoteFile, localFile)) { + Log.i("[File Utils] Copy successful") + result = localFile.absolutePath + } else { + Log.e("[File Utils] Copy failed") + } + remoteFile?.close() + } catch (e: IOException) { + Log.e("[File Utils] getFilePath exception: ", e) + } + + return result + } + + private fun getNameFromUri(uri: Uri, context: Context): String { + var name = "" + if (uri.scheme == "content") { + val returnCursor = + context.contentResolver.query(uri, null, null, null, null) + if (returnCursor != null) { + returnCursor.moveToFirst() + val nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (nameIndex != -1) { + name = returnCursor.getString(nameIndex) + } else { + Log.e("[File Utils] Couldn't get DISPLAY_NAME column index for URI $uri") + } + returnCursor.close() + } + } else if (uri.scheme == "file") { + name = uri.lastPathSegment ?: "" + } + return name + } + + fun copyFileTo(filePath: String, outputStream: OutputStream?): Boolean { + if (outputStream == null) { + Log.e("[File Utils] Can't copy file $filePath to given null output stream") + return false + } + + val file = File(filePath) + if (!file.exists()) { + Log.e("[File Utils] Can't copy file $filePath, it doesn't exists") + return false + } + + try { + val inputStream = FileInputStream(file) + val buffer = ByteArray(4096) + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } >= 0) { + outputStream.write(buffer, 0, bytesRead) + } + return true + } catch (e: IOException) { + Log.e("[File Utils] copyFileTo exception: $e") + } + return false + } + + private fun copyToFile(inputStream: InputStream?, destFile: File?): Boolean { + if (inputStream == null || destFile == null) return false + try { + FileOutputStream(destFile).use { out -> + val buffer = ByteArray(4096) + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } >= 0) { + out.write(buffer, 0, bytesRead) + } + } + return true + } catch (e: IOException) { + Log.e("[File Utils] copyToFile exception: $e") + } + return false + } + + private fun createFile(fileName: String): File { + var fileName = fileName + + if (fileName.isEmpty()) fileName = getStartDate() + if (!fileName.contains(".")) { + fileName = "$fileName.unknown" + } + + return getFileStoragePath(fileName) + } + + private fun getStartDate(): String { + return try { + SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ROOT).format(Date()) + } catch (e: RuntimeException) { + SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + } + } + } +} diff --git a/app/src/main/java/org/linphone/utils/ImageUtils.java b/app/src/main/java/org/linphone/utils/ImageUtils.java deleted file mode 100644 index 0053af9e1..000000000 --- a/app/src/main/java/org/linphone/utils/ImageUtils.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.utils; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffXfermode; -import android.graphics.Rect; -import android.net.Uri; -import android.provider.MediaStore; -import android.util.DisplayMetrics; -import android.util.TypedValue; - -public class ImageUtils { - - public static Bitmap getRoundBitmapFromUri(Context context, Uri fromPictureUri) { - Bitmap bm = null; - Bitmap roundBm; - if (fromPictureUri != null) { - try { - bm = - MediaStore.Images.Media.getBitmap( - context.getContentResolver(), fromPictureUri); - } catch (Exception e) { - return null; - } - } - if (bm != null) { - roundBm = getRoundBitmap(bm); - if (roundBm != null) { - bm.recycle(); - bm = roundBm; - } - } - return bm; - } - - public static Bitmap getRoundBitmap(Bitmap bitmap) { - Bitmap output = - Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(output); - - final int color = 0xff424242; - final Paint paint = new Paint(); - final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); - - paint.setAntiAlias(true); - canvas.drawARGB(0, 0, 0, 0); - paint.setColor(color); - canvas.drawCircle( - bitmap.getWidth() / 2, bitmap.getHeight() / 2, bitmap.getWidth() / 2, paint); - paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); - canvas.drawBitmap(bitmap, rect, rect, paint); - - return output; - } - - public static float dpToPixels(Context context, float dp) { - return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); - } - - public static float pixelsToDp(Context context, float pixels) { - return pixels - / ((float) context.getResources().getDisplayMetrics().densityDpi - / DisplayMetrics.DENSITY_DEFAULT); - } - - public static Bitmap rotateImage(Bitmap source, float angle) { - Matrix matrix = new Matrix(); - matrix.postRotate(angle); - Bitmap rotatedBitmap = - Bitmap.createBitmap( - source, 0, 0, source.getWidth(), source.getHeight(), matrix, true); - source.recycle(); - return rotatedBitmap; - } -} diff --git a/app/src/main/java/org/linphone/utils/ImageUtils.kt b/app/src/main/java/org/linphone/utils/ImageUtils.kt new file mode 100644 index 000000000..c3e761947 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/ImageUtils.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +import android.content.Context +import android.graphics.* +import android.net.Uri +import android.provider.MediaStore + +class ImageUtils { + companion object { + fun getRoundBitmapFromUri( + context: Context, + fromPictureUri: Uri? + ): Bitmap? { + var bm: Bitmap? = null + val roundBm: Bitmap? + if (fromPictureUri != null) { + bm = try { + MediaStore.Images.Media.getBitmap( + context.contentResolver, fromPictureUri + ) + } catch (e: Exception) { + return null + } + } + if (bm != null) { + roundBm = getRoundBitmap(bm) + if (roundBm != null) { + bm.recycle() + bm = roundBm + } + } + return bm + } + + fun rotateImage(source: Bitmap, angle: Float): Bitmap { + val matrix = Matrix() + matrix.postRotate(angle) + val rotatedBitmap = Bitmap.createBitmap( + source, 0, 0, source.width, source.height, matrix, true + ) + source.recycle() + return rotatedBitmap + } + + private fun getRoundBitmap(bitmap: Bitmap): Bitmap? { + val output = + Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(output) + val color = -0xbdbdbe + val paint = Paint() + val rect = + Rect(0, 0, bitmap.width, bitmap.height) + paint.isAntiAlias = true + canvas.drawARGB(0, 0, 0, 0) + paint.color = color + canvas.drawCircle( + bitmap.width / 2.toFloat(), + bitmap.height / 2.toFloat(), + bitmap.width / 2.toFloat(), + paint + ) + paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) + canvas.drawBitmap(bitmap, rect, rect, paint) + return output + } + } +} diff --git a/app/src/main/java/org/linphone/utils/LifecycleListAdapter.kt b/app/src/main/java/org/linphone/utils/LifecycleListAdapter.kt new file mode 100644 index 000000000..bbb9a8d26 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/LifecycleListAdapter.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter + +/** + * This class prevents having to do in each adapter the viewHolder.attach/detach calls + * to create lifecycle events that are required for the data binding to work correctly + */ +abstract class LifecycleListAdapter(diff: DiffUtil.ItemCallback) : ListAdapter(diff) { + override fun onViewAttachedToWindow(holder: VH) { + super.onViewAttachedToWindow(holder) + holder.attach() + } + + override fun onViewDetachedFromWindow(holder: VH) { + super.onViewDetachedFromWindow(holder) + holder.detach() + } + + fun getItemAt(position: Int): T { + return getItem(position) + } +} diff --git a/app/src/main/java/org/linphone/utils/LifecycleViewHolder.kt b/app/src/main/java/org/linphone/utils/LifecycleViewHolder.kt new file mode 100644 index 000000000..e1a18c294 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/LifecycleViewHolder.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +import androidx.databinding.ViewDataBinding +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.recyclerview.widget.RecyclerView + +/** + * This class allows us to use ViewHolder as lifecycle owner so items in lists can refresh when + * a live data value changes without having to notify the adapter that the item as changed. + */ +abstract class LifecycleViewHolder(viewBinding: ViewDataBinding) : RecyclerView.ViewHolder(viewBinding.root), LifecycleOwner { + private val lifecycleRegistry = LifecycleRegistry(this) // TODO FIXME leak ? + + init { + lifecycleRegistry.currentState = Lifecycle.State.INITIALIZED + } + + override fun getLifecycle(): Lifecycle { + return lifecycleRegistry + } + + fun attach() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) + } + + fun detach() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) + } +} diff --git a/app/src/main/java/org/linphone/utils/LinphoneShortcutManager.java b/app/src/main/java/org/linphone/utils/LinphoneShortcutManager.java deleted file mode 100644 index 25b1ad125..000000000 --- a/app/src/main/java/org/linphone/utils/LinphoneShortcutManager.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.utils; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ShortcutInfo; -import android.graphics.Bitmap; -import android.graphics.drawable.Icon; -import android.util.ArraySet; -import java.util.Set; -import org.linphone.R; -import org.linphone.chat.ChatActivity; -import org.linphone.contacts.ContactsActivity; -import org.linphone.contacts.LinphoneContact; -import org.linphone.core.tools.Log; - -@TargetApi(25) -public class LinphoneShortcutManager { - private Context mContext; - private Set mCategories; - - public LinphoneShortcutManager(Context context) { - mContext = context; - mCategories = new ArraySet<>(); - mCategories.add(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION); - } - - public void destroy() { - mContext = null; - } - - public ShortcutInfo createChatRoomShortcutInfo( - LinphoneContact contact, String chatRoomAddress) { - if (contact == null) return null; - - Bitmap bm = null; - if (contact.getThumbnailUri() != null) { - bm = ImageUtils.getRoundBitmapFromUri(mContext, contact.getThumbnailUri()); - } - Icon icon = - bm == null - ? Icon.createWithResource(mContext, R.drawable.avatar) - : Icon.createWithBitmap(bm); - - try { - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setClass(mContext, ChatActivity.class); - intent.addFlags( - Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - intent.putExtra("RemoteSipUri", chatRoomAddress); - - return new ShortcutInfo.Builder(mContext, chatRoomAddress) - .setShortLabel(contact.getFullName()) - .setIcon(icon) - .setCategories(mCategories) - .setIntent(intent) - .build(); - } catch (Exception e) { - Log.e("[Shortcuts Manager] ShortcutInfo.Builder exception: " + e); - } - - return null; - } - - public ShortcutInfo createContactShortcutInfo(LinphoneContact contact) { - if (contact == null) return null; - - Bitmap bm = null; - if (contact.getThumbnailUri() != null) { - bm = ImageUtils.getRoundBitmapFromUri(mContext, contact.getThumbnailUri()); - } - Icon icon = - bm == null - ? Icon.createWithResource(mContext, R.drawable.avatar) - : Icon.createWithBitmap(bm); - - try { - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setClass(mContext, ContactsActivity.class); - intent.addFlags( - Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - intent.putExtra("ContactId", contact.getContactId()); - - return new ShortcutInfo.Builder(mContext, contact.getContactId()) - .setShortLabel(contact.getFullName()) - .setIcon(icon) - .setCategories(mCategories) - .setIntent(intent) - .build(); - } catch (Exception e) { - Log.e("[Shortcuts Manager] ShortcutInfo.Builder exception: " + e); - } - - return null; - } -} diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.java b/app/src/main/java/org/linphone/utils/LinphoneUtils.java deleted file mode 100644 index 09fbd9bb1..000000000 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.java +++ /dev/null @@ -1,392 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.utils; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.Handler; -import android.os.Looper; -import android.telephony.TelephonyManager; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; -import androidx.core.content.ContextCompat; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Locale; -import org.linphone.LinphoneContext; -import org.linphone.LinphoneManager; -import org.linphone.R; -import org.linphone.core.Address; -import org.linphone.core.Call; -import org.linphone.core.CallLog; -import org.linphone.core.ChatMessage; -import org.linphone.core.Content; -import org.linphone.core.Core; -import org.linphone.core.EventLog; -import org.linphone.core.Factory; -import org.linphone.core.LogCollectionState; -import org.linphone.core.ProxyConfig; -import org.linphone.core.tools.Log; -import org.linphone.settings.LinphonePreferences; - -/** Helpers. */ -public final class LinphoneUtils { - private static final Handler sHandler = new Handler(Looper.getMainLooper()); - - private LinphoneUtils() {} - - public static void configureLoggingService(boolean isDebugEnabled, String appName) { - if (!LinphonePreferences.instance().useJavaLogger()) { - Factory.instance().enableLogCollection(LogCollectionState.Enabled); - Factory.instance().setDebugMode(isDebugEnabled, appName); - } else { - Factory.instance().setDebugMode(isDebugEnabled, appName); - Factory.instance() - .enableLogCollection(LogCollectionState.EnabledWithoutPreviousLogHandler); - if (isDebugEnabled) { - if (LinphoneContext.isReady()) { - Factory.instance() - .getLoggingService() - .addListener(LinphoneContext.instance().getJavaLoggingService()); - } - } else { - if (LinphoneContext.isReady()) { - Factory.instance() - .getLoggingService() - .removeListener(LinphoneContext.instance().getJavaLoggingService()); - } - } - } - } - - public static void dispatchOnUIThread(Runnable r) { - sHandler.post(r); - } - - public static void dispatchOnUIThreadAfter(Runnable r, long after) { - sHandler.postDelayed(r, after); - } - - public static void removeFromUIThreadDispatcher(Runnable r) { - sHandler.removeCallbacks(r); - } - - private static boolean isSipAddress(String numberOrAddress) { - Factory.instance().createAddress(numberOrAddress); - return true; - } - - public static boolean isNumberAddress(String numberOrAddress) { - ProxyConfig proxy = LinphoneManager.getCore().createProxyConfig(); - return proxy.normalizePhoneNumber(numberOrAddress) != null; - } - - public static boolean isStrictSipAddress(String numberOrAddress) { - return isSipAddress(numberOrAddress) && numberOrAddress.startsWith("sip:"); - } - - public static String getDisplayableAddress(Address addr) { - return "sip:" + addr.getUsername() + "@" + addr.getDomain(); - } - - public static String getAddressDisplayName(String uri) { - Address lAddress; - lAddress = Factory.instance().createAddress(uri); - return getAddressDisplayName(lAddress); - } - - public static String getAddressDisplayName(Address address) { - if (address == null) return null; - - String displayName = address.getDisplayName(); - if (displayName == null || displayName.isEmpty()) { - displayName = address.getUsername(); - } - if (displayName == null || displayName.isEmpty()) { - displayName = address.asStringUriOnly(); - } - return displayName; - } - - public static String timestampToHumanDate(Context context, long timestamp, int format) { - return timestampToHumanDate(context, timestamp, context.getString(format)); - } - - public static String timestampToHumanDate(Context context, long timestamp, String format) { - try { - Calendar cal = Calendar.getInstance(); - cal.setTimeInMillis(timestamp * 1000); // Core returns timestamps in seconds... - - SimpleDateFormat dateFormat; - if (isToday(cal)) { - dateFormat = - new SimpleDateFormat( - context.getResources().getString(R.string.today_date_format), - Locale.getDefault()); - } else { - dateFormat = new SimpleDateFormat(format, Locale.getDefault()); - } - - return dateFormat.format(cal.getTime()); - } catch (NumberFormatException nfe) { - return String.valueOf(timestamp); - } - } - - private static boolean isToday(Calendar cal) { - return isSameDay(cal, Calendar.getInstance()); - } - - private static boolean isSameDay(Calendar cal1, Calendar cal2) { - if (cal1 == null || cal2 == null) { - return false; - } - - return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) - && cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) - && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)); - } - - private static boolean isCallRunning(Call call) { - if (call == null) { - return false; - } - - Call.State state = call.getState(); - - return state == Call.State.Connected - || state == Call.State.Updating - || state == Call.State.UpdatedByRemote - || state == Call.State.StreamsRunning - || state == Call.State.Resuming; - } - - public static boolean isCallEstablished(Call call) { - if (call == null) { - return false; - } - - Call.State state = call.getState(); - - return isCallRunning(call) - || state == Call.State.Paused - || state == Call.State.PausedByRemote - || state == Call.State.Pausing; - } - - public static boolean isHighBandwidthConnection(Context context) { - ConnectivityManager cm = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo info = cm.getActiveNetworkInfo(); - return (info != null - && info.isConnected() - && isConnectionFast(info.getType(), info.getSubtype())); - } - - private static boolean isConnectionFast(int type, int subType) { - if (type == ConnectivityManager.TYPE_MOBILE) { - switch (subType) { - case TelephonyManager.NETWORK_TYPE_EDGE: - case TelephonyManager.NETWORK_TYPE_GPRS: - case TelephonyManager.NETWORK_TYPE_IDEN: - return false; - } - } - // in doubt, assume connection is good. - return true; - } - - public static void reloadVideoDevices() { - Core core = LinphoneManager.getCore(); - if (core == null) return; - - Log.i("[Utils] Reloading camera devices"); - core.reloadVideoDevices(); - - LinphoneManager.getInstance().resetCameraFromPreferences(); - } - - public static String getDisplayableUsernameFromAddress(String sipAddress) { - String username = sipAddress; - Core core = LinphoneManager.getCore(); - if (core == null) return username; - - if (username.startsWith("sip:")) { - username = username.substring(4); - } - - if (username.contains("@")) { - String[] split = username.split("@"); - if (split.length > 1) { - String domain = split[1]; - ProxyConfig lpc = core.getDefaultProxyConfig(); - if (lpc != null) { - if (domain.equals(lpc.getDomain())) { - return split[0]; - } - } else { - if (domain.equals( - LinphoneContext.instance() - .getApplicationContext() - .getString(R.string.default_domain))) { - return split[0]; - } - } - } - return split[0]; - } - return username; - } - - public static String getFullAddressFromUsername(String username) { - String sipAddress = username; - Core core = LinphoneManager.getCore(); - if (core == null || username == null) return sipAddress; - - if (!sipAddress.startsWith("sip:")) { - sipAddress = "sip:" + sipAddress; - } - - if (!sipAddress.contains("@")) { - ProxyConfig lpc = core.getDefaultProxyConfig(); - if (lpc != null) { - sipAddress = sipAddress + "@" + lpc.getDomain(); - } else { - sipAddress = - sipAddress - + "@" - + LinphoneContext.instance() - .getApplicationContext() - .getString(R.string.default_domain); - } - } - return sipAddress; - } - - public static void displayErrorAlert(String msg, Context ctxt) { - if (ctxt != null && msg != null) { - AlertDialog.Builder builder = new AlertDialog.Builder(ctxt); - builder.setMessage(msg) - .setCancelable(false) - .setNeutralButton(ctxt.getString(R.string.ok), null) - .show(); - } - } - - public static void showTrustDeniedDialog(Context context) { - final Dialog dialog = new Dialog(context); - dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); - Drawable d = new ColorDrawable(ContextCompat.getColor(context, R.color.dark_grey_color)); - d.setAlpha(200); - dialog.setContentView(R.layout.dialog); - dialog.getWindow() - .setLayout( - WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.MATCH_PARENT); - dialog.getWindow().setBackgroundDrawable(d); - - TextView title = dialog.findViewById(R.id.dialog_title); - title.setVisibility(View.GONE); - - TextView message = dialog.findViewById(R.id.dialog_message); - message.setVisibility(View.VISIBLE); - message.setText(context.getString(R.string.trust_denied)); - - ImageView icon = dialog.findViewById(R.id.dialog_icon); - icon.setVisibility(View.VISIBLE); - icon.setImageResource(R.drawable.security_alert_indicator); - - Button delete = dialog.findViewById(R.id.dialog_delete_button); - delete.setVisibility(View.GONE); - Button cancel = dialog.findViewById(R.id.dialog_cancel_button); - cancel.setVisibility(View.VISIBLE); - Button call = dialog.findViewById(R.id.dialog_ok_button); - call.setVisibility(View.VISIBLE); - call.setText(R.string.call); - - cancel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - dialog.dismiss(); - } - }); - - call.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - CallLog[] logs = LinphoneManager.getCore().getCallLogs(); - CallLog lastLog = logs[0]; - Address addressToCall = - lastLog.getDir() == Call.Dir.Incoming - ? lastLog.getFromAddress() - : lastLog.getToAddress(); - LinphoneManager.getCallManager() - .newOutgoingCall(addressToCall.asString(), null); - dialog.dismiss(); - } - }); - dialog.show(); - } - - public static Dialog getDialog(Context context, String text) { - final Dialog dialog = new Dialog(context); - dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); - Drawable d = new ColorDrawable(ContextCompat.getColor(context, R.color.dark_grey_color)); - d.setAlpha(200); - dialog.setContentView(R.layout.dialog); - dialog.getWindow() - .setLayout( - WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.MATCH_PARENT); - dialog.getWindow().setBackgroundDrawable(d); - - TextView customText = dialog.findViewById(R.id.dialog_message); - customText.setText(text); - return dialog; - } - - public static void deleteFileContentIfExists(EventLog eventLog) { - if (eventLog.getType() == EventLog.Type.ConferenceChatMessage) { - ChatMessage message = eventLog.getChatMessage(); - if (message != null) { - for (Content content : message.getContents()) { - if (content.isFile() && content.getFilePath() != null) { - Log.w( - "[Linphone Utils] Chat message is being deleted, file ", - content.getFilePath(), - " will also be deleted"); - FileUtils.deleteFile(content.getFilePath()); - } - } - } - } - } -} diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt new file mode 100644 index 000000000..bff5df05a --- /dev/null +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.core.* +import org.linphone.core.tools.Log + +/** + * Various utility methods for Linphone SDK + */ +class LinphoneUtils { + companion object { + fun getDisplayName(address: Address): String { + return address.displayName ?: address.username + } + + fun isLimeAvailable(): Boolean { + val core = coreContext.core + val defaultProxy = core.defaultProxyConfig + return core.limeX3DhAvailable() && core.limeX3DhEnabled() && core.limeX3DhServerUrl != null && defaultProxy != null + } + + fun isGroupChatAvailable(): Boolean { + val core = coreContext.core + val defaultProxy = core.defaultProxyConfig + return defaultProxy != null && defaultProxy.conferenceFactoryUri != null + } + + fun createOneToOneChatRoom(participant: Address, isSecured: Boolean = false): ChatRoom? { + val core: Core = coreContext.core + val defaultProxyConfig = core.defaultProxyConfig + + if (defaultProxyConfig != null) { + val room = core.findOneToOneChatRoom(defaultProxyConfig.identityAddress, participant, isSecured) + if (room != null) { + return room + } else { + return if (defaultProxyConfig.conferenceFactoryUri != null && isSecured /*|| !LinphonePreferences.instance().useBasicChatRoomFor1To1()*/) { + val params = core.createDefaultChatRoomParams() + params.enableEncryption(isSecured) + params.enableGroup(false) + // We don't want a basic chat room, so if isSecured is false we have to set this manually + params.backend = ChatRoomBackend.FlexisipChat + + val participants = arrayOfNulls
(1) + participants[0] = participant + core.createChatRoom(params, AppUtils.getString(R.string.chat_room_dummy_subject), participants) + } else { + core.getChatRoom(participant) + } + } + } else { + if (isSecured) { + Log.e("[Linphone Utils] Can't create a secured chat room without proxy config") + return null + } + return core.getChatRoom(participant) + } + } + + fun deleteFilesAttachedToEventLog(eventLog: EventLog) { + if (eventLog.type == EventLog.Type.ConferenceChatMessage) { + val message = eventLog.chatMessage + if (message != null) deleteFilesAttachedToChatMessage(message) + } + } + + fun deleteFilesAttachedToChatMessage(chatMessage: ChatMessage) { + for (content in chatMessage.contents) { + val filePath = content.filePath + if (filePath != null) { + Log.i("[Linphone Utils] Deleting file $filePath") + FileUtils.deleteFile(filePath) + } + } + } + + fun getRecordingFilePathForAddress(address: Address): String { + val displayName = getDisplayName(address) + val dateFormat: DateFormat = SimpleDateFormat("dd-MM-yyyy-HH-mm-ss", Locale.getDefault()) + val fileName = "${displayName}_${dateFormat.format(Date())}.mkv" + return FileUtils.getFileStoragePath(fileName).absolutePath + } + } +} diff --git a/app/src/main/java/org/linphone/utils/MediaScanner.java b/app/src/main/java/org/linphone/utils/MediaScanner.java deleted file mode 100644 index 122cd55fa..000000000 --- a/app/src/main/java/org/linphone/utils/MediaScanner.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.utils; - -import android.content.Context; -import android.media.MediaScannerConnection; -import android.net.Uri; -import java.io.File; -import org.linphone.core.tools.Log; - -public class MediaScanner implements MediaScannerConnection.MediaScannerConnectionClient { - private final MediaScannerConnection mMediaConnection; - private boolean mIsConnected; - private File mFileWaitingForScan; - private MediaScannerListener mListener; - - public MediaScanner(Context context) { - mIsConnected = false; - mMediaConnection = new MediaScannerConnection(context, this); - mMediaConnection.connect(); - mFileWaitingForScan = null; - } - - @Override - public void onMediaScannerConnected() { - mIsConnected = true; - Log.i("[MediaScanner] Connected"); - if (mFileWaitingForScan != null) { - scanFile(mFileWaitingForScan, null); - mFileWaitingForScan = null; - } - } - - public void scanFile(File file, MediaScannerListener listener) { - scanFile(file, FileUtils.getMimeFromFile(file.getAbsolutePath()), listener); - } - - private void scanFile(File file, String mime, MediaScannerListener listener) { - mListener = listener; - - if (!mIsConnected) { - Log.w("[MediaScanner] Not connected yet..."); - mFileWaitingForScan = file; - return; - } - - Log.i("[MediaScanner] Scanning file " + file.getAbsolutePath() + " with MIME " + mime); - mMediaConnection.scanFile(file.getAbsolutePath(), mime); - } - - @Override - public void onScanCompleted(String path, Uri uri) { - Log.i("[MediaScanner] Scan completed : " + path + " => " + uri); - if (mListener != null) { - mListener.onMediaScanned(path, uri); - } - } - - public void destroy() { - Log.i("[MediaScanner] Disconnecting"); - mMediaConnection.disconnect(); - mIsConnected = false; - } -} diff --git a/app/src/main/java/org/linphone/utils/MediaScannerListener.java b/app/src/main/java/org/linphone/utils/MediaScannerListener.java deleted file mode 100644 index 062e29263..000000000 --- a/app/src/main/java/org/linphone/utils/MediaScannerListener.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.utils; - -import android.net.Uri; - -public interface MediaScannerListener { - void onMediaScanned(String path, Uri uri); -} diff --git a/app/src/main/java/org/linphone/utils/PermissionHelper.kt b/app/src/main/java/org/linphone/utils/PermissionHelper.kt new file mode 100644 index 000000000..14575d499 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/PermissionHelper.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +import android.Manifest +import android.content.Context +import org.linphone.compatibility.Compatibility +import org.linphone.core.tools.Log + +/** + * Helper methods to check whether a permission has been granted and log the result + */ +class PermissionHelper private constructor(private val context: Context) { + companion object : SingletonHolder(::PermissionHelper) + + private fun hasPermission(permission: String): Boolean { + val granted = Compatibility.hasPermission(context, permission) + val result = if (granted) "granted" else "denied" + Log.i("[Permission Helper] Permission $permission is $result") + return granted + } + + fun hasReadContactsPermission(): Boolean { + return hasPermission(Manifest.permission.READ_CONTACTS) + } + + fun hasWriteContactsPermission(): Boolean { + return hasPermission(Manifest.permission.WRITE_CONTACTS) + } + + fun hasReadPhoneState(): Boolean { + return hasPermission(Manifest.permission.READ_PHONE_STATE) + } + + fun hasReadExternalStorage(): Boolean { + return hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE) + } + + fun hasWriteExternalStorage(): Boolean { + return hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + + fun hasCameraPermission(): Boolean { + return hasPermission(Manifest.permission.CAMERA) + } + + fun hasRecordAudioPermission(): Boolean { + return hasPermission(Manifest.permission.RECORD_AUDIO) + } +} diff --git a/app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt b/app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt new file mode 100644 index 000000000..78b4807c3 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.telephony.TelephonyManager +import org.linphone.core.DialPlan +import org.linphone.core.Factory +import org.linphone.core.tools.Log + +class PhoneNumberUtils { + companion object { + fun getDialPlanForCurrentCountry(context: Context): DialPlan? { + try { + val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + val countryIso = tm.networkCountryIso + return getDialPlanFromCountryCode(countryIso) + } catch (e: java.lang.Exception) { + Log.e("[Phone Number Utils] $e") + } + return null + } + + @SuppressLint("MissingPermission") + fun getDevicePhoneNumber(context: Context): String? { + if (PermissionHelper.get().hasReadPhoneState()) { + try { + val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + return tm.line1Number + } catch (e: java.lang.Exception) { + Log.e("[Phone Number Utils] $e") + } + } + return null + } + + fun getDialPlanFromCountryCallingPrefix(countryCode: String): DialPlan? { + for (c in Factory.instance().dialPlans) { + if (countryCode == c.countryCallingCode) return c + } + return null + } + + private fun getDialPlanFromCountryCode(countryCode: String): DialPlan? { + for (c in Factory.instance().dialPlans) { + if (countryCode.equals(c.isoCountryCode, ignoreCase = true)) return c + } + return null + } + } +} diff --git a/app/src/main/java/org/linphone/utils/PushNotificationUtils.java b/app/src/main/java/org/linphone/utils/PushNotificationUtils.java deleted file mode 100644 index 0b7401eab..000000000 --- a/app/src/main/java/org/linphone/utils/PushNotificationUtils.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.utils; - -import android.content.Context; -import java.lang.reflect.Constructor; -import org.linphone.R; -import org.linphone.core.tools.Log; - -public class PushNotificationUtils { - private static PushHelperInterface mHelper; - - public static void init(Context context) { - mHelper = null; - String push_type = context.getString(R.string.push_type); - - if (push_type.equals("firebase")) { - String className = "org.linphone.firebase.FirebasePushHelper"; - try { - Class pushHelper = Class.forName(className); - Class[] types = {}; - Constructor constructor = pushHelper.getConstructor(types); - Object[] parameters = {}; - mHelper = (PushHelperInterface) constructor.newInstance(parameters); - mHelper.init(context); - } catch (NoSuchMethodException e) { - Log.w("[Push Utils] Couldn't get push helper constructor"); - } catch (ClassNotFoundException e) { - Log.w("[Push Utils] Couldn't find class " + className); - } catch (Exception e) { - Log.w("[Push Utils] Couldn't get push helper instance: " + e); - } - } else { - Log.w("[Push Utils] Unknow push type " + push_type); - } - } - - public static boolean isAvailable(Context context) { - if (mHelper == null) return false; - return mHelper.isAvailable(context); - } - - public interface PushHelperInterface { - void init(Context context); - - boolean isAvailable(Context context); - } -} diff --git a/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt b/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt new file mode 100644 index 000000000..d2963ff7e --- /dev/null +++ b/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.util.SparseArray +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView + +class RecyclerViewHeaderDecoration(private val adapter: HeaderAdapter) : RecyclerView.ItemDecoration() { + private val headers: SparseArray = SparseArray() + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + val position = (view.layoutParams as RecyclerView.LayoutParams).viewAdapterPosition + + if (position != RecyclerView.NO_POSITION && adapter.displayHeaderForPosition(position)) { + val headerView: View = adapter.getHeaderViewForPosition(view.context, position) + headers.put(position, headerView) + measureHeaderView(headerView, parent) + outRect.top = headerView.height + } else { + headers.remove(position) + } + } + + private fun measureHeaderView(view: View, parent: ViewGroup) { + if (view.layoutParams == null) { + view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + val displayMetrics = parent.context.resources.displayMetrics + val widthSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, View.MeasureSpec.EXACTLY) + val heightSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, View.MeasureSpec.EXACTLY) + val childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingLeft + parent.paddingRight, view.layoutParams.width) + val childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, view.layoutParams.height) + + view.measure(childWidth, childHeight) + view.layout(0, 0, view.measuredWidth, view.measuredHeight) + } + + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + for (i in 0 until parent.childCount) { + val child = parent.getChildAt(i) + val position = parent.getChildAdapterPosition(child) + if (position != RecyclerView.NO_POSITION && adapter.displayHeaderForPosition(position)) { + canvas.save() + val headerView: View = headers.get(position) ?: adapter.getHeaderViewForPosition(parent.context, position) + canvas.translate(0f, child.y - headerView.height) + headerView.draw(canvas) + canvas.restore() + } + } + } +} + +interface HeaderAdapter { + fun displayHeaderForPosition(position: Int): Boolean + + fun getHeaderViewForPosition(context: Context, position: Int): View +} diff --git a/app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt b/app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt new file mode 100644 index 000000000..8d4d5db3f --- /dev/null +++ b/app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.Typeface +import android.graphics.drawable.ColorDrawable +import android.text.TextPaint +import android.util.TypedValue +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import org.linphone.core.tools.Log + +/** + * Helper class to properly display swipe actions in list items. + */ +class RecyclerViewSwipeUtils( + direction: Int, + configuration: RecyclerViewSwipeConfiguration, + listener: RecyclerViewSwipeListener +) : ItemTouchHelper(RecyclerViewSwipeUtilsCallback(direction, configuration, listener)) + +class RecyclerViewSwipeConfiguration { + class Action( + val text: String = "", + val textColor: Int = Color.WHITE, + val backgroundColor: Int = 0, + val icon: Int = 0, + val iconTint: Int = 0 + ) + + val iconMargin = 16f + + val actionTextSizeUnit = TypedValue.COMPLEX_UNIT_SP + val actionTextFont: Typeface = Typeface.SANS_SERIF + val actionTextSize = 14f + + var leftToRightAction = Action() + var rightToLeftAction = Action() +} + +private class RecyclerViewSwipeUtilsCallback( + direction: Int, + val configuration: RecyclerViewSwipeConfiguration, + val listener: RecyclerViewSwipeListener +) : ItemTouchHelper.SimpleCallback(0, direction) { + + fun leftToRightSwipe( + canvas: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float + ) { + if (configuration.leftToRightAction.backgroundColor != 0) { + val background = ColorDrawable(configuration.leftToRightAction.backgroundColor) + background.setBounds( + viewHolder.itemView.left, + viewHolder.itemView.top, + viewHolder.itemView.left + dX.toInt(), + viewHolder.itemView.bottom + ) + background.draw(canvas) + } + + val iconHorizontalMargin: Int = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + configuration.iconMargin, + recyclerView.context.resources.displayMetrics + ).toInt() + var iconSize = 0 + + if (configuration.leftToRightAction.icon != 0 && dX > iconHorizontalMargin) { + val icon = + ContextCompat.getDrawable(recyclerView.context, configuration.leftToRightAction.icon) + if (icon != null) { + iconSize = icon.intrinsicHeight + val halfIcon = iconSize / 2 + val top = + viewHolder.itemView.top + ((viewHolder.itemView.bottom - viewHolder.itemView.top) / 2 - halfIcon) + + icon.setBounds( + viewHolder.itemView.left + iconHorizontalMargin, + top, + viewHolder.itemView.left + iconHorizontalMargin + icon.intrinsicWidth, + top + icon.intrinsicHeight + ) + + if (configuration.leftToRightAction.iconTint != 0) icon.setColorFilter( + configuration.leftToRightAction.iconTint, + PorterDuff.Mode.SRC_IN + ) + icon.draw(canvas) + } + } + + if (configuration.leftToRightAction.text.isNotEmpty() && dX > iconHorizontalMargin + iconSize) { + val textPaint = TextPaint() + textPaint.isAntiAlias = true + textPaint.textSize = TypedValue.applyDimension( + configuration.actionTextSizeUnit, + configuration.actionTextSize, + recyclerView.context.resources.displayMetrics + ) + textPaint.color = configuration.leftToRightAction.textColor + textPaint.typeface = configuration.actionTextFont + + val margin = if (iconSize > 0) iconHorizontalMargin / 2 else 0 + val textX = + (viewHolder.itemView.left + iconHorizontalMargin + iconSize + margin).toFloat() + val textY = + (viewHolder.itemView.top + (viewHolder.itemView.bottom - viewHolder.itemView.top) / 2.0 + textPaint.textSize / 2).toFloat() + canvas.drawText( + configuration.leftToRightAction.text, + textX, + textY, + textPaint + ) + } + } + + fun rightToLeftSwipe( + canvas: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float + ) { + if (configuration.rightToLeftAction.backgroundColor != 0) { + val background = ColorDrawable(configuration.rightToLeftAction.backgroundColor) + background.setBounds( + viewHolder.itemView.right + dX.toInt(), + viewHolder.itemView.top, + viewHolder.itemView.right, + viewHolder.itemView.bottom + ) + background.draw(canvas) + } + + val iconHorizontalMargin: Int = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + configuration.iconMargin, + recyclerView.context.resources.displayMetrics + ).toInt() + var iconSize = 0 + var imageLeftBorder = viewHolder.itemView.right + + if (configuration.rightToLeftAction.icon != 0 && dX < -iconHorizontalMargin) { + val icon = + ContextCompat.getDrawable(recyclerView.context, configuration.rightToLeftAction.icon) + if (icon != null) { + iconSize = icon.intrinsicHeight + val halfIcon = iconSize / 2 + val top = + viewHolder.itemView.top + ((viewHolder.itemView.bottom - viewHolder.itemView.top) / 2 - halfIcon) + imageLeftBorder = + viewHolder.itemView.right - iconHorizontalMargin - halfIcon * 2 + icon.setBounds( + imageLeftBorder, + top, + viewHolder.itemView.right - iconHorizontalMargin, + top + icon.intrinsicHeight + ) + if (configuration.rightToLeftAction.iconTint != 0) icon.setColorFilter( + configuration.rightToLeftAction.iconTint, + PorterDuff.Mode.SRC_IN + ) + icon.draw(canvas) + } + } + if (configuration.rightToLeftAction.text.isNotEmpty() && dX < -iconHorizontalMargin - iconSize) { + val textPaint = TextPaint() + textPaint.isAntiAlias = true + textPaint.textSize = TypedValue.applyDimension( + configuration.actionTextSizeUnit, + configuration.actionTextSize, + recyclerView.context.resources.displayMetrics + ) + textPaint.color = configuration.rightToLeftAction.textColor + textPaint.typeface = configuration.actionTextFont + + val margin = + if (imageLeftBorder == viewHolder.itemView.right) iconHorizontalMargin else iconHorizontalMargin / 2 + val textX = + imageLeftBorder - textPaint.measureText(configuration.rightToLeftAction.text) - margin + val textY = + (viewHolder.itemView.top + (viewHolder.itemView.bottom - viewHolder.itemView.top) / 2.0 + textPaint.textSize / 2).toFloat() + canvas.drawText( + configuration.rightToLeftAction.text, + textX, + textY, + textPaint + ) + } + } + + fun applyConfiguration( + canvas: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + actionState: Int + ) { + try { + if (actionState != ItemTouchHelper.ACTION_STATE_SWIPE) return + + if (dX > 0) { + leftToRightSwipe(canvas, recyclerView, viewHolder, dX) + } else if (dX < 0) { + rightToLeftSwipe(canvas, recyclerView, viewHolder, dX) + } + } catch (e: Exception) { + Log.e("[RecyclerView Swipe Utils] $e") + } + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + return false + } + + override fun onSwiped( + viewHolder: RecyclerView.ViewHolder, + direction: Int + ) { + if (direction == ItemTouchHelper.LEFT) { + listener.onRightToLeftSwipe(viewHolder) + } else if (direction == ItemTouchHelper.RIGHT) { + listener.onLeftToRightSwipe(viewHolder) + } + } + + override fun onChildDraw( + canvas: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean + ) { + applyConfiguration(canvas, recyclerView, viewHolder, dX, actionState) + super.onChildDraw( + canvas, + recyclerView, + viewHolder, + dX, + dY, + actionState, + isCurrentlyActive + ) + } +} + +interface RecyclerViewSwipeListener { + fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) + fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) +} diff --git a/app/src/main/java/org/linphone/utils/SelectableAdapter.java b/app/src/main/java/org/linphone/utils/SelectableAdapter.java deleted file mode 100644 index aa779a6ec..000000000 --- a/app/src/main/java/org/linphone/utils/SelectableAdapter.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.utils; - -import android.util.SparseBooleanArray; -import androidx.recyclerview.widget.RecyclerView; -import java.util.ArrayList; -import java.util.List; - -public abstract class SelectableAdapter - extends RecyclerView.Adapter { - private final SparseBooleanArray mSelectedItems; - private boolean mIsEditionEnabled = false; - private final SelectableHelper mListHelper; - - protected SelectableAdapter(SelectableHelper helper) { - mSelectedItems = new SparseBooleanArray(); - mListHelper = helper; - } - - public boolean isEditionEnabled() { - return mIsEditionEnabled; - } - - public void enableEdition(boolean set) { - mIsEditionEnabled = set; - - mSelectedItems.clear(); - notifyDataSetChanged(); - } - - /** - * Indicates if the item at position position is selected - * - * @param position Position of the item to check - * @return true if the item is selected, false otherwise - */ - protected boolean isSelected(int position) { - return getSelectedItems().contains(position); - } - - /** - * Toggle the selection status of the item at a given position - * - * @param position Position of the item to toggle the selection status for - */ - public void toggleSelection(int position) { - if (mSelectedItems.get(position, false)) { - mSelectedItems.delete(position); - } else { - mSelectedItems.put(position, true); - } - mListHelper.updateSelectionButtons( - getSelectedItemCount() == 0, getSelectedItemCount() == getItemCount()); - - notifyItemChanged(position); - } - - /** - * Count the selected items - * - * @return Selected items count - */ - public int getSelectedItemCount() { - return mSelectedItems.size(); - } - - /** - * Indicates the list of selected items - * - * @return List of selected items ids - */ - public List getSelectedItems() { - List items = new ArrayList<>(mSelectedItems.size()); - for (int i = 0; i < mSelectedItems.size(); ++i) { - items.add(mSelectedItems.keyAt(i)); - } - return items; - } - - public void selectAll() { - for (int i = 0; i < getItemCount(); i++) { - mSelectedItems.put(i, true); - notifyDataSetChanged(); - } - mListHelper.updateSelectionButtons(false, true); - } - - public void deselectAll() { - mSelectedItems.clear(); - mListHelper.updateSelectionButtons(true, false); - notifyDataSetChanged(); - } - - public abstract Object getItem(int position); -} diff --git a/app/src/main/java/org/linphone/utils/SelectableHelper.java b/app/src/main/java/org/linphone/utils/SelectableHelper.java deleted file mode 100644 index cf6367889..000000000 --- a/app/src/main/java/org/linphone/utils/SelectableHelper.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ - -package org.linphone.utils; - -import android.app.Dialog; -import android.content.Context; -import android.view.View; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.LinearLayout; -import androidx.recyclerview.widget.RecyclerView; -import org.linphone.R; - -public class SelectableHelper { - private final ImageView mEditButton; - private final ImageView mSelectAllButton; - private final ImageView mDeselectAllButton; - private final ImageView mDeleteSelectionButton; - private final LinearLayout mEditTopBar; - private final LinearLayout mTopBar; - private SelectableAdapter mAdapter; - private final DeleteListener mDeleteListener; - private final Context mContext; - private int mDialogDeleteMessageResourceId; - - public SelectableHelper(View view, DeleteListener listener) { - mContext = view.getContext(); - mDeleteListener = listener; - - mEditTopBar = view.findViewById(R.id.edit_list); - mTopBar = view.findViewById(R.id.top_bar); - - ImageView cancelButton = view.findViewById(R.id.cancel); - cancelButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - quitEditionMode(); - } - }); - - mEditButton = view.findViewById(R.id.edit); - mEditButton.setEnabled(false); - - mEditButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - if (mAdapter.getItemCount() > 0) { - enterEditionMode(); - mTopBar.setVisibility(View.GONE); - mEditTopBar.setVisibility(View.VISIBLE); - } - } - }); - - mSelectAllButton = view.findViewById(R.id.select_all); - mSelectAllButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - mAdapter.selectAll(); - } - }); - - mDeselectAllButton = view.findViewById(R.id.deselect_all); - mDeselectAllButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - mAdapter.deselectAll(); - } - }); - - mDeleteSelectionButton = view.findViewById(R.id.delete); - mDeleteSelectionButton.setEnabled(false); - - mDeleteSelectionButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - final Dialog dialog = - LinphoneUtils.getDialog( - mContext, - mContext.getString(mDialogDeleteMessageResourceId)); - Button delete = dialog.findViewById(R.id.dialog_delete_button); - Button cancel = dialog.findViewById(R.id.dialog_cancel_button); - - delete.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - mDeleteListener.onDeleteSelection(getSelectedObjects()); - mEditButton.setEnabled(mAdapter.getItemCount() != 0); - dialog.dismiss(); - quitEditionMode(); - } - }); - - cancel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - dialog.dismiss(); - quitEditionMode(); - } - }); - dialog.show(); - } - }); - - mDialogDeleteMessageResourceId = R.string.delete_text; - } - - public void setDialogMessage(int id) { - mDialogDeleteMessageResourceId = id; - } - - public SelectableAdapter getAdapter() { - return mAdapter; - } - - public void setAdapter(SelectableAdapter adapter) { - mAdapter = adapter; - mEditButton.setEnabled(mAdapter.getItemCount() != 0); - } - - public void updateSelectionButtons(boolean isSelectionEmpty, boolean isSelectionFull) { - if (isSelectionEmpty) { - mDeleteSelectionButton.setEnabled(false); - } else { - mDeleteSelectionButton.setEnabled(true); - } - - if (isSelectionFull) { - mSelectAllButton.setVisibility(View.GONE); - mDeselectAllButton.setVisibility(View.VISIBLE); - } else { - mSelectAllButton.setVisibility(View.VISIBLE); - mDeselectAllButton.setVisibility(View.GONE); - } - } - - private void quitEditionMode() { - mAdapter.enableEdition(false); - mTopBar.setVisibility(View.VISIBLE); - mEditTopBar.setVisibility(View.GONE); - mDeleteSelectionButton.setEnabled(false); - mSelectAllButton.setVisibility(View.GONE); - mDeselectAllButton.setVisibility(View.VISIBLE); - } - - public void enterEditionMode() { - mAdapter.enableEdition(true); - mTopBar.setVisibility(View.GONE); - mEditTopBar.setVisibility(View.VISIBLE); - mDeleteSelectionButton.setEnabled(false); - mSelectAllButton.setVisibility(View.VISIBLE); - mDeselectAllButton.setVisibility(View.GONE); - } - - private Object[] getSelectedObjects() { - Object[] objects = new Object[mAdapter.getSelectedItemCount()]; - int index = 0; - for (Integer i : mAdapter.getSelectedItems()) { - objects[index] = mAdapter.getItem(i); - index++; - } - return objects; - } - - public void setEditButtonVisibility(boolean visible) { - mEditButton.setVisibility(visible ? View.VISIBLE : View.GONE); - } - - public interface DeleteListener { - void onDeleteSelection(Object[] objectsToDelete); - } -} diff --git a/app/src/main/java/org/linphone/utils/SingletonHolder.kt b/app/src/main/java/org/linphone/utils/SingletonHolder.kt new file mode 100644 index 000000000..379ba4e3a --- /dev/null +++ b/app/src/main/java/org/linphone/utils/SingletonHolder.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +/** + * Helper class to create singletons like CoreContext. + */ +open class SingletonHolder(val creator: (A) -> T) { + @Volatile private var instance: T? = null + + fun exists(): Boolean { + return instance != null + } + + fun destroy() { + instance = null + } + + fun get(): T { + // Will throw NPE if needed + return instance!! + } + + fun create(arg: A): T { + val i = instance + if (i != null) { + return i + } + + return synchronized(this) { + val i2 = instance + if (i2 != null) { + i2 + } else { + val created = creator(arg) + instance = created + created + } + } + } + + fun required(arg: A): T { + return instance ?: create(arg) + } +} diff --git a/app/src/main/java/org/linphone/utils/TimestampUtils.kt b/app/src/main/java/org/linphone/utils/TimestampUtils.kt new file mode 100644 index 000000000..ee62a6e73 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/TimestampUtils.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.utils + +import java.text.SimpleDateFormat +import java.util.* + +class TimestampUtils { + companion object { + fun isToday(timestamp: Long, timestampInSecs: Boolean = true): Boolean { + val cal = Calendar.getInstance() + cal.timeInMillis = if (timestampInSecs) timestamp * 1000 else timestamp + return isSameDay(cal, Calendar.getInstance()) + } + + fun isYesterday(timestamp: Long, timestampInSecs: Boolean = true): Boolean { + val yesterday = Calendar.getInstance() + yesterday.roll(Calendar.DAY_OF_MONTH, -1) + val cal = Calendar.getInstance() + cal.timeInMillis = if (timestampInSecs) timestamp * 1000 else timestamp + return isSameDay(cal, yesterday) + } + + fun isSameDay(timestamp1: Long, timestamp2: Long, timestampInSecs: Boolean = true): Boolean { + val cal1 = Calendar.getInstance() + cal1.timeInMillis = if (timestampInSecs) timestamp1 * 1000 else timestamp1 + val cal2 = Calendar.getInstance() + cal2.timeInMillis = if (timestampInSecs) timestamp2 * 1000 else timestamp2 + return isSameDay(cal1, cal2) + } + + fun isSameDay( + cal1: Date, + cal2: Date + ): Boolean { + return isSameDay(cal1.time, cal2.time) + } + + fun toString(timestamp: Long, onlyDate: Boolean = false, timestampInSecs: Boolean = true): String { + val format = if (isToday(timestamp)) { + "HH:mm" + } else { + if (onlyDate) "dd/MM" else "dd/MM HH:mm" + } + val millis = if (timestampInSecs) timestamp * 1000 else timestamp + return SimpleDateFormat(format, Locale.getDefault()).format(Date(millis)) + } + + private fun isSameDay( + cal1: Calendar, + cal2: Calendar + ): Boolean { + return cal1[Calendar.ERA] == cal2[Calendar.ERA] && + cal1[Calendar.YEAR] == cal2[Calendar.YEAR] && + cal1[Calendar.DAY_OF_YEAR] == cal2[Calendar.DAY_OF_YEAR] + } + } +} diff --git a/app/src/main/java/org/linphone/views/MarqueeTextView.java b/app/src/main/java/org/linphone/views/MarqueeTextView.java deleted file mode 100644 index 5d43d2b19..000000000 --- a/app/src/main/java/org/linphone/views/MarqueeTextView.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.views; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.widget.TextView; -import androidx.annotation.Nullable; - -/** - * The purpose of this class is to have a Textview automatically configured for marquee ellipsize - */ -@SuppressLint("AppCompatCustomView") -public class MarqueeTextView extends TextView { - public MarqueeTextView(Context context) { - super(context); - init(); - } - - public MarqueeTextView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - init(); - } - - public MarqueeTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); - } - - public MarqueeTextView( - Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - init(); - } - - public void init() { - setEllipsize(TextUtils.TruncateAt.MARQUEE); - setMarqueeRepeatLimit(0xffffffff); - setSelected(true); - } -} diff --git a/app/src/main/java/org/linphone/views/MarqueeTextView.kt b/app/src/main/java/org/linphone/views/MarqueeTextView.kt new file mode 100644 index 000000000..55cd5d3eb --- /dev/null +++ b/app/src/main/java/org/linphone/views/MarqueeTextView.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ +package org.linphone.views + +import android.content.Context +import android.text.TextUtils +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView + +/** + * The purpose of this class is to have a TextView automatically configured for marquee ellipsize. + */ +class MarqueeTextView : AppCompatTextView { + constructor(context: Context) : super(context) { + init() + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + init() + } + + fun init() { + ellipsize = TextUtils.TruncateAt.MARQUEE + marqueeRepeatLimit = -0x1 + isSelected = true + } +} diff --git a/app/src/main/java/org/linphone/views/MultiLineWrapContentWidthTextView.java b/app/src/main/java/org/linphone/views/MultiLineWrapContentWidthTextView.java deleted file mode 100644 index 175deeb37..000000000 --- a/app/src/main/java/org/linphone/views/MultiLineWrapContentWidthTextView.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.views; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.text.Layout; -import android.util.AttributeSet; -import android.widget.TextView; -import androidx.annotation.Nullable; - -/** - * The purpose of this class is to have a TextView declared with wrap_content as width that won't - * fill it's parent if it is multi line - */ -@SuppressLint("AppCompatCustomView") -public class MultiLineWrapContentWidthTextView extends TextView { - - public MultiLineWrapContentWidthTextView(Context context) { - super(context); - } - - public MultiLineWrapContentWidthTextView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public MultiLineWrapContentWidthTextView( - Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @Override - protected void onMeasure(int widthSpec, int heightSpec) { - int widthMode = MeasureSpec.getMode(widthSpec); - - if (widthMode == MeasureSpec.AT_MOST) { - Layout layout = getLayout(); - if (layout != null) { - int maxWidth = - (int) Math.ceil(getMaxLineWidth(layout)) - + getTotalPaddingLeft() - + getTotalPaddingRight(); - widthSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST); - } - } - - super.onMeasure(widthSpec, heightSpec); - } - - private float getMaxLineWidth(Layout layout) { - float max_width = 0.0f; - int lines = layout.getLineCount(); - for (int i = 0; i < lines; i++) { - if (layout.getLineWidth(i) > max_width) { - max_width = layout.getLineWidth(i); - } - } - return max_width; - } -} diff --git a/app/src/main/java/org/linphone/views/RichEditText.java b/app/src/main/java/org/linphone/views/RichEditText.java deleted file mode 100644 index f9ed8f92a..000000000 --- a/app/src/main/java/org/linphone/views/RichEditText.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2010-2019 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ -package org.linphone.views; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.Bundle; -import android.util.AttributeSet; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; -import android.widget.EditText; -import androidx.core.view.inputmethod.EditorInfoCompat; -import androidx.core.view.inputmethod.InputConnectionCompat; -import androidx.core.view.inputmethod.InputContentInfoCompat; - -@SuppressLint("AppCompatCustomView") -public class RichEditText extends EditText { - public interface RichInputListener { - boolean onCommitContent( - InputContentInfoCompat inputContentInfo, - int flags, - Bundle opts, - String[] contentMimeTypes); - } - - private RichInputListener mListener; - private String[] mSupportedMimeTypes; - - public RichEditText(Context context) { - super(context); - } - - public RichEditText(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public RichEditText(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public RichEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - public void setListener(RichInputListener listener) { - mListener = listener; - mSupportedMimeTypes = new String[] {"image/png", "image/gif", "image/jpeg"}; - } - - @Override - public InputConnection onCreateInputConnection(EditorInfo editorInfo) { - final InputConnection ic = super.onCreateInputConnection(editorInfo); - EditorInfoCompat.setContentMimeTypes(editorInfo, mSupportedMimeTypes); - - final InputConnectionCompat.OnCommitContentListener callback = - new InputConnectionCompat.OnCommitContentListener() { - @Override - public boolean onCommitContent( - InputContentInfoCompat inputContentInfo, int flags, Bundle opts) { - if (mListener != null) { - return mListener.onCommitContent( - inputContentInfo, flags, opts, mSupportedMimeTypes); - } - return false; - } - }; - - if (ic != null) { - return InputConnectionCompat.createWrapper(ic, editorInfo, callback); - } - return null; - } -} diff --git a/app/src/main/res/anim/slide_in_bottom_to_top.xml b/app/src/main/res/anim/slide_in_bottom_to_top.xml deleted file mode 100644 index f7bb0ec08..000000000 --- a/app/src/main/res/anim/slide_in_bottom_to_top.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_left_to_right.xml b/app/src/main/res/anim/slide_in_left_to_right.xml deleted file mode 100644 index d57afb2b5..000000000 --- a/app/src/main/res/anim/slide_in_left_to_right.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_right_to_left.xml b/app/src/main/res/anim/slide_in_right_to_left.xml deleted file mode 100644 index 47fd9ee8a..000000000 --- a/app/src/main/res/anim/slide_in_right_to_left.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_top_to_bottom.xml b/app/src/main/res/anim/slide_in_top_to_bottom.xml deleted file mode 100644 index b928aabfa..000000000 --- a/app/src/main/res/anim/slide_in_top_to_bottom.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_bottom_to_top.xml b/app/src/main/res/anim/slide_out_bottom_to_top.xml deleted file mode 100644 index 190217cd8..000000000 --- a/app/src/main/res/anim/slide_out_bottom_to_top.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_left_to_right.xml b/app/src/main/res/anim/slide_out_left_to_right.xml deleted file mode 100644 index fd1dae982..000000000 --- a/app/src/main/res/anim/slide_out_left_to_right.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_right_to_left.xml b/app/src/main/res/anim/slide_out_right_to_left.xml deleted file mode 100644 index 2f19c3f6f..000000000 --- a/app/src/main/res/anim/slide_out_right_to_left.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_top_to_bottom.xml b/app/src/main/res/anim/slide_out_top_to_bottom.xml deleted file mode 100644 index fdfe2a714..000000000 --- a/app/src/main/res/anim/slide_out_top_to_bottom.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/.directory b/app/src/main/res/drawable-xhdpi/.directory deleted file mode 100644 index d5c44ccc7..000000000 --- a/app/src/main/res/drawable-xhdpi/.directory +++ /dev/null @@ -1,4 +0,0 @@ -[Dolphin] -PreviewsShown=true -Timestamp=2019,3,5,11,4,59 -Version=4 diff --git a/app/src/main/res/drawable-xhdpi/add_field_default.png b/app/src/main/res/drawable-xhdpi/add_field_default.png deleted file mode 100644 index 1d0fbdd3403fe485e946835899e7b7dd816914f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 478 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)Y)RhkE)4%caKYZ?lYt_f1s;*b zKpodXn9)gNb_Gz7y~NYkmHiQyBnN}e86(|e3=E9fo-U3d6}R5r+~}q3$k6^V--TtX zz*?PmLK`*;3c6TZug+fob;IUQfjYS^N1STd!!2oXtF;R}9xLyDoWl2m)#vJ*Tg-94 zj_nd%AMJl>wM0GZysVmgzM4<7LcYhkiLB?I!~A*6Va^S)UY^1G4(-ALMsGy33s2dz1P8w}Yw)+%wi%mP*ZdAsx*s_Uo8l!|Se}!ujtFS6C;k zpHg(PeCm@0k9L0O+t2)0JqhBlKTPwE?O;N4<~F?t$wwpjd3XGobX4WMyx(H^NhLbL q`j4U}JyvP=oTjS;4h|Jh-S6^VoEx{ttkVTW4}+(xpUXO@geCxZ^T3J# diff --git a/app/src/main/res/drawable-xhdpi/avatar_mask.png b/app/src/main/res/drawable-xhdpi/avatar_mask.png deleted file mode 100644 index 0ab25721f34f5f16683aa80cf66996a6936b5903..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15535 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 diff --git a/app/src/main/res/drawable-xhdpi/chat_send_default.png b/app/src/main/res/drawable-xhdpi/chat_send_default.png deleted file mode 100644 index 6f16bc29eb32ef2ebb70309a0f424d30e6fa39c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1175 zcmV;I1Zew-P)DP8$}q0pK-kTz!6~750zF(B<6%bRER@>gNawMA`pp+jC$Y@ln{!b5uA`X z!M?KqB6P2bU0h~}PkD8$#M zX%<@&_<(3`0}qw{#&MkKmNTt%I-PJFXAF3|Of+u^A-)3Mlv2J}(stW0jAyHTZ?uV` zd0z-|MF??ub93{LSS&URc(6s_QT2SemN7)@8BNQw%0?5I?&|8gIy*bN-PhOG6^q5b z2U4ni5%>pq4S3Me(Q&mYfisy*d$Cv?l2VR4j*}{hW)YYHJ~s^GwpZzc)jilqBr@i@ z?(YCn%4>6Tb5>IUA10dqhH|i5TU*23-Q5#FhpL;db$lY|XsW4rU^FX%&FAwwx~~5U zJXNb%1rp(Eqgf&B16|j31zWRORSjIt+N=gvmB-a*&1O{(A5Az!!(vdVFgZ0A*HLRh%#kW2$K`0CZhH3tRx+ zDoONS%R!b$e}X?T2_gKGoUT`HK+Z6!3(4$J4-*2f+d) z5{VChG=S^6@2{_~|JgEkS_1=*%2wFH!U?y(}wr)gOq{|ByX-euhp`D);qRYQPH)3l|O zmsIg=x&1nsOdbPX59%1$P+&EeF9DuWT`bc+8ssss;lT6r^M4B=uBqa=WHNcCB(oaz z?yPdTT)yRj)l~h=%i*KZ=!Fuq3iKG*6u^dI`~>`{iti?q$t$9)eu=lw7WIv$X&X|?Gg8V4P18?58|E+D}08~tQ1fhKnyUY z;wri??ajQ)B?Pn_U^d&Kd(yZ`_I07*qoM6N<$g4vgKUH||9 diff --git a/app/src/main/res/drawable-xhdpi/clean_field_default.png b/app/src/main/res/drawable-xhdpi/clean_field_default.png index 320dc12d2a45f36a7e6ffdb368655d3b419cda7e..3fae16b48c8ae66af888b58ece0d3d8ae5b4e9db 100644 GIT binary patch delta 1900 zcmV-y2b1{V5uXo`BYy#fX+uL$Nkc;*aB^>EX>4Tx04R}tkv&MmKpe$iQ>7|Z2aAX} zWT*~eK~%(1t5Adrp;l;lcSTOi7~m6$XPIHz#2duZo3_DupE$(wvPyhTJZ91bi66PHc>KmW=d!>v zLuN8DPaGl^b4{!?G0U48@f2}b)pW`iQXZ?Ew>WFXGOOQ{zc855SC+X>a|kgkVhIvN zC@74qx{BWb2Zz8&jea`ba?>W!=p7(u@4QurRkOiz&ED?wS zo&%NugTMsP4YVtE4fqofLO@}_YrtvXmJ+lc`wut|6ao>!2pR``3UnEO)UsjVtn!2` zy5fN^fQJ@<`r8z60dOt`=n~){Ft7li{x%MLuH=CQQ3~+uB7pi^2k@%Zu!Txyl_0Ty zao~tmupa~7AcCD~HGtjBq2|jV1)XW*}VV45GhcK+qegSNO0GB)ius$meA~2@sPVU^fQ#>Az zh=|;|b4M~WGY$1jSXh{xJ9kcI>d>J>Mta$y2K<>Ghc=r{dU|@E2+rg2$fix3bi>xv z)QG=2Iy#Kp#CQB*?Le;{XAXx$yk76D00#yJWaGw-^S*g!&YTf7wY9Yw0shf{ANX}6 zk6>M0oobMMeSNZF!-jxitE;P3{dm1zDJm*50z9ECR!r+#BfxQSanjP#q8jXj2M=WZ z`t@`AsI06MEge35*wi7)ro7mO9IjfmN?Kc6RRg|%|Gs2oWN5ZMapHs~Y-wq!Swqy< zK47O=<6eAxyxhEbQ#J73-dFn&3Ky~VW)G4dy*6#xR zW93CsQ&VTn#MD$(RV6l?&1&HHQTA~xy^z!Cl>YvHP1u^6nuUNB;7FL!MXx}Vm6b(I zOpNC7?Cfk35)u|-6JZ3P$I81}TwE+uQ&V$3(l>70kl5H*t9!Tz;BTugP*_+flarGH zC#~k@W?8v%rIo;W0F73EIN!5pk0xwgU7ZXL4yqoze*L<{#Kc$yycXaavmNi=y;~+G zCRBlHYiq@Bx69_uo2M5I{B`x}Rf&#{HXC-rw=?mHnT~ht*de2%qnbUteEIU}bF;Ft zWN2te^<1~xZ8mhPFLULZ<<#YJX~Ndm*FOR4vz(k986F;1ZPV0$)Fe?+QDy;G`+y^r zZAU{6w{6=dBO@cKfnU0GNgNJ`X4~A{TzT~9k!st9h6V`_4>tq2VCMMbHsWmS)~%Yb zfd;&N`}U_k9d^6j2<#~E{7m2v^*T#TOw@c2uUxqz5fKpq`^wA9ld-WeRbK}V954cW zL2Ze4N{_Ss`}eDV2H4oxD3Oto^Y*uE*De_!AD`u~qN2hGaK75Kt6q<@+}vE#Gi-i- z{;Xlm<#Oru*QvEpzD4i4^vreS%a<>kGI4OZT;g`SrMbD;=;M6wX_M>U^g3R%W{sq$ zrw7~KQqNqKd|QGnV57D-kWZ-ZpAp!mK=+4VT%-y+Gi?ZeNS)7z@>?MeT%>1p;HwaY zJ*{^+WGPS=Vz56Lt;mbyafNy)z&84J7K~~I@Nxi~ delta 2226 zcmV;j2u=5&58x4yBa^fNCw~ZlNkl4#EyyFz6`iq-mqkm}oS_x-Ncfo0P%~4q6CX#b|0& zgkgj&DvBS7AS!q6=?{f~bNRa5QKnBaf9^T+-20sOJzvi~m*7RDrGKUUKoEpP0JDfF z7Ql1>p>KWw&_zVuobzTPa&pca7-Kc%<>htWdL~kuFEusw6VCas0T=;Q0_h^#i7t>L{2Ua(-n_ta{24(I%L00QA% zhyc#G-R>_O4#%x`Dt|aGE>0a98oHKq{v`k{-Ys5n&i^uD!h~(5rKKHS7p&LoKOmy7 z0Q?y55uEcT04pjgDr!daKhhk5wJ!6h=ettgl^YhyWU>(LOi0C7YMt?I~tJR*pckiA!7Qv)a zsg4oRYK(u((`Yn5ii?XYxpCtLAFL7^rqBM7h(3kyVCj=5Pxi=)aB6DmrvUy8-$Q&% zOpMFra{Xt-gH~Ev+Q)9U`&$71@J&1iFyC&s-yAljmoHyF!R>Y*^I@?75CGtCette1 zR`BD;kFx=M1b^R0d{tG|>LF9wXf#HOqIeU)B>aD%wY$6fgX-$)XOa;XMX^AxSbu+i zL`O%%&(BY(xfOc79&6UDL416?T$=A|G@9Q_MmQxUC5kcT1Tg9+PDn^VZf-98{r%D2 z-j2MyJk-_6-@`M;uyyNJBqt~LUE9BZKhB&vBZuz3Ab$umd-v3=UyH7nBbJD;apOk# z`@bIJLPA1du~;x`)-2g#O(xR-u>i1a*)qB97w+fh_p1RZot~aPk%(5wVNn4aWkIjkdzAI`^o*s4tQ;V?r>AG3+h5Gt>k20-R3#-*SXrze< zn>KC2;>C+4BfD?kKAbvrN{LZF2>_VUX#5bs)PJ!Oc6N3mJ3AW<4Gof!-o1Obhv?ok zUbt|fBPoF-m zH@Bo>a znYerRu1DEmVvip`?lrM*5Xv~`lfA{>*4Bp2n>VAiwRPCKCr+Hekt0XEy_uNAh$zUL zcW7&CLrzW(Iy*ZDty5N3hQo&sdwuJ~Ie*tMjMAHygT%zdLDLuj=FOW2t=9W5Fo>v| zy|F{Q=c+ATiq)%!Ha(|LpN>6y_IRwxy@01)6})8060BXj7DU7Q{gIK8uvjbz4i5Ha zNyea{1eI#wS|}nS0*@X&k~A1bh;Pcu%8vC} zJdCjt0B*U_mFj2LuV07RY{rWhBR_FeTU(2Qf&$6ZkBEpE%XD;NfQe_b+3o`Pr`%o9 z+o-Fn8?;{zhl>|4V%xTD9)F^*T)84w?|1F(?cek_x9}#d|4T%_9WC4a`}Z+*>QuzW z#`g8uyu3Vgb#=*(NJ~o#oK7dg!^6?h(ShRPV%a7h5q(irRn<2W4VXU+216}?I61@- z5fMfK|S%mWWmjb{=S}jYi`nQ55X};(b8u zzq-1*(rRjIx`(PX**F(_QmfS(h7&t%gnN;Yke~?&2>6PK7LP-* zhXq0Stg^CF7)j+3S+p%JEkaaO)Yn7=BKqlYnemR$4d4%UyZy_irY3R3gW72CVl*0) zL{U73_j_jg3V@Y%yZzdzM{zlK>-P!3HUN{~q0nX`+G4ZWN`C;zas$Tnk%z%xm;qq( zn}v!u53{ubuy%KMALxI`?41klMOs=~s2~Vy0IVRQj};Or05}KWgdhliRqER%r9aey z!C;ufIbTdfKLe16(VY&u1E2yxg<7pHEiEl=Q+y@n%?_8JpU*B_xDcsQsbWP@jAe|4 z00`!sGXO6*=O>Se=%FZzPPJO?EGsK}?Cn9|f7o~h_Tb`C8vpbl A#sB~S diff --git a/app/src/main/res/drawable-xhdpi/conference_start.png b/app/src/main/res/drawable-xhdpi/conference_start.png deleted file mode 100644 index 544b88b5a0d4762a6c9fde28ba16cbcd1752833f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5240 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 Z_y3vvWjEihK&}7)002ovPDHLkV1fw#dyfDB diff --git a/app/src/main/res/drawable-xhdpi/delete_field_over.png b/app/src/main/res/drawable-xhdpi/delete_field_over.png deleted file mode 100644 index ca6aa09fcea8ba5bf9260eb43a8b1988f2228552..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1268 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 diff --git a/app/src/main/res/drawable-xhdpi/ephemeral_messages_small_default.png b/app/src/main/res/drawable-xhdpi/ephemeral_messages_small_default.png deleted file mode 100644 index dc5bb984e38ceda2161b23c3965fb2642abd8aed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1553 zcmV+s2JZQZP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91CIA2c03*hiQvd)3dr3q=R9FeMm}`htRTRhn`#Pp( zC6W1<)GVb069mhGzSM_`Fw+R7Q1VqaHTb2d$SkEoB9szQ!*Db)5)stieF*DAP#*$C zvV=m*SCN(vEFH(${nkGFoO_P<&YclX8}{9Mt-aRzuf6tbo$J)Y;%DhzM7PLmKm(Ny zRrIRcpw}A66ox7VsxdLC8*tNZLS|ZHGsV$MaiFN#73Jl+8mh-H-+_844R}3w!0V3& zcB(_JhjLM)sj6A*c|z$NE1H(RdRympSe?2RZY%6!sGUB8^D(>N!&IdwZA}FHeD(H^ zX}MPN2dG^d%XFMR!I?GC&FF2Vf0Ftv$FnWUWKGe5S^5$1G1;hf z9AWpmF5@{&dhLNeNKI!ul>TsJ$1#?V>UsFRsY!AATsLTnTcY-qRonw!KQoYbz-Qo2 z-vH1?{m8A?pQ(X->*wfN4AOX}xGl3+QU9zTJNfkz%8xSp2g6uJE3`om zyJb3au~GxNKl(F>%3e;;8=6Qou)#D}1f5PT3>Y~B*%EO4Mj-wZWBqKM*(_7pEOxxQ zEdFr~W;^}frGWGW_rf$RpV5A8$GOSK{;R$k!?6zgpc;X5;RWl5z<$W`VfuX5o?~wBun5qG6}V@aQ_Ep2|u1y@6?OIc10V5#fVu6q3{|R znKfs<1o*^+*Ta4Z6ZY{w0n@N80%Ie{A@`nsDGjjJ&>)_$i4o&a93E>9G)Llatxm}t zuTd9aHCqm64~bpMN37vK7|7WkX#H->`EFmvq6x2IfnivdX@K1X@P$B#({JS*nSc*> zcpyvtaomwfNwC1SO%Y~Jn=VB{mx`PjD`J6Nb--~>tvGUm|Er|7Q>@@XZ|w?rUW4se z@SN_#iG!v4fK9V)7({z;un%^{ts@C>+8donE+obKyIe1lHzB?Bz%=aSCRCPe%(kRd zSLjWgI$0XN*}`V9EzOR0!=;xv;ItNIJE0lHDVXn8={OE7EYX35!RA>#888eh!A#x) z*5&|=)e&$p>of$O(KT7@B|GJ`&TN@YFkT7zUYU`)TqzOS_%E)m6HSXmpaK ztb+GMP`vb?cX@_d)rX0m(pSv>4on;;le{@Z1R~u73XEQw4zD8!p5h~B)rNG%hprCcyXM-85UM$;0F@ zDV9ar&Xf8*ZA~HLodDCmCuic&$S81(b~?+kGtz~oO9xCUhTV?i0u1)aG?P2x5On%( zG}E_e49LlxqrZ|bhox?fY){lJ4IM}|Sj!Ov-#)jdP{*hIx~&UW`3@GLEls`a%0*_6 z9LO8stLJ*wb{w^tJzz5@R7Aq|K}CDGUVqKYch~<9^~;-#>++Et00000NkvXXu0mjf D{wUVi diff --git a/app/src/main/res/drawable-xhdpi/field_add.png b/app/src/main/res/drawable-xhdpi/field_add.png new file mode 100644 index 0000000000000000000000000000000000000000..d447eee25f6c6c5e99a99c7f7a9ae01035068f5b GIT binary patch literal 981 zcmeAS@N?(olHy`uVBq!ia0vp^DImc}*5j~)%+dH@S4~lN>kccI6@Fqat}TvrHWwT8+1=>2U{3!^fupX$!qQ4}mq+J+Z!X;=8;f_xcUSlCE^i3XH$MAHg5BhPk*8P6c0eUD$uG48u}v|`%-o%Zhivo=cz2y~{;OuThO`pD(k+YVOF zw>Z?b$*acCChZbyr~J-!6@QNHe6Z<&v2vP~slSY(7?5A4U_a^T*m~%ON+D^Y>4o;mUP9YbK6^mMoa*kwN_1rjN(cc~Y^#{IsKM6V! z%A|apN$Y~b?z#{5KklxbI)BcGNrws-d_G!e=hm<;c0pkAocGDcGv_cIOn=~&|5Lj( z;o0ObQQqBhjC*e%I9lo*{H@`%?1KL%C-~l++GNsNBUfR+IR6w=?RJNZv#;0g?7z&e z;2W!&-_Y!wu*Ko#YrFUF^4`C_Z+!M&9fOT%(ZrVf6NG`$!j|Oi?!xdN1Q+aGJ{c&& zS>O>_42;3+Ak65bF}ngN$X?><>&pIwNt%mSf4$*04q!}P@^oae_ zpW}rLH8La($_2Ju(~5ng`{qqfk?6hFhzdrtZDpB@TN9=#MYsj#{84E-SeujP=%JR7 zwCVG?dGVXyoY{27I{o?NM8`=go^TYU;hCpWIqymEq`fNqvD%-zH_k~hcegEgdq(|4 zTHp6gPYZpe;=Wz2pOYtWd`kqkd^6AKE$LN~DG|{fjyy>M%@Rix*bGsadv;ow+>0rh zo(h#bQ84%RA@xXRIp*?4@8v(b?jP7L{7j!Yy!u(~ySRFW_H>D3iQi?7;~SQTeqFOt z!Bg#k^aHWSTS7zqqqvILZzS%y7k)L$`}l;@J-4&Ai)W~v(0=f^qxnt9)h#m>Lca>{ zV1LtZaEv`+_0G*}-bHL;S;Opo;FM%}Dr?OD*MF@(ta!fpd-bw-%cqj}&xAj44Xt%| zPs;yd^H)xvr@b`MW0KK{hNdkwTk|k{^3&c?jcsQ|@OFXnNHiacS)Vw_`MGW(_dT0Q zUZ1|rX!ncTUG_};Y1w!2jmGaM-N~u+F>TVg-q8DQ#-`X%_J1L&i140t?xwtF`Mb!v TJk>eC2xstg^>bP0l+XkKkCv@z literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/field_remove.png b/app/src/main/res/drawable-xhdpi/field_remove.png new file mode 100644 index 0000000000000000000000000000000000000000..b4ec88f481350b491ad1296b1a556142e48ee37d GIT binary patch literal 736 zcmeAS@N?(olHy`uVBq!ia0vp^DImc}*5j~)%+dH@S4~lN>kccI6@Fqat}TvrHWwT8+1=>2U{3!^fupX$!qQ4}mq+J+Z!X;=8;f_xcUSlCE^i3XH$MAHg5BhPk*8P6c0eUD$uG48u}v|`%-o%Zhivo=cz2y~{;OuThO`pD(k+YVOF zw>Z?b$*acCChZbyr~J-!6@QNHe6Z<&v2vP~slSY(7?5A4U_a^T*m~%ON+D^Y>4o;mUP9YbK6^mMoa*kwN_1rjN(cc~Y^#{IsKM6V! z%A|apN$Y~b?z#{5KklxbI)BcGNrws-d_G!e=hm<;c0pkAocGDcGv_cIOn=~&|5Lj( z;o0ObQQqBhjC*e%I9lo*{H@`%?1KL%C-~l++GNsNBUfR+IR6w=?RJNZv#;0g?7z&e z;2W!&-_Y!wu*Ko#YrFUF^4`C_Z+!M&9fOT%(ZrVf6NG`$!j|Oi?!xdN1Q+aGJ{c&& zS>O>_42;3+Ak65bF}ngN$X?><>&pIwNt%nt(&?X(1W@R%r;B4q#jUs3Z1q|L1=t>x zdu-Vrt|{V`#2nGORO206j#o0{mMp=wG23^1G{4p$QxiXLxqbe#l%i)oOhE0Z;Llnq z$$NL%J^Lo@`P;mEpL*oq2Y<3B&0ZDdyXo@X+M9RfZS~LJt+m@Je(COV&vj{5p>dPm zgkSyJ@nNl_>-;knXWpw=N{ia9QtUVWUp;@B*(Nd1yvc9QZjdbZz;b6UL9=WLv;y|_d(l^QT^19q`XbZDj-QuS3j3^P6}nkdAa|N(qD}ok&+eia=-rQiM=cq=tZCAWHj7 z)zAqY0V4!ZFf@T1-uJ!t-0zvvC&$HRt-I+W$Gtp;e;AH>+0L%~r9SZ<} zl9T*Bk&cG^>HR#s9RQ$s3f0y&gJ^3D2R!!ofWq7Xfa_UNSsDi2W|yCtTI$~5q?J!j ze&kgwm7JpD&6#2*GS2W`?d8XP)1V3`9l=6 zFm>6!Cw%#r=19lJ?m|?o8CZUPo0m4YyV2r#Th5m{LydYm7b=nDWQifAjYq2Vi^uKN zes4RtM}O~}G#{g04adgns1M#76j=~1SqQ#2a?&FzDZ%?o_18OLVbD1+Uy#Nv`6{0Y6foRPhg-9K1i-el5mT#j{o_sCe=whiwnX zBLg#YU3zzwPwzja%A`)IQn)zqLW~H}#3W?M(f+^m7By zhdeDbZbte#fb)x28Kyd$+(I85`c$Xva6Q-oLE4kb6z21AT>X}?nlGH_q&&#j6e zH}M4P+6HU;`}(^31p~AnySoOvKN1du273wXLyXOA;#jT#0D_$m9WATySz>Wm+pGH@ zI(M##p~Paj4d{Bm-n~>wi@c)B7J2Qpgn$;+;u)5N)x)mb ziE&}LNfFM~Mt}HD>=Kn08xt3kd|q)X|g!# z;U;~GJx_z55Qt}@Zh~-4ehv3SxK~~eG8&;-)AVbLV(t+f&vX>S023oL2KavcUhY;- zq?R9lE{i}jr_yMdu?{qFEkEUt7gg-p1I0BNv?x^D~6yfR3=PQG#3bz zIXJOj&>J$s^9gRt9v6NGUov-vQ@BjU*Pi{%CKv+xhW za<>H0ORcAW_}^_LT(n&8s^Oj%jrbQWUtP@2Gki-^6H_m`%Kqr~BGtaj$~y92^u7m6 zh@%VyFO0J(*jlOIf3$)S2kboiA6aAFYVeokl?$a_f#xEF7_oV07cQG5_`j6#C8zY5 zGH^&e1kGRhk8`3DV)I}^`w%pmCXMcYU_A$dG>T*~KrxL=b^it-E+J-2gU;SJyfW5^ zWJlC7wEmld%bXClkCe4n2_-Ug*#ct4JTm{5my$r-JOoTT;xrLX?{<~t#foW}@`8=p z;S+;6$vZ_ zSdQ@)3tyL5r;=jWr#?nz#J<7y6S{VKA{)xe4vT1Qk=k2-Xf%Q16qn*42u)^l^O*j9 zicpFL!1fD1lXp$?DcECd6vFAPWl2!A`V^Hltu-(Ya9pL=xff&;C zT?YHsREb(oft!T4sZgTA$|_xK`ry**cGe_ zWJgyaw-4-W)Y$5mf?10NS$e*0UNojDq3HvfQkYVj8hQ^t(3=tb&(E0f@wrFvWIar% zxMWqt#RlTfxhv{;>Cl5A(LL_0dEc)JSu&i2HNvK%{9i|*O1zs>JPR4a2TA2`u7&dM zj*03N5If0>k_Br`IPc)7m$zJNXKd5MWrLbFb?}THHQ5iveDM%XOk7LcpV)ffY3#GH z{u^A?fSpR)C+y)g;f}a8#1tV3R$sIgz!PB5&Q~)V%tf78-R1!P!?dS>C5rwv#ZDju zWuDJlQ=a*CBq1T#*9g5UnWH!?o6FI-VD$TLuhEaltiH=J^&nZ2=&lXqe9Tx!Cqqm$ z5$G`MB=K+y1Ds=;qniUk?FW+?@Ergn&!!307Vdr$5#74AD+LB_LL$3TwPfk01-KcQ zj+z9Guc3=+)S2e!(@fSRwZDQrPTt!MM52)ua_fx@yBaQ~3guRGh!Wa5N|_vm*Y|aG zfBNpw^HbGkKXq*(m{4y6w+-T9l%Q^s7@jd0MNPc6Gg7(atOFaSY z7L6|{5BS}}cfQvCz{Irkw!t7!`SLW+v`V_zg!j9AOV1^bC6aZKz2V9-olCgQmen(j zYUDKE4MJ}nSM)sxhoYb-u$Y!UxwK*1#An`8-EA|-KX;@t9C!`b4)i3;aa1v>Ng%U% z%Bo^M@0Iq^i=j-?Z1bMcHz9oKjG#L_;Hs`}l_w#5a2~ z4I79wq3*(ukS_gs(ZS;Wcbv2X15aHymrYV@j7}%KcRAs}TQW3FQQN5kygZVck~=Svr5=qj$#QRw64^~53Qd=EJzD33!tuPN zzPfnj>F9A%E8o&V1WbrZ-Sb2apW1RqYNxbhgSLfX#{^e0NSb^TcH&i-0e&a&qT%#Z z^{;R0cp?dEz^h}^CqA}WYIJghPm-c_)8%FbUEox^@x;L~ z-g=LBr9hZ7%im#A=O1pKqA|K+BGGrz2VP5D)3*Aq{QD;U+I?a1MBwOjr^GO1^4$}l zZw8ftiSaYG{(bcH)ES-@X(4P`Ix_YaE{_sUIaQY=pyp58DNOcRd z%}i&rd3ROw4NIY=NyBw2-+Eg00@(b!M`t@eCrnGr_Brr`!OleDFmL9L64nw8t_iV6 z88jX~TWY~@60fE%-e_b!nGwu&`jM*8CM5}odHF!V7&CWn2rhU1!*MC-2NVA*HQ-6! zbbUfBE$qDMUFry<)T%l<=I)8tSSA~1#0M;?ubKPFyM0l|E*-4~)!nB=4MU5P-Z%%G zOoD3E#y?_1))GO0X{W0*p*uA!oCo}@$SA`{Ylz(UrYRq8SH94apAPbI`CCgg0}eFO z-}F|h;BK8ci;!lFVU*Yqg+_hO)s@+;YlL-(JF`F$(|kEU2bV=8e)EKEBfrvC_x&EX zdVZYSP#fi&fsXR4SYP+JG5Qnx!wxQk-atpaCN>Y6jTk!XK^I81a?JPh<~#-gtRQx$ zw~@G~Xnwl6i62><_>*eEtt&pGB~nE(w{k*Mc5Uq@?WZ-rkwR2M!zNAyUQQl5F3j+D zvT;uEgt)pe1+)@Z2hJ=SPcdRo*QK@W(A0XNhv#Ql4K%A4&Q^t?P2&0R(jc(~K(ut# zf2PKn``;dQzPnWd7}x_xJo~ zj^6!F-*aIpbI$zfGrgCu2OFn=ZF8ilZeC72Om+53#_0V3P5A5?G!_4tZnubybG}2w zS$fEfuDa5I&yMr?!8#|qXg9VgFlF9y2w*UxPbh(mcKr})`s zt(!6*-E~dhLKojs+=}{qp`IX&S+%i-aH%xb~8#pQlI~tm!xzYP%HjDpL(u1_}nQXDeltoB$aJZ$!RlK9VzyXr&}4d=VrzzPfhM?gBhgeeFwaVG7oVMObctmQAszPoQj(hCjG`}m+a9@L3Tw^A# zV9i|kT=vQ(F+^jSKbWvC-a-s6^DG`!Iw~MaibdgigY_m?E!wU8{UD`DV#BYrrS>yX zf(;rC4~j<&RKAfwJg}LX!;_CAcVSwPev1B3+2V;<%+LMK@aw$`!^JJjgc%leRHT+$ zH+o&O{|5D{x^RTr(9_^c&|Oj5svLr3`rr zdN5T`X3!O3iK9k@QqdTwnc#ZjQ;%208#nq9XPK}MZ;{ccdi|IBI0T{-M4*DKGWtz- zs5AM(iRC+GnZc#V5YT%mf<8jQr&kI^IRS49<*GMF}?{m);)E7eVVHLLj48tUjI=Vx!Lili9&0ypIR7)_WAb ze`rBwFi0G;8hMZU#JPh`KNgBShL{@8-o>%#PD>R(Zi`Cq5@6c0%Ea_hY#OvHJ}hjl zROr8~U{5|ky?@$xP=Ex+DrP^BJe|J>xk5Q;q}E!t!n}F6dhKO7*FT18iY=Bg^>Q?w zKJZ1#fuLJAMRjynCB7+Q6A?~ZcX>^Lo8q499^nNxsYdOz+@ge;cwCpf;lZKwbR@!??vT1+$-1 z#Xj^YXyyeYkqKk7rLXS7TwUdKNthf=*+%?lwmhF0w8*Il+H1S_*pWE<9e)~dHbLb{V@M*(+m4Yiqm3_Vk`y4}G zJV2<$6YK~=^POe%rP%q$2hp*vpvH|}gr0}V9%g*(Or?Tt(=Oqk`5{v5&?rePKu6$# z!d)DYI(c^9kPR+xF&_yEYOfd@zhj~#tu0`hYU88fOZqt1k%s%HXZwx3R&vU*O*sL@ z${uBZXI3aH29K)(H9PY)cv4`Nz=H=`OFxE~PukNve`@$1snwRDN=462^1XGp8-_*| z*r!{Z;zRRTy72|sGTpiO>T2kasEX#)5B<0Gsa0 z&}fj`wo}NnKarF{wty+>N#E~;a_HGw44^TR+6mz} zh0Yh7<$iBMioCDjKfh|tT5wvu-W!KL7}@H$fm$S(PmFZ1arOqpWUsdbR50i3g_m4^ zF62KtVNh8C&xgipRC@>xAIoj~+)-cODxRPJ$2`R@vR0L~*L!??{WJtML$y>H+2>T~ zZ7a3fwN<=mu$668*KUE_XW^7uu9^O#3aN>!oG#f_gK#gIkzokTwuUzyz^+&Usqt!&#t0cu- zEvq!?0;8Zj&jOm9n7fxYrds3mLe7HO?IcrD;E#QwEf^OyX}OhpIry;UvR@m_P5MEO zEs?%Ox3;3q5y#VVrm3SFT{WrNb%STDu!D*OM?E?lvMZ@<*HS0d2dT3ROuy_LOZG(; z=WJO1Hldc8VP5g~E?xy!8w=H09FjwA{gCVdTY#Ajj?SDtCq2xeO+D%}X2wqVq5V$I z?$N@FLrFzdU;97@@}_-f^5HxPqjf^x_h9xDXSwKgW<*t3Xw{R1aPUKWO!wdn;emy- z{$Jg{oGY86ljg{xl-AL}LZ9T6vip2ry~Zo5rx|`7`1Lp^r`V}1k{EcXpT(U%CUP*6 z4L{Equoi}7%L(!;I=89ZQuRG{r7zv-M$Afh-T6DCIUTw!IcYi+v8GK{c2CL-O}>O= z*G-IkLInhdKAsK!#G&atA{WKAZ1MB3=)`EYoNV4vpLi`iv4+lYv|QS$%Jg&Z%$E506v(Qc=EKI(Vy5JU)y3mgPg0C}a#}e*0|Co*h*>g!Q?@8NRI8Yzy_Tqwm zzy#djwt8B!%+oh~MTSkte#2_VKCeYwn=2}Y<=>xi65Dut%(OwzmXf@?<8iNm19}!`=Gqv;r4LlM#C5!A+F!^ zXk#r6`rfa@lUB>TM=fPc?12+f>jKB>Z8_(!W4R@6=R+F3Hr<#?|LH+7?^c;N>G3QA z&(f56MpO@+fEb1#6V?g~{(PIg?As&j(+E#=b%>eUGVncb@6<__CU*vySQL-wx{0OJh23ufIeV0XGc*% zbC#actSz#uE{CMmket;CoLzLIT8L{73yTU230=}Ye@YdQa zdWw{S?L{AurxV3+3-32x{$c0JO$+k9!M4xc#Q!O=-QV9t+3dy8t1SU`!Dqj7ka7zS z!7q^J_xl!Cux|v^Z1SfUBz1JZ?jygYNcw^wY&=n>8u`cZ!?moi1;xX#x&eOLps~*4 zop&M8FH$YQjVLtz+56~GD!=P?1>0o+Co^@2z)E=Hj8bViR+1q(Np(&6Gs&;OS!M?irWdY|UqdV(b*2n8 zb*ttbX~g{E@9A4ZACQ+dD!##1fZK9k={9~4*LlbL4#k%Y7SqYfR+aYHwrPeoRb!*e zJTdwd)ts68ii#f{<9~jz!4-jOXfHt*39^j?hqtCkuAFIZTY}}HUevCQH+BO?5?nkW zF;|zeC=-jb_5}mN!W(7&qLZTz(XBLRj!&0R^JAn4^r)fsuZtL#ZL4_a}FmM>UUnwO z`5AvQlfzQ5G7znp z49;ta3QSo|FZ{;!3ATB1lFs`+Gx#;q*@io25f$~02#2x06^VvZR`~8*CX>M#*!Y%+ zhu!u)5ySgf?G`zqm0P2f$SER5+~*_6wXNUtn_9eXMiCHeux%pIbsXt^W^udw{el?CY zHY4sR_E{VS)?PQb40eDBE+#$@o97?{R*aKn%%$#A=0BhMvB`epuZxOEdruR|Q9IsEMO;xzYn7Mu2J19WNJ~V$5A=s*U!~;T zPW^_cBY1pvLlLRTGpx1H_uT?Al?XCiPNj{_?)Y}~{4TDvd#`di(@%+7?Jn*+8>d$0 z4u~WgpkrHZ^-7db=f?&=$U66!FZ0nXpkmr#(ciC@eqSM6bvs_zrn1oR1R84D&0G0< z@Sck&#I$u+UXylIAuxL0EOWq%aYcLkWfX=IgpACAJhnpamQ-ud#w?=;(M&@~bZZ=+RUIht^ zF<6Qq1ZkD>_dnIfkoQokmiuorphBb$h1zdc(Qx3j8P2)zkf+2TUX;bgM{_Y98TBYA z^3v^YV^oIO6P1pxBr?lCB_}qq;)uRP#X?++kl1-T>~oWICRHzf`Ab!L*I;%-)cY?3 z8kV?>xMHEe zO1{D?)vRq5Um%j4TfaaS^A?e5vF<&u4m3?a`iAvma>siahVF!d~ z&}>t1$uab>fgU5q8WL4 z|GVJE{=bFe$+LbTOgk|K8v^O1- literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/linphone_logo.png b/app/src/main/res/drawable-xhdpi/linphone_logo.png index ad50cfadea529e5c7af14b29a0191948b87cea83..533a934505fb98865512a50b233314d9ca12985b 100644 GIT binary patch literal 15831 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

_O!H$*+{7L@|QWsq+6$*6?cUS|Lw0r;h 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 z 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 diff --git a/app/src/main/res/drawable-xhdpi/linphone_logo_orange.png b/app/src/main/res/drawable-xhdpi/linphone_logo_orange.png deleted file mode 100644 index a1d0a8bdab6e177847db56989d7a40a4fd7a43cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9311 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 E0PAWps{jB1 diff --git a/app/src/main/res/drawable-xhdpi/linphone_notification_icon.png b/app/src/main/res/drawable-xhdpi/linphone_notification_icon.png deleted file mode 100644 index 533a934505fb98865512a50b233314d9ca12985b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15831 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

uW&&F3A?MglII{KUF8TPttCSlB}U`fd7Hn zAzLg?vW5>l9#7wKF_`&Xi<7M7i{so20FWfGNF=BYZ;wIyGWyXvd>JTwBO?%b~RTjwRO;vd0a@LguU#G)x9*za<=TtuXw zDojDw^>r3U8S{z_Ckin{jA%6aB20MMWFan?i2ic&mxIKps;W8$V3oyH+Og@xg_Wli zBbLaY0MKLcl|Bd|PWduth#1w?)onzy)}kzJcw3g`PYsCy0L;7+fM)TQ7Cbhr$DHa* zMNxdr{J6!N>3C65l(!QS0{|pRS^=QZ;?Fd654W36Trp%>?j@poEDlY>ipt8$rliCG zfY@%6Z(2O2IH+K5P*pYVl=!&&+G?6s51_@OG->ool60eylV{?JQC(f#CWN>J)_d}# zKtw;5W!cc{2}7p@$g=z%fE5Pj*C`iUMS0QM?#*IXe)ld{|qAf5P;j_{FFE}i5(v}-cp9UB|0*58LoS!=VIneZ|0dIqdGq`I=Md3G09B(@hzs0`^^9z7y;vw0{|W?EG%48 zR#w)XsYWw?POI1J&2u;$ON9`3Gjm;ziQotD2US&nUsF@lnd!!}dT6fC=c|<@>1F`+ z06f_mJ;&yWty2`mn@v7);sj%6wlz05FN{W`mlDwe0Cnl;>7CGCLu~ohJ9fK$b45i( zPRyXp$>XG(nwlm?qtW?9bYU#N)B-pYfIW%9SpYx-fIR@V3L&=0vb;YR?H(~_d&j)T zqwBhhh@8w^Ohm=ZToU_#h={tFxr>N8iKsOk4(~NG5g=FaKP2-8E5$K;$p8QV07*qo IM6N<$f+LBH1ONa4 diff --git a/app/src/main/res/drawable-xhdpi/next_default.png b/app/src/main/res/drawable-xhdpi/next_default.png deleted file mode 100644 index 0b552b5ce98f532ec65ce98246f79b1ddd9745ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1554 zcmV+t2JQKYP)a@9+Qs1)oVo zK~#9!?VE3G6LlEJzt7!uOb$EA*l6$e zeR*QcL*e$VfFe}05AQcFwAL-~AuH4$BwBFQR!%}070#!KXNs>;*VzGxk4Tp$!1GsJz z3R#wQG8T(H;aNDkuAdbIVJU!1HsP6;WqmHo@*1!99H^>#20#SB&o*H{5gn9edDCR& zBcjZdPN#=Mp-`_N2nv8XV=pEVtq6z1fpj{pdkzi&oIihlBoqoABBCdWsK#bULv3yC ztgf!ELDMwZ^*+Z=@p#;y%jNbl^BRX0?$dlezs@wxKV9E*#ww9WWM<5mu@=Aw4lCSh zNsZ;c&r}!Rh9e}+6s%*l)0a&i<`dLrH0f1O6)@)hUF#ywS!XwPwZWxBOfVRHmYMfB40&3Tq=cd<(>+%?<8agGh^ST5v|nA{bDZ3CZEfu^5w+M9 zzyko<>+0$T(&_Y7uT{=Jl4bdMBH9bUXA{0?S=RDoGWm@s;S_E+6aYt^+%yqAqiLGz zNjQZHdYp&`0NigA9%bfE!!QoHcC{)c!!S+*aB|aCM6_Q~l$SlxkR6)zmAbmR!vLa% zraZPt+QzzRg2&X>))vfUGJOD+*u|Oo<9t59(KO8*6&?Z1X6DzJc@Y4~1B|8tDD{%C zCm0NFppK3Xad>$6EPzKarDDA=3=IuEIt9j7F9?z(T?SxHvG}h^6jQ0xAK`G=S6J$% zbVGR}+DJu4X#o+<^~j%Z0I=0zwazf}dc!b|cy1||WqB-(WliM^Tg=MD&isR60jQiw(p0zWAW)4PRqpqa=#rhX7u57~=S0 zrmNvZB9VY3Ne2L|au_lY2n0I%`ueVW>XAj^S@CfI%WVHR(MQ>AcC%@kIj3_-E}csA zh-i?R7dn91scG6<07|~9ab4v!H8sr_1YwYf9<&KtM6^ZIv|TrIE?2>kWjR7bp8>ef zCY&Xrmo!b=ceCds7lCOdq9XvRZNk?8wClP)uGhlk5-=rQuVq|{Vrauq}wA_h(S``IHmgSk%)zwKNT4@(&<~_k+aFx^Tz<4E9&{Ud7 zL?-|&a2T@PFpTa0=Xzb&bDEl(=8K|u0)X8<2#c9tHw4;xOcNRaI5%;NalJ$;zp0 z;P`yLO#r$b!fGy;Ytwc8_sPtuoHpGn4xt0VU~r{rnj@2)Q#ozA|H%f}u7-w&^}W5l zqjx2zGFqXg12~8qJMpdo*sAOLyO`3yA;sfye>vpx7oE`jWjW#!7XSbN07*qoM6N<$ Ef+l?8_y7O^ diff --git a/app/src/main/res/drawable-xhdpi/quit_default.png b/app/src/main/res/drawable-xhdpi/quit_default.png index b47b451f860fa3d1d4883e445f1c26e40ddeef02..a8896a98a1d8d3b10738b2effdc4b6ca1515581f 100644 GIT binary patch literal 11802 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�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 IM6N<$f{vSzCjbBd delta 3379 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 JPDHLkV1n5^d0YSh diff --git a/app/src/main/res/drawable-xhdpi/quit_over.png b/app/src/main/res/drawable-xhdpi/quit_over.png deleted file mode 100644 index bdf8031ec3709871ab46648b3a092253c789641e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4389 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�EBh&tZ?uXLZMf-q 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 YQ*OU!hW>ud?O%ucS?@Ds*RVVP184GC761SM diff --git a/app/src/main/res/drawable-xhdpi/record_play.png b/app/src/main/res/drawable-xhdpi/record_play.png index 486dfb0de0dd04b85846f36e0ded1e764f828396..f3095fd98814a39cef926ddcd3425a1b13c2e5e6 100644 GIT binary patch literal 6663 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 C8dyyL literal 7888 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 Q2>c`Nb=Xt6>*#O)2f>lKasU7T diff --git a/app/src/main/res/drawable-xhdpi/resizable_chat_bubble_incoming.9.png b/app/src/main/res/drawable-xhdpi/resizable_chat_bubble_incoming.9.png deleted file mode 100644 index 9c200e4d476e9414a8bd56c981aa488293e676b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^JRmj)8<3o<+3y6TbUa-gLn>}1Cp0wtcS-7a)Gu!@ z)S5WU&Gdo`Gc%`@!hxelpV}MFsNr3*M3upR`TRM_Gal3e^)Yz5`njxgN@xNA?d~28 diff --git a/app/src/main/res/drawable-xhdpi/resizable_chat_bubble_outgoing.9.png b/app/src/main/res/drawable-xhdpi/resizable_chat_bubble_outgoing.9.png deleted file mode 100644 index cca504a1d80fc5dbd749e599d392648b77384e76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^JRmj)8<3o<+3y6TbUa-gLn>}1Cp0wtcS-7a^#A=l xq1MD%Zl)Jpn3*}H78r`NZ)xT5*~b=iNr=Hcb=5S(wyPlR44$rjF6*2UngH}F9LfLy diff --git a/app/src/main/res/drawable-xhdpi/resizable_confirm_delete_button.9.png b/app/src/main/res/drawable-xhdpi/resizable_confirm_delete_button.9.png deleted file mode 100644 index 509a20e498eeb1147b554fef654ef3e2ba7ec1ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 298 zcmV+_0oDGAP)+U>dVFH-ZRwE&&-Q#po0h4yY{H=0xY4Z0ARL(w!DH42_Q|; zGh~erQg=bsEV5G2H|g@thBmZK5NT@K&?#Z)#D;DOL)SL+L>PLsp^9=hCj+Pws6YiO zP=N|mpaK=BKn41b&@=JFoic~J4Poet-Ft*uxvy`2cq!(*u7}Sy0E?5f)&Kwi07*qoM6N<$f`1u-BLDyZ diff --git a/app/src/main/res/drawable-xhdpi/resizable_textfield.9.png b/app/src/main/res/drawable-xhdpi/resizable_text_field.9.png similarity index 100% rename from app/src/main/res/drawable-xhdpi/resizable_textfield.9.png rename to app/src/main/res/drawable-xhdpi/resizable_text_field.9.png diff --git a/app/src/main/res/drawable-xhdpi/resizable_textfield_error.9.png b/app/src/main/res/drawable-xhdpi/resizable_textfield_error.9.png deleted file mode 100644 index 3b6fe11406ba973db50d4077849436576e6c1d41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 516 zcmeAS@N?(olHy`uVBq!ia0vp^S|H591|*LjJ{b+9*pj^6T^Rm@;DWu&Cj&(|3p^r= zfjX{(Fr$;k>;O(#UN;bqyy)cV!=dNi}9d~J;VP`I$arf_A^Ua$L<|q7{ zprdxgrdh_=#F0~7C{JaJ%89^a$Fn=?v%j24<~(>ms7%ArJT@ovvZ~A(pWQ9C510fF zSIluVIvH)z!^0Bc)=?&X{H<=(&YsmlhSQ>)4m_36O61uXaQVav@eRB}4!>&L%9EejWnC}O`OyD)J!jm?9<3Ek=;+1wfQ?Lk8;gSP zvkL5;R~xkAwcGaOHaiB3S-!th3X*o3JFjQhQghGj!d9n)FXCn|Tz+8l>M-ZoJRA6C zo_@II`oqTx-+7kx9}_-cxoq*r>mDq3-aUQst9DK=Thz*y)lsq&)!ZKkCoWEMc0cs5 xU;o?6n#-|D>$0`(=hWY1{byh~P3*`$#)6Wd-3AfQR|6xF!PC{xWt~$(699aP-+ll9 diff --git a/app/src/main/res/drawable-xhdpi/splashscreen.png b/app/src/main/res/drawable-xhdpi/splashscreen.png deleted file mode 100644 index e6f98a8d6d27410f574a533fa7941db72a4b6191..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23756 zcmeFZ^;?u}*ES5p0E0A0N(><=Aq`5+&>+&?AT22^IRlcC(v8yHjifXP(hW)q(%s+5 zb>GkRy#K)W%e%eXc5cL+=d4-Bv1(uYIv`Y8Q3m%By^{_Vj3XE8Nq*8ae|kwI#CF-zx^%TPHZp z^y2-!lGVq!!xhc7(}TW1jmlHCwTkP9HG;xkuVOa&{4tccR91;jo_s?#v@4jBzK%A&Hc(==wk}!VoNvnH*rzdIHv32VRIQtwechZ z_2SBPTzv}SD4hMuJ&a(x_%d$QZPuE}&5BBtqej&p%OH|Vqc0>MT+NOLNVV+LUSUNe z%E9F77h#z_n8a4atn)DYBz(N3L|f|nf;TtW-fToF0$P!&PEqf?iv@I+yqBU-#A?&| z-tts)w37G^5y5Q(`)pXds(R2XmtH@|;@vQvFl|+hxjY1!ippgA�b+g3}0%m4Agl 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 diff --git a/app/src/main/res/drawable-xhdpi/topbar_service_notification.png b/app/src/main/res/drawable-xhdpi/topbar_service_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..6a9f2d327a86f65c0ff5cef99c6cb697401f307a GIT binary patch literal 7443 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 + + + + diff --git a/app/src/main/res/drawable/call_back.xml b/app/src/main/res/drawable/call_back.xml index 06367ef0d..1442cb11c 100644 --- a/app/src/main/res/drawable/call_back.xml +++ b/app/src/main/res/drawable/call_back.xml @@ -1,15 +1,7 @@ - - - - - - + android:tint="@color/white_color"/> diff --git a/app/src/main/res/drawable/hangup.xml b/app/src/main/res/drawable/call_hangup_background.xml similarity index 100% rename from app/src/main/res/drawable/hangup.xml rename to app/src/main/res/drawable/call_hangup_background.xml diff --git a/app/src/main/res/drawable/chat_group_add.xml b/app/src/main/res/drawable/chat_group_add.xml new file mode 100644 index 000000000..9ee4744df --- /dev/null +++ b/app/src/main/res/drawable/chat_group_add.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/src/main/res/drawable/chat_room_group_infos.xml b/app/src/main/res/drawable/chat_room_group_infos.xml deleted file mode 100644 index 96f25a4c2..000000000 --- a/app/src/main/res/drawable/chat_room_group_infos.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/chat_send_ephemeral_message.xml b/app/src/main/res/drawable/chat_send_ephemeral_message.xml deleted file mode 100644 index c3aaa77a4..000000000 --- a/app/src/main/res/drawable/chat_send_ephemeral_message.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable/delete_field.xml b/app/src/main/res/drawable/delete_field.xml deleted file mode 100644 index ab5826102..000000000 --- a/app/src/main/res/drawable/delete_field.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/dialer.xml b/app/src/main/res/drawable/dialer.xml deleted file mode 100644 index ea7c17f05..000000000 --- a/app/src/main/res/drawable/dialer.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/edit_list.xml b/app/src/main/res/drawable/edit_list.xml deleted file mode 100644 index fd959e163..000000000 --- a/app/src/main/res/drawable/edit_list.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ephemeral_messages.xml b/app/src/main/res/drawable/ephemeral_messages.xml new file mode 100644 index 000000000..3b8fb391d --- /dev/null +++ b/app/src/main/res/drawable/ephemeral_messages.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/round_button_background.xml b/app/src/main/res/drawable/field_button_background.xml similarity index 56% rename from app/src/main/res/drawable/round_button_background.xml rename to app/src/main/res/drawable/field_button_background.xml index 315bdc63a..6e7b32df8 100644 --- a/app/src/main/res/drawable/round_button_background.xml +++ b/app/src/main/res/drawable/field_button_background.xml @@ -1,7 +1,7 @@ + android:drawable="@drawable/field_button_background_over" /> + android:drawable="@drawable/field_button_background_default" /> diff --git a/app/src/main/res/drawable/round_button_background_default.xml b/app/src/main/res/drawable/field_button_background_default.xml similarity index 79% rename from app/src/main/res/drawable/round_button_background_default.xml rename to app/src/main/res/drawable/field_button_background_default.xml index 70045e2d8..579aad9ab 100644 --- a/app/src/main/res/drawable/round_button_background_default.xml +++ b/app/src/main/res/drawable/field_button_background_default.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/round_button_background_over.xml b/app/src/main/res/drawable/field_button_background_over.xml similarity index 79% rename from app/src/main/res/drawable/round_button_background_over.xml rename to app/src/main/res/drawable/field_button_background_over.xml index 211384486..6be50af16 100644 --- a/app/src/main/res/drawable/round_button_background_over.xml +++ b/app/src/main/res/drawable/field_button_background_over.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/history_detail_background.xml b/app/src/main/res/drawable/history_detail_background.xml new file mode 100644 index 000000000..c1c0dbff4 --- /dev/null +++ b/app/src/main/res/drawable/history_detail_background.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/history_detail_background_pressed.xml b/app/src/main/res/drawable/history_detail_background_pressed.xml new file mode 100644 index 000000000..50f07b529 --- /dev/null +++ b/app/src/main/res/drawable/history_detail_background_pressed.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/launch_screen.xml b/app/src/main/res/drawable/launch_screen.xml index 3e0583649..5a3965dfc 100644 --- a/app/src/main/res/drawable/launch_screen.xml +++ b/app/src/main/res/drawable/launch_screen.xml @@ -1,11 +1,15 @@ - - + - + android:src="@drawable/linphone_logo" + android:tint="@color/primary_color"/> + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/linphone_logo_tinted.xml b/app/src/main/res/drawable/linphone_logo_tinted.xml new file mode 100644 index 000000000..e04a976a4 --- /dev/null +++ b/app/src/main/res/drawable/linphone_logo_tinted.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/list_detail.xml b/app/src/main/res/drawable/list_detail.xml index 1ca68cb09..271ded33e 100644 --- a/app/src/main/res/drawable/list_detail.xml +++ b/app/src/main/res/drawable/list_detail.xml @@ -1,7 +1,7 @@ - - + + + diff --git a/app/src/main/res/drawable/next.xml b/app/src/main/res/drawable/next.xml deleted file mode 100644 index 968192312..000000000 --- a/app/src/main/res/drawable/next.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/quit.xml b/app/src/main/res/drawable/quit.xml deleted file mode 100644 index 2369f4f01..000000000 --- a/app/src/main/res/drawable/quit.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/recording_play_pause.xml b/app/src/main/res/drawable/recording_play_pause.xml new file mode 100644 index 000000000..4de1934d3 --- /dev/null +++ b/app/src/main/res/drawable/recording_play_pause.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/status_level.xml b/app/src/main/res/drawable/status_level.xml deleted file mode 100644 index 2852a9c03..000000000 --- a/app/src/main/res/drawable/status_level.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout-land/about.xml b/app/src/main/res/layout-land/about.xml deleted file mode 100644 index ed6924ab0..000000000 --- a/app/src/main/res/layout-land/about.xml +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -