make contact integration depend on manifest, not flavor

This commit is contained in:
Daniel Gultsch 2024-02-19 10:12:52 +01:00
parent 9b832e1285
commit 8e73b7f477
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
7 changed files with 138 additions and 57 deletions

View file

@ -62,8 +62,8 @@ public class JabberIdContact extends AbstractPhoneContact {
return jid; return jid;
} }
public static Map<Jid, JabberIdContact> load(Context context) { public static Map<Jid, JabberIdContact> load(final Context context) {
if (!QuickConversationsService.isFreeOrQuicksyFlavor() if (!QuickConversationsService.isContactListIntegration(context)
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& context.checkSelfPermission(Manifest.permission.READ_CONTACTS) && context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED)) { != PackageManager.PERMISSION_GRANTED)) {

View file

@ -1,14 +1,22 @@
package eu.siacs.conversations.services; package eu.siacs.conversations.services;
import android.Manifest;
import android.content.Context;
import android.content.Intent; 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 eu.siacs.conversations.BuildConfig;
import java.util.Arrays;
public abstract class AbstractQuickConversationsService { 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; protected final XmppConnectionService service;
@ -30,8 +38,31 @@ public abstract class AbstractQuickConversationsService {
return "playstore".equals(BuildConfig.FLAVOR_distribution); return "playstore".equals(BuildConfig.FLAVOR_distribution);
} }
public static boolean isFreeOrQuicksyFlavor() { public static boolean isContactListIntegration(final Context context) {
return "free".equals(BuildConfig.FLAVOR_distribution) || "quicksy".equals(BuildConfig.FLAVOR_mode); 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() { public static boolean isQuicksyPlayStore() {

View file

@ -1290,7 +1290,7 @@ public class XmppConnectionService extends Service {
restoreFromDatabase(); restoreFromDatabase();
if (QuickConversationsService.isFreeOrQuicksyFlavor() if (QuickConversationsService.isContactListIntegration(this)
&& (Build.VERSION.SDK_INT < Build.VERSION_CODES.M && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|| ContextCompat.checkSelfPermission( || ContextCompat.checkSelfPermission(
this, Manifest.permission.READ_CONTACTS) this, Manifest.permission.READ_CONTACTS)

View file

@ -120,13 +120,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
private void checkContactPermissionAndShowAddDialog() { private void checkContactPermissionAndShowAddDialog() {
if (hasContactsPermission()) { if (hasContactsPermission()) {
showAddToPhoneBookDialog(); 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); requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
} }
} }
private boolean hasContactsPermission() { 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; return checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
} else { } else {
return true; return true;
@ -525,7 +525,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
} }
private void onBadgeClick(final View view) { private void onBadgeClick(final View view) {
if (QuickConversationsService.isFreeOrQuicksyFlavor()) { if (QuickConversationsService.isContactListIntegration(this)) {
final Uri systemAccount = contact.getSystemAccount(); final Uri systemAccount = contact.getSystemAccount();
if (systemAccount == null) { if (systemAccount == null) {
checkContactPermissionAndShowAddDialog(); checkContactPermissionAndShowAddDialog();

View file

@ -6,12 +6,14 @@ import android.app.Dialog;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Editable; import android.text.Editable;
import android.text.Html; import android.text.Html;
import android.text.TextWatcher; 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 { 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"; public static final String EXTRA_INVITE_URI = "eu.siacs.conversations.invite_uri";
private final int REQUEST_SYNC_CONTACTS = 0x28cf; private final int REQUEST_SYNC_CONTACTS = 0x28cf;
@ -761,50 +765,96 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
} }
private void askForContactsPermissions() { private void askForContactsPermissions() {
if (QuickConversationsService.isFreeOrQuicksyFlavor() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (QuickConversationsService.isContactListIntegration(this)
if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
if (mRequestedContactsPermission.compareAndSet(false, true)) { 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 AlertDialog.Builder builder = new AlertDialog.Builder(this);
final AtomicBoolean requestPermission = new AtomicBoolean(false); final AtomicBoolean requestPermission = new AtomicBoolean(false);
if (QuickConversationsService.isQuicksy()) { if (QuickConversationsService.isQuicksy()) {
builder.setTitle(R.string.quicksy_wants_your_consent); 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 { } else {
builder.setTitle(R.string.sync_with_contacts); 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; @StringRes int confirmButtonText;
if (QuickConversationsService.isConversations()) { if (requiresConsent) {
confirmButtonText = R.string.next;
} else {
confirmButtonText = R.string.agree_and_continue; confirmButtonText = R.string.agree_and_continue;
} else {
confirmButtonText = R.string.next;
} }
builder.setPositiveButton(confirmButtonText, (dialog, which) -> { builder.setPositiveButton(
if (requestPermission.compareAndSet(false, true)) { confirmButtonText,
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); (dialog, which) -> {
} if (requiresConsent) {
}); PreferenceManager.getDefaultSharedPreferences(
builder.setOnDismissListener(dialog -> { getApplicationContext())
if (QuickConversationsService.isConversations() && requestPermission.compareAndSet(false, true)) { .edit()
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); .putString(
} PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed")
}); .apply();
if (QuickConversationsService.isQuicksy()) { }
builder.setNegativeButton(R.string.decline, null); 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(); final AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(QuickConversationsService.isQuicksy()); dialog.setCanceledOnTouchOutside(requiresConsent);
dialog.setOnShowListener(dialogInterface -> { dialog.setOnShowListener(
final TextView tv = dialog.findViewById(android.R.id.message); dialogInterface -> {
if (tv != null) { final TextView tv = dialog.findViewById(android.R.id.message);
tv.setMovementMethod(LinkMovementMethod.getInstance()); if (tv != null) {
} tv.setMovementMethod(LinkMovementMethod.getInstance());
}); }
});
dialog.show(); dialog.show();
} else { } 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 @Override
protected void onBackendConnected() { protected void onBackendConnected() {
if (QuickConversationsService.isFreeOrQuicksyFlavor() if (QuickConversationsService.isContactListIntegration(this)
&& (Build.VERSION.SDK_INT < Build.VERSION_CODES.M && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|| checkSelfPermission(Manifest.permission.READ_CONTACTS) || checkSelfPermission(Manifest.permission.READ_CONTACTS)
== PackageManager.PERMISSION_GRANTED)) { == PackageManager.PERMISSION_GRANTED)) {

View file

@ -10,6 +10,8 @@ import android.os.Build;
import android.provider.ContactsContract.Profile; import android.provider.ContactsContract.Profile;
import android.provider.Settings; import android.provider.Settings;
import com.google.common.base.Strings;
import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.QuickConversationsService;
public class PhoneHelper { public class PhoneHelper {
@ -20,27 +22,25 @@ public class PhoneHelper {
} }
public static Uri getProfilePictureUri(final Context context) { public static Uri getProfilePictureUri(final Context context) {
if (!QuickConversationsService.isFreeOrQuicksyFlavor() if (!QuickConversationsService.isContactListIntegration(context)
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& context.checkSelfPermission(Manifest.permission.READ_CONTACTS) && context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED)) { != PackageManager.PERMISSION_GRANTED)) {
return null; return null;
} }
final String[] projection = new String[] {Profile._ID, Profile.PHOTO_URI}; final String[] projection = new String[] {Profile._ID, Profile.PHOTO_URI};
final Cursor cursor; try (final Cursor cursor =
try { context.getContentResolver()
cursor = .query(Profile.CONTENT_URI, projection, null, null, null)) {
context.getContentResolver() if (cursor != null && cursor.moveToFirst()) {
.query(Profile.CONTENT_URI, projection, null, null, null); final var photoUri = cursor.getString(1);
} catch (Throwable e) { if (Strings.isNullOrEmpty(photoUri)) {
return null; return null;
}
return Uri.parse(photoUri);
}
} }
if (cursor == null) { return null;
return null;
}
final String uri = cursor.moveToFirst() ? cursor.getString(1) : null;
cursor.close();
return uri == null ? null : Uri.parse(uri);
} }
public static boolean isEmulator() { public static boolean isEmulator() {

View file

@ -518,8 +518,8 @@
<string name="no_storage_permission">Grant %1$s access to external storage</string> <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="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="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">Contact list integration</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_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="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_on_all_messages">Notify on all messages</string>
<string name="notify_only_when_highlighted">Notify only when mentioned</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">Report spam</string>
<string name="report_spam_and_block">Report spam and block spammer</string> <string name="report_spam_and_block">Report spam and block spammer</string>
<string name="privacy_policy">Privacy policy</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> </resources>