Detailed changes
@@ -62,8 +62,8 @@ public class JabberIdContact extends AbstractPhoneContact {
return jid;
}
- public static Map<Jid, JabberIdContact> load(Context context) {
- if (!QuickConversationsService.isFreeOrQuicksyFlavor()
+ public static Map<Jid, JabberIdContact> load(final Context context) {
+ if (!QuickConversationsService.isContactListIntegration(context)
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED)) {
@@ -1,14 +1,22 @@
package eu.siacs.conversations.services;
+import android.Manifest;
+import android.content.Context;
import android.content.Intent;
-import android.os.Build;
+import android.content.pm.PackageManager;
+
+import com.google.common.collect.Iterables;
import eu.siacs.conversations.BuildConfig;
+import java.util.Arrays;
+
public abstract class AbstractQuickConversationsService {
+ public static final String SMS_RETRIEVED_ACTION =
+ "com.google.android.gms.auth.api.phone.SMS_RETRIEVED";
- public static final String SMS_RETRIEVED_ACTION = "com.google.android.gms.auth.api.phone.SMS_RETRIEVED";
+ private static Boolean declaredReadContacts = null;
protected final XmppConnectionService service;
@@ -30,8 +38,31 @@ public abstract class AbstractQuickConversationsService {
return "playstore".equals(BuildConfig.FLAVOR_distribution);
}
- public static boolean isFreeOrQuicksyFlavor() {
- return "free".equals(BuildConfig.FLAVOR_distribution) || "quicksy".equals(BuildConfig.FLAVOR_mode);
+ public static boolean isContactListIntegration(final Context context) {
+ if ("quicksy".equals(BuildConfig.FLAVOR_mode)) {
+ return true;
+ }
+ final var readContacts = AbstractQuickConversationsService.declaredReadContacts;
+ if (readContacts != null) {
+ return Boolean.TRUE.equals(readContacts);
+ }
+ AbstractQuickConversationsService.declaredReadContacts = hasDeclaredReadContacts(context);
+ return AbstractQuickConversationsService.declaredReadContacts;
+ }
+
+ private static boolean hasDeclaredReadContacts(final Context context) {
+ final String[] permissions;
+ try {
+ permissions =
+ context.getPackageManager()
+ .getPackageInfo(
+ context.getPackageName(), PackageManager.GET_PERMISSIONS)
+ .requestedPermissions;
+ } catch (final PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ return Iterables.any(
+ Arrays.asList(permissions), p -> p.equals(Manifest.permission.READ_CONTACTS));
}
public static boolean isQuicksyPlayStore() {
@@ -1290,7 +1290,7 @@ public class XmppConnectionService extends Service {
restoreFromDatabase();
- if (QuickConversationsService.isFreeOrQuicksyFlavor()
+ if (QuickConversationsService.isContactListIntegration(this)
&& (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|| ContextCompat.checkSelfPermission(
this, Manifest.permission.READ_CONTACTS)
@@ -120,13 +120,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
private void checkContactPermissionAndShowAddDialog() {
if (hasContactsPermission()) {
showAddToPhoneBookDialog();
- } else if (QuickConversationsService.isFreeOrQuicksyFlavor() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ } else if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
}
}
private boolean hasContactsPermission() {
- if (QuickConversationsService.isFreeOrQuicksyFlavor() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
} else {
return true;
@@ -525,7 +525,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
}
private void onBadgeClick(final View view) {
- if (QuickConversationsService.isFreeOrQuicksyFlavor()) {
+ if (QuickConversationsService.isContactListIntegration(this)) {
final Uri systemAccount = contact.getSystemAccount();
if (systemAccount == null) {
checkContactPermissionAndShowAddDialog();
@@ -6,12 +6,14 @@ import android.app.Dialog;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
+import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.Html;
import android.text.TextWatcher;
@@ -91,6 +93,8 @@ import eu.siacs.conversations.xmpp.XmppConnection;
public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreatePrivateGroupChatDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener, CreatePublicChannelDialog.CreatePublicChannelDialogListener {
+ private static final String PREF_KEY_CONTACT_INTEGRATION_CONSENT = "contact_list_integration_consent";
+
public static final String EXTRA_INVITE_URI = "eu.siacs.conversations.invite_uri";
private final int REQUEST_SYNC_CONTACTS = 0x28cf;
@@ -761,50 +765,96 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
private void askForContactsPermissions() {
- if (QuickConversationsService.isFreeOrQuicksyFlavor() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+ if (QuickConversationsService.isContactListIntegration(this)
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
if (mRequestedContactsPermission.compareAndSet(false, true)) {
- if (QuickConversationsService.isQuicksy() || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
+ final String consent =
+ PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
+ .getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null);
+ final boolean requiresConsent =
+ (QuickConversationsService.isQuicksy()
+ || QuickConversationsService.isPlayStoreFlavor())
+ && !"agreed".equals(consent);
+ if (requiresConsent && "declined".equals(consent)) {
+ Log.d(Config.LOGTAG,"not asking for contacts permission because consent has been declined");
+ return;
+ }
+ if (requiresConsent
+ || shouldShowRequestPermissionRationale(
+ Manifest.permission.READ_CONTACTS)) {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
final AtomicBoolean requestPermission = new AtomicBoolean(false);
if (QuickConversationsService.isQuicksy()) {
builder.setTitle(R.string.quicksy_wants_your_consent);
- builder.setMessage(Html.fromHtml(getString(R.string.sync_with_contacts_quicksy_static)));
+ builder.setMessage(
+ Html.fromHtml(
+ getString(R.string.sync_with_contacts_quicksy_static)));
} else {
builder.setTitle(R.string.sync_with_contacts);
- builder.setMessage(getString(R.string.sync_with_contacts_long, getString(R.string.app_name)));
+ builder.setMessage(
+ getString(
+ R.string.sync_with_contacts_long,
+ getString(R.string.app_name)));
}
@StringRes int confirmButtonText;
- if (QuickConversationsService.isConversations()) {
- confirmButtonText = R.string.next;
- } else {
+ if (requiresConsent) {
confirmButtonText = R.string.agree_and_continue;
+ } else {
+ confirmButtonText = R.string.next;
}
- builder.setPositiveButton(confirmButtonText, (dialog, which) -> {
- if (requestPermission.compareAndSet(false, true)) {
- requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
- }
- });
- builder.setOnDismissListener(dialog -> {
- if (QuickConversationsService.isConversations() && requestPermission.compareAndSet(false, true)) {
- requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
- }
- });
- if (QuickConversationsService.isQuicksy()) {
- builder.setNegativeButton(R.string.decline, null);
+ builder.setPositiveButton(
+ confirmButtonText,
+ (dialog, which) -> {
+ if (requiresConsent) {
+ PreferenceManager.getDefaultSharedPreferences(
+ getApplicationContext())
+ .edit()
+ .putString(
+ PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed")
+ .apply();
+ }
+ if (requestPermission.compareAndSet(false, true)) {
+ requestPermissions(
+ new String[] {Manifest.permission.READ_CONTACTS},
+ REQUEST_SYNC_CONTACTS);
+ }
+ });
+ if (requiresConsent) {
+ builder.setNegativeButton(R.string.decline, (dialog, which) -> PreferenceManager.getDefaultSharedPreferences(
+ getApplicationContext())
+ .edit()
+ .putString(
+ PREF_KEY_CONTACT_INTEGRATION_CONSENT, "declined")
+ .apply());
+ } else {
+ builder.setOnDismissListener(
+ dialog -> {
+ if (requestPermission.compareAndSet(false, true)) {
+ requestPermissions(
+ new String[] {
+ Manifest.permission.READ_CONTACTS
+ },
+ REQUEST_SYNC_CONTACTS);
+ }
+ });
}
- builder.setCancelable(QuickConversationsService.isQuicksy());
+ builder.setCancelable(requiresConsent);
final AlertDialog dialog = builder.create();
- dialog.setCanceledOnTouchOutside(QuickConversationsService.isQuicksy());
- dialog.setOnShowListener(dialogInterface -> {
- final TextView tv = dialog.findViewById(android.R.id.message);
- if (tv != null) {
- tv.setMovementMethod(LinkMovementMethod.getInstance());
- }
- });
+ dialog.setCanceledOnTouchOutside(requiresConsent);
+ dialog.setOnShowListener(
+ dialogInterface -> {
+ final TextView tv = dialog.findViewById(android.R.id.message);
+ if (tv != null) {
+ tv.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ });
dialog.show();
} else {
- requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
+ requestPermissions(
+ new String[] {Manifest.permission.READ_CONTACTS},
+ REQUEST_SYNC_CONTACTS);
}
}
}
@@ -840,7 +890,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
@Override
protected void onBackendConnected() {
- if (QuickConversationsService.isFreeOrQuicksyFlavor()
+ if (QuickConversationsService.isContactListIntegration(this)
&& (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|| checkSelfPermission(Manifest.permission.READ_CONTACTS)
== PackageManager.PERMISSION_GRANTED)) {
@@ -10,6 +10,8 @@ import android.os.Build;
import android.provider.ContactsContract.Profile;
import android.provider.Settings;
+import com.google.common.base.Strings;
+
import eu.siacs.conversations.services.QuickConversationsService;
public class PhoneHelper {
@@ -20,27 +22,25 @@ public class PhoneHelper {
}
public static Uri getProfilePictureUri(final Context context) {
- if (!QuickConversationsService.isFreeOrQuicksyFlavor()
+ if (!QuickConversationsService.isContactListIntegration(context)
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED)) {
return null;
}
final String[] projection = new String[] {Profile._ID, Profile.PHOTO_URI};
- final Cursor cursor;
- try {
- cursor =
- context.getContentResolver()
- .query(Profile.CONTENT_URI, projection, null, null, null);
- } catch (Throwable e) {
- return null;
- }
- if (cursor == null) {
- return null;
+ try (final Cursor cursor =
+ context.getContentResolver()
+ .query(Profile.CONTENT_URI, projection, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ final var photoUri = cursor.getString(1);
+ if (Strings.isNullOrEmpty(photoUri)) {
+ return null;
+ }
+ return Uri.parse(photoUri);
+ }
}
- final String uri = cursor.moveToFirst() ? cursor.getString(1) : null;
- cursor.close();
- return uri == null ? null : Uri.parse(uri);
+ return null;
}
public static boolean isEmulator() {
@@ -518,8 +518,8 @@
<string name="no_storage_permission">Grant %1$s access to external storage</string>
<string name="no_camera_permission">Grant %1$s access to the camera</string>
<string name="quicksy_wants_your_consent">Quicksy asks for your consent to use your data</string>
- <string name="sync_with_contacts">Synchronize with contacts</string>
- <string name="sync_with_contacts_long">%1$s wants permission to access your address book to match it with your XMPP contact list.\nThis will display your contactsβ full names and avatars.\n\n%1$s will only read your address book and match it locally without uploading anything to your server.</string>
+ <string name="sync_with_contacts">Contact list integration</string>
+ <string name="sync_with_contacts_long">%1$s processes your contact list locally, on your device, to show you the names and profile pictures for matching contacts on XMPP.\n\nNo contact list data ever leaves your device!</string>
<string name="sync_with_contacts_quicksy_static" translatable="false"><![CDATA[Quicksy syncs your contact list in regular intervals to make suggestions about possible contacts, who are already using the app, even when the app is closed or not in use.<br><br>Find more information in our <a href="https://quicksy.im/privacy.htm">Privacy Policy</a>.]]></string>
<string name="notify_on_all_messages">Notify on all messages</string>
<string name="notify_only_when_highlighted">Notify only when mentioned</string>
@@ -1024,5 +1024,5 @@
<string name="report_spam">Report spam</string>
<string name="report_spam_and_block">Report spam and block spammer</string>
<string name="privacy_policy">Privacy policy</string>
- <string name="contact_list_integration_not_available">Address book integration is not available</string>
+ <string name="contact_list_integration_not_available">Contact list integration is not available</string>
</resources>