Support for selective permissions

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

View File

@@ -17,7 +17,10 @@
package org.thoughtcrime.securesms;
import android.Manifest;
import android.annotation.SuppressLint;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
@@ -27,10 +30,16 @@ import android.support.v4.content.Loader;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
@@ -38,10 +47,14 @@ import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.DirectoryHelper;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -72,6 +85,10 @@ public class ContactSelectionListFragment extends Fragment
private Set<String> selectedContacts;
private OnContactSelectedListener onContactSelectedListener;
private SwipeRefreshLayout swipeRefresh;
private View showContactsLayout;
private Button showContactsButton;
private TextView showContactsDescription;
private ProgressWheel showContactsProgress;
private String cursorFilter;
private RecyclerView recyclerView;
private RecyclerViewFastScroller fastScroller;
@@ -79,27 +96,48 @@ public class ContactSelectionListFragment extends Fragment
@Override
public void onActivityCreated(Bundle icicle) {
super.onActivityCreated(icicle);
initializeCursor();
}
@Override
public void onResume() {
super.onResume();
}
public void onStart() {
super.onStart();
@Override
public void onPause() {
super.onPause();
Permissions.with(this)
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
.ifNecessary()
.onAllGranted(() -> {
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
handleContactPermissionGranted();
} else {
this.getLoaderManager().initLoader(0, null, this);
}
})
.onAnyDenied(() -> {
getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
if (getActivity().getIntent().getBooleanExtra(RECENTS, false)) {
getLoaderManager().initLoader(0, null, ContactSelectionListFragment.this);
} else {
initializeNoContactsPermission();
}
})
.execute();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
emptyText = ViewUtil.findById(view, android.R.id.empty);
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
swipeRefresh = ViewUtil.findById(view, R.id.swipe_refresh);
fastScroller = ViewUtil.findById(view, R.id.fast_scroller);
emptyText = ViewUtil.findById(view, android.R.id.empty);
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
swipeRefresh = ViewUtil.findById(view, R.id.swipe_refresh);
fastScroller = ViewUtil.findById(view, R.id.fast_scroller);
showContactsLayout = view.findViewById(R.id.show_contacts_container);
showContactsButton = view.findViewById(R.id.show_contacts_button);
showContactsDescription = view.findViewById(R.id.show_contacts_description);
showContactsProgress = view.findViewById(R.id.progress);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
swipeRefresh.setEnabled(getActivity().getIntent().getBooleanExtra(REFRESHABLE, true) &&
@@ -108,6 +146,11 @@ public class ContactSelectionListFragment extends Fragment
return view;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
public @NonNull List<String> getSelectedContacts() {
List<String> selected = new LinkedList<>();
if (selectedContacts != null) {
@@ -130,7 +173,28 @@ public class ContactSelectionListFragment extends Fragment
selectedContacts = adapter.getSelectedContacts();
recyclerView.setAdapter(adapter);
recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter, true, true));
this.getLoaderManager().initLoader(0, null, this);
}
private void initializeNoContactsPermission() {
swipeRefresh.setVisibility(View.GONE);
showContactsLayout.setVisibility(View.VISIBLE);
showContactsProgress.setVisibility(View.INVISIBLE);
showContactsDescription.setText(R.string.contact_selection_list_fragment__signal_needs_access_to_your_contacts_in_order_to_display_them);
showContactsButton.setVisibility(View.VISIBLE);
showContactsButton.setOnClickListener(v -> {
Permissions.with(this)
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.ContactSelectionListFragment_signal_requires_the_contacts_permission_in_order_to_display_your_contacts))
.onSomeGranted(permissions -> {
if (permissions.contains(Manifest.permission.WRITE_CONTACTS)) {
handleContactPermissionGranted();
}
})
.execute();
});
}
public void setQueryFilter(String filter) {
@@ -161,6 +225,9 @@ public class ContactSelectionListFragment extends Fragment
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
swipeRefresh.setVisibility(View.VISIBLE);
showContactsLayout.setVisibility(View.GONE);
((CursorRecyclerViewAdapter) recyclerView.getAdapter()).changeCursor(data);
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
boolean useFastScroller = (recyclerView.getAdapter().getItemCount() > 20);
@@ -177,6 +244,44 @@ public class ContactSelectionListFragment extends Fragment
fastScroller.setVisibility(View.GONE);
}
@SuppressLint("StaticFieldLeak")
private void handleContactPermissionGranted() {
new AsyncTask<Void, Void, Boolean>() {
@Override
protected void onPreExecute() {
swipeRefresh.setVisibility(View.GONE);
showContactsLayout.setVisibility(View.VISIBLE);
showContactsButton.setVisibility(View.INVISIBLE);
showContactsDescription.setText("Loading...");
showContactsProgress.setVisibility(View.VISIBLE);
showContactsProgress.spin();
}
@Override
protected Boolean doInBackground(Void... voids) {
try {
DirectoryHelper.refreshDirectory(getContext(), null, false);
return true;
} catch (IOException e) {
Log.w(TAG, e);
}
return false;
}
@Override
protected void onPostExecute(Boolean result) {
if (result) {
showContactsLayout.setVisibility(View.GONE);
swipeRefresh.setVisibility(View.VISIBLE);
reset();
} else {
Toast.makeText(getContext(), R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection, Toast.LENGTH_LONG).show();
initializeNoContactsPermission();
}
}
}.execute();
}
private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener {
@Override
public void onItemClick(ContactSelectionListItem contact) {

View File

@@ -16,6 +16,7 @@
*/
package org.thoughtcrime.securesms;
import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ActivityNotFoundException;
@@ -132,6 +133,7 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -186,7 +188,7 @@ import static org.whispersystems.signalservice.internal.push.SignalServiceProtos
public class ConversationActivity extends PassphraseRequiredActionBarActivity
implements ConversationFragment.ConversationFragmentListener,
AttachmentManager.AttachmentListener,
RecipientModifiedListener,
RecipientModifiedListener,
OnKeyboardShownListener,
AttachmentDrawerListener,
InputPanel.Listener,
@@ -572,6 +574,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
updateReminders(recipient.hasSeenInviteReminder());
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
//////// Event Handlers
private void handleReturnToConversationList() {
@@ -826,14 +833,23 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (recipient == null) return;
if (isSecureText) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL);
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_ADDRESS, recipient.getAddress());
startService(intent);
Permissions.with(ConversationActivity.this)
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.ConversationActivity_to_call_s_signal_needs_access_to_your_microphone_and_camera, recipient.toShortString()),
R.drawable.ic_mic_white_48dp, R.drawable.ic_videocam_white_48dp)
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.toShortString()))
.onAllGranted(() -> {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL);
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_ADDRESS, recipient.getAddress());
startService(intent);
Intent activityIntent = new Intent(this, WebRtcCallActivity.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(activityIntent);
Intent activityIntent = new Intent(this, WebRtcCallActivity.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(activityIntent);
})
.execute();
} else {
try {
Intent dialIntent = new Intent(Intent.ACTION_DIAL,
@@ -1785,6 +1801,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void onCameraStop() {}
@Override
public void onRecorderPermissionRequired() {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withRationaleDialog(getString(R.string.ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone), R.drawable.ic_mic_white_48dp)
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages))
.execute();
}
@Override
public void onRecorderStarted() {
Vibrator vibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE);
@@ -1911,8 +1937,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void onClick(View v) {
if (!quickAttachmentDrawer.isShowing()) {
composeText.clearFocus();
container.show(composeText, quickAttachmentDrawer);
Permissions.with(ConversationActivity.this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_photo_camera_white_48dp)
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
.onAllGranted(() -> {
composeText.clearFocus();
container.show(composeText, quickAttachmentDrawer);
})
.onAnyDenied(() -> Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())
.execute();
} else {
container.hideAttachedInput(false);
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms;
import android.Manifest;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.app.Activity;
@@ -12,6 +13,7 @@ import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.text.Editable;
@@ -40,6 +42,7 @@ import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
import org.thoughtcrime.securesms.profiles.SystemProfileUtil;
@@ -137,6 +140,11 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@@ -208,17 +216,11 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
this.avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(this, getResources().getColor(R.color.grey_400)));
this.avatar.setOnClickListener(view -> {
try {
captureFile = File.createTempFile("capture", "jpg", getExternalCacheDir());
} catch (IOException e) {
Log.w(TAG, e);
captureFile = null;
}
Intent chooserIntent = createAvatarSelectionIntent(captureFile, avatarBytes != null);
startActivityForResult(chooserIntent, REQUEST_CODE_AVATAR);
});
this.avatar.setOnClickListener(view -> Permissions.with(this)
.request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.onAnyResult(this::handleAvatarSelectionWithPermissions)
.execute());
this.name.addTextChangedListener(new TextWatcher() {
@Override
@@ -360,7 +362,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
this.name.setOnClickListener(v -> container.showSoftkey(name));
}
private Intent createAvatarSelectionIntent(@Nullable File captureFile, boolean includeClear) {
private Intent createAvatarSelectionIntent(@Nullable File captureFile, boolean includeClear, boolean includeCamera) {
List<Intent> extraIntents = new LinkedList<>();
Intent galleryIntent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI);
galleryIntent.setType("image/*");
@@ -370,11 +372,13 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
galleryIntent.setType("image/*");
}
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (includeCamera) {
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (captureFile != null && cameraIntent.resolveActivity(getPackageManager()) != null) {
cameraIntent.putExtra(EXTRA_OUTPUT, Uri.fromFile(captureFile));
extraIntents.add(cameraIntent);
if (captureFile != null && cameraIntent.resolveActivity(getPackageManager()) != null) {
cameraIntent.putExtra(EXTRA_OUTPUT, Uri.fromFile(captureFile));
extraIntents.add(cameraIntent);
}
}
if (includeClear) {
@@ -382,12 +386,31 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
}
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.CreateProfileActivity_profile_photo));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Intent[0]));
if (!extraIntents.isEmpty()) {
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Intent[0]));
}
return chooserIntent;
}
private void handleAvatarSelectionWithPermissions() {
boolean hasCameraPermission = Permissions.hasAll(this, Manifest.permission.CAMERA);
if (hasCameraPermission) {
try {
captureFile = File.createTempFile("capture", "jpg", getExternalCacheDir());
} catch (IOException e) {
Log.w(TAG, e);
captureFile = null;
}
}
Intent chooserIntent = createAvatarSelectionIntent(captureFile, avatarBytes != null, hasCameraPermission);
startActivityForResult(chooserIntent, REQUEST_CODE_AVATAR);
}
private void handleUpload() {
final String name;
final StreamDetails avatar;
@@ -484,6 +507,4 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
reveal.setVisibility(View.VISIBLE);
animation.start();
}
}

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
@@ -19,6 +20,7 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.qr.ScanListener;
import org.thoughtcrime.securesms.util.Base64;
@@ -93,46 +95,56 @@ public class DeviceActivity extends PassphraseRequiredActionBarActivity
@Override
public void onClick(View v) {
getSupportFragmentManager().beginTransaction()
.replace(android.R.id.content, deviceAddFragment)
.addToBackStack(null)
.commit();
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code))
.onAllGranted(() -> {
getSupportFragmentManager().beginTransaction()
.replace(android.R.id.content, deviceAddFragment)
.addToBackStack(null)
.commitAllowingStateLoss();
})
.onAnyDenied(() -> Toast.makeText(this, R.string.DeviceActivity_unable_to_scan_a_qr_code_without_the_camera_permission, Toast.LENGTH_LONG).show())
.execute();
}
@Override
public void onQrDataFound(final String data) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
Uri uri = Uri.parse(data);
deviceLinkFragment.setLinkClickedListener(uri, DeviceActivity.this);
Util.runOnMain(() -> {
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
Uri uri = Uri.parse(data);
deviceLinkFragment.setLinkClickedListener(uri, DeviceActivity.this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
deviceAddFragment.setSharedElementReturnTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
deviceAddFragment.setExitTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
deviceAddFragment.setSharedElementReturnTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
deviceAddFragment.setExitTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
deviceLinkFragment.setSharedElementEnterTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
deviceLinkFragment.setEnterTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
deviceLinkFragment.setSharedElementEnterTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
deviceLinkFragment.setEnterTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
getSupportFragmentManager().beginTransaction()
.addToBackStack(null)
.addSharedElement(deviceAddFragment.getDevicesImage(), "devices")
.replace(android.R.id.content, deviceLinkFragment)
.commit();
getSupportFragmentManager().beginTransaction()
.addToBackStack(null)
.addSharedElement(deviceAddFragment.getDevicesImage(), "devices")
.replace(android.R.id.content, deviceLinkFragment)
.commit();
} else {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_from_bottom, R.anim.slide_to_bottom,
R.anim.slide_from_bottom, R.anim.slide_to_bottom)
.replace(android.R.id.content, deviceLinkFragment)
.addToBackStack(null)
.commit();
}
} else {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_from_bottom, R.anim.slide_to_bottom,
R.anim.slide_from_bottom, R.anim.slide_to_bottom)
.replace(android.R.id.content, deviceLinkFragment)
.addToBackStack(null)
.commit();
}
});
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@SuppressLint("StaticFieldLeak")
@Override
public void onLink(final Uri uri) {

View File

@@ -1,12 +1,13 @@
package org.thoughtcrime.securesms;
import android.app.Dialog;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog;
import android.util.Log;
@@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.database.PlaintextBackupExporter;
import org.thoughtcrime.securesms.database.PlaintextBackupImporter;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
import java.io.IOException;
@@ -26,6 +28,9 @@ import java.io.IOException;
public class ImportExportFragment extends Fragment {
@SuppressWarnings("unused")
private static final String TAG = ImportExportFragment.class.getSimpleName();
private static final int SUCCESS = 0;
private static final int NO_SD_CARD = 1;
private static final int ERROR_IO = 2;
@@ -46,26 +51,9 @@ public class ImportExportFragment extends Fragment {
View importPlaintextView = layout.findViewById(R.id.import_plaintext_backup);
View exportPlaintextView = layout.findViewById(R.id.export_plaintext_backup);
importSmsView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleImportSms();
}
});
importPlaintextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleImportPlaintextBackup();
}
});
exportPlaintextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleExportPlaintextBackup();
}
});
importSmsView.setOnClickListener(v -> handleImportSms());
importPlaintextView.setOnClickListener(v -> handleImportPlaintextBackup());
exportPlaintextView.setOnClickListener(v -> handleExportPlaintextBackup());
return layout;
}
@@ -80,60 +68,82 @@ public class ImportExportFragment extends Fragment {
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@SuppressWarnings("CodeBlock2Expr")
private void handleImportSms() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setIconAttribute(R.attr.dialog_info_icon);
builder.setTitle(getActivity().getString(R.string.ImportFragment_import_system_sms_database));
builder.setMessage(getActivity().getString(R.string.ImportFragment_this_will_import_messages_from_the_system));
builder.setPositiveButton(getActivity().getString(R.string.ImportFragment_import), new AlertDialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(getActivity(), ApplicationMigrationService.class);
intent.setAction(ApplicationMigrationService.MIGRATE_DATABASE);
intent.putExtra("master_secret", masterSecret);
getActivity().startService(intent);
builder.setPositiveButton(getActivity().getString(R.string.ImportFragment_import), (dialog, which) -> {
Permissions.with(this)
.request(Manifest.permission.READ_SMS)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.ImportExportFragment_signal_needs_the_sms_permission_in_order_to_import_sms_messages))
.onAllGranted(() -> {
Intent intent = new Intent(getActivity(), ApplicationMigrationService.class);
intent.setAction(ApplicationMigrationService.MIGRATE_DATABASE);
intent.putExtra("master_secret", masterSecret);
getActivity().startService(intent);
Intent nextIntent = new Intent(getActivity(), ConversationListActivity.class);
Intent nextIntent = new Intent(getActivity(), ConversationListActivity.class);
Intent activityIntent = new Intent(getActivity(), DatabaseMigrationActivity.class);
activityIntent.putExtra("next_intent", nextIntent);
getActivity().startActivity(activityIntent);
}
Intent activityIntent = new Intent(getActivity(), DatabaseMigrationActivity.class);
activityIntent.putExtra("next_intent", nextIntent);
getActivity().startActivity(activityIntent);
})
.onAnyDenied(() -> Toast.makeText(getContext(), R.string.ImportExportFragment_signal_needs_the_sms_permission_in_order_to_import_sms_messages_toast, Toast.LENGTH_LONG).show())
.execute();
});
builder.setNegativeButton(getActivity().getString(R.string.ImportFragment_cancel), null);
builder.show();
}
@SuppressWarnings("CodeBlock2Expr")
@SuppressLint("InlinedApi")
private void handleImportPlaintextBackup() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setIconAttribute(R.attr.dialog_alert_icon);
builder.setTitle(getActivity().getString(R.string.ImportFragment_import_plaintext_backup));
builder.setMessage(getActivity().getString(R.string.ImportFragment_this_will_import_messages_from_a_plaintext_backup));
builder.setPositiveButton(getActivity().getString(R.string.ImportFragment_import), new AlertDialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new ImportPlaintextBackupTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
builder.setPositiveButton(getActivity().getString(R.string.ImportFragment_import), (dialog, which) -> {
Permissions.with(ImportExportFragment.this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_read_from_external_storage_but_it_has_been_permanently_denied))
.onAllGranted(() -> new ImportPlaintextBackupTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR))
.onAnyDenied(() -> Toast.makeText(getContext(), R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_read_from_external_storage, Toast.LENGTH_LONG).show())
.execute();
});
builder.setNegativeButton(getActivity().getString(R.string.ImportFragment_cancel), null);
builder.show();
}
@SuppressWarnings("CodeBlock2Expr")
@SuppressLint("InlinedApi")
private void handleExportPlaintextBackup() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setIconAttribute(R.attr.dialog_alert_icon);
builder.setTitle(getActivity().getString(R.string.ExportFragment_export_plaintext_to_storage));
builder.setMessage(getActivity().getString(R.string.ExportFragment_warning_this_will_export_the_plaintext_contents));
builder.setPositiveButton(getActivity().getString(R.string.ExportFragment_export), new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new ExportPlaintextTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
builder.setPositiveButton(getActivity().getString(R.string.ExportFragment_export), (dialog, which) -> {
Permissions.with(ImportExportFragment.this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
.onAllGranted(() -> new ExportPlaintextTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR))
.onAnyDenied(() -> Toast.makeText(getContext(), R.string.ImportExportFragment_signal_needs_the_storage_permission_in_order_to_write_to_external_storage, Toast.LENGTH_LONG).show())
.execute();
});
builder.setNegativeButton(getActivity().getString(R.string.ExportFragment_cancel), null);
builder.show();
}
@SuppressLint("StaticFieldLeak")
private class ImportPlaintextBackupTask extends AsyncTask<Void, Void, Integer> {
@Override
@@ -187,6 +197,7 @@ public class ImportExportFragment extends Fragment {
}
}
@SuppressLint("StaticFieldLeak")
private class ExportPlaintextTask extends AsyncTask<Void, Void, Integer> {
private ProgressDialog dialog;

View File

@@ -16,6 +16,7 @@
*/
package org.thoughtcrime.securesms;
import android.Manifest;
import android.annotation.TargetApi;
import android.content.Intent;
import android.net.Uri;
@@ -37,6 +38,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
import org.thoughtcrime.securesms.util.DateUtils;
@@ -91,6 +93,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
initializeActionBar();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@TargetApi(VERSION_CODES.JELLY_BEAN)
private void setFullscreenIfPossible() {
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
@@ -211,9 +218,17 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private void saveToDisk() {
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this, masterSecret, image);
long saveDate = (date > 0) ? date : System.currentTimeMillis();
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaUri, mediaType, saveDate, null));
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
.onAllGranted(() -> {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this, masterSecret, image);
long saveDate = (date > 0) ? date : System.currentTimeMillis();
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaUri, mediaType, saveDate, null));
})
.execute();
});
}

