Support for selective permissions

This commit is contained in:
Moxie Marlinspike 2017-11-24 22:00:30 -08:00
parent 99a26e2bcc
commit 64c8b4b2ef
71 changed files with 1309 additions and 317 deletions

View File

@ -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"

View File

@ -239,7 +239,7 @@ android {
versionName "4.13.7"
minSdkVersion 14
targetSdkVersion 22
targetSdkVersion 23
multiDexEnabled true
vectorDrawables.useSupportLibrary = true

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 B

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 557 B

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1013 B

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View 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>

View File

@ -1,8 +1,9 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
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
android:id="@+id/swipe_refresh"
@ -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>

View 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>

View File

@ -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"

View File

@ -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 -->

View File

@ -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>

View File

@ -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,27 +96,48 @@ 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
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
emptyText = ViewUtil.findById(view, android.R.id.empty);
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
swipeRefresh = ViewUtil.findById(view, R.id.swipe_refresh);
fastScroller = ViewUtil.findById(view, R.id.fast_scroller);
emptyText = ViewUtil.findById(view, android.R.id.empty);
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) {

View File

@ -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;
@ -186,7 +188,7 @@ import static org.whispersystems.signalservice.internal.push.SignalServiceProtos
public class ConversationActivity extends PassphraseRequiredActionBarActivity
implements ConversationFragment.ConversationFragmentListener,
AttachmentManager.AttachmentListener,
RecipientModifiedListener,
RecipientModifiedListener,
OnKeyboardShownListener,
AttachmentDrawerListener,
InputPanel.Listener,
@ -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,14 +833,23 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (recipient == null) return;
if (isSecureText) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL);
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_ADDRESS, recipient.getAddress());
startService(intent);
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());
startService(intent);
Intent activityIntent = new Intent(this, WebRtcCallActivity.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(activityIntent);
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()) {
composeText.clearFocus();
container.show(composeText, quickAttachmentDrawer);
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);
}

View File

@ -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,11 +372,13 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
galleryIntent.setType("image/*");
}
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
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 (captureFile != null && cameraIntent.resolveActivity(getPackageManager()) != null) {
cameraIntent.putExtra(EXTRA_OUTPUT, Uri.fromFile(captureFile));
extraIntents.add(cameraIntent);
}
}
if (includeClear) {
@ -382,12 +386,31 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
}
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.CreateProfileActivity_profile_photo));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Intent[0]));
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();
}
}

View File

@ -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,46 +95,56 @@ public class DeviceActivity extends PassphraseRequiredActionBarActivity
@Override
public void onClick(View v) {
getSupportFragmentManager().beginTransaction()
.replace(android.R.id.content, deviceAddFragment)
.addToBackStack(null)
.commit();
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)
.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() {
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
Uri uri = Uri.parse(data);
deviceLinkFragment.setLinkClickedListener(uri, DeviceActivity.this);
Util.runOnMain(() -> {
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
Uri uri = Uri.parse(data);
deviceLinkFragment.setLinkClickedListener(uri, DeviceActivity.this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
deviceAddFragment.setSharedElementReturnTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
deviceAddFragment.setExitTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
deviceAddFragment.setSharedElementReturnTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
deviceAddFragment.setExitTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
deviceLinkFragment.setSharedElementEnterTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
deviceLinkFragment.setEnterTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
deviceLinkFragment.setSharedElementEnterTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
deviceLinkFragment.setEnterTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
getSupportFragmentManager().beginTransaction()
.addToBackStack(null)
.addSharedElement(deviceAddFragment.getDevicesImage(), "devices")
.replace(android.R.id.content, deviceLinkFragment)
.commit();
getSupportFragmentManager().beginTransaction()
.addToBackStack(null)
.addSharedElement(deviceAddFragment.getDevicesImage(), "devices")
.replace(android.R.id.content, deviceLinkFragment)
.commit();
} else {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_from_bottom, R.anim.slide_to_bottom,
R.anim.slide_from_bottom, R.anim.slide_to_bottom)
.replace(android.R.id.content, deviceLinkFragment)
.addToBackStack(null)
.commit();
}
} else {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_from_bottom, R.anim.slide_to_bottom,
R.anim.slide_from_bottom, R.anim.slide_to_bottom)
.replace(android.R.id.content, deviceLinkFragment)
.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) {

View File

@ -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,60 +68,82 @@ 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) {
Intent intent = new Intent(getActivity(), ApplicationMigrationService.class);
intent.setAction(ApplicationMigrationService.MIGRATE_DATABASE);
intent.putExtra("master_secret", masterSecret);
getActivity().startService(intent);
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);
getActivity().startService(intent);
Intent nextIntent = new Intent(getActivity(), ConversationListActivity.class);
Intent nextIntent = new Intent(getActivity(), ConversationListActivity.class);
Intent activityIntent = new Intent(getActivity(), DatabaseMigrationActivity.class);
activityIntent.putExtra("next_intent", nextIntent);
getActivity().startActivity(activityIntent);
}
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;

View File

@ -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) -> {
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));
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();
});
}

View File

