Support for selective permissions
@ -26,27 +26,42 @@
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_MMS"/>
|
||||
<uses-permission android:name="android.permission.READ_SMS"/>
|
||||
<uses-permission android:name="android.permission.SEND_SMS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SMS"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_CALL_LOG" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
|
||||
<uses-permission android:name="android.permission.READ_CALL_STATE"/>
|
||||
|
||||
<!-- For sending/receiving events -->
|
||||
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR"/>
|
||||
|
||||
|
||||
<!-- Normal -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_CALL_LOG" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||
|
||||
<!-- For sending location tiles in the future -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||
|
||||
<!-- So we can add a TextSecure 'Account' -->
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
@ -57,10 +72,6 @@
|
||||
<!-- For conversation 'shortcuts' on the desktop -->
|
||||
<uses-permission android:name="android.permission.INSTALL_SHORTCUT"/>
|
||||
|
||||
<!-- For sending/receiving events -->
|
||||
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR"/>
|
||||
|
||||
<!-- For fixing MMS -->
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||
@ -68,20 +79,12 @@
|
||||
<!-- Set image as wallpaper -->
|
||||
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
|
||||
|
||||
<!-- Permissions from RedPhone -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
|
||||
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
|
||||
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
<uses-permission android:name="android.permission.CALL_PRIVILEGED" />
|
||||
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
|
||||
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
|
||||
<uses-permission android:name="android.permission.READ_CALL_STATE"/>
|
||||
<uses-permission android:name="android.permission.READ_LOGS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
|
||||
<permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE"
|
||||
|
@ -239,7 +239,7 @@ android {
|
||||
versionName "4.13.7"
|
||||
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 22
|
||||
targetSdkVersion 23
|
||||
multiDexEnabled true
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
BIN
res/drawable-hdpi/ic_contacts_white_48dp.png
Normal file
After Width: | Height: | Size: 448 B |
BIN
res/drawable-hdpi/ic_folder_white_48dp.png
Normal file
After Width: | Height: | Size: 245 B |
Before Width: | Height: | Size: 767 B After Width: | Height: | Size: 606 B |
BIN
res/drawable-hdpi/ic_photo_camera_white_48dp.png
Normal file
After Width: | Height: | Size: 666 B |
BIN
res/drawable-hdpi/ic_textsms_white_48dp.png
Normal file
After Width: | Height: | Size: 265 B |
BIN
res/drawable-hdpi/ic_videocam_white_48dp.png
Normal file
After Width: | Height: | Size: 234 B |
BIN
res/drawable-hdpi/no_contacts.png
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
res/drawable-mdpi/ic_contacts_white_48dp.png
Normal file
After Width: | Height: | Size: 310 B |
BIN
res/drawable-mdpi/ic_folder_white_48dp.png
Normal file
After Width: | Height: | Size: 181 B |
Before Width: | Height: | Size: 557 B After Width: | Height: | Size: 436 B |
BIN
res/drawable-mdpi/ic_photo_camera_white_48dp.png
Normal file
After Width: | Height: | Size: 446 B |
BIN
res/drawable-mdpi/ic_textsms_white_48dp.png
Normal file
After Width: | Height: | Size: 197 B |
BIN
res/drawable-mdpi/ic_videocam_white_48dp.png
Normal file
After Width: | Height: | Size: 178 B |
BIN
res/drawable-mdpi/no_contacts.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
res/drawable-xhdpi/ic_contacts_white_48dp.png
Normal file
After Width: | Height: | Size: 562 B |
BIN
res/drawable-xhdpi/ic_folder_white_48dp.png
Normal file
After Width: | Height: | Size: 325 B |
Before Width: | Height: | Size: 1013 B After Width: | Height: | Size: 819 B |
BIN
res/drawable-xhdpi/ic_photo_camera_white_48dp.png
Normal file
After Width: | Height: | Size: 894 B |
BIN
res/drawable-xhdpi/ic_textsms_white_48dp.png
Normal file
After Width: | Height: | Size: 338 B |
BIN
res/drawable-xhdpi/ic_videocam_white_48dp.png
Normal file
After Width: | Height: | Size: 290 B |
BIN
res/drawable-xhdpi/no_contacts.png
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
res/drawable-xxhdpi/ic_contacts_white_48dp.png
Normal file
After Width: | Height: | Size: 849 B |
BIN
res/drawable-xxhdpi/ic_folder_white_48dp.png
Normal file
After Width: | Height: | Size: 499 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
res/drawable-xxhdpi/ic_photo_camera_white_48dp.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
res/drawable-xxhdpi/ic_textsms_white_48dp.png
Normal file
After Width: | Height: | Size: 504 B |
BIN
res/drawable-xxhdpi/ic_videocam_white_48dp.png
Normal file
After Width: | Height: | Size: 437 B |
BIN
res/drawable-xxhdpi/no_contacts.png
Normal file
After Width: | Height: | Size: 293 KiB |
BIN
res/drawable-xxxhdpi/ic_contacts_white_48dp.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
res/drawable-xxxhdpi/ic_folder_white_48dp.png
Normal file
After Width: | Height: | Size: 681 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
res/drawable-xxxhdpi/ic_photo_camera_white_48dp.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
res/drawable-xxxhdpi/ic_textsms_white_48dp.png
Normal file
After Width: | Height: | Size: 696 B |
BIN
res/drawable-xxxhdpi/ic_videocam_white_48dp.png
Normal file
After Width: | Height: | Size: 591 B |
BIN
res/drawable-xxxhdpi/no_contacts.png
Normal file
After Width: | Height: | Size: 23 KiB |
5
res/drawable/permission_rationale_dialog_corners.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
||||
<solid android:color="@color/white"/>
|
||||
<corners android:radius="20dp"/>
|
||||
</shape>
|
@ -1,7 +1,8 @@
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v4.widget.SwipeRefreshLayout
|
||||
@ -33,4 +34,59 @@
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="end"/>
|
||||
|
||||
<LinearLayout android:id="@+id/show_contacts_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.pnikosis.materialishprogress.ProgressWheel
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="invisible"
|
||||
app:matProg_circleRadius="145dp"
|
||||
app:matProg_barWidth="6dp"
|
||||
app:matProg_rimColor="@color/signal_primary"
|
||||
app:matProg_barColor="@color/signal_primary_dark"
|
||||
app:matProg_progressIndeterminate="true"
|
||||
tools:visibility="visible"
|
||||
/>
|
||||
|
||||
<ImageView android:layout_gravity="center"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/no_contacts"/>
|
||||
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<TextView android:id="@+id/show_contacts_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginLeft="50dp"
|
||||
android:layout_marginRight="50dp"
|
||||
android:textSize="15sp"
|
||||
android:lineSpacingMultiplier="1.3"
|
||||
android:gravity="center"
|
||||
android:text="@string/contact_selection_list_fragment__signal_needs_access_to_your_contacts_in_order_to_display_them"/>
|
||||
|
||||
<Button android:id="@+id/show_contacts_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:background="@color/signal_primary"
|
||||
android:textColor="@color/white"
|
||||
android:padding="10dp"
|
||||
android:text="@string/contact_selection_list_fragment__show_contacts"/>
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
29
res/layout/permissions_rationale_dialog.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout android:id="@+id/header_container"
|
||||
android:background="@color/signal_primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:padding="40dp">
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView android:id="@+id/message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="40dp"
|
||||
android:paddingBottom="40dp"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
android:textSize="15sp"
|
||||
android:lineSpacingMultiplier="1.3"
|
||||
tools:text="Signal needs access to your contacts and media in order to connect with friends, exchange messages, and make secure calls."/>
|
||||
|
||||
</LinearLayout>
|
@ -5,6 +5,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginRight="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
app:square_height="true">
|
||||
|
||||
<ImageView android:id="@+id/thumbnail"
|
||||
|
@ -56,6 +56,10 @@
|
||||
|
||||
<!-- AttchmentManager -->
|
||||
<string name="AttachmentManager_cant_open_media_selection">Can\'t find an app to select media.</string>
|
||||
<string name="AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio">Signal requires the External Storage permission in order to attach photos, videos, or audio, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Storage\".</string>
|
||||
<string name="AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information">Signal requires Contacts permission in order to attach contact information, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and eanble \"Contacts\".</string>
|
||||
<string name="AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location">Signal requires Location permission in order to attach a location, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Location\".</string>
|
||||
<string name="AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied">Signal requires the Camera permission in order to take photos, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Camera\".</string>
|
||||
|
||||
<!-- AttachmentTypeSelectorAdapter -->
|
||||
|
||||
@ -83,6 +87,10 @@
|
||||
<string name="ConfirmIdentityDialog_you_may_wish_to_verify_your_safety_number_with_this_contact">You may wish to verify your safety number with this contact.</string>
|
||||
<string name="ConfirmIdentityDialog_accept">Accept</string>
|
||||
|
||||
<!-- ContactsCursorLoader -->
|
||||
<string name="ContactsCursorLoader_recent_chats">Recent chats</string>
|
||||
<string name="ContactsCursorLoader_contacts">Contacts</string>
|
||||
|
||||
<!-- ContactsDatabase -->
|
||||
<string name="ContactsDatabase_message_s">Message %s</string>
|
||||
<string name="ContactsDatabase_signal_call_s">Signal Call %s</string>
|
||||
@ -134,6 +142,14 @@
|
||||
<string name="ConversationActivity_error_sending_voice_message">Error sending voice message</string>
|
||||
<string name="ConversationActivity_there_is_no_app_available_to_handle_this_link_on_your_device">There is no app available to handle this link on your device.</string>
|
||||
|
||||
<string name="ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone">To send audio messages, allow Signal access to your microphone.</string>
|
||||
<string name="ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages">Signal requires the Microphone permission in order to send audio messages, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Microphone\".</string>
|
||||
<string name="ConversationActivity_to_call_s_signal_needs_access_to_your_microphone_and_camera">To call %s, Signal needs access to your microphone and camera.</string>
|
||||
<string name="ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s">Signal needs the Microphone and Camera permissions in order to call %s, but they have been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Microphone\" and \"Camera\".</string>
|
||||
<string name="ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera">To capture photos and video, allow Signal access to the camera.</string>
|
||||
<string name="ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video">Signal needs the Camera permission to take photos or video, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Camera\".</string>
|
||||
<string name="ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video">Signal needs Camera permissions to take photos or video</string>
|
||||
|
||||
<!-- ConversationAdapter -->
|
||||
<plurals name="ConversationAdapter_n_unread_messages">
|
||||
<item quantity="one">%d unread message</item>
|
||||
@ -182,6 +198,8 @@
|
||||
<string name="ConversationListActivity_there_is_no_browser_installed_on_your_device">There is no browser installed on your device.</string>
|
||||
|
||||
<!-- ConversationListFragment -->
|
||||
<string name="ConversationListFragment_no_results_found_for_s_">No results found for \'%s\'</string>
|
||||
|
||||
<plurals name="ConversationListFragment_delete_selected_conversations">
|
||||
<item quantity="one">Delete selected conversation?</item>
|
||||
<item quantity="other">Delete selected conversations?</item>
|
||||
@ -338,6 +356,13 @@
|
||||
<string name="ImportFragment_error_importing_backup">Error importing backup!</string>
|
||||
<string name="ImportFragment_import_complete">Import complete!</string>
|
||||
|
||||
<string name="ImportExportFragment_signal_needs_the_sms_permission_in_order_to_import_sms_messages">Signal needs the SMS permission in order to import SMS messages, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"SMS\".</string>
|
||||
<string name="ImportExportFragment_signal_needs_the_sms_permission_in_order_to_import_sms_messages_toast">Signal needs the SMS permission in order to import SMS messages</string>
|
||||
<string name="ImportExportFragment_signal_needs_the_storage_permission_in_order_to_read_from_external_storage_but_it_has_been_permanently_denied">Signal needs the Storage permission in order to read from external storage, but it has been permanently denied. Please continue to app settings, select \"Permissions\", then enable \"Storage\".</string>
|
||||
<string name="ImportExportFragment_signal_needs_the_storage_permission_in_order_to_read_from_external_storage">Signal needs the Storage permission in order to read from external storage.</string>
|
||||
<string name="ImportExportFragment_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied">Signal needs the Storage permission in order to write to external storage, but it has been permanently denied. Please continue to app settings, select \"Permissions\", then enable \"Storage\".</string>
|
||||
<string name="ImportExportFragment_signal_needs_the_storage_permission_in_order_to_write_to_external_storage">Signal needs the Storage permission in order to write to external storage.</string>
|
||||
|
||||
<!-- InputPanel -->
|
||||
<string name="InputPanel_tap_and_hold_to_record_a_voice_message_release_to_send">Tap and hold to record a voice message, release to send</string>
|
||||
|
||||
@ -436,6 +461,9 @@
|
||||
<string name="DeviceProvisioningActivity_link_a_signal_device">Link a Signal device?</string>
|
||||
<string name="DeviceProvisioningActivity_it_looks_like_youre_trying_to_link_a_signal_device_using_a_3rd_party_scanner">It looks like you\'re trying to link a Signal device using a 3rd party scanner. For your protection, please scan the code again from within Signal.</string>
|
||||
|
||||
<string name="DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code">Signal needs the Camera permission in order to scan a QR code, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Camera\".</string>
|
||||
<string name="DeviceActivity_unable_to_scan_a_qr_code_without_the_camera_permission">Unable to scan a QR code without the Camera permission</string>
|
||||
|
||||
<!-- ExpirationDialog -->
|
||||
<string name="ExpirationDialog_disappearing_messages">Disappearing messages</string>
|
||||
<string name="ExpirationDialog_your_messages_will_not_expire">Your messages will not expire.</string>
|
||||
@ -504,6 +532,10 @@
|
||||
<string name="RegistrationActivity_google_play_services_is_updating_or_unavailable">Google Play Services is updating or temporarily unavailable. Please try again.</string>
|
||||
<string name="RegistrationActivity_more_information">More information</string>
|
||||
<string name="RegistrationActivity_less_information">Less information</string>
|
||||
<string name="RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends">Signal needs access to your contacts and media in order to connect with friends, exchange messages, and make secure calls</string>
|
||||
<string name="RegistrationActivity_unable_to_connect_to_service">Unable to connect to service. Please check network connection and try again.</string>
|
||||
<string name="RegistrationActivity_to_easily_verify_your_phone_number_signal_can_automatically_detect_your_verification_code">To easily verify your phone number, Signal can automatically detect your verification code if you allow Signal to view SMS messages.</string>
|
||||
|
||||
|
||||
<!-- RegistrationProblemsActivity -->
|
||||
|
||||
@ -584,6 +616,8 @@
|
||||
<string name="VerifyIdentityActivity_our_signal_safety_number">Our Signal safety number:</string>
|
||||
<string name="VerifyIdentityActivity_no_app_to_share_to">It looks like you don\'t have any apps to share to.</string>
|
||||
<string name="VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard">No safety number to compare was found in the clipboard</string>
|
||||
<string name="VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied">Signal needs the Camera permission in order to scan a QR code, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Camera\".</string>
|
||||
<string name="VerifyIdentityActivity_unable_to_scan_qr_code_without_camera_permission">Unable to scan QR code without Camera permission</string>
|
||||
|
||||
<!-- KeyExchangeInitiator -->
|
||||
|
||||
@ -609,6 +643,9 @@
|
||||
<!-- MuteDialog -->
|
||||
<string name="MuteDialog_mute_notifications">Mute notifications</string>
|
||||
|
||||
<!-- OutdatedBuildReminder -->
|
||||
<string name="OutdatedBuildReminder_no_web_browser_installed">No web browser installed!</string>
|
||||
|
||||
<!-- ApplicationMigrationService -->
|
||||
<string name="ApplicationMigrationService_import_in_progress">Import in progress</string>
|
||||
<string name="ApplicationMigrationService_importing_text_messages">Importing text messages</string>
|
||||
@ -625,6 +662,9 @@
|
||||
<string name="MediaPreviewActivity_you">You</string>
|
||||
<string name="MediaPreviewActivity_unssuported_media_type">Unsupported media type</string>
|
||||
<string name="MediaPreviewActivity_draft">Draft</string>
|
||||
<string name="MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied">Signal needs the Storage permission in order to save to external storage, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Storage\".</string>
|
||||
<string name="MediaPreviewActivity_unable_to_write_to_external_storage_without_permission">Unable to save to external storage without permissions</string>
|
||||
|
||||
|
||||
<!-- MessageNotifier -->
|
||||
<string name="MessageNotifier_d_new_messages_in_d_conversations">%1$d new messages in %2$d conversations</string>
|
||||
@ -650,13 +690,24 @@
|
||||
<!-- SaveAttachmentTask -->
|
||||
<string name="SaveAttachmentTask_open_directory">Open directory</string>
|
||||
|
||||
<!-- SearchToolbar -->
|
||||
<string name="SearchToolbar_search">Search</string>
|
||||
|
||||
<!-- SingleRecipientNotificationBuilder -->
|
||||
<string name="SingleRecipientNotificationBuilder_signal">Signal</string>
|
||||
<string name="SingleRecipientNotificationBuilder_new_message">New message</string>
|
||||
|
||||
<!-- UnauthorizedReminder -->
|
||||
<string name="UnauthorizedReminder_device_no_longer_registered">Device no longer registered</string>
|
||||
<string name="UnauthorizedReminder_this_is_likely_because_you_registered_your_phone_number_with_Signal_on_a_different_device">This is likely because you registered your phone number with Signal on a different device. Tap to re-register.</string>
|
||||
|
||||
<!-- VideoPlayer -->
|
||||
<string name="VideoPlayer_error_playing_video">Error playing video</string>
|
||||
|
||||
<!-- WebRtcCallActivity -->
|
||||
<string name="WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone">To answer the call from %s, give Signal access to your microphone.</string>
|
||||
<string name="WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls">Signal requires Microphone and Camera permissions in order to make or receive calls, but they have been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Microphone\" and \"Camera\".</string>
|
||||
|
||||
<!-- WebRtcCallScreen -->
|
||||
<string name="WebRtcCallScreen_new_safety_numbers">The safety number for your conversation with %1$s has changed. This could either mean that someone is trying to intercept your communication, or that %2$s simply re-installed Signal.</string>
|
||||
<string name="WebRtcCallScreen_you_may_wish_to_verify_this_contact">You may wish to verify your safety number with this contact.</string>
|
||||
@ -700,12 +751,18 @@
|
||||
<string name="SingleContactSelectionActivity_contact_photo">Contact Photo</string>
|
||||
|
||||
<!-- ContactSelectionListFragment-->
|
||||
<string name="ContactSelectionListFragment_signal_requires_the_contacts_permission_in_order_to_display_your_contacts">Signal requires the Contacts permission in order to display your contacts, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Contacts\".</string>
|
||||
<string name="ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection">Error retrieving contacts, check your network connection</string>
|
||||
|
||||
<!-- blocked_contacts_fragment -->
|
||||
<string name="blocked_contacts_fragment__no_blocked_contacts">No blocked contacts</string>
|
||||
|
||||
<!-- contact_selection_recent_activity -->
|
||||
|
||||
<!-- contact_selection_list_fragment -->
|
||||
<string name="contact_selection_list_fragment__signal_needs_access_to_your_contacts_in_order_to_display_them">Signal needs access to your contacts in order to display them.</string>
|
||||
<string name="contact_selection_list_fragment__show_contacts">Show Contacts</string>
|
||||
|
||||
<!-- conversation_title_view -->
|
||||
|
||||
<!-- conversation_activity -->
|
||||
@ -732,6 +789,7 @@
|
||||
<string name="conversation_item_sent__send_failed_indicator_description">Send Failed</string>
|
||||
<string name="conversation_item_sent__pending_approval_description">Pending Approval</string>
|
||||
<string name="conversation_item_sent__delivered_description">Delivered</string>
|
||||
<string name="conversation_item_sent__message_read">Message read</string>
|
||||
|
||||
<!-- conversation_item_received -->
|
||||
<string name="conversation_item_received__contact_photo_description">Contact photo</string>
|
||||
@ -765,6 +823,11 @@
|
||||
<!-- experience_upgrade_activity -->
|
||||
<string name="experience_upgrade_activity__continue">continue</string>
|
||||
|
||||
<string name="experience_upgrade_preference_fragment__read_receipts_are_here">Read receipts are here</string>
|
||||
<string name="experience_upgrade_preference_fragment__optionally_see_and_share_when_messages_have_been_read">Optionally see and share when messages have been read</string>
|
||||
<string name="experience_upgrade_preference_fragment__enable_read_receipts">Enable read receipts</string>
|
||||
|
||||
|
||||
<!-- expiration -->
|
||||
|
||||
<string name="expiration_off">Off</string>
|
||||
@ -889,6 +952,7 @@
|
||||
<string name="profile_create_activity__your_name">Your name</string>
|
||||
|
||||
<!-- recipient_preferences_activity -->
|
||||
<string name="recipient_preference_activity__shared_media">Shared media</string>
|
||||
|
||||
<!-- recipient_preferences -->
|
||||
<string name="recipient_preferences__mute_conversation">Mute conversation</string>
|
||||
@ -908,6 +972,8 @@
|
||||
<!-- registration_activity -->
|
||||
<string name="registration_activity__phone_number">PHONE NUMBER</string>
|
||||
<string name="registration_activity__registration_will_transmit_some_contact_information_to_the_server_temporariliy">Signal makes it easy to communicate by using your existing phone number and address book. Friends and contacts who already know how to contact you by phone will be able to easily get in touch by Signal.\n\nRegistration transmits some contact information to the server. It is not stored.</string>
|
||||
<string name="registration_activity__verify_your_number">Verify Your Number</string>
|
||||
<string name="registration_activity__please_enter_your_mobile_number_to_receive_a_verification_code_carrier_rates_may_apply">Please enter your mobile number to receive a verification code. Carrier rates may apply.</string>
|
||||
|
||||
<!-- registration_problems -->
|
||||
|
||||
@ -932,6 +998,10 @@
|
||||
<!-- verify_identity -->
|
||||
<string name="verify_identity__share_safety_number">Share safety number</string>
|
||||
|
||||
<!-- webrtc_answer_decline_button -->
|
||||
<string name="webrtc_answer_decline_button__swipe_up_to_answer">Swipe up to answer</string>
|
||||
<string name="webrtc_answer_decline_button__swipe_down_to_reject">Swipe down to reject</string>
|
||||
|
||||
<!-- message_details_header -->
|
||||
<string name="message_details_header__issues_need_your_attention">Some issues need your attention.</string>
|
||||
<string name="message_details_header__sent">Sent</string>
|
||||
@ -1145,8 +1215,14 @@
|
||||
<string name="conversation_list_item_view__contact_photo_image">Contact Photo Image</string>
|
||||
<string name="conversation_list_item_view__archived">Archived</string>
|
||||
|
||||
<string name="conversation_list_item_inbox_zero__inbox_zeeerrro">Inbox zeeerrro</string>
|
||||
<string name="conversation_list_item_inbox_zero__zip_zilch_zero_nada_nyou_re_all_caught_up">Zip. Zilch. Zero. Nada.\nYou\'re all caught up!</string>
|
||||
|
||||
|
||||
<!-- conversation_list_fragment -->
|
||||
<string name="conversation_list_fragment__fab_content_description">New conversation</string>
|
||||
<string name="conversation_list_fragment__give_your_inbox_something_to_write_home_about_get_started_by_messaging_a_friend">Give your inbox something to write home about. Get started by messaging a friend.</string>
|
||||
|
||||
|
||||
<!-- conversation_secure_verified -->
|
||||
<string name="conversation_secure_verified__menu_reset_secure_session">Reset secure session</string>
|
||||
@ -1217,6 +1293,7 @@
|
||||
<string name="media_preview__all_media_title">All media</string>
|
||||
|
||||
<!-- media_overview -->
|
||||
<string name="media_overview_documents_fragment__no_documents_found">No documents</string>
|
||||
|
||||
<!-- media_preview_activity -->
|
||||
<string name="media_preview_activity__media_content_description">Media preview</string>
|
||||
@ -1232,27 +1309,7 @@
|
||||
|
||||
<!-- transport_selection_list_item -->
|
||||
<string name="transport_selection_list_item__transport_icon">Transport icon</string>
|
||||
<string name="conversation_item_sent__message_read">Message read</string>
|
||||
|
||||
<string name="media_overview_documents_fragment__no_documents_found">No documents</string>
|
||||
<string name="experience_upgrade_preference_fragment__read_receipts_are_here">Read receipts are here</string>
|
||||
<string name="experience_upgrade_preference_fragment__optionally_see_and_share_when_messages_have_been_read">Optionally see and share when messages have been read</string>
|
||||
<string name="experience_upgrade_preference_fragment__enable_read_receipts">Enable read receipts</string>
|
||||
<string name="recipient_preference_activity__shared_media">Shared media</string>
|
||||
<string name="registration_activity__verify_your_number">Verify Your Number</string>
|
||||
<string name="registration_activity__please_enter_your_mobile_number_to_receive_a_verification_code_carrier_rates_may_apply">Please enter your mobile number to receive a verification code. Carrier rates may apply.</string>
|
||||
<string name="conversation_list_fragment__give_your_inbox_something_to_write_home_about_get_started_by_messaging_a_friend">Give your inbox something to write home about. Get started by messaging a friend.</string>
|
||||
<string name="conversation_list_item_inbox_zero__inbox_zeeerrro">Inbox zeeerrro</string>
|
||||
<string name="conversation_list_item_inbox_zero__zip_zilch_zero_nada_nyou_re_all_caught_up">Zip. Zilch. Zero. Nada.\nYou\'re all caught up!</string>
|
||||
<string name="ConversationListFragment_no_results_found_for_s_">No results found for \'%s\'</string>
|
||||
<string name="SearchToolbar_search">Search</string>
|
||||
<string name="UnauthorizedReminder_device_no_longer_registered">Device no longer registered</string>
|
||||
<string name="UnauthorizedReminder_this_is_likely_because_you_registered_your_phone_number_with_Signal_on_a_different_device">This is likely because you registered your phone number with Signal on a different device. Tap to re-register.</string>
|
||||
<string name="OutdatedBuildReminder_no_web_browser_installed">No web browser installed!</string>
|
||||
<string name="ContactsCursorLoader_recent_chats">Recent chats</string>
|
||||
<string name="ContactsCursorLoader_contacts">Contacts</string>
|
||||
<string name="webrtc_answer_decline_button__swipe_up_to_answer">Swipe up to answer</string>
|
||||
<string name="webrtc_answer_decline_button__swipe_down_to_reject">Swipe down to reject</string>
|
||||
|
||||
|
||||
<!-- EOF -->
|
||||
|
@ -348,4 +348,8 @@
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
|
||||
<item name="search_toolbar_background">@color/black</item>
|
||||
</style>
|
||||
|
||||
<style name="RationaleDialog" parent="Theme.AppCompat.Light.Dialog.Alert">
|
||||
<item name="android:windowBackground">@drawable/permission_rationale_dialog_corners</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
@ -17,7 +17,10 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
@ -27,10 +30,16 @@ import android.support.v4.content.Loader;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
|
||||
@ -38,10 +47,14 @@ import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@ -72,6 +85,10 @@ public class ContactSelectionListFragment extends Fragment
|
||||
private Set<String> selectedContacts;
|
||||
private OnContactSelectedListener onContactSelectedListener;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
private View showContactsLayout;
|
||||
private Button showContactsButton;
|
||||
private TextView showContactsDescription;
|
||||
private ProgressWheel showContactsProgress;
|
||||
private String cursorFilter;
|
||||
private RecyclerView recyclerView;
|
||||
private RecyclerViewFastScroller fastScroller;
|
||||
@ -79,17 +96,34 @@ public class ContactSelectionListFragment extends Fragment
|
||||
@Override
|
||||
public void onActivityCreated(Bundle icicle) {
|
||||
super.onActivityCreated(icicle);
|
||||
|
||||
initializeCursor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
|
||||
handleContactPermissionGranted();
|
||||
} else {
|
||||
this.getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
})
|
||||
.onAnyDenied(() -> {
|
||||
getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
||||
|
||||
if (getActivity().getIntent().getBooleanExtra(RECENTS, false)) {
|
||||
getLoaderManager().initLoader(0, null, ContactSelectionListFragment.this);
|
||||
} else {
|
||||
initializeNoContactsPermission();
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -100,6 +134,10 @@ public class ContactSelectionListFragment extends Fragment
|
||||
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
|
||||
swipeRefresh = ViewUtil.findById(view, R.id.swipe_refresh);
|
||||
fastScroller = ViewUtil.findById(view, R.id.fast_scroller);
|
||||
showContactsLayout = view.findViewById(R.id.show_contacts_container);
|
||||
showContactsButton = view.findViewById(R.id.show_contacts_button);
|
||||
showContactsDescription = view.findViewById(R.id.show_contacts_description);
|
||||
showContactsProgress = view.findViewById(R.id.progress);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
|
||||
swipeRefresh.setEnabled(getActivity().getIntent().getBooleanExtra(REFRESHABLE, true) &&
|
||||
@ -108,6 +146,11 @@ public class ContactSelectionListFragment extends Fragment
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
public @NonNull List<String> getSelectedContacts() {
|
||||
List<String> selected = new LinkedList<>();
|
||||
if (selectedContacts != null) {
|
||||
@ -130,7 +173,28 @@ public class ContactSelectionListFragment extends Fragment
|
||||
selectedContacts = adapter.getSelectedContacts();
|
||||
recyclerView.setAdapter(adapter);
|
||||
recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter, true, true));
|
||||
this.getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
private void initializeNoContactsPermission() {
|
||||
swipeRefresh.setVisibility(View.GONE);
|
||||
|
||||
showContactsLayout.setVisibility(View.VISIBLE);
|
||||
showContactsProgress.setVisibility(View.INVISIBLE);
|
||||
showContactsDescription.setText(R.string.contact_selection_list_fragment__signal_needs_access_to_your_contacts_in_order_to_display_them);
|
||||
showContactsButton.setVisibility(View.VISIBLE);
|
||||
|
||||
showContactsButton.setOnClickListener(v -> {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.ContactSelectionListFragment_signal_requires_the_contacts_permission_in_order_to_display_your_contacts))
|
||||
.onSomeGranted(permissions -> {
|
||||
if (permissions.contains(Manifest.permission.WRITE_CONTACTS)) {
|
||||
handleContactPermissionGranted();
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
public void setQueryFilter(String filter) {
|
||||
@ -161,6 +225,9 @@ public class ContactSelectionListFragment extends Fragment
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
swipeRefresh.setVisibility(View.VISIBLE);
|
||||
showContactsLayout.setVisibility(View.GONE);
|
||||
|
||||
((CursorRecyclerViewAdapter) recyclerView.getAdapter()).changeCursor(data);
|
||||
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
|
||||
boolean useFastScroller = (recyclerView.getAdapter().getItemCount() > 20);
|
||||
@ -177,6 +244,44 @@ public class ContactSelectionListFragment extends Fragment
|
||||
fastScroller.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleContactPermissionGranted() {
|
||||
new AsyncTask<Void, Void, Boolean>() {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
swipeRefresh.setVisibility(View.GONE);
|
||||
showContactsLayout.setVisibility(View.VISIBLE);
|
||||
showContactsButton.setVisibility(View.INVISIBLE);
|
||||
showContactsDescription.setText("Loading...");
|
||||
showContactsProgress.setVisibility(View.VISIBLE);
|
||||
showContactsProgress.spin();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
try {
|
||||
DirectoryHelper.refreshDirectory(getContext(), null, false);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
if (result) {
|
||||
showContactsLayout.setVisibility(View.GONE);
|
||||
swipeRefresh.setVisibility(View.VISIBLE);
|
||||
reset();
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection, Toast.LENGTH_LONG).show();
|
||||
initializeNoContactsPermission();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener {
|
||||
@Override
|
||||
public void onItemClick(ContactSelectionListItem contact) {
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ActivityNotFoundException;
|
||||
@ -132,6 +133,7 @@ import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@ -572,6 +574,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
updateReminders(recipient.hasSeenInviteReminder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
//////// Event Handlers
|
||||
|
||||
private void handleReturnToConversationList() {
|
||||
@ -826,6 +833,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
if (recipient == null) return;
|
||||
|
||||
if (isSecureText) {
|
||||
Permissions.with(ConversationActivity.this)
|
||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.ConversationActivity_to_call_s_signal_needs_access_to_your_microphone_and_camera, recipient.toShortString()),
|
||||
R.drawable.ic_mic_white_48dp, R.drawable.ic_videocam_white_48dp)
|
||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.toShortString()))
|
||||
.onAllGranted(() -> {
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL);
|
||||
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_ADDRESS, recipient.getAddress());
|
||||
@ -834,6 +848,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
Intent activityIntent = new Intent(this, WebRtcCallActivity.class);
|
||||
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(activityIntent);
|
||||
})
|
||||
.execute();
|
||||
} else {
|
||||
try {
|
||||
Intent dialIntent = new Intent(Intent.ACTION_DIAL,
|
||||
@ -1785,6 +1801,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
public void onCameraStop() {}
|
||||
|
||||
@Override
|
||||
public void onRecorderPermissionRequired() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.RECORD_AUDIO)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone), R.drawable.ic_mic_white_48dp)
|
||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages))
|
||||
.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecorderStarted() {
|
||||
Vibrator vibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE);
|
||||
@ -1911,8 +1937,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (!quickAttachmentDrawer.isShowing()) {
|
||||
Permissions.with(ConversationActivity.this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_photo_camera_white_48dp)
|
||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
|
||||
.onAllGranted(() -> {
|
||||
composeText.clearFocus();
|
||||
container.show(composeText, quickAttachmentDrawer);
|
||||
})
|
||||
.onAnyDenied(() -> Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())
|
||||
.execute();
|
||||
} else {
|
||||
container.hideAttachedInput(false);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.Animator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
@ -12,6 +13,7 @@ import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.text.Editable;
|
||||
@ -40,6 +42,7 @@ import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
|
||||
import org.thoughtcrime.securesms.profiles.SystemProfileUtil;
|
||||
@ -137,6 +140,11 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
@ -208,17 +216,11 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
|
||||
|
||||
this.avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(this, getResources().getColor(R.color.grey_400)));
|
||||
|
||||
this.avatar.setOnClickListener(view -> {
|
||||
try {
|
||||
captureFile = File.createTempFile("capture", "jpg", getExternalCacheDir());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
captureFile = null;
|
||||
}
|
||||
|
||||
Intent chooserIntent = createAvatarSelectionIntent(captureFile, avatarBytes != null);
|
||||
startActivityForResult(chooserIntent, REQUEST_CODE_AVATAR);
|
||||
});
|
||||
this.avatar.setOnClickListener(view -> Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAnyResult(this::handleAvatarSelectionWithPermissions)
|
||||
.execute());
|
||||
|
||||
this.name.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
@ -360,7 +362,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
|
||||
this.name.setOnClickListener(v -> container.showSoftkey(name));
|
||||
}
|
||||
|
||||
private Intent createAvatarSelectionIntent(@Nullable File captureFile, boolean includeClear) {
|
||||
private Intent createAvatarSelectionIntent(@Nullable File captureFile, boolean includeClear, boolean includeCamera) {
|
||||
List<Intent> extraIntents = new LinkedList<>();
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI);
|
||||
galleryIntent.setType("image/*");
|
||||
@ -370,24 +372,45 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
|
||||
galleryIntent.setType("image/*");
|
||||
}
|
||||
|
||||
if (includeCamera) {
|
||||
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
|
||||
if (captureFile != null && cameraIntent.resolveActivity(getPackageManager()) != null) {
|
||||
cameraIntent.putExtra(EXTRA_OUTPUT, Uri.fromFile(captureFile));
|
||||
extraIntents.add(cameraIntent);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeClear) {
|
||||
extraIntents.add(new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO"));
|
||||
}
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.CreateProfileActivity_profile_photo));
|
||||
|
||||
if (!extraIntents.isEmpty()) {
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Intent[0]));
|
||||
}
|
||||
|
||||
|
||||
return chooserIntent;
|
||||
}
|
||||
|
||||
private void handleAvatarSelectionWithPermissions() {
|
||||
boolean hasCameraPermission = Permissions.hasAll(this, Manifest.permission.CAMERA);
|
||||
|
||||
if (hasCameraPermission) {
|
||||
try {
|
||||
captureFile = File.createTempFile("capture", "jpg", getExternalCacheDir());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
captureFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
Intent chooserIntent = createAvatarSelectionIntent(captureFile, avatarBytes != null, hasCameraPermission);
|
||||
startActivityForResult(chooserIntent, REQUEST_CODE_AVATAR);
|
||||
}
|
||||
|
||||
private void handleUpload() {
|
||||
final String name;
|
||||
final StreamDetails avatar;
|
||||
@ -484,6 +507,4 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
|
||||
reveal.setVisibility(View.VISIBLE);
|
||||
animation.start();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
@ -19,6 +20,7 @@ import android.widget.Toast;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
import org.thoughtcrime.securesms.qr.ScanListener;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
@ -93,17 +95,23 @@ public class DeviceActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code))
|
||||
.onAllGranted(() -> {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(android.R.id.content, deviceAddFragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
.commitAllowingStateLoss();
|
||||
})
|
||||
.onAnyDenied(() -> Toast.makeText(this, R.string.DeviceActivity_unable_to_scan_a_qr_code_without_the_camera_permission, Toast.LENGTH_LONG).show())
|
||||
.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQrDataFound(final String data) {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Util.runOnMain(() -> {
|
||||
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
|
||||
Uri uri = Uri.parse(data);
|
||||
deviceLinkFragment.setLinkClickedListener(uri, DeviceActivity.this);
|
||||
@ -129,10 +137,14 @@ public class DeviceActivity extends PassphraseRequiredActionBarActivity
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
public void onLink(final Uri uri) {
|
||||
|
@ -1,12 +1,13 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.database.PlaintextBackupExporter;
|
||||
import org.thoughtcrime.securesms.database.PlaintextBackupImporter;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -26,6 +28,9 @@ import java.io.IOException;
|
||||
|
||||
public class ImportExportFragment extends Fragment {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = ImportExportFragment.class.getSimpleName();
|
||||
|
||||
private static final int SUCCESS = 0;
|
||||
private static final int NO_SD_CARD = 1;
|
||||
private static final int ERROR_IO = 2;
|
||||
@ -46,26 +51,9 @@ public class ImportExportFragment extends Fragment {
|
||||
View importPlaintextView = layout.findViewById(R.id.import_plaintext_backup);
|
||||
View exportPlaintextView = layout.findViewById(R.id.export_plaintext_backup);
|
||||
|
||||
importSmsView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
handleImportSms();
|
||||
}
|
||||
});
|
||||
|
||||
importPlaintextView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
handleImportPlaintextBackup();
|
||||
}
|
||||
});
|
||||
|
||||
exportPlaintextView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
handleExportPlaintextBackup();
|
||||
}
|
||||
});
|
||||
importSmsView.setOnClickListener(v -> handleImportSms());
|
||||
importPlaintextView.setOnClickListener(v -> handleImportPlaintextBackup());
|
||||
exportPlaintextView.setOnClickListener(v -> handleExportPlaintextBackup());
|
||||
|
||||
return layout;
|
||||
}
|
||||
@ -80,14 +68,23 @@ public class ImportExportFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@SuppressWarnings("CodeBlock2Expr")
|
||||
private void handleImportSms() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setIconAttribute(R.attr.dialog_info_icon);
|
||||
builder.setTitle(getActivity().getString(R.string.ImportFragment_import_system_sms_database));
|
||||
builder.setMessage(getActivity().getString(R.string.ImportFragment_this_will_import_messages_from_the_system));
|
||||
builder.setPositiveButton(getActivity().getString(R.string.ImportFragment_import), new AlertDialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
builder.setPositiveButton(getActivity().getString(R.string.ImportFragment_import), (dialog, which) -> {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.READ_SMS)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.ImportExportFragment_signal_needs_the_sms_permission_in_order_to_import_sms_messages))
|
||||
.onAllGranted(() -> {
|
||||
Intent intent = new Intent(getActivity(), ApplicationMigrationService.class);
|
||||
intent.setAction(ApplicationMigrationService.MIGRATE_DATABASE);
|
||||
intent.putExtra("master_secret", masterSecret);
|
||||
@ -98,42 +95,55 @@ public class ImportExportFragment extends Fragment {
|
||||
Intent activityIntent = new Intent(getActivity(), DatabaseMigrationActivity.class);
|
||||
activityIntent.putExtra("next_intent", nextIntent);
|
||||
getActivity().startActivity(activityIntent);
|
||||
}
|
||||
})
|
||||
.onAnyDenied(() -> Toast.makeText(getContext(), R.string.ImportExportFragment_signal_needs_the_sms_permission_in_order_to_import_sms_messages_toast, Toast.LENGTH_LONG).show())
|
||||
.execute();
|
||||
});
|
||||
builder.setNegativeButton(getActivity().getString(R.string.ImportFragment_cancel), null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@SuppressWarnings("CodeBlock2Expr")
|
||||
@SuppressLint("InlinedApi")
|
||||
private void handleImportPlaintextBackup() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
builder.setTitle(getActivity().getString(R.string.ImportFragment_import_plaintext_backup));
|
||||
builder.setMessage(getActivity().getString(R.string.ImportFragment_this_will_import_messages_from_a_plaintext_backup));
|
||||
builder.setPositiveButton(getActivity().getString(R.string.ImportFragment_import), new AlertDialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
new ImportPlaintextBackupTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
builder.setPositiveButton(getActivity().getString(R.string.ImportFragment_import), (dialog, which) -> {
|
||||
Permissions.with(ImportExportFragment.this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_read_from_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAllGranted(() -> new ImportPlaintextBackupTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR))
|
||||
.onAnyDenied(() -> Toast.makeText(getContext(), R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_read_from_external_storage, Toast.LENGTH_LONG).show())
|
||||
.execute();
|
||||
});
|
||||
builder.setNegativeButton(getActivity().getString(R.string.ImportFragment_cancel), null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@SuppressWarnings("CodeBlock2Expr")
|
||||
@SuppressLint("InlinedApi")
|
||||
private void handleExportPlaintextBackup() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
builder.setTitle(getActivity().getString(R.string.ExportFragment_export_plaintext_to_storage));
|
||||
builder.setMessage(getActivity().getString(R.string.ExportFragment_warning_this_will_export_the_plaintext_contents));
|
||||
builder.setPositiveButton(getActivity().getString(R.string.ExportFragment_export), new Dialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
new ExportPlaintextTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
builder.setPositiveButton(getActivity().getString(R.string.ExportFragment_export), (dialog, which) -> {
|
||||
Permissions.with(ImportExportFragment.this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAllGranted(() -> new ExportPlaintextTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR))
|
||||
.onAnyDenied(() -> Toast.makeText(getContext(), R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_write_to_external_storage, Toast.LENGTH_LONG).show())
|
||||
.execute();
|
||||
});
|
||||
builder.setNegativeButton(getActivity().getString(R.string.ExportFragment_cancel), null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private class ImportPlaintextBackupTask extends AsyncTask<Void, Void, Integer> {
|
||||
|
||||
@Override
|
||||
@ -187,6 +197,7 @@ public class ImportExportFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private class ExportPlaintextTask extends AsyncTask<Void, Void, Integer> {
|
||||
private ProgressDialog dialog;
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
@ -37,6 +38,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
@ -91,6 +93,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
initializeActionBar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.JELLY_BEAN)
|
||||
private void setFullscreenIfPossible() {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
|
||||
@ -211,9 +218,17 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
|
||||
private void saveToDisk() {
|
||||
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.onAllGranted(() -> {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this, masterSecret, image);
|
||||
long saveDate = (date > 0) ? date : System.currentTimeMillis();
|
||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaUri, mediaType, saveDate, null));
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
@ -67,6 +67,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener, LoaderManager.LoaderCallbacks<Cursor>
|
||||
{
|
||||
private static final String TAG = RecipientPreferenceActivity.class.getSimpleName();
|
||||
@ -233,7 +234,6 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
||||
implements RecipientModifiedListener
|
||||
{
|
||||
private Recipient recipient;
|
||||
private BroadcastReceiver staleReceiver;
|
||||
private boolean canHaveSafetyNumber;
|
||||
|
||||
@Override
|
||||
@ -274,7 +274,6 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
this.recipient.removeListener(this);
|
||||
getActivity().unregisterReceiver(staleReceiver);
|
||||
}
|
||||
|
||||
private void initializeRecipients() {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.Animator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.BroadcastReceiver;
|
||||
@ -53,6 +54,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.GcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
@ -118,6 +120,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
||||
|
||||
initializeResources();
|
||||
initializeSpinner();
|
||||
initializePermissions();
|
||||
initializeNumber();
|
||||
initializeChallengeListener();
|
||||
}
|
||||
@ -138,6 +141,11 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
TextView skipButton = findViewById(R.id.skip_button);
|
||||
View informationToggle = findViewById(R.id.information_link_container);
|
||||
@ -205,8 +213,13 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private void initializeNumber() {
|
||||
Optional<Phonenumber.PhoneNumber> localNumber = Util.getDeviceNumber(this);
|
||||
Optional<Phonenumber.PhoneNumber> localNumber = Optional.absent();
|
||||
|
||||
if (Permissions.hasAll(this, Manifest.permission.READ_PHONE_STATE)) {
|
||||
localNumber = Util.getDeviceNumber(this);
|
||||
}
|
||||
|
||||
if (localNumber.isPresent()) {
|
||||
this.countryCode.setText(String.valueOf(localNumber.get().getCountryCode()));
|
||||
@ -220,6 +233,24 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private void initializePermissions() {
|
||||
Permissions.with(RegistrationActivity.this)
|
||||
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_CALL_LOG,
|
||||
Manifest.permission.PROCESS_OUTGOING_CALLS, Manifest.permission.ANSWER_PHONE_CALLS)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends),
|
||||
R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp)
|
||||
.onSomeGranted(permissions -> {
|
||||
if (permissions.contains(Manifest.permission.READ_PHONE_STATE)) {
|
||||
initializeNumber();
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
private void setCountryDisplay(String value) {
|
||||
this.countrySpinnerAdapter.clear();
|
||||
this.countrySpinnerAdapter.add(value);
|
||||
@ -249,6 +280,25 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
||||
return;
|
||||
}
|
||||
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.READ_SMS)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.RegistrationActivity_to_easily_verify_your_phone_number_signal_can_automatically_detect_your_verification_code), R.drawable.ic_textsms_white_48dp)
|
||||
.onAnyResult(this::handleRegisterWithPermissions)
|
||||
.execute();
|
||||
}
|
||||
|
||||
private void handleRegisterWithPermissions() {
|
||||
if (TextUtils.isEmpty(countryCode.getText())) {
|
||||
Toast.makeText(this, getString(R.string.RegistrationActivity_you_must_specify_your_country_code), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(number.getText())) {
|
||||
Toast.makeText(this, getString(R.string.RegistrationActivity_you_must_specify_your_phone_number), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
final String e164number = getConfiguredE164Number();
|
||||
|
||||
if (!PhoneNumberFormatter.isValidNumber(e164number)) {
|
||||
@ -305,7 +355,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
||||
|
||||
protected void onPostExecute(@Nullable Pair<String, Optional<String>> result) {
|
||||
if (result == null) {
|
||||
Toast.makeText(RegistrationActivity.this, "Unable to connect to service. Please check network connection and try again.", Toast.LENGTH_LONG).show();
|
||||
Toast.makeText(RegistrationActivity.this, R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.MmsCharacterCalculator;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.SmsCharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
|
||||
@ -146,7 +148,13 @@ public class TransportOptions {
|
||||
{
|
||||
List<TransportOption> results = new LinkedList<>();
|
||||
SubscriptionManagerCompat subscriptionManager = new SubscriptionManagerCompat(context);
|
||||
List<SubscriptionInfoCompat> subscriptions = subscriptionManager.getActiveSubscriptionInfoList();
|
||||
List<SubscriptionInfoCompat> subscriptions;
|
||||
|
||||
if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE)) {
|
||||
subscriptions = subscriptionManager.getActiveSubscriptionInfoList();
|
||||
} else {
|
||||
subscriptions = new LinkedList<>();
|
||||
}
|
||||
|
||||
if (subscriptions.size() < 2) {
|
||||
results.add(new TransportOption(Type.SMS, R.drawable.ic_send_sms_white_24dp,
|
||||
|
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (C) 2016 Open Whisper Systems
|
||||
/*
|
||||
* Copyright (C) 2016-2017 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -16,8 +16,11 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.*;
|
||||
import android.Manifest;
|
||||
import android.animation.TypeEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@ -69,6 +72,7 @@ import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.qr.QrCode;
|
||||
import org.thoughtcrime.securesms.qr.ScanListener;
|
||||
import org.thoughtcrime.securesms.qr.ScanningThread;
|
||||
@ -96,6 +100,7 @@ import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener, ScanListener, View.OnClickListener {
|
||||
|
||||
private static final String TAG = VerifyIdentityActivity.class.getSimpleName();
|
||||
@ -151,36 +156,41 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public void onModified(final Recipient recipient) {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setActionBarNotificationBarColor(recipient.getColor());
|
||||
}
|
||||
});
|
||||
Util.runOnMain(() -> setActionBarNotificationBarColor(recipient.getColor()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQrDataFound(final String data) {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Util.runOnMain(() -> {
|
||||
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
|
||||
|
||||
getSupportFragmentManager().popBackStack();
|
||||
displayFragment.setScannedFingerprint(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied))
|
||||
.onAllGranted(() -> {
|
||||
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
|
||||
transaction.setCustomAnimations(R.anim.slide_from_top, R.anim.slide_to_bottom,
|
||||
R.anim.slide_from_bottom, R.anim.slide_to_top);
|
||||
|
||||
transaction.replace(android.R.id.content, scanFragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
.commitAllowingStateLoss();
|
||||
})
|
||||
.onAnyDenied(() -> Toast.makeText(this, R.string.VerifyIdentityActivity_unable_to_scan_qr_code_without_camera_permission, Toast.LENGTH_LONG).show())
|
||||
.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
private void setActionBarNotificationBarColor(MaterialColor color) {
|
||||
@ -295,12 +305,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public void onModified(final Recipient recipient) {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setRecipientText(recipient);
|
||||
}
|
||||
});
|
||||
Util.runOnMain(() -> setRecipientText(recipient));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
@ -40,6 +41,7 @@ import org.thoughtcrime.securesms.components.webrtc.WebRtcCallControls;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.MessageRetrievalService;
|
||||
@ -116,6 +118,11 @@ public class WebRtcCallActivity extends Activity {
|
||||
super.onConfigurationChanged(newConfiguration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
private void initializeScreenshotSecurity() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH &&
|
||||
TextSecurePreferences.isScreenSecurityEnabled(this))
|
||||
@ -156,11 +163,21 @@ public class WebRtcCallActivity extends Activity {
|
||||
WebRtcViewModel event = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
|
||||
|
||||
if (event != null) {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, event.getRecipient().toShortString()),
|
||||
R.drawable.ic_mic_white_48dp, R.drawable.ic_videocam_white_48dp)
|
||||
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
|
||||
.onAllGranted(() -> {
|
||||
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_answering));
|
||||
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_ANSWER_CALL);
|
||||
startService(intent);
|
||||
})
|
||||
.onAnyDenied(this::handleDenyCall)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.Animator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
@ -26,6 +27,7 @@ import android.widget.LinearLayout;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class AttachmentTypeSelector extends PopupWindow {
|
||||
@ -40,8 +42,11 @@ public class AttachmentTypeSelector extends PopupWindow {
|
||||
|
||||
private static final int ANIMATION_DURATION = 300;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = AttachmentTypeSelector.class.getSimpleName();
|
||||
|
||||
private final @NonNull LoaderManager loaderManager;
|
||||
private final @NonNull RecentPhotoViewRail recentRail;
|
||||
private final @NonNull ImageView imageButton;
|
||||
private final @NonNull ImageView audioButton;
|
||||
private final @NonNull ImageView documentButton;
|
||||
@ -59,9 +64,10 @@ public class AttachmentTypeSelector extends PopupWindow {
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.attachment_type_selector, null, true);
|
||||
RecentPhotoViewRail recentPhotos = ViewUtil.findById(layout, R.id.recent_photos);
|
||||
|
||||
this.listener = listener;
|
||||
this.loaderManager = loaderManager;
|
||||
this.recentRail = ViewUtil.findById(layout, R.id.recent_photos);
|
||||
this.imageButton = ViewUtil.findById(layout, R.id.gallery_button);
|
||||
this.audioButton = ViewUtil.findById(layout, R.id.audio_button);
|
||||
this.documentButton = ViewUtil.findById(layout, R.id.document_button);
|
||||
@ -79,7 +85,7 @@ public class AttachmentTypeSelector extends PopupWindow {
|
||||
this.locationButton.setOnClickListener(new PropagatingClickListener(ADD_LOCATION));
|
||||
this.gifButton.setOnClickListener(new PropagatingClickListener(ADD_GIF));
|
||||
this.closeButton.setOnClickListener(new CloseClickListener());
|
||||
recentPhotos.setListener(new RecentPhotoSelectedListener());
|
||||
this.recentRail.setListener(new RecentPhotoSelectedListener());
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
|
||||
ViewUtil.findById(layout, R.id.location_linear_layout).setVisibility(View.INVISIBLE);
|
||||
@ -94,10 +100,17 @@ public class AttachmentTypeSelector extends PopupWindow {
|
||||
setFocusable(true);
|
||||
setTouchable(true);
|
||||
|
||||
loaderManager.initLoader(1, null, recentPhotos);
|
||||
loaderManager.initLoader(1, null, recentRail);
|
||||
}
|
||||
|
||||
public void show(@NonNull Activity activity, final @NonNull View anchor) {
|
||||
if (Permissions.hasAll(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
recentRail.setVisibility(View.VISIBLE);
|
||||
loaderManager.restartLoader(1, null, recentRail);
|
||||
} else {
|
||||
recentRail.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
this.currentAnchor = anchor;
|
||||
|
||||
showAtLocation(anchor, Gravity.BOTTOM, 0, 0);
|
||||
|
@ -102,12 +102,7 @@ public class InputPanel extends LinearLayout
|
||||
public void setListener(final @NonNull Listener listener) {
|
||||
this.listener = listener;
|
||||
|
||||
emojiToggle.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onEmojiToggle();
|
||||
}
|
||||
});
|
||||
emojiToggle.setOnClickListener(v -> listener.onEmojiToggle());
|
||||
}
|
||||
|
||||
public void setMediaListener(@NonNull MediaListener listener) {
|
||||
@ -118,6 +113,11 @@ public class InputPanel extends LinearLayout
|
||||
emojiToggle.attach(emojiDrawer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecordPermissionRequired() {
|
||||
if (listener != null) listener.onRecorderPermissionRequired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecordPressed(float startPositionX) {
|
||||
if (listener != null) listener.onRecorderStarted();
|
||||
@ -211,10 +211,11 @@ public class InputPanel extends LinearLayout
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
public void onRecorderStarted();
|
||||
public void onRecorderFinished();
|
||||
public void onRecorderCanceled();
|
||||
public void onEmojiToggle();
|
||||
void onRecorderStarted();
|
||||
void onRecorderFinished();
|
||||
void onRecorderCanceled();
|
||||
void onRecorderPermissionRequired();
|
||||
void onEmojiToggle();
|
||||
}
|
||||
|
||||
private static class SlideToCancel {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.support.annotation.Nullable;
|
||||
@ -18,6 +19,7 @@ import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class MicrophoneRecorderView extends FrameLayout implements View.OnTouchListener {
|
||||
@ -60,9 +62,13 @@ public class MicrophoneRecorderView extends FrameLayout implements View.OnTouchL
|
||||
public boolean onTouch(View v, final MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
if (!Permissions.hasAll(getContext(), Manifest.permission.RECORD_AUDIO)) {
|
||||
if (listener != null) listener.onRecordPermissionRequired();
|
||||
} else {
|
||||
this.actionInProgress = true;
|
||||
this.floatingRecordButton.display(event.getX());
|
||||
if (listener != null) listener.onRecordPressed(event.getX());
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
@ -88,10 +94,11 @@ public class MicrophoneRecorderView extends FrameLayout implements View.OnTouchL
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
public void onRecordPressed(float x);
|
||||
public void onRecordReleased(float x);
|
||||
public void onRecordCanceled(float x);
|
||||
public void onRecordMoved(float x, float absoluteX);
|
||||
void onRecordPressed(float x);
|
||||
void onRecordReleased(float x);
|
||||
void onRecordCanceled(float x);
|
||||
void onRecordMoved(float x, float absoluteX);
|
||||
void onRecordPermissionRequired();
|
||||
}
|
||||
|
||||
private static class FloatingRecordButton {
|
||||
|
@ -78,6 +78,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
|
||||
|
||||
private static class RecentPhotoAdapter extends CursorRecyclerViewAdapter<RecentPhotoAdapter.RecentPhotoViewHolder> {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = RecentPhotoAdapter.class.getName();
|
||||
|
||||
@NonNull private final Uri baseUri;
|
||||
@ -117,11 +118,8 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(viewHolder.imageView);
|
||||
|
||||
viewHolder.imageView.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
viewHolder.imageView.setOnClickListener(v -> {
|
||||
if (clickedListener != null) clickedListener.onItemClicked(uri);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
@ -143,6 +141,6 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
|
||||
}
|
||||
|
||||
public interface OnItemClickedListener {
|
||||
public void onItemClicked(Uri uri);
|
||||
void onItemClicked(Uri uri);
|
||||
}
|
||||
}
|
||||
|
@ -159,7 +159,11 @@ public class WebRtcAnswerDeclineButton extends LinearLayout implements View.OnTo
|
||||
|
||||
fab.setTranslationY(difference);
|
||||
|
||||
if (percentageToThreshold == 1 && listener != null) listener.onAnswered();
|
||||
if (percentageToThreshold == 1 && listener != null) {
|
||||
fab.setVisibility(View.INVISIBLE);
|
||||
lastY = event.getRawY();
|
||||
listener.onAnswered();
|
||||
}
|
||||
} else {
|
||||
differenceThreshold = ViewUtil.dpToPx(getContext(), DECLINE_THRESHOLD);
|
||||
percentageToThreshold = Math.min(1, difference / differenceThreshold);
|
||||
@ -173,7 +177,11 @@ public class WebRtcAnswerDeclineButton extends LinearLayout implements View.OnTo
|
||||
|
||||
fab.setRotation(135 * percentageToThreshold);
|
||||
|
||||
if (percentageToThreshold == 1 && listener != null) listener.onDeclined();
|
||||
if (percentageToThreshold == 1 && listener != null) {
|
||||
fab.setVisibility(View.INVISIBLE);
|
||||
lastY = event.getRawY();
|
||||
listener.onDeclined();
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.contacts;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
@ -36,6 +37,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.NumberUtil;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@ -102,6 +104,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
}
|
||||
}
|
||||
|
||||
if (Permissions.hasAny(getContext(), Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
|
||||
if (mode != MODE_SMS_ONLY) {
|
||||
cursorList.add(contactsDatabase.queryTextSecureContacts(filter));
|
||||
}
|
||||
@ -111,6 +114,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
} else if (mode == MODE_SMS_ONLY) {
|
||||
cursorList.add(filterNonPushContacts(contactsDatabase.querySystemContacts(filter)));
|
||||
}
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(filter) && NumberUtil.isValidSmsOrEmail(filter)) {
|
||||
MatrixCursor newNumberCursor = new MatrixCursor(CONTACT_PROJECTION, 1);
|
||||
@ -122,7 +126,8 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
cursorList.add(newNumberCursor);
|
||||
}
|
||||
|
||||
return new MergeCursor(cursorList.toArray(new Cursor[0]));
|
||||
if (cursorList.size() > 0) return new MergeCursor(cursorList.toArray(new Cursor[0]));
|
||||
else return null;
|
||||
}
|
||||
|
||||
private @NonNull Cursor filterNonPushContacts(@NonNull Cursor cursor) {
|
||||
|
@ -1,13 +1,14 @@
|
||||
package org.thoughtcrime.securesms.database.loaders;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
|
||||
public class RecentPhotosLoader extends CursorLoader {
|
||||
|
||||
@ -30,9 +31,13 @@ public class RecentPhotosLoader extends CursorLoader {
|
||||
|
||||
@Override
|
||||
public Cursor loadInBackground() {
|
||||
if (Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
return context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
PROJECTION, null, null,
|
||||
MediaStore.Images.ImageColumns.DATE_MODIFIED + " DESC");
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -7,7 +7,6 @@ import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.DirectoryHelper;
|
||||
@ -58,7 +57,6 @@ public class DirectoryRefreshJob extends ContextJob {
|
||||
} else {
|
||||
DirectoryHelper.refreshDirectoryFor(context, masterSecret, recipient);
|
||||
}
|
||||
SecurityEvent.broadcastSecurityUpdateEvent(context);
|
||||
} finally {
|
||||
if (wakeLock.isHeld()) wakeLock.release();
|
||||
}
|
||||
|
@ -16,6 +16,8 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
@ -49,6 +51,7 @@ import org.thoughtcrime.securesms.components.location.SignalMapView;
|
||||
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
@ -199,6 +202,7 @@ public class AttachmentManager {
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public void setMedia(@NonNull final MasterSecret masterSecret,
|
||||
@NonNull final GlideRequests glideRequests,
|
||||
@NonNull final Uri uri,
|
||||
@ -318,28 +322,57 @@ public class AttachmentManager {
|
||||
}
|
||||
|
||||
public static void selectDocument(Activity activity, int requestCode) {
|
||||
selectMediaType(activity, "*/*", null, requestCode);
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectGallery(Activity activity, int requestCode) {
|
||||
selectMediaType(activity, "image/*", new String[] {"image/*", "video/*"}, requestCode);
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.onAllGranted(() -> selectMediaType(activity, "image/*", new String[] {"image/*", "video/*"}, requestCode))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectAudio(Activity activity, int requestCode) {
|
||||
selectMediaType(activity, "audio/*", null, requestCode);
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.onAllGranted(() -> selectMediaType(activity, "audio/*", null, requestCode))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectContactInfo(Activity activity, int requestCode) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_CONTACTS)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information))
|
||||
.onAllGranted(() -> {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectLocation(Activity activity, int requestCode) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location))
|
||||
.onAllGranted(() -> {
|
||||
try {
|
||||
activity.startActivityForResult(new PlacePicker.IntentBuilder().build(activity), requestCode);
|
||||
} catch (GooglePlayServicesRepairableException | GooglePlayServicesNotAvailableException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectGif(Activity activity, int requestCode, boolean isForMms) {
|
||||
@ -357,6 +390,11 @@ public class AttachmentManager {
|
||||
}
|
||||
|
||||
public void capturePhoto(Activity activity, int requestCode) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied))
|
||||
.onAllGranted(() -> {
|
||||
try {
|
||||
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
||||
@ -371,6 +409,8 @@ public class AttachmentManager {
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
|
||||
|
330
src/org/thoughtcrime/securesms/permissions/Permissions.java
Normal file
@ -0,0 +1,330 @@
|
||||
package org.thoughtcrime.securesms.permissions;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Display;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.annimon.stream.function.Consumer;
|
||||
|
||||
import org.thoughtcrime.securesms.util.LRUCache;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class Permissions {
|
||||
|
||||
private static final Map<Integer, PermissionsRequest> OUTSTANDING = new LRUCache<>(2);
|
||||
|
||||
public static PermissionsBuilder with(@NonNull Activity activity) {
|
||||
return new PermissionsBuilder(new ActivityPermissionObject(activity));
|
||||
}
|
||||
|
||||
public static PermissionsBuilder with(@NonNull Fragment fragment) {
|
||||
return new PermissionsBuilder(new FragmentPermissionObject(fragment));
|
||||
}
|
||||
|
||||
public static class PermissionsBuilder {
|
||||
|
||||
private final PermissionObject permissionObject;
|
||||
|
||||
private String[] requestedPermissions;
|
||||
|
||||
private Runnable allGrantedListener;
|
||||
|
||||
private Runnable anyDeniedListener;
|
||||
private Runnable anyPermanentlyDeniedListener;
|
||||
private Runnable anyResultListener;
|
||||
|
||||
private Consumer<List<String>> someGrantedListener;
|
||||
private Consumer<List<String>> someDeniedListener;
|
||||
private Consumer<List<String>> somePermanentlyDeniedListener;
|
||||
|
||||
private @DrawableRes int[] rationalDialogHeader;
|
||||
private String rationaleDialogMessage;
|
||||
|
||||
private boolean ifNecesary;
|
||||
|
||||
PermissionsBuilder(PermissionObject permissionObject) {
|
||||
this.permissionObject = permissionObject;
|
||||
}
|
||||
|
||||
public PermissionsBuilder request(String... requestedPermissions) {
|
||||
this.requestedPermissions = requestedPermissions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder ifNecessary() {
|
||||
this.ifNecesary = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder withRationaleDialog(@NonNull String message, @NonNull @DrawableRes int... headers) {
|
||||
this.rationalDialogHeader = headers;
|
||||
this.rationaleDialogMessage = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder withPermanentDenialDialog(@NonNull String message) {
|
||||
return onAnyPermanentlyDenied(new SettingsDialogListener(permissionObject.getContext(), message));
|
||||
}
|
||||
|
||||
public PermissionsBuilder onAllGranted(Runnable allGrantedListener) {
|
||||
this.allGrantedListener = allGrantedListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder onAnyDenied(Runnable anyDeniedListener) {
|
||||
this.anyDeniedListener = anyDeniedListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public PermissionsBuilder onAnyPermanentlyDenied(Runnable anyPermanentlyDeniedListener) {
|
||||
this.anyPermanentlyDeniedListener = anyPermanentlyDeniedListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder onAnyResult(Runnable anyResultListener) {
|
||||
this.anyResultListener = anyResultListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder onSomeGranted(Consumer<List<String>> someGrantedListener) {
|
||||
this.someGrantedListener = someGrantedListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder onSomeDenied(Consumer<List<String>> someDeniedListener) {
|
||||
this.someDeniedListener = someDeniedListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder onSomePermanentlyDenied(Consumer<List<String>> somePermanentlyDeniedListener) {
|
||||
this.somePermanentlyDeniedListener = somePermanentlyDeniedListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
PermissionsRequest request = new PermissionsRequest(allGrantedListener, anyDeniedListener, anyPermanentlyDeniedListener, anyResultListener,
|
||||
someGrantedListener, someDeniedListener, somePermanentlyDeniedListener);
|
||||
|
||||
if (ifNecesary && permissionObject.hasAll(requestedPermissions)) {
|
||||
executePreGrantedPermissionsRequest(request);
|
||||
} else if (rationaleDialogMessage != null && rationalDialogHeader != null) {
|
||||
executePermissionsRequestWithRationale(request);
|
||||
} else {
|
||||
executePermissionsRequest(request);
|
||||
}
|
||||
}
|
||||
|
||||
private void executePreGrantedPermissionsRequest(PermissionsRequest request) {
|
||||
int[] grantResults = new int[requestedPermissions.length];
|
||||
for (int i=0;i<grantResults.length;i++) grantResults[i] = PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
request.onResult(requestedPermissions, grantResults, new boolean[requestedPermissions.length]);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void executePermissionsRequestWithRationale(PermissionsRequest request) {
|
||||
RationaleDialog.createFor(permissionObject.getContext(), rationaleDialogMessage, rationalDialogHeader)
|
||||
.setPositiveButton("Continue", (dialog, which) -> executePermissionsRequest(request))
|
||||
.setNegativeButton("Not now", null)
|
||||
.show()
|
||||
.getWindow()
|
||||
.setLayout((int)(permissionObject.getWindowWidth() * .75), ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
|
||||
private void executePermissionsRequest(PermissionsRequest request) {
|
||||
int requestCode = new SecureRandom().nextInt(65434) + 100;
|
||||
|
||||
synchronized (OUTSTANDING) {
|
||||
OUTSTANDING.put(requestCode, request);
|
||||
}
|
||||
|
||||
for (String permission : requestedPermissions) {
|
||||
request.addMapping(permission, permissionObject.shouldShouldPermissionRationale(permission));
|
||||
}
|
||||
|
||||
permissionObject.requestPermissions(requestCode, requestedPermissions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void requestPermissions(@NonNull Activity activity, int requestCode, String... permissions) {
|
||||
ActivityCompat.requestPermissions(activity, filterNotGranted(activity, permissions), requestCode);
|
||||
}
|
||||
|
||||
private static void requestPermissions(@NonNull Fragment fragment, int requestCode, String... permissions) {
|
||||
fragment.requestPermissions(filterNotGranted(fragment.getContext(), permissions), requestCode);
|
||||
}
|
||||
|
||||
private static String[] filterNotGranted(@NonNull Context context, String... permissions) {
|
||||
return Stream.of(permissions)
|
||||
.filter(permission -> ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED)
|
||||
.toList()
|
||||
.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public static boolean hasAny(@NonNull Context context, String... permissions) {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
|
||||
Stream.of(permissions).anyMatch(permission -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
|
||||
|
||||
}
|
||||
|
||||
public static boolean hasAll(@NonNull Context context, String... permissions) {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
|
||||
Stream.of(permissions).allMatch(permission -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
|
||||
|
||||
}
|
||||
|
||||
public static void onRequestPermissionsResult(Fragment fragment, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
onRequestPermissionsResult(new FragmentPermissionObject(fragment), requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
public static void onRequestPermissionsResult(Activity activity, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
onRequestPermissionsResult(new ActivityPermissionObject(activity), requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
private static void onRequestPermissionsResult(@NonNull PermissionObject context, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
PermissionsRequest resultListener;
|
||||
|
||||
synchronized (OUTSTANDING) {
|
||||
resultListener = OUTSTANDING.remove(requestCode);
|
||||
}
|
||||
|
||||
if (resultListener == null) return;
|
||||
|
||||
boolean[] shouldShowRationaleDialog = new boolean[permissions.length];
|
||||
|
||||
for (int i=0;i<permissions.length;i++) {
|
||||
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
|
||||
shouldShowRationaleDialog[i] = context.shouldShouldPermissionRationale(permissions[i]);
|
||||
}
|
||||
}
|
||||
|
||||
resultListener.onResult(permissions, grantResults, shouldShowRationaleDialog);
|
||||
}
|
||||
|
||||
private static Intent getApplicationSettingsIntent(@NonNull Context context) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", context.getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
private abstract static class PermissionObject {
|
||||
|
||||
abstract Context getContext();
|
||||
abstract boolean shouldShouldPermissionRationale(String permission);
|
||||
abstract boolean hasAll(String... permissions);
|
||||
abstract void requestPermissions(int requestCode, String... permissions);
|
||||
|
||||
int getWindowWidth() {
|
||||
WindowManager windowManager = ServiceUtil.getWindowManager(getContext());
|
||||
Display display = windowManager.getDefaultDisplay();
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
display.getMetrics(metrics);
|
||||
|
||||
return metrics.widthPixels;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ActivityPermissionObject extends PermissionObject {
|
||||
|
||||
private Activity activity;
|
||||
|
||||
ActivityPermissionObject(@NonNull Activity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context getContext() {
|
||||
return activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShouldPermissionRationale(String permission) {
|
||||
return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAll(String... permissions) {
|
||||
return Permissions.hasAll(activity, permissions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestPermissions(int requestCode, String... permissions) {
|
||||
Permissions.requestPermissions(activity, requestCode, permissions);
|
||||
}
|
||||
}
|
||||
|
||||
private static class FragmentPermissionObject extends PermissionObject {
|
||||
|
||||
private Fragment fragment;
|
||||
|
||||
FragmentPermissionObject(@NonNull Fragment fragment) {
|
||||
this.fragment = fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context getContext() {
|
||||
return fragment.getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShouldPermissionRationale(String permission) {
|
||||
return fragment.shouldShowRequestPermissionRationale(permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAll(String... permissions) {
|
||||
return Permissions.hasAll(fragment.getContext(), permissions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestPermissions(int requestCode, String... permissions) {
|
||||
Permissions.requestPermissions(fragment, requestCode, permissions);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SettingsDialogListener implements Runnable {
|
||||
|
||||
private final Context context;
|
||||
private final String message;
|
||||
|
||||
SettingsDialogListener(Context context, String message) {
|
||||
this.message = message;
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle("Permission required")
|
||||
.setMessage(message)
|
||||
.setPositiveButton("Continue", (dialog, which) -> context.startActivity(getApplicationSettingsIntent(context)))
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package org.thoughtcrime.securesms.permissions;
|
||||
|
||||
|
||||
import android.content.pm.PackageManager;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.function.Consumer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
class PermissionsRequest {
|
||||
|
||||
private final Map<String, Boolean> PRE_REQUEST_MAPPING = new HashMap<>();
|
||||
|
||||
private final @Nullable Runnable allGrantedListener;
|
||||
|
||||
private final @Nullable Runnable anyDeniedListener;
|
||||
private final @Nullable Runnable anyPermanentlyDeniedListener;
|
||||
private final @Nullable Runnable anyResultListener;
|
||||
|
||||
private final @Nullable Consumer<List<String>> someGrantedListener;
|
||||
private final @Nullable Consumer<List<String>> someDeniedListener;
|
||||
private final @Nullable Consumer<List<String>> somePermanentlyDeniedListener;
|
||||
|
||||
PermissionsRequest(@Nullable Runnable allGrantedListener,
|
||||
@Nullable Runnable anyDeniedListener,
|
||||
@Nullable Runnable anyPermanentlyDeniedListener,
|
||||
@Nullable Runnable anyResultListener,
|
||||
@Nullable Consumer<List<String>> someGrantedListener,
|
||||
@Nullable Consumer<List<String>> someDeniedListener,
|
||||
@Nullable Consumer<List<String>> somePermanentlyDeniedListener)
|
||||
{
|
||||
this.allGrantedListener = allGrantedListener;
|
||||
|
||||
this.anyDeniedListener = anyDeniedListener;
|
||||
this.anyPermanentlyDeniedListener = anyPermanentlyDeniedListener;
|
||||
this.anyResultListener = anyResultListener;
|
||||
|
||||
this.someGrantedListener = someGrantedListener;
|
||||
this.someDeniedListener = someDeniedListener;
|
||||
this.somePermanentlyDeniedListener = somePermanentlyDeniedListener;
|
||||
}
|
||||
|
||||
void onResult(String[] permissions, int[] grantResults, boolean[] shouldShowRationaleDialog) {
|
||||
List<String> granted = new ArrayList<>(permissions.length);
|
||||
List<String> denied = new ArrayList<>(permissions.length);
|
||||
List<String> permanentlyDenied = new ArrayList<>(permissions.length);
|
||||
|
||||
for (int i = 0; i < permissions.length; i++) {
|
||||
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
|
||||
granted.add(permissions[i]);
|
||||
} else {
|
||||
boolean preRequestShouldShowRationaleDialog = PRE_REQUEST_MAPPING.get(permissions[i]);
|
||||
|
||||
if ((somePermanentlyDeniedListener != null || anyPermanentlyDeniedListener != null) &&
|
||||
!preRequestShouldShowRationaleDialog && !shouldShowRationaleDialog[i])
|
||||
{
|
||||
permanentlyDenied.add(permissions[i]);
|
||||
} else {
|
||||
denied.add(permissions[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allGrantedListener != null && granted.size() > 0 && (denied.size() == 0 && permanentlyDenied.size() == 0)) {
|
||||
allGrantedListener.run();
|
||||
} else if (someGrantedListener != null && granted.size() > 0) {
|
||||
someGrantedListener.accept(granted);
|
||||
}
|
||||
|
||||
if (denied.size() > 0) {
|
||||
if (anyDeniedListener != null) anyDeniedListener.run();
|
||||
if (someDeniedListener != null) someDeniedListener.accept(denied);
|
||||
}
|
||||
|
||||
if (permanentlyDenied.size() > 0) {
|
||||
if (anyPermanentlyDeniedListener != null) anyPermanentlyDeniedListener.run();
|
||||
if (somePermanentlyDeniedListener != null) somePermanentlyDeniedListener.accept(permanentlyDenied);
|
||||
}
|
||||
|
||||
if (anyResultListener != null) {
|
||||
anyResultListener.run();
|
||||
}
|
||||
}
|
||||
|
||||
void addMapping(String permission, boolean shouldShowRationaleDialog) {
|
||||
PRE_REQUEST_MAPPING.put(permission, shouldShowRationaleDialog);
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package org.thoughtcrime.securesms.permissions;
|
||||
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout.LayoutParams;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class RationaleDialog {
|
||||
|
||||
public static AlertDialog.Builder createFor(@NonNull Context context, @NonNull String message, @DrawableRes int... drawables) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.permissions_rationale_dialog, null);
|
||||
ViewGroup header = view.findViewById(R.id.header_container);
|
||||
TextView text = view.findViewById(R.id.message);
|
||||
|
||||
for (int i=0;i<drawables.length;i++) {
|
||||
ImageView imageView = new ImageView(context);
|
||||
imageView.setImageDrawable(context.getResources().getDrawable(drawables[i]));
|
||||
imageView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
|
||||
|
||||
header.addView(imageView);
|
||||
|
||||
if (i != drawables.length - 1) {
|
||||
TextView plus = new TextView(context);
|
||||
plus.setText("+");
|
||||
plus.setTextSize(TypedValue.COMPLEX_UNIT_SP, 40);
|
||||
plus.setTextColor(Color.WHITE);
|
||||
|
||||
LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
layoutParams.setMargins(ViewUtil.dpToPx(context, 20), 0, ViewUtil.dpToPx(context, 20), 0);
|
||||
|
||||
plus.setLayoutParams(layoutParams);
|
||||
header.addView(plus);
|
||||
}
|
||||
}
|
||||
|
||||
text.setText(message);
|
||||
|
||||
return new AlertDialog.Builder(context, R.style.RationaleDialog).setView(view);
|
||||
}
|
||||
|
||||
}
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.profiles;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
@ -14,8 +15,6 @@ import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.bumptech.glide.load.data.StreamLocalUriFetcher;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
@ -26,6 +25,7 @@ public class SystemProfileUtil {
|
||||
|
||||
private static final String TAG = SystemProfileUtil.class.getSimpleName();
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public static ListenableFuture<byte[]> getSystemProfileAvatar(final @NonNull Context context, MediaConstraints mediaConstraints) {
|
||||
SettableFuture<byte[]> future = new SettableFuture<>();
|
||||
|
||||
@ -45,6 +45,8 @@ public class SystemProfileUtil {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SecurityException se) {
|
||||
Log.w(TAG, se);
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,6 +63,7 @@ public class SystemProfileUtil {
|
||||
return future;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public static ListenableFuture<String> getSystemProfileName(final @NonNull Context context) {
|
||||
SettableFuture<String> future = new SettableFuture<>();
|
||||
|
||||
@ -74,6 +77,8 @@ public class SystemProfileUtil {
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Profile.DISPLAY_NAME));
|
||||
}
|
||||
} catch (SecurityException se) {
|
||||
Log.w(TAG, se);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,9 +139,8 @@ class RecipientProvider {
|
||||
|
||||
if (address.isPhone() && !TextUtils.isEmpty(address.toPhoneString())) {
|
||||
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address.toPhoneString()));
|
||||
Cursor cursor = context.getContentResolver().query(uri, CALLER_ID_PROJECTION, null, null, null);
|
||||
|
||||
try {
|
||||
try (Cursor cursor = context.getContentResolver().query(uri, CALLER_ID_PROJECTION, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
final String resultNumber = cursor.getString(3);
|
||||
if (resultNumber != null) {
|
||||
@ -162,9 +161,8 @@ class RecipientProvider {
|
||||
Log.w(TAG, "resultNumber is null");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
} catch (SecurityException se) {
|
||||
Log.w(TAG, se);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
@ -31,6 +32,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FutureTaskListener;
|
||||
import org.thoughtcrime.securesms.util.ListenableFutureTask;
|
||||
@ -340,7 +342,12 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
|
||||
@Override
|
||||
public void onSuccessContinue(List<PeerConnection.IceServer> result) {
|
||||
try {
|
||||
boolean isSystemContact = ContactAccessor.getInstance().isSystemContact(WebRtcCallService.this, recipient.getAddress().serialize());
|
||||
boolean isSystemContact = false;
|
||||
|
||||
if (Permissions.hasAny(WebRtcCallService.this, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
|
||||
isSystemContact = ContactAccessor.getInstance().isSystemContact(WebRtcCallService.this, recipient.getAddress().serialize());
|
||||
}
|
||||
|
||||
boolean isAlwaysTurn = TextSecurePreferences.isTurnOnly(WebRtcCallService.this);
|
||||
|
||||
WebRtcCallService.this.peerConnection = new PeerConnectionWrapper(WebRtcCallService.this, peerConnectionFactory, WebRtcCallService.this, localRenderer, result, !isSystemContact || isAlwaysTurn);
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.Manifest;
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.content.ContentResolver;
|
||||
@ -28,6 +29,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
|
||||
@ -51,6 +53,7 @@ public class DirectoryHelper {
|
||||
throws IOException
|
||||
{
|
||||
if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) return;
|
||||
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) return;
|
||||
|
||||
List<Address> newlyActiveUsers = refreshDirectory(context, AccountManagerFactory.createManager(context));
|
||||
|
||||
@ -70,6 +73,10 @@ public class DirectoryHelper {
|
||||
return new LinkedList<>();
|
||||
}
|
||||
|
||||
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
|
||||
return new LinkedList<>();
|
||||
}
|
||||
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
Stream<String> eligibleRecipientDatabaseContactNumbers = Stream.of(recipientDatabase.getAllRecipients()).map(recipient -> recipient.getAddress().serialize());
|
||||
Stream<String> eligibleSystemDatabaseContactNumbers = Stream.of(ContactAccessor.getInstance().getAllContactsWithNumbers(context)).map(Address::serialize);
|
||||
@ -126,7 +133,9 @@ public class DirectoryHelper {
|
||||
if (details.isPresent()) {
|
||||
recipientDatabase.setRegistered(recipient, RegisteredState.REGISTERED);
|
||||
|
||||
if (Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
|
||||
updateContactsDatabase(context, Util.asList(recipient.getAddress()), false);
|
||||
}
|
||||
|
||||
if (!activeUser && TextSecurePreferences.isMultiDevice(context)) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceContactUpdateJob(context));
|
||||
|
@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@ -32,6 +32,7 @@ import android.os.Looper;
|
||||
import android.provider.Telephony;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.RequiresPermission;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
@ -50,7 +51,6 @@ import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingLegacyMmsConnection;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
@ -113,12 +113,9 @@ public class Util {
|
||||
public static ExecutorService newSingleThreadedLifoExecutor() {
|
||||
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingLifoQueue<Runnable>());
|
||||
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
executor.execute(() -> {
|
||||
// Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
||||
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
|
||||
}
|
||||
});
|
||||
|
||||
return executor;
|
||||
@ -243,6 +240,12 @@ public class Util {
|
||||
return total;
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = {
|
||||
android.Manifest.permission.READ_PHONE_STATE,
|
||||
android.Manifest.permission.READ_SMS,
|
||||
android.Manifest.permission.READ_PHONE_NUMBERS
|
||||
})
|
||||
@SuppressLint("MissingPermission")
|
||||
public static Optional<Phonenumber.PhoneNumber> getDeviceNumber(Context context) {
|
||||
try {
|
||||
final String localNumber = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number();
|
||||
@ -388,14 +391,12 @@ public class Util {
|
||||
runnable.run();
|
||||
} else {
|
||||
final CountDownLatch sync = new CountDownLatch(1);
|
||||
runOnMain(new Runnable() {
|
||||
@Override public void run() {
|
||||
runOnMain(() -> {
|
||||
try {
|
||||
runnable.run();
|
||||
} finally {
|
||||
sync.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
sync.await();
|
||||
@ -438,7 +439,7 @@ public class Util {
|
||||
}
|
||||
|
||||
public static @Nullable String readTextFromClipboard(@NonNull Context context) {
|
||||
if (VERSION.SDK_INT >= 11) {
|
||||
{
|
||||
ClipboardManager clipboardManager = (ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
||||
if (clipboardManager.hasPrimaryClip() && clipboardManager.getPrimaryClip().getItemCount() > 0) {
|
||||
@ -446,24 +447,13 @@ public class Util {
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
android.text.ClipboardManager clipboardManager = (android.text.ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
||||
if (clipboardManager.hasText()) {
|
||||
return clipboardManager.getText().toString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeTextToClipboard(@NonNull Context context, @NonNull String text) {
|
||||
if (VERSION.SDK_INT >= 11) {
|
||||
{
|
||||
ClipboardManager clipboardManager = (ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("Safety numbers", text));
|
||||
} else {
|
||||
android.text.ClipboardManager clipboardManager = (android.text.ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboardManager.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
|