View File

@@ -1,6 +1,6 @@
package org.thoughtcrime.securesms;
import android.content.BroadcastReceiver;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -67,6 +67,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.util.concurrent.ExecutionException;
@SuppressLint("StaticFieldLeak")
public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener, LoaderManager.LoaderCallbacks<Cursor>
{
private static final String TAG = RecipientPreferenceActivity.class.getSimpleName();
@@ -232,9 +233,8 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
extends CorrectedPreferenceFragment
implements RecipientModifiedListener
{
private Recipient recipient;
private BroadcastReceiver staleReceiver;
private boolean canHaveSafetyNumber;
private Recipient recipient;
private boolean canHaveSafetyNumber;
@Override
public void onCreate(Bundle icicle) {
@@ -274,7 +274,6 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
public void onDestroy() {
super.onDestroy();
this.recipient.removeListener(this);
getActivity().unregisterReceiver(staleReceiver);
}
private void initializeRecipients() {

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms;
import android.Manifest;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
@@ -53,6 +54,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.GcmRefreshJob;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
@@ -118,6 +120,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
initializeResources();
initializeSpinner();
initializePermissions();
initializeNumber();
initializeChallengeListener();
}
@@ -138,6 +141,11 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
private void initializeResources() {
TextView skipButton = findViewById(R.id.skip_button);
View informationToggle = findViewById(R.id.information_link_container);
@@ -205,8 +213,13 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
});
}
@SuppressLint("MissingPermission")
private void initializeNumber() {
Optional<Phonenumber.PhoneNumber> localNumber = Util.getDeviceNumber(this);
Optional<Phonenumber.PhoneNumber> localNumber = Optional.absent();
if (Permissions.hasAll(this, Manifest.permission.READ_PHONE_STATE)) {
localNumber = Util.getDeviceNumber(this);
}
if (localNumber.isPresent()) {
this.countryCode.setText(String.valueOf(localNumber.get().getCountryCode()));
@@ -220,6 +233,24 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
}
}
@SuppressLint("InlinedApi")
private void initializePermissions() {
Permissions.with(RegistrationActivity.this)
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_CALL_LOG,
Manifest.permission.PROCESS_OUTGOING_CALLS, Manifest.permission.ANSWER_PHONE_CALLS)
.ifNecessary()
.withRationaleDialog(getString(R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends),
R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp)
.onSomeGranted(permissions -> {
if (permissions.contains(Manifest.permission.READ_PHONE_STATE)) {
initializeNumber();
}
})
.execute();
}
private void setCountryDisplay(String value) {
this.countrySpinnerAdapter.clear();
this.countrySpinnerAdapter.add(value);
@@ -249,6 +280,25 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
return;
}
Permissions.with(this)
.request(Manifest.permission.READ_SMS)
.ifNecessary()
.withRationaleDialog(getString(R.string.RegistrationActivity_to_easily_verify_your_phone_number_signal_can_automatically_detect_your_verification_code), R.drawable.ic_textsms_white_48dp)
.onAnyResult(this::handleRegisterWithPermissions)
.execute();
}
private void handleRegisterWithPermissions() {
if (TextUtils.isEmpty(countryCode.getText())) {
Toast.makeText(this, getString(R.string.RegistrationActivity_you_must_specify_your_country_code), Toast.LENGTH_LONG).show();
return;
}
if (TextUtils.isEmpty(number.getText())) {
Toast.makeText(this, getString(R.string.RegistrationActivity_you_must_specify_your_phone_number), Toast.LENGTH_LONG).show();
return;
}
final String e164number = getConfiguredE164Number();
if (!PhoneNumberFormatter.isValidNumber(e164number)) {
@@ -305,7 +355,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
protected void onPostExecute(@Nullable Pair<String, Optional<String>> result) {
if (result == null) {
Toast.makeText(RegistrationActivity.this, "Unable to connect to service. Please check network connection and try again.", Toast.LENGTH_LONG).show();
Toast.makeText(RegistrationActivity.this, R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
return;
}

View File

@@ -1,11 +1,13 @@
package org.thoughtcrime.securesms;
import android.Manifest;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.util.CharacterCalculator;
import org.thoughtcrime.securesms.util.MmsCharacterCalculator;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
import org.thoughtcrime.securesms.util.SmsCharacterCalculator;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
@@ -146,7 +148,13 @@ public class TransportOptions {
{
List<TransportOption> results = new LinkedList<>();
SubscriptionManagerCompat subscriptionManager = new SubscriptionManagerCompat(context);
List<SubscriptionInfoCompat> subscriptions = subscriptionManager.getActiveSubscriptionInfoList();
List<SubscriptionInfoCompat> subscriptions;
if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE)) {
subscriptions = subscriptionManager.getActiveSubscriptionInfoList();
} else {
subscriptions = new LinkedList<>();
}
if (subscriptions.size() < 2) {
results.add(new TransportOption(Type.SMS, R.drawable.ic_send_sms_white_24dp,

View File

@@ -1,5 +1,5 @@
/**
* Copyright (C) 2016 Open Whisper Systems
/*
* Copyright (C) 2016-2017 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,8 +16,11 @@
*/
package org.thoughtcrime.securesms;
import android.*;
import android.Manifest;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
@@ -69,6 +72,7 @@ import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.qr.QrCode;
import org.thoughtcrime.securesms.qr.ScanListener;
import org.thoughtcrime.securesms.qr.ScanningThread;
@@ -96,6 +100,7 @@ import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
*
* @author Moxie Marlinspike
*/
@SuppressLint("StaticFieldLeak")
public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener, ScanListener, View.OnClickListener {
private static final String TAG = VerifyIdentityActivity.class.getSimpleName();
@@ -151,36 +156,41 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
@Override
public void onModified(final Recipient recipient) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
setActionBarNotificationBarColor(recipient.getColor());
}
});
Util.runOnMain(() -> setActionBarNotificationBarColor(recipient.getColor()));
}
@Override
public void onQrDataFound(final String data) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
Util.runOnMain(() -> {
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
getSupportFragmentManager().popBackStack();
displayFragment.setScannedFingerprint(data);
}
getSupportFragmentManager().popBackStack();
displayFragment.setScannedFingerprint(data);
});
}
@Override
public void onClick(View v) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.setCustomAnimations(R.anim.slide_from_top, R.anim.slide_to_bottom,
R.anim.slide_from_bottom, R.anim.slide_to_top);
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied))
.onAllGranted(() -> {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.setCustomAnimations(R.anim.slide_from_top, R.anim.slide_to_bottom,
R.anim.slide_from_bottom, R.anim.slide_to_top);
transaction.replace(android.R.id.content, scanFragment)
.addToBackStack(null)
.commit();
transaction.replace(android.R.id.content, scanFragment)
.addToBackStack(null)
.commitAllowingStateLoss();
})
.onAnyDenied(() -> Toast.makeText(this, R.string.VerifyIdentityActivity_unable_to_scan_qr_code_without_camera_permission, Toast.LENGTH_LONG).show())
.execute();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
private void setActionBarNotificationBarColor(MaterialColor color) {
@@ -295,12 +305,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
@Override
public void onModified(final Recipient recipient) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
setRecipientText(recipient);
}
});
Util.runOnMain(() -> setRecipientText(recipient));
}
@Override

