diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index fcb8f3e195..e50d780fc7 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -26,27 +26,42 @@
tools:ignore="ProtectedPermissions"/>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
@@ -57,10 +72,6 @@
-
-
-
-
@@ -68,20 +79,12 @@
-
-
-
-
-
+
-
-
-
-
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/contact_selection_list_fragment.xml b/res/layout/contact_selection_list_fragment.xml
index 27814414e2..db9c7fc6fe 100644
--- a/res/layout/contact_selection_list_fragment.xml
+++ b/res/layout/contact_selection_list_fragment.xml
@@ -1,8 +1,9 @@
-
+ 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">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/permissions_rationale_dialog.xml b/res/layout/permissions_rationale_dialog.xml
new file mode 100644
index 0000000000..36cb60648f
--- /dev/null
+++ b/res/layout/permissions_rationale_dialog.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/recent_photo_view_item.xml b/res/layout/recent_photo_view_item.xml
index a85a38f7e0..b59d2ab483 100644
--- a/res/layout/recent_photo_view_item.xml
+++ b/res/layout/recent_photo_view_item.xml
@@ -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">
Can\'t find an app to select media.
+ 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\".
+ 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\".
+ 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\".
+ 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\".
@@ -83,6 +87,10 @@
You may wish to verify your safety number with this contact.
Accept
+
+ Recent chats
+ Contacts
+
Message %s
Signal Call %s
@@ -134,6 +142,14 @@
Error sending voice message
There is no app available to handle this link on your device.
+ To send audio messages, allow Signal access to your microphone.
+ 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\".
+ To call %s, Signal needs access to your microphone and camera.
+ 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\".
+ To capture photos and video, allow Signal access to the camera.
+ 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\".
+ Signal needs Camera permissions to take photos or video
+
- %d unread message
@@ -182,6 +198,8 @@
There is no browser installed on your device.
+ No results found for \'%s\'
+
- Delete selected conversation?
- Delete selected conversations?
@@ -338,6 +356,13 @@
Error importing backup!
Import complete!
+ 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\".
+ Signal needs the SMS permission in order to import SMS messages
+ 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\".
+ Signal needs the Storage permission in order to read from external storage.
+ 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\".
+ Signal needs the Storage permission in order to write to external storage.
+
Tap and hold to record a voice message, release to send
@@ -436,6 +461,9 @@
Link a Signal device?
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.
+ 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\".
+ Unable to scan a QR code without the Camera permission
+
Disappearing messages
Your messages will not expire.
@@ -504,6 +532,10 @@
Google Play Services is updating or temporarily unavailable. Please try again.
More information
Less information
+ Signal needs access to your contacts and media in order to connect with friends, exchange messages, and make secure calls
+ Unable to connect to service. Please check network connection and try again.
+ To easily verify your phone number, Signal can automatically detect your verification code if you allow Signal to view SMS messages.
+
@@ -584,6 +616,8 @@
Our Signal safety number:
It looks like you don\'t have any apps to share to.
No safety number to compare was found in the clipboard
+ 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\".
+ Unable to scan QR code without Camera permission
@@ -609,6 +643,9 @@
Mute notifications
+
+ No web browser installed!
+
Import in progress
Importing text messages
@@ -625,6 +662,9 @@
You
Unsupported media type
Draft
+ 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\".
+ Unable to save to external storage without permissions
+
%1$d new messages in %2$d conversations
@@ -650,13 +690,24 @@
Open directory
+
+ Search
+
Signal
New message
+
+ Device no longer registered
+ This is likely because you registered your phone number with Signal on a different device. Tap to re-register.
+
Error playing video
+
+ To answer the call from %s, give Signal access to your microphone.
+ 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\".
+
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.
You may wish to verify your safety number with this contact.
@@ -700,12 +751,18 @@
Contact Photo
+ 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\".
+ Error retrieving contacts, check your network connection
No blocked contacts
+
+ Signal needs access to your contacts in order to display them.
+ Show Contacts
+
@@ -732,6 +789,7 @@
Send Failed
Pending Approval
Delivered
+ Message read
Contact photo
@@ -765,6 +823,11 @@
continue
+ Read receipts are here
+ Optionally see and share when messages have been read
+ Enable read receipts
+
+
Off
@@ -889,6 +952,7 @@
Your name
+ Shared media
Mute conversation
@@ -908,6 +972,8 @@
PHONE NUMBER
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.
+ Verify Your Number
+ Please enter your mobile number to receive a verification code. Carrier rates may apply.
@@ -931,7 +997,11 @@
Share safety number
-
+
+
+ Swipe up to answer
+ Swipe down to reject
+
Some issues need your attention.
Sent
@@ -1145,8 +1215,14 @@
Contact Photo Image
Archived
+ Inbox zeeerrro
+ Zip. Zilch. Zero. Nada.\nYou\'re all caught up!
+
+
New conversation
+ Give your inbox something to write home about. Get started by messaging a friend.
+
Reset secure session
@@ -1217,6 +1293,7 @@
All media
+ No documents
Media preview
@@ -1232,27 +1309,7 @@
Transport icon
- Message read
- No documents
- Read receipts are here
- Optionally see and share when messages have been read
- Enable read receipts
- Shared media
- Verify Your Number
- Please enter your mobile number to receive a verification code. Carrier rates may apply.
- Give your inbox something to write home about. Get started by messaging a friend.
- Inbox zeeerrro
- Zip. Zilch. Zero. Nada.\nYou\'re all caught up!
- No results found for \'%s\'
- Search
- Device no longer registered
- This is likely because you registered your phone number with Signal on a different device. Tap to re-register.
- No web browser installed!
- Recent chats
- Contacts
- Swipe up to answer
- Swipe down to reject
diff --git a/res/values/themes.xml b/res/values/themes.xml
index cef4ef6bb0..b1cbdfe2e7 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -348,4 +348,8 @@
- @style/PreferenceThemeOverlay.Fix
- @color/black
+
+
diff --git a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java
index 0d6bb13d88..45c06efcef 100644
--- a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java
+++ b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java
@@ -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 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 getSelectedContacts() {
List 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 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() {
+ @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) {
diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java
index b98f89ba51..afcc0d358a 100644
--- a/src/org/thoughtcrime/securesms/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/ConversationActivity.java
@@ -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);
}
diff --git a/src/org/thoughtcrime/securesms/CreateProfileActivity.java b/src/org/thoughtcrime/securesms/CreateProfileActivity.java
index cc84134531..b06254af0e 100644
--- a/src/org/thoughtcrime/securesms/CreateProfileActivity.java
+++ b/src/org/thoughtcrime/securesms/CreateProfileActivity.java
@@ -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 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();
}
-
-
}
diff --git a/src/org/thoughtcrime/securesms/DeviceActivity.java b/src/org/thoughtcrime/securesms/DeviceActivity.java
index 24b152137b..bf12b9e415 100644
--- a/src/org/thoughtcrime/securesms/DeviceActivity.java
+++ b/src/org/thoughtcrime/securesms/DeviceActivity.java
@@ -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) {
diff --git a/src/org/thoughtcrime/securesms/ImportExportFragment.java b/src/org/thoughtcrime/securesms/ImportExportFragment.java
index d1562e1968..cdbcb239c6 100644
--- a/src/org/thoughtcrime/securesms/ImportExportFragment.java
+++ b/src/org/thoughtcrime/securesms/ImportExportFragment.java
@@ -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 {
@Override
@@ -187,6 +197,7 @@ public class ImportExportFragment extends Fragment {
}
}
+ @SuppressLint("StaticFieldLeak")
private class ExportPlaintextTask extends AsyncTask {
private ProgressDialog dialog;
diff --git a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
index d11cef8973..6489b73e32 100644
--- a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
+++ b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
@@ -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();
});
}
diff --git a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
index 7564d66e6c..92dab2f949 100644
--- a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
+++ b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
@@ -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
{
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() {
diff --git a/src/org/thoughtcrime/securesms/RegistrationActivity.java b/src/org/thoughtcrime/securesms/RegistrationActivity.java
index a064b511bb..6396dd6771 100644
--- a/src/org/thoughtcrime/securesms/RegistrationActivity.java
+++ b/src/org/thoughtcrime/securesms/RegistrationActivity.java
@@ -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 localNumber = Util.getDeviceNumber(this);
+ Optional 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> 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;
}
diff --git a/src/org/thoughtcrime/securesms/TransportOptions.java b/src/org/thoughtcrime/securesms/TransportOptions.java
index 52219506ce..26eb16efce 100644
--- a/src/org/thoughtcrime/securesms/TransportOptions.java
+++ b/src/org/thoughtcrime/securesms/TransportOptions.java
@@ -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 results = new LinkedList<>();
SubscriptionManagerCompat subscriptionManager = new SubscriptionManagerCompat(context);
- List subscriptions = subscriptionManager.getActiveSubscriptionInfoList();
+ List 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,
diff --git a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java
index d92b325821..586ff28fa2 100644
--- a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java
+++ b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java
@@ -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
diff --git a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java
index 6f5a6e0621..39f0833d97 100644
--- a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java
+++ b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java
@@ -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();
}
}
diff --git a/src/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java b/src/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java
index febc2d3781..fb50fab701 100644
--- a/src/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java
+++ b/src/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java
@@ -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);
diff --git a/src/org/thoughtcrime/securesms/components/InputPanel.java b/src/org/thoughtcrime/securesms/components/InputPanel.java
index 321cca0aef..d565a6e20a 100644
--- a/src/org/thoughtcrime/securesms/components/InputPanel.java
+++ b/src/org/thoughtcrime/securesms/components/InputPanel.java
@@ -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 {
diff --git a/src/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java b/src/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java
index 7346f96122..1ae7691e8f 100644
--- a/src/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java
+++ b/src/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java
@@ -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 {
diff --git a/src/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java b/src/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java
index 2024325bf8..96c7c44b25 100644
--- a/src/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java
+++ b/src/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java
@@ -78,6 +78,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
private static class RecentPhotoAdapter extends CursorRecyclerViewAdapter {
+ @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);
}
}
diff --git a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcAnswerDeclineButton.java b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcAnswerDeclineButton.java
index dd0c06bf2e..4032786809 100644
--- a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcAnswerDeclineButton.java
+++ b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcAnswerDeclineButton.java
@@ -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) {
diff --git a/src/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java b/src/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java
index 30b52afc3f..7ea52a3e6a 100644
--- a/src/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java
+++ b/src/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java
@@ -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) {
diff --git a/src/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java b/src/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java
index 4a649572f9..0502e2caa3 100644
--- a/src/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java
+++ b/src/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java
@@ -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;
+ }
}
diff --git a/src/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java b/src/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java
index e1a9fffa3c..4f3bd29f54 100644
--- a/src/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java
@@ -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();
}
diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
index 4afd5b068d..432c0609ec 100644
--- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
+++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
@@ -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) {
diff --git a/src/org/thoughtcrime/securesms/permissions/Permissions.java b/src/org/thoughtcrime/securesms/permissions/Permissions.java
new file mode 100644
index 0000000000..89c0c9556a
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/permissions/Permissions.java
@@ -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 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> someGrantedListener;
+ private Consumer> someDeniedListener;
+ private Consumer> 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> someGrantedListener) {
+ this.someGrantedListener = someGrantedListener;
+ return this;
+ }
+
+ public PermissionsBuilder onSomeDenied(Consumer> someDeniedListener) {
+ this.someDeniedListener = someDeniedListener;
+ return this;
+ }
+
+ public PermissionsBuilder onSomePermanentlyDenied(Consumer> 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 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 context.startActivity(getApplicationSettingsIntent(context)))
+ .setNegativeButton("Cancel", null)
+ .show();
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/permissions/PermissionsRequest.java b/src/org/thoughtcrime/securesms/permissions/PermissionsRequest.java
new file mode 100644
index 0000000000..4837911005
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/permissions/PermissionsRequest.java
@@ -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 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> someGrantedListener;
+ private final @Nullable Consumer> someDeniedListener;
+ private final @Nullable Consumer> somePermanentlyDeniedListener;
+
+ PermissionsRequest(@Nullable Runnable allGrantedListener,
+ @Nullable Runnable anyDeniedListener,
+ @Nullable Runnable anyPermanentlyDeniedListener,
+ @Nullable Runnable anyResultListener,
+ @Nullable Consumer> someGrantedListener,
+ @Nullable Consumer> someDeniedListener,
+ @Nullable Consumer> 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 granted = new ArrayList<>(permissions.length);
+ List denied = new ArrayList<>(permissions.length);
+ List 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);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/permissions/RationaleDialog.java b/src/org/thoughtcrime/securesms/permissions/RationaleDialog.java
new file mode 100644
index 0000000000..39de72ecaa
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/permissions/RationaleDialog.java
@@ -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 getSystemProfileAvatar(final @NonNull Context context, MediaConstraints mediaConstraints) {
SettableFuture 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 getSystemProfileName(final @NonNull Context context) {
SettableFuture 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);
}
}
diff --git a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java
index 713c782ed1..9f3cee64dc 100644
--- a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java
+++ b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java
@@ -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);
}
}
diff --git a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java
index 246083a7d3..51d86c59a0 100644
--- a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java
+++ b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java
@@ -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 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));
diff --git a/src/org/thoughtcrime/securesms/util/DirectoryHelper.java b/src/org/thoughtcrime/securesms/util/DirectoryHelper.java
index 856602a738..9f393c80e3 100644
--- a/src/org/thoughtcrime/securesms/util/DirectoryHelper.java
+++ b/src/org/thoughtcrime/securesms/util/DirectoryHelper.java
@@ -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 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 eligibleRecipientDatabaseContactNumbers = Stream.of(recipientDatabase.getAllRecipients()).map(recipient -> recipient.getAddress().serialize());
Stream 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));
diff --git a/src/org/thoughtcrime/securesms/util/Util.java b/src/org/thoughtcrime/securesms/util/Util.java
index 243082a0e5..9656aa2ef1 100644
--- a/src/org/thoughtcrime/securesms/util/Util.java
+++ b/src/org/thoughtcrime/securesms/util/Util.java
@@ -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());
- 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 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);
}
}