@ -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();
@ -232,9 +233,8 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
extends CorrectedPreferenceFragment
implements RecipientModifiedListener
{
private Recipient recipient;
private BroadcastReceiver staleReceiver;
private boolean canHaveSafetyNumber;
private Recipient recipient;
private boolean canHaveSafetyNumber;
@Override
public void onCreate(Bundle icicle) {
@ -274,7 +274,6 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
public void onDestroy() {
super.onDestroy();
this.recipient.removeListener(this);
getActivity().unregisterReceiver(staleReceiver);
}
private void initializeRecipients() {

View File

@ -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;
}

View File

@ -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,

View File

@ -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() {
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
Util.runOnMain(() -> {
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
getSupportFragmentManager().popBackStack();
displayFragment.setScannedFingerprint(data);
}
getSupportFragmentManager().popBackStack();
displayFragment.setScannedFingerprint(data);
});
}
@Override
public void onClick(View v) {
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);
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();
transaction.replace(android.R.id.content, scanFragment)
.addToBackStack(null)
.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

View File

@ -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) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_answering));
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);
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_ANSWER_CALL);
startService(intent);
})
.onAnyDenied(this::handleDenyCall)
.execute();
}
}

View File

@ -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,16 +42,19 @@ 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 ImageView imageButton;
private final @NonNull ImageView audioButton;
private final @NonNull ImageView documentButton;
private final @NonNull ImageView contactButton;
private final @NonNull ImageView cameraButton;
private final @NonNull ImageView locationButton;
private final @NonNull ImageView gifButton;
private final @NonNull ImageView closeButton;
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;
private final @NonNull ImageView contactButton;
private final @NonNull ImageView cameraButton;
private final @NonNull ImageView locationButton;
private final @NonNull ImageView gifButton;
private final @NonNull ImageView closeButton;
private @Nullable View currentAnchor;
private @Nullable AttachmentClickedListener listener;
@ -57,11 +62,12 @@ public class AttachmentTypeSelector extends PopupWindow {
public AttachmentTypeSelector(@NonNull Context context, @NonNull LoaderManager loaderManager, @Nullable AttachmentClickedListener listener) {
super(context);
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);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.attachment_type_selector, null, true);
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);

View File

@ -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 {

View File

@ -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:
this.actionInProgress = true;
this.floatingRecordButton.display(event.getX());
if (listener != null) listener.onRecordPressed(event.getX());
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 {

View File

@ -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) {
if (clickedListener != null) clickedListener.onItemClicked(uri);
}
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);
}
}

View File

@ -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) {

View File

@ -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,14 +104,16 @@ public class ContactsCursorLoader extends CursorLoader {
}
}
if (mode != MODE_SMS_ONLY) {
cursorList.add(contactsDatabase.queryTextSecureContacts(filter));
}
if (Permissions.hasAny(getContext(), Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
if (mode != MODE_SMS_ONLY) {
cursorList.add(contactsDatabase.queryTextSecureContacts(filter));
}
if (mode == MODE_ALL) {
cursorList.add(contactsDatabase.querySystemContacts(filter));
} else if (mode == MODE_SMS_ONLY) {
cursorList.add(filterNonPushContacts(contactsDatabase.querySystemContacts(filter)));
if (mode == MODE_ALL) {
cursorList.add(contactsDatabase.querySystemContacts(filter));
} else if (mode == MODE_SMS_ONLY) {
cursorList.add(filterNonPushContacts(contactsDatabase.querySystemContacts(filter)));
}
}
if (!TextUtils.isEmpty(filter) && NumberUtil.isValidSmsOrEmail(filter)) {
@ -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) {

View File

@ -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() {
return context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
PROJECTION, null, null,
MediaStore.Images.ImageColumns.DATE_MODIFIED + " DESC");
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;
}
}

View File

@ -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();
}

View File

@ -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) {
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
activity.startActivityForResult(intent, 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) {
try {
activity.startActivityForResult(new PlacePicker.IntentBuilder().build(activity), requestCode);
} catch (GooglePlayServicesRepairableException | GooglePlayServicesNotAvailableException e) {
Log.w(TAG, e);
}
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,20 +390,27 @@ public class AttachmentManager {
}
public void capturePhoto(Activity activity, int requestCode) {
try {
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
if (captureUri == null) {
captureUri = PersistentBlobProvider.getInstance(context)
.createForExternal(MediaUtil.IMAGE_JPEG);
}
Log.w(TAG, "captureUri path is " + captureUri.getPath());
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri);
activity.startActivityForResult(captureIntent, requestCode);
}
} catch (IOException ioe) {
Log.w(TAG, ioe);
}
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) {
if (captureUri == null) {
captureUri = PersistentBlobProvider.getInstance(context)
.createForExternal(MediaUtil.IMAGE_JPEG);
}
Log.w(TAG, "captureUri path is " + captureUri.getPath());
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri);
activity.startActivityForResult(captureIntent, requestCode);
}
} catch (IOException ioe) {
Log.w(TAG, ioe);
}
})
.execute();
}
private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {

View 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();
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -138,10 +138,9 @@ 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);
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address.toPhoneString()));
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);
}
}

View File

@ -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,8 +342,13 @@ 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 isAlwaysTurn = TextSecurePreferences.isTurnOnly(WebRtcCallService.this);
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);
WebRtcCallService.this.peerConnection.setRemoteDescription(new SessionDescription(SessionDescription.Type.OFFER, offer));

View File

@ -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);
updateContactsDatabase(context, Util.asList(recipient.getAddress()), false);
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));

View File

@ -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);
}
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,13 +391,11 @@ public class Util {
runnable.run();
} else {
final CountDownLatch sync = new CountDownLatch(1);
runOnMain(new Runnable() {
@Override public void run() {
try {
runnable.run();
} finally {
sync.countDown();
}
runOnMain(() -> {
try {
runnable.run();
} finally {
sync.countDown();
}
});
try {
@ -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);
}
}