View File

@@ -17,6 +17,7 @@
package org.thoughtcrime.securesms;
import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
@@ -40,6 +41,7 @@ import org.thoughtcrime.securesms.components.webrtc.WebRtcCallControls;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.MessageRetrievalService;
@@ -116,6 +118,11 @@ public class WebRtcCallActivity extends Activity {
super.onConfigurationChanged(newConfiguration);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
private void initializeScreenshotSecurity() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH &&
TextSecurePreferences.isScreenSecurityEnabled(this))
@@ -156,11 +163,21 @@ public class WebRtcCallActivity extends Activity {
WebRtcViewModel event = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
if (event != null) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_answering));
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, event.getRecipient().toShortString()),
R.drawable.ic_mic_white_48dp, R.drawable.ic_videocam_white_48dp)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_answering));
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_ANSWER_CALL);
startService(intent);
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_ANSWER_CALL);
startService(intent);
})
.onAnyDenied(this::handleDenyCall)
.execute();
}
}

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components;
import android.Manifest;
import android.animation.Animator;
import android.annotation.TargetApi;
import android.app.Activity;
@@ -26,6 +27,7 @@ import android.widget.LinearLayout;
import android.widget.PopupWindow;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.ViewUtil;
public class AttachmentTypeSelector extends PopupWindow {
@@ -40,16 +42,19 @@ public class AttachmentTypeSelector extends PopupWindow {
private static final int ANIMATION_DURATION = 300;
@SuppressWarnings("unused")
private static final String TAG = AttachmentTypeSelector.class.getSimpleName();
private final @NonNull ImageView imageButton;
private final @NonNull ImageView audioButton;
private final @NonNull ImageView documentButton;
private final @NonNull ImageView contactButton;
private final @NonNull ImageView cameraButton;
private final @NonNull ImageView locationButton;
private final @NonNull ImageView gifButton;
private final @NonNull ImageView closeButton;
private final @NonNull LoaderManager loaderManager;
private final @NonNull RecentPhotoViewRail recentRail;
private final @NonNull ImageView imageButton;
private final @NonNull ImageView audioButton;
private final @NonNull ImageView documentButton;
private final @NonNull ImageView contactButton;
private final @NonNull ImageView cameraButton;
private final @NonNull ImageView locationButton;
private final @NonNull ImageView gifButton;
private final @NonNull ImageView closeButton;
private @Nullable View currentAnchor;
private @Nullable AttachmentClickedListener listener;
@@ -57,11 +62,12 @@ public class AttachmentTypeSelector extends PopupWindow {
public AttachmentTypeSelector(@NonNull Context context, @NonNull LoaderManager loaderManager, @Nullable AttachmentClickedListener listener) {
super(context);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.attachment_type_selector, null, true);
RecentPhotoViewRail recentPhotos = ViewUtil.findById(layout, R.id.recent_photos);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.attachment_type_selector, null, true);
this.listener = listener;
this.loaderManager = loaderManager;
this.recentRail = ViewUtil.findById(layout, R.id.recent_photos);
this.imageButton = ViewUtil.findById(layout, R.id.gallery_button);
this.audioButton = ViewUtil.findById(layout, R.id.audio_button);
this.documentButton = ViewUtil.findById(layout, R.id.document_button);
@@ -79,7 +85,7 @@ public class AttachmentTypeSelector extends PopupWindow {
this.locationButton.setOnClickListener(new PropagatingClickListener(ADD_LOCATION));
this.gifButton.setOnClickListener(new PropagatingClickListener(ADD_GIF));
this.closeButton.setOnClickListener(new CloseClickListener());
recentPhotos.setListener(new RecentPhotoSelectedListener());
this.recentRail.setListener(new RecentPhotoSelectedListener());
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
ViewUtil.findById(layout, R.id.location_linear_layout).setVisibility(View.INVISIBLE);
@@ -94,10 +100,17 @@ public class AttachmentTypeSelector extends PopupWindow {
setFocusable(true);
setTouchable(true);
loaderManager.initLoader(1, null, recentPhotos);
loaderManager.initLoader(1, null, recentRail);
}
public void show(@NonNull Activity activity, final @NonNull View anchor) {
if (Permissions.hasAll(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
recentRail.setVisibility(View.VISIBLE);
loaderManager.restartLoader(1, null, recentRail);
} else {
recentRail.setVisibility(View.GONE);
}
this.currentAnchor = anchor;
showAtLocation(anchor, Gravity.BOTTOM, 0, 0);

View File

@@ -102,12 +102,7 @@ public class InputPanel extends LinearLayout
public void setListener(final @NonNull Listener listener) {
this.listener = listener;
emojiToggle.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
listener.onEmojiToggle();
}
});
emojiToggle.setOnClickListener(v -> listener.onEmojiToggle());
}
public void setMediaListener(@NonNull MediaListener listener) {
@@ -118,6 +113,11 @@ public class InputPanel extends LinearLayout
emojiToggle.attach(emojiDrawer);
}
@Override
public void onRecordPermissionRequired() {
if (listener != null) listener.onRecorderPermissionRequired();
}
@Override
public void onRecordPressed(float startPositionX) {
if (listener != null) listener.onRecorderStarted();
@@ -211,10 +211,11 @@ public class InputPanel extends LinearLayout
}
public interface Listener {
public void onRecorderStarted();
public void onRecorderFinished();
public void onRecorderCanceled();
public void onEmojiToggle();
void onRecorderStarted();
void onRecorderFinished();
void onRecorderCanceled();
void onRecorderPermissionRequired();
void onEmojiToggle();
}
private static class SlideToCancel {

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components;
import android.Manifest;
import android.content.Context;
import android.graphics.PorterDuff;
import android.support.annotation.Nullable;
@@ -18,6 +19,7 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.ViewUtil;
public class MicrophoneRecorderView extends FrameLayout implements View.OnTouchListener {
@@ -60,9 +62,13 @@ public class MicrophoneRecorderView extends FrameLayout implements View.OnTouchL
public boolean onTouch(View v, final MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
this.actionInProgress = true;
this.floatingRecordButton.display(event.getX());
if (listener != null) listener.onRecordPressed(event.getX());
if (!Permissions.hasAll(getContext(), Manifest.permission.RECORD_AUDIO)) {
if (listener != null) listener.onRecordPermissionRequired();
} else {
this.actionInProgress = true;
this.floatingRecordButton.display(event.getX());
if (listener != null) listener.onRecordPressed(event.getX());
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
@@ -88,10 +94,11 @@ public class MicrophoneRecorderView extends FrameLayout implements View.OnTouchL
}
public interface Listener {
public void onRecordPressed(float x);
public void onRecordReleased(float x);
public void onRecordCanceled(float x);
public void onRecordMoved(float x, float absoluteX);
void onRecordPressed(float x);
void onRecordReleased(float x);
void onRecordCanceled(float x);
void onRecordMoved(float x, float absoluteX);
void onRecordPermissionRequired();
}
private static class FloatingRecordButton {

View File

@@ -78,6 +78,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
private static class RecentPhotoAdapter extends CursorRecyclerViewAdapter<RecentPhotoAdapter.RecentPhotoViewHolder> {
@SuppressWarnings("unused")
private static final String TAG = RecentPhotoAdapter.class.getName();
@NonNull private final Uri baseUri;
@@ -117,11 +118,8 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(viewHolder.imageView);
viewHolder.imageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (clickedListener != null) clickedListener.onItemClicked(uri);
}
viewHolder.imageView.setOnClickListener(v -> {
if (clickedListener != null) clickedListener.onItemClicked(uri);
});
}
@@ -143,6 +141,6 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
}
public interface OnItemClickedListener {
public void onItemClicked(Uri uri);
void onItemClicked(Uri uri);
}
}

View File

@@ -159,7 +159,11 @@ public class WebRtcAnswerDeclineButton extends LinearLayout implements View.OnTo
fab.setTranslationY(difference);
if (percentageToThreshold == 1 && listener != null) listener.onAnswered();
if (percentageToThreshold == 1 && listener != null) {
fab.setVisibility(View.INVISIBLE);
lastY = event.getRawY();
listener.onAnswered();
}
} else {
differenceThreshold = ViewUtil.dpToPx(getContext(), DECLINE_THRESHOLD);
percentageToThreshold = Math.min(1, difference / differenceThreshold);
@@ -173,7 +177,11 @@ public class WebRtcAnswerDeclineButton extends LinearLayout implements View.OnTo
fab.setRotation(135 * percentageToThreshold);
if (percentageToThreshold == 1 && listener != null) listener.onDeclined();
if (percentageToThreshold == 1 && listener != null) {
fab.setVisibility(View.INVISIBLE);
lastY = event.getRawY();
listener.onDeclined();
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

View File

@@ -16,6 +16,7 @@
*/
package org.thoughtcrime.securesms.contacts;
import android.Manifest;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
@@ -36,6 +37,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.NumberUtil;
import org.thoughtcrime.securesms.permissions.Permissions;
import java.util.ArrayList;
@@ -102,14 +104,16 @@ public class ContactsCursorLoader extends CursorLoader {
}
}
if (mode != MODE_SMS_ONLY) {
cursorList.add(contactsDatabase.queryTextSecureContacts(filter));
}
if (Permissions.hasAny(getContext(), Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
if (mode != MODE_SMS_ONLY) {
cursorList.add(contactsDatabase.queryTextSecureContacts(filter));
}
if (mode == MODE_ALL) {
cursorList.add(contactsDatabase.querySystemContacts(filter));
} else if (mode == MODE_SMS_ONLY) {
cursorList.add(filterNonPushContacts(contactsDatabase.querySystemContacts(filter)));
if (mode == MODE_ALL) {
cursorList.add(contactsDatabase.querySystemContacts(filter));
} else if (mode == MODE_SMS_ONLY) {
cursorList.add(filterNonPushContacts(contactsDatabase.querySystemContacts(filter)));
}
}
if (!TextUtils.isEmpty(filter) && NumberUtil.isValidSmsOrEmail(filter)) {
@@ -122,7 +126,8 @@ public class ContactsCursorLoader extends CursorLoader {
cursorList.add(newNumberCursor);
}
return new MergeCursor(cursorList.toArray(new Cursor[0]));
if (cursorList.size() > 0) return new MergeCursor(cursorList.toArray(new Cursor[0]));
else return null;
}
private @NonNull Cursor filterNonPushContacts(@NonNull Cursor cursor) {

View File

@@ -1,13 +1,14 @@
package org.thoughtcrime.securesms.database.loaders;
import android.Manifest;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.v4.content.CursorLoader;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.permissions.Permissions;
public class RecentPhotosLoader extends CursorLoader {
@@ -30,9 +31,13 @@ public class RecentPhotosLoader extends CursorLoader {
@Override
public Cursor loadInBackground() {
return context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
PROJECTION, null, null,
MediaStore.Images.ImageColumns.DATE_MODIFIED + " DESC");
if (Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
return context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
PROJECTION, null, null,
MediaStore.Images.ImageColumns.DATE_MODIFIED + " DESC");
} else {
return null;
}
}

View File

@@ -7,7 +7,6 @@ import android.support.annotation.Nullable;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.DirectoryHelper;
@@ -58,7 +57,6 @@ public class DirectoryRefreshJob extends ContextJob {
} else {
DirectoryHelper.refreshDirectoryFor(context, masterSecret, recipient);
}
SecurityEvent.broadcastSecurityUpdateEvent(context);
} finally {
if (wakeLock.isHeld()) wakeLock.release();
}

View File

@@ -16,6 +16,8 @@
*/
package org.thoughtcrime.securesms.mms;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
@@ -49,6 +51,7 @@ import org.thoughtcrime.securesms.components.location.SignalMapView;
import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
import org.thoughtcrime.securesms.util.BitmapUtil;
@@ -199,6 +202,7 @@ public class AttachmentManager {
});
}
@SuppressLint("StaticFieldLeak")
public void setMedia(@NonNull final MasterSecret masterSecret,
@NonNull final GlideRequests glideRequests,
@NonNull final Uri uri,
@@ -318,28 +322,57 @@ public class AttachmentManager {
}
public static void selectDocument(Activity activity, int requestCode) {
selectMediaType(activity, "*/*", null, requestCode);
Permissions.with(activity)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode))
.execute();
}
public static void selectGallery(Activity activity, int requestCode) {
selectMediaType(activity, "image/*", new String[] {"image/*", "video/*"}, requestCode);
Permissions.with(activity)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
.onAllGranted(() -> selectMediaType(activity, "image/*", new String[] {"image/*", "video/*"}, requestCode))
.execute();
}
public static void selectAudio(Activity activity, int requestCode) {
selectMediaType(activity, "audio/*", null, requestCode);
Permissions.with(activity)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
.onAllGranted(() -> selectMediaType(activity, "audio/*", null, requestCode))
.execute();
}
public static void selectContactInfo(Activity activity, int requestCode) {
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
activity.startActivityForResult(intent, requestCode);
Permissions.with(activity)
.request(Manifest.permission.WRITE_CONTACTS)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information))
.onAllGranted(() -> {
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
activity.startActivityForResult(intent, requestCode);
})
.execute();
}
public static void selectLocation(Activity activity, int requestCode) {
try {
activity.startActivityForResult(new PlacePicker.IntentBuilder().build(activity), requestCode);
} catch (GooglePlayServicesRepairableException | GooglePlayServicesNotAvailableException e) {
Log.w(TAG, e);
}
Permissions.with(activity)
.request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location))
.onAllGranted(() -> {
try {
activity.startActivityForResult(new PlacePicker.IntentBuilder().build(activity), requestCode);
} catch (GooglePlayServicesRepairableException | GooglePlayServicesNotAvailableException e) {
Log.w(TAG, e);
}
})
.execute();
}
public static void selectGif(Activity activity, int requestCode, boolean isForMms) {
@@ -357,20 +390,27 @@ public class AttachmentManager {
}
public void capturePhoto(Activity activity, int requestCode) {
try {
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
if (captureUri == null) {
captureUri = PersistentBlobProvider.getInstance(context)
.createForExternal(MediaUtil.IMAGE_JPEG);
}
Log.w(TAG, "captureUri path is " + captureUri.getPath());
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri);
activity.startActivityForResult(captureIntent, requestCode);
}
} catch (IOException ioe) {
Log.w(TAG, ioe);
}
Permissions.with(activity)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied))
.onAllGranted(() -> {
try {
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
if (captureUri == null) {
captureUri = PersistentBlobProvider.getInstance(context)
.createForExternal(MediaUtil.IMAGE_JPEG);
}
Log.w(TAG, "captureUri path is " + captureUri.getPath());
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri);
activity.startActivityForResult(captureIntent, requestCode);
}
} catch (IOException ioe) {
Log.w(TAG, ioe);
}
})
.execute();
}
private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {

View File

@@ -0,0 +1,330 @@
package org.thoughtcrime.securesms.permissions;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.ViewGroup;
import android.view.WindowManager;
import com.annimon.stream.Stream;
import com.annimon.stream.function.Consumer;
import org.thoughtcrime.securesms.util.LRUCache;
import org.thoughtcrime.securesms.util.ServiceUtil;
import java.security.SecureRandom;
import java.util.List;
import java.util.Map;
public class Permissions {
private static final Map<Integer, PermissionsRequest> OUTSTANDING = new LRUCache<>(2);
public static PermissionsBuilder with(@NonNull Activity activity) {
return new PermissionsBuilder(new ActivityPermissionObject(activity));
}
public static PermissionsBuilder with(@NonNull Fragment fragment) {
return new PermissionsBuilder(new FragmentPermissionObject(fragment));
}
public static class PermissionsBuilder {
private final PermissionObject permissionObject;
private String[] requestedPermissions;
private Runnable allGrantedListener;
private Runnable anyDeniedListener;
private Runnable anyPermanentlyDeniedListener;
private Runnable anyResultListener;
private Consumer<List<String>> someGrantedListener;
private Consumer<List<String>> someDeniedListener;
private Consumer<List<String>> somePermanentlyDeniedListener;
private @DrawableRes int[] rationalDialogHeader;
private String rationaleDialogMessage;
private boolean ifNecesary;
PermissionsBuilder(PermissionObject permissionObject) {
this.permissionObject = permissionObject;
}
public PermissionsBuilder request(String... requestedPermissions) {
this.requestedPermissions = requestedPermissions;
return this;
}
public PermissionsBuilder ifNecessary() {
this.ifNecesary = true;
return this;
}
public PermissionsBuilder withRationaleDialog(@NonNull String message, @NonNull @DrawableRes int... headers) {
this.rationalDialogHeader = headers;
this.rationaleDialogMessage = message;
return this;
}
public PermissionsBuilder withPermanentDenialDialog(@NonNull String message) {
return onAnyPermanentlyDenied(new SettingsDialogListener(permissionObject.getContext(), message));
}
public PermissionsBuilder onAllGranted(Runnable allGrantedListener) {
this.allGrantedListener = allGrantedListener;
return this;
}
public PermissionsBuilder onAnyDenied(Runnable anyDeniedListener) {
this.anyDeniedListener = anyDeniedListener;
return this;
}
@SuppressWarnings("WeakerAccess")
public PermissionsBuilder onAnyPermanentlyDenied(Runnable anyPermanentlyDeniedListener) {
this.anyPermanentlyDeniedListener = anyPermanentlyDeniedListener;
return this;
}
public PermissionsBuilder onAnyResult(Runnable anyResultListener) {
this.anyResultListener = anyResultListener;
return this;
}
public PermissionsBuilder onSomeGranted(Consumer<List<String>> someGrantedListener) {
this.someGrantedListener = someGrantedListener;
return this;
}
public PermissionsBuilder onSomeDenied(Consumer<List<String>> someDeniedListener) {
this.someDeniedListener = someDeniedListener;
return this;
}
public PermissionsBuilder onSomePermanentlyDenied(Consumer<List<String>> somePermanentlyDeniedListener) {
this.somePermanentlyDeniedListener = somePermanentlyDeniedListener;
return this;
}
public void execute() {
PermissionsRequest request = new PermissionsRequest(allGrantedListener, anyDeniedListener, anyPermanentlyDeniedListener, anyResultListener,
someGrantedListener, someDeniedListener, somePermanentlyDeniedListener);
if (ifNecesary && permissionObject.hasAll(requestedPermissions)) {
executePreGrantedPermissionsRequest(request);
} else if (rationaleDialogMessage != null && rationalDialogHeader != null) {
executePermissionsRequestWithRationale(request);
} else {
executePermissionsRequest(request);
}
}
private void executePreGrantedPermissionsRequest(PermissionsRequest request) {
int[] grantResults = new int[requestedPermissions.length];
for (int i=0;i<grantResults.length;i++) grantResults[i] = PackageManager.PERMISSION_GRANTED;
request.onResult(requestedPermissions, grantResults, new boolean[requestedPermissions.length]);
}
@SuppressWarnings("ConstantConditions")
private void executePermissionsRequestWithRationale(PermissionsRequest request) {
RationaleDialog.createFor(permissionObject.getContext(), rationaleDialogMessage, rationalDialogHeader)
.setPositiveButton("Continue", (dialog, which) -> executePermissionsRequest(request))
.setNegativeButton("Not now", null)
.show()
.getWindow()
.setLayout((int)(permissionObject.getWindowWidth() * .75), ViewGroup.LayoutParams.WRAP_CONTENT);
}
private void executePermissionsRequest(PermissionsRequest request) {
int requestCode = new SecureRandom().nextInt(65434) + 100;
synchronized (OUTSTANDING) {
OUTSTANDING.put(requestCode, request);
}
for (String permission : requestedPermissions) {
request.addMapping(permission, permissionObject.shouldShouldPermissionRationale(permission));
}
permissionObject.requestPermissions(requestCode, requestedPermissions);
}
}
private static void requestPermissions(@NonNull Activity activity, int requestCode, String... permissions) {
ActivityCompat.requestPermissions(activity, filterNotGranted(activity, permissions), requestCode);
}
private static void requestPermissions(@NonNull Fragment fragment, int requestCode, String... permissions) {
fragment.requestPermissions(filterNotGranted(fragment.getContext(), permissions), requestCode);
}
private static String[] filterNotGranted(@NonNull Context context, String... permissions) {
return Stream.of(permissions)
.filter(permission -> ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED)
.toList()
.toArray(new String[0]);
}
public static boolean hasAny(@NonNull Context context, String... permissions) {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
Stream.of(permissions).anyMatch(permission -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
}
public static boolean hasAll(@NonNull Context context, String... permissions) {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
Stream.of(permissions).allMatch(permission -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
}
public static void onRequestPermissionsResult(Fragment fragment, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
onRequestPermissionsResult(new FragmentPermissionObject(fragment), requestCode, permissions, grantResults);
}
public static void onRequestPermissionsResult(Activity activity, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
onRequestPermissionsResult(new ActivityPermissionObject(activity), requestCode, permissions, grantResults);
}
private static void onRequestPermissionsResult(@NonNull PermissionObject context, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
PermissionsRequest resultListener;
synchronized (OUTSTANDING) {
resultListener = OUTSTANDING.remove(requestCode);
}
if (resultListener == null) return;
boolean[] shouldShowRationaleDialog = new boolean[permissions.length];
for (int i=0;i<permissions.length;i++) {
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
shouldShowRationaleDialog[i] = context.shouldShouldPermissionRationale(permissions[i]);
}
}
resultListener.onResult(permissions, grantResults, shouldShowRationaleDialog);
}
private static Intent getApplicationSettingsIntent(@NonNull Context context) {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", context.getPackageName(), null);
intent.setData(uri);
return intent;
}
private abstract static class PermissionObject {
abstract Context getContext();
abstract boolean shouldShouldPermissionRationale(String permission);
abstract boolean hasAll(String... permissions);
abstract void requestPermissions(int requestCode, String... permissions);
int getWindowWidth() {
WindowManager windowManager = ServiceUtil.getWindowManager(getContext());
Display display = windowManager.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
return metrics.widthPixels;
}
}
private static class ActivityPermissionObject extends PermissionObject {
private Activity activity;
ActivityPermissionObject(@NonNull Activity activity) {
this.activity = activity;
}
@Override
public Context getContext() {
return activity;
}
@Override
public boolean shouldShouldPermissionRationale(String permission) {
return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission);
}
@Override
public boolean hasAll(String... permissions) {
return Permissions.hasAll(activity, permissions);
}
@Override
public void requestPermissions(int requestCode, String... permissions) {
Permissions.requestPermissions(activity, requestCode, permissions);
}
}
private static class FragmentPermissionObject extends PermissionObject {
private Fragment fragment;
FragmentPermissionObject(@NonNull Fragment fragment) {
this.fragment = fragment;
}
@Override
public Context getContext() {
return fragment.getContext();
}
@Override
public boolean shouldShouldPermissionRationale(String permission) {
return fragment.shouldShowRequestPermissionRationale(permission);
}
@Override
public boolean hasAll(String... permissions) {
return Permissions.hasAll(fragment.getContext(), permissions);
}
@Override
public void requestPermissions(int requestCode, String... permissions) {
Permissions.requestPermissions(fragment, requestCode, permissions);
}
}
private static class SettingsDialogListener implements Runnable {
private final Context context;
private final String message;
SettingsDialogListener(Context context, String message) {
this.message = message;
this.context = context.getApplicationContext();
}
@Override
public void run() {
new AlertDialog.Builder(context)
.setTitle("Permission required")
.setMessage(message)
.setPositiveButton("Continue", (dialog, which) -> context.startActivity(getApplicationSettingsIntent(context)))
.setNegativeButton("Cancel", null)
.show();
}
}
}

View File

@@ -0,0 +1,92 @@
package org.thoughtcrime.securesms.permissions;
import android.content.pm.PackageManager;
import android.support.annotation.Nullable;
import com.annimon.stream.function.Consumer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class PermissionsRequest {
private final Map<String, Boolean> PRE_REQUEST_MAPPING = new HashMap<>();
private final @Nullable Runnable allGrantedListener;
private final @Nullable Runnable anyDeniedListener;
private final @Nullable Runnable anyPermanentlyDeniedListener;
private final @Nullable Runnable anyResultListener;
private final @Nullable Consumer<List<String>> someGrantedListener;
private final @Nullable Consumer<List<String>> someDeniedListener;
private final @Nullable Consumer<List<String>> somePermanentlyDeniedListener;
PermissionsRequest(@Nullable Runnable allGrantedListener,
@Nullable Runnable anyDeniedListener,
@Nullable Runnable anyPermanentlyDeniedListener,
@Nullable Runnable anyResultListener,
@Nullable Consumer<List<String>> someGrantedListener,
@Nullable Consumer<List<String>> someDeniedListener,
@Nullable Consumer<List<String>> somePermanentlyDeniedListener)
{
this.allGrantedListener = allGrantedListener;
this.anyDeniedListener = anyDeniedListener;
this.anyPermanentlyDeniedListener = anyPermanentlyDeniedListener;
this.anyResultListener = anyResultListener;
this.someGrantedListener = someGrantedListener;
this.someDeniedListener = someDeniedListener;
this.somePermanentlyDeniedListener = somePermanentlyDeniedListener;
}
void onResult(String[] permissions, int[] grantResults, boolean[] shouldShowRationaleDialog) {
List<String> granted = new ArrayList<>(permissions.length);
List<String> denied = new ArrayList<>(permissions.length);
List<String> permanentlyDenied = new ArrayList<>(permissions.length);
for (int i = 0; i < permissions.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
granted.add(permissions[i]);
} else {
boolean preRequestShouldShowRationaleDialog = PRE_REQUEST_MAPPING.get(permissions[i]);
if ((somePermanentlyDeniedListener != null || anyPermanentlyDeniedListener != null) &&
!preRequestShouldShowRationaleDialog && !shouldShowRationaleDialog[i])
{
permanentlyDenied.add(permissions[i]);
} else {
denied.add(permissions[i]);
}
}
}
if (allGrantedListener != null && granted.size() > 0 && (denied.size() == 0 && permanentlyDenied.size() == 0)) {
allGrantedListener.run();
} else if (someGrantedListener != null && granted.size() > 0) {
someGrantedListener.accept(granted);
}
if (denied.size() > 0) {
if (anyDeniedListener != null) anyDeniedListener.run();
if (someDeniedListener != null) someDeniedListener.accept(denied);
}
if (permanentlyDenied.size() > 0) {
if (anyPermanentlyDeniedListener != null) anyPermanentlyDeniedListener.run();
if (somePermanentlyDeniedListener != null) somePermanentlyDeniedListener.accept(permanentlyDenied);
}
if (anyResultListener != null) {
anyResultListener.run();
}
}
void addMapping(String permission, boolean shouldShowRationaleDialog) {
PRE_REQUEST_MAPPING.put(permission, shouldShowRationaleDialog);
}
}

View File

@@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.permissions;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.Color;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
public class RationaleDialog {
public static AlertDialog.Builder createFor(@NonNull Context context, @NonNull String message, @DrawableRes int... drawables) {
View view = LayoutInflater.from(context).inflate(R.layout.permissions_rationale_dialog, null);
ViewGroup header = view.findViewById(R.id.header_container);
TextView text = view.findViewById(R.id.message);
for (int i=0;i<drawables.length;i++) {
ImageView imageView = new ImageView(context);
imageView.setImageDrawable(context.getResources().getDrawable(drawables[i]));
imageView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
header.addView(imageView);
if (i != drawables.length - 1) {
TextView plus = new TextView(context);
plus.setText("+");
plus.setTextSize(TypedValue.COMPLEX_UNIT_SP, 40);
plus.setTextColor(Color.WHITE);
LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
layoutParams.setMargins(ViewUtil.dpToPx(context, 20), 0, ViewUtil.dpToPx(context, 20), 0);
plus.setLayoutParams(layoutParams);
header.addView(plus);
}
}
text.setText(message);
return new AlertDialog.Builder(context, R.style.RationaleDialog).setView(view);
}
}

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.profiles;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
@@ -14,8 +15,6 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import com.bumptech.glide.load.data.StreamLocalUriFetcher;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
@@ -26,6 +25,7 @@ public class SystemProfileUtil {
private static final String TAG = SystemProfileUtil.class.getSimpleName();
@SuppressLint("StaticFieldLeak")
public static ListenableFuture<byte[]> getSystemProfileAvatar(final @NonNull Context context, MediaConstraints mediaConstraints) {
SettableFuture<byte[]> future = new SettableFuture<>();
@@ -45,6 +45,8 @@ public class SystemProfileUtil {
}
}
}
} catch (SecurityException se) {
Log.w(TAG, se);
}
}
@@ -61,6 +63,7 @@ public class SystemProfileUtil {
return future;
}
@SuppressLint("StaticFieldLeak")
public static ListenableFuture<String> getSystemProfileName(final @NonNull Context context) {
SettableFuture<String> future = new SettableFuture<>();
@@ -74,6 +77,8 @@ public class SystemProfileUtil {
if (cursor != null && cursor.moveToNext()) {
name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Profile.DISPLAY_NAME));
}
} catch (SecurityException se) {
Log.w(TAG, se);
}
}

View File

@@ -138,10 +138,9 @@ class RecipientProvider {
}
if (address.isPhone() && !TextUtils.isEmpty(address.toPhoneString())) {
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address.toPhoneString()));
Cursor cursor = context.getContentResolver().query(uri, CALLER_ID_PROJECTION, null, null, null);
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address.toPhoneString()));
try {
try (Cursor cursor = context.getContentResolver().query(uri, CALLER_ID_PROJECTION, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
final String resultNumber = cursor.getString(3);
if (resultNumber != null) {
@@ -162,9 +161,8 @@ class RecipientProvider {
Log.w(TAG, "resultNumber is null");
}
}
} finally {
if (cursor != null)
cursor.close();
} catch (SecurityException se) {
Log.w(TAG, se);
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.service;
import android.Manifest;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -31,6 +32,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
@@ -340,8 +342,13 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
@Override
public void onSuccessContinue(List<PeerConnection.IceServer> result) {
try {
boolean isSystemContact = ContactAccessor.getInstance().isSystemContact(WebRtcCallService.this, recipient.getAddress().serialize());
boolean isAlwaysTurn = TextSecurePreferences.isTurnOnly(WebRtcCallService.this);
boolean isSystemContact = false;
if (Permissions.hasAny(WebRtcCallService.this, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
isSystemContact = ContactAccessor.getInstance().isSystemContact(WebRtcCallService.this, recipient.getAddress().serialize());
}
boolean isAlwaysTurn = TextSecurePreferences.isTurnOnly(WebRtcCallService.this);
WebRtcCallService.this.peerConnection = new PeerConnectionWrapper(WebRtcCallService.this, peerConnectionFactory, WebRtcCallService.this, localRenderer, result, !isSystemContact || isAlwaysTurn);
WebRtcCallService.this.peerConnection.setRemoteDescription(new SessionDescription(SessionDescription.Type.OFFER, offer));

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.util;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentResolver;
@@ -28,6 +29,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
@@ -51,6 +53,7 @@ public class DirectoryHelper {
throws IOException
{
if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) return;
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) return;
List<Address> newlyActiveUsers = refreshDirectory(context, AccountManagerFactory.createManager(context));
@@ -70,6 +73,10 @@ public class DirectoryHelper {
return new LinkedList<>();
}
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
return new LinkedList<>();
}
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
Stream<String> eligibleRecipientDatabaseContactNumbers = Stream.of(recipientDatabase.getAllRecipients()).map(recipient -> recipient.getAddress().serialize());
Stream<String> eligibleSystemDatabaseContactNumbers = Stream.of(ContactAccessor.getInstance().getAllContactsWithNumbers(context)).map(Address::serialize);
@@ -126,7 +133,9 @@ public class DirectoryHelper {
if (details.isPresent()) {
recipientDatabase.setRegistered(recipient, RegisteredState.REGISTERED);
updateContactsDatabase(context, Util.asList(recipient.getAddress()), false);
if (Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
updateContactsDatabase(context, Util.asList(recipient.getAddress()), false);
}
if (!activeUser && TextSecurePreferences.isMultiDevice(context)) {
ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceContactUpdateJob(context));

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
@@ -32,6 +32,7 @@ import android.os.Looper;
import android.provider.Telephony;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresPermission;
import android.telephony.TelephonyManager;
import android.text.Spannable;
import android.text.SpannableString;
@@ -50,7 +51,6 @@ import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.mms.OutgoingLegacyMmsConnection;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -113,12 +113,9 @@ public class Util {
public static ExecutorService newSingleThreadedLifoExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingLifoQueue<Runnable>());
executor.execute(new Runnable() {
@Override
public void run() {
executor.execute(() -> {
// Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
}
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
});
return executor;
@@ -243,6 +240,12 @@ public class Util {
return total;
}
@RequiresPermission(anyOf = {
android.Manifest.permission.READ_PHONE_STATE,
android.Manifest.permission.READ_SMS,
android.Manifest.permission.READ_PHONE_NUMBERS
})
@SuppressLint("MissingPermission")
public static Optional<Phonenumber.PhoneNumber> getDeviceNumber(Context context) {
try {
final String localNumber = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number();
@@ -388,13 +391,11 @@ public class Util {
runnable.run();
} else {
final CountDownLatch sync = new CountDownLatch(1);
runOnMain(new Runnable() {
@Override public void run() {
try {
runnable.run();
} finally {
sync.countDown();
}
runOnMain(() -> {
try {
runnable.run();
} finally {
sync.countDown();
}
});
try {
@@ -438,7 +439,7 @@ public class Util {
}
public static @Nullable String readTextFromClipboard(@NonNull Context context) {
if (VERSION.SDK_INT >= 11) {
{
ClipboardManager clipboardManager = (ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboardManager.hasPrimaryClip() && clipboardManager.getPrimaryClip().getItemCount() > 0) {
@@ -446,24 +447,13 @@ public class Util {
} else {
return null;
}
} else {
android.text.ClipboardManager clipboardManager = (android.text.ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboardManager.hasText()) {
return clipboardManager.getText().toString();
} else {
return null;
}
}
}
public static void writeTextToClipboard(@NonNull Context context, @NonNull String text) {
if (VERSION.SDK_INT >= 11) {
{
ClipboardManager clipboardManager = (ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
clipboardManager.setPrimaryClip(ClipData.newPlainText("Safety numbers", text));
} else {
android.text.ClipboardManager clipboardManager = (android.text.ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
clipboardManager.setText(text);
}
}