refactored phone contact loading in preperation for sync

This commit is contained in:
Daniel Gultsch 2018-10-27 00:32:09 +02:00
parent 4df0cc3657
commit a49a5790c7
13 changed files with 296 additions and 186 deletions

View file

@ -15,4 +15,8 @@ public class QuickConversationsService {
public static boolean isFull() { public static boolean isFull() {
return true; return true;
} }
public void considerSync() {
}
} }

View file

@ -0,0 +1,39 @@
package eu.siacs.conversations.android;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.text.TextUtils;
abstract class AbstractPhoneContact {
private final Uri lookupUri;
private final String displayName;
private final String photoUri;
AbstractPhoneContact(Cursor cursor) {
int phoneId = cursor.getInt(cursor.getColumnIndex(ContactsContract.Data._ID));
String lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY));
this.lookupUri = ContactsContract.Contacts.getLookupUri(phoneId, lookupKey);
this.displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
this.photoUri = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.PHOTO_URI));
}
public Uri getLookupUri() {
return lookupUri;
}
public String getDisplayName() {
return displayName;
}
public String getPhotoUri() {
return photoUri;
}
public int rating() {
return (TextUtils.isEmpty(displayName) ? 0 : 2) + (TextUtils.isEmpty(photoUri) ? 0 : 1);
}
}

View file

@ -0,0 +1,74 @@
package eu.siacs.conversations.android;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Build;
import android.provider.ContactsContract;
import android.util.Log;
import java.util.Collections;
import java.util.HashMap;
import eu.siacs.conversations.Config;
import rocks.xmpp.addr.Jid;
public class JabberIdContact extends AbstractPhoneContact {
private final Jid jid;
private JabberIdContact(Cursor cursor) throws IllegalArgumentException {
super(cursor);
try {
this.jid = Jid.of(cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)));
} catch (IllegalArgumentException | NullPointerException e) {
throw new IllegalArgumentException(e);
}
}
public Jid getJid() {
return jid;
}
public static void load(Context context, OnPhoneContactsLoaded<JabberIdContact> callback) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
callback.onPhoneContactsLoaded(Collections.emptyList());
return;
}
final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
ContactsContract.Data.DISPLAY_NAME,
ContactsContract.Data.PHOTO_URI,
ContactsContract.Data.LOOKUP_KEY,
ContactsContract.CommonDataKinds.Im.DATA};
final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\""
+ ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE
+ "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL
+ "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER
+ "\")";
final Cursor cursor;
try {
cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null, null);
} catch (Exception e) {
callback.onPhoneContactsLoaded(Collections.emptyList());
return;
}
final HashMap<Jid, JabberIdContact> contacts = new HashMap<>();
while (cursor != null && cursor.moveToNext()) {
try {
final JabberIdContact contact = new JabberIdContact(cursor);
final JabberIdContact preexisting = contacts.put(contact.getJid(), contact);
if (preexisting == null || preexisting.rating() < contact.rating()) {
contacts.put(contact.getJid(), contact);
}
} catch (IllegalArgumentException e) {
Log.d(Config.LOGTAG,"unable to create jabber id contact");
}
}
if (cursor != null) {
cursor.close();
}
callback.onPhoneContactsLoaded(contacts.values());
}
}

View file

@ -0,0 +1,8 @@
package eu.siacs.conversations.android;
import java.util.Collection;
public interface OnPhoneContactsLoaded<T extends AbstractPhoneContact> {
void onPhoneContactsLoaded(Collection<T> contacts);
}

View file

@ -48,7 +48,7 @@ public class Contact implements ListItem, Blockable {
private String commonName; private String commonName;
protected Jid jid; protected Jid jid;
private int subscription = 0; private int subscription = 0;
private String systemAccount; private Uri systemAccount;
private String photoUri; private String photoUri;
private final JSONObject keys; private final JSONObject keys;
private JSONArray groups = new JSONArray(); private JSONArray groups = new JSONArray();
@ -62,7 +62,7 @@ public class Contact implements ListItem, Blockable {
public Contact(final String account, final String systemName, final String serverName, public Contact(final String account, final String systemName, final String serverName,
final Jid jid, final int subscription, final String photoUri, final Jid jid, final int subscription, final String photoUri,
final String systemAccount, final String keys, final String avatar, final long lastseen, final Uri systemAccount, final String keys, final String avatar, final long lastseen,
final String presence, final String groups) { final String presence, final String groups) {
this.accountUuid = account; this.accountUuid = account;
this.systemName = systemName; this.systemName = systemName;
@ -105,13 +105,19 @@ public class Contact implements ListItem, Blockable {
// TODO: Borked DB... handle this somehow? // TODO: Borked DB... handle this somehow?
return null; return null;
} }
Uri systemAccount;
try {
systemAccount = Uri.parse(cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)));
} catch (Exception e) {
systemAccount = null;
}
return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)), return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)),
cursor.getString(cursor.getColumnIndex(SYSTEMNAME)), cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
cursor.getString(cursor.getColumnIndex(SERVERNAME)), cursor.getString(cursor.getColumnIndex(SERVERNAME)),
jid, jid,
cursor.getInt(cursor.getColumnIndex(OPTIONS)), cursor.getInt(cursor.getColumnIndex(OPTIONS)),
cursor.getString(cursor.getColumnIndex(PHOTOURI)), cursor.getString(cursor.getColumnIndex(PHOTOURI)),
cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)), systemAccount,
cursor.getString(cursor.getColumnIndex(KEYS)), cursor.getString(cursor.getColumnIndex(KEYS)),
cursor.getString(cursor.getColumnIndex(AVATAR)), cursor.getString(cursor.getColumnIndex(AVATAR)),
cursor.getLong(cursor.getColumnIndex(LAST_TIME)), cursor.getLong(cursor.getColumnIndex(LAST_TIME)),
@ -200,7 +206,7 @@ public class Contact implements ListItem, Blockable {
values.put(SERVERNAME, serverName); values.put(SERVERNAME, serverName);
values.put(JID, jid.toString()); values.put(JID, jid.toString());
values.put(OPTIONS, subscription); values.put(OPTIONS, subscription);
values.put(SYSTEMACCOUNT, systemAccount); values.put(SYSTEMACCOUNT, systemAccount != null ? systemAccount.toString() : null);
values.put(PHOTOURI, photoUri); values.put(PHOTOURI, photoUri);
values.put(KEYS, keys.toString()); values.put(KEYS, keys.toString());
values.put(AVATAR, avatar == null ? null : avatar.getFilename()); values.put(AVATAR, avatar == null ? null : avatar.getFilename());
@ -270,21 +276,11 @@ public class Contact implements ListItem, Blockable {
} }
public Uri getSystemAccount() { public Uri getSystemAccount() {
if (systemAccount == null) { return systemAccount;
return null;
} else {
String[] parts = systemAccount.split("#");
if (parts.length != 2) {
return null;
} else {
long id = Long.parseLong(parts[0]);
return ContactsContract.Contacts.getLookupUri(id, parts[1]);
}
}
} }
public void setSystemAccount(String account) { public void setSystemAccount(Uri lookupUri) {
this.systemAccount = account; this.systemAccount = lookupUri;
} }
private Collection<String> getGroups(final boolean unique) { private Collection<String> getGroups(final boolean unique) {
@ -343,7 +339,7 @@ public class Contact implements ListItem, Blockable {
} }
public boolean showInPhoneBook() { public boolean showInPhoneBook() {
return systemAccount != null && !systemAccount.trim().isEmpty(); return systemAccount != null;
} }
public void parseSubscriptionFromElement(Element item) { public void parseSubscriptionFromElement(Element item) {

View file

@ -25,7 +25,7 @@ import rocks.xmpp.addr.Jid;
public class ShortcutService { public class ShortcutService {
private final XmppConnectionService xmppConnectionService; private final XmppConnectionService xmppConnectionService;
private final ReplacingSerialSingleThreadExecutor replacingSerialSingleThreadExecutor = new ReplacingSerialSingleThreadExecutor(false); private final ReplacingSerialSingleThreadExecutor replacingSerialSingleThreadExecutor = new ReplacingSerialSingleThreadExecutor(ShortcutService.class.getSimpleName());
public ShortcutService(XmppConnectionService xmppConnectionService) { public ShortcutService(XmppConnectionService xmppConnectionService) {
this.xmppConnectionService = xmppConnectionService; this.xmppConnectionService = xmppConnectionService;

View file

@ -71,6 +71,7 @@ import java.util.concurrent.atomic.AtomicLong;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.android.JabberIdContact;
import eu.siacs.conversations.crypto.OmemoSetting; import eu.siacs.conversations.crypto.OmemoSetting;
import eu.siacs.conversations.crypto.PgpDecryptionService; import eu.siacs.conversations.crypto.PgpDecryptionService;
import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.crypto.PgpEngine;
@ -115,7 +116,6 @@ import eu.siacs.conversations.utils.ConversationsFileObserver;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.QuickLoader; import eu.siacs.conversations.utils.QuickLoader;
import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor; import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
@ -192,7 +192,7 @@ public class XmppConnectionService extends Service {
} }
}; };
public DatabaseBackend databaseBackend; public DatabaseBackend databaseBackend;
private ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor(true); private ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger");
private long mLastActivity = 0; private long mLastActivity = 0;
private FileBackend fileBackend = new FileBackend(this); private FileBackend fileBackend = new FileBackend(this);
private MemorizingTrustManager mMemorizingTrustManager; private MemorizingTrustManager mMemorizingTrustManager;
@ -1519,26 +1519,16 @@ public class XmppConnectionService extends Service {
} }
public void loadPhoneContacts() { public void loadPhoneContacts() {
mContactMergerExecutor.execute(() -> PhoneHelper.loadPhoneContacts(XmppConnectionService.this, new OnPhoneContactsLoadedListener() { mContactMergerExecutor.execute(() -> {
@Override JabberIdContact.load(this, contacts -> {
public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
Log.d(Config.LOGTAG, "start merging phone contacts with roster"); Log.d(Config.LOGTAG, "start merging phone contacts with roster");
for (Account account : accounts) { for (Account account : accounts) {
List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts(); List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts();
for (Bundle phoneContact : phoneContacts) { for (JabberIdContact jidContact : contacts) {
Jid jid; final Contact contact = account.getRoster().getContact(jidContact.getJid());
try { contact.setSystemAccount(jidContact.getLookupUri());
jid = Jid.of(phoneContact.getString("jid")); boolean needsCacheClean = contact.setPhotoUri(jidContact.getPhotoUri());
} catch (final IllegalArgumentException e) { needsCacheClean |= contact.setSystemName(jidContact.getDisplayName());
continue;
}
final Contact contact = account.getRoster().getContact(jid);
String systemAccount = phoneContact.getInt("phoneid")
+ "#"
+ phoneContact.getString("lookup");
contact.setSystemAccount(systemAccount);
boolean needsCacheClean = contact.setPhotoUri(phoneContact.getString("photouri"));
needsCacheClean |= contact.setSystemName(phoneContact.getString("displayname"));
if (needsCacheClean) { if (needsCacheClean) {
getAvatarService().clear(contact); getAvatarService().clear(contact);
} }
@ -1556,8 +1546,9 @@ public class XmppConnectionService extends Service {
Log.d(Config.LOGTAG, "finished merging phone contacts"); Log.d(Config.LOGTAG, "finished merging phone contacts");
mShortcutService.refresh(mInitialAddressbookSyncCompleted.compareAndSet(false, true)); mShortcutService.refresh(mInitialAddressbookSyncCompleted.compareAndSet(false, true));
updateRosterUi(); updateRosterUi();
} });
})); mQuickConversationsService.considerSync();
});
} }

View file

@ -24,55 +24,6 @@ public class PhoneHelper {
return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
} }
public static void loadPhoneContacts(Context context, final OnPhoneContactsLoadedListener listener) {
final List<Bundle> phoneContacts = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
listener.onPhoneContactsLoaded(phoneContacts);
return;
}
final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
ContactsContract.Data.DISPLAY_NAME,
ContactsContract.Data.PHOTO_URI,
ContactsContract.Data.LOOKUP_KEY,
ContactsContract.CommonDataKinds.Im.DATA};
final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\""
+ ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE
+ "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL
+ "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER
+ "\")";
CursorLoader mCursorLoader = new NotThrowCursorLoader(context,
ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null,
null);
mCursorLoader.registerListener(0, (arg0, c) -> {
if (c != null) {
while (c.moveToNext()) {
Bundle contact = new Bundle();
contact.putInt("phoneid", c.getInt(c.getColumnIndex(ContactsContract.Data._ID)));
contact.putString("displayname", c.getString(c.getColumnIndex(ContactsContract.Data.DISPLAY_NAME)));
contact.putString("photouri", c.getString(c.getColumnIndex(ContactsContract.Data.PHOTO_URI)));
contact.putString("lookup", c.getString(c.getColumnIndex(ContactsContract.Data.LOOKUP_KEY)));
contact.putString("jid", c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)));
phoneContacts.add(contact);
}
c.close();
}
if (listener != null) {
listener.onPhoneContactsLoaded(phoneContacts);
}
});
try {
mCursorLoader.startLoading();
} catch (RejectedExecutionException e) {
if (listener != null) {
listener.onPhoneContactsLoaded(phoneContacts);
}
}
}
public static Uri getProfilePictureUri(Context context) { public static Uri getProfilePictureUri(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
return null; return null;
@ -104,22 +55,4 @@ public class PhoneHelper {
return "unknown"; return "unknown";
} }
} }
private static class NotThrowCursorLoader extends CursorLoader {
private NotThrowCursorLoader(Context c, Uri u, String[] p, String s, String[] sa, String so) {
super(c, u, p, s, sa, so);
}
@Override
public Cursor loadInBackground() {
try {
return (super.loadInBackground());
} catch (Throwable e) {
return (null);
}
}
}
} }

View file

@ -3,17 +3,13 @@ package eu.siacs.conversations.utils;
public class ReplacingSerialSingleThreadExecutor extends SerialSingleThreadExecutor { public class ReplacingSerialSingleThreadExecutor extends SerialSingleThreadExecutor {
public ReplacingSerialSingleThreadExecutor(String name) { public ReplacingSerialSingleThreadExecutor(String name) {
super(name, false); super(name);
}
public ReplacingSerialSingleThreadExecutor(boolean prepareLooper) {
super(ReplacingSerialSingleThreadExecutor.class.getName(), prepareLooper);
} }
@Override @Override
public synchronized void execute(final Runnable r) { public synchronized void execute(final Runnable r) {
tasks.clear(); tasks.clear();
if (active != null && active instanceof Cancellable) { if (active instanceof Cancellable) {
((Cancellable) active).cancel(); ((Cancellable) active).cancel();
} }
super.execute(r); super.execute(r);
@ -21,7 +17,7 @@ public class ReplacingSerialSingleThreadExecutor extends SerialSingleThreadExecu
public synchronized void cancelRunningTasks() { public synchronized void cancelRunningTasks() {
tasks.clear(); tasks.clear();
if (active != null && active instanceof Cancellable) { if (active instanceof Cancellable) {
((Cancellable) active).cancel(); ((Cancellable) active).cancel();
} }
} }

View file

@ -42,7 +42,7 @@ public class ReplacingTaskManager {
synchronized (this.executors) { synchronized (this.executors) {
executor = this.executors.get(account); executor = this.executors.get(account);
if (executor == null) { if (executor == null) {
executor = new ReplacingSerialSingleThreadExecutor(false); executor = new ReplacingSerialSingleThreadExecutor(ReplacingTaskManager.class.getSimpleName());
this.executors.put(account, executor); this.executors.put(account, executor);
} }
executor.execute(runnable); executor.execute(runnable);

View file

@ -1,31 +1,22 @@
package eu.siacs.conversations.utils; package eu.siacs.conversations.utils;
import android.os.Looper;
import android.util.Log; import android.util.Log;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.services.AttachFileToConversationRunnable;
public class SerialSingleThreadExecutor implements Executor { public class SerialSingleThreadExecutor implements Executor {
private final Executor executor = Executors.newSingleThreadExecutor();
final ArrayDeque<Runnable> tasks = new ArrayDeque<>(); final ArrayDeque<Runnable> tasks = new ArrayDeque<>();
protected Runnable active; private final Executor executor = Executors.newSingleThreadExecutor();
private final String name; private final String name;
protected Runnable active;
public SerialSingleThreadExecutor(String name) { public SerialSingleThreadExecutor(String name) {
this(name, false);
}
SerialSingleThreadExecutor(String name, boolean prepareLooper) {
if (prepareLooper) {
execute(Looper::prepare);
}
this.name = name; this.name = name;
} }

View file

@ -0,0 +1,69 @@
package eu.siacs.conversations.android;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Build;
import android.provider.ContactsContract;
import android.util.Log;
import java.util.Collections;
import java.util.HashMap;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
import io.michaelrocks.libphonenumber.android.NumberParseException;
public class PhoneNumberContact extends AbstractPhoneContact {
private String phoneNumber;
public String getPhoneNumber() {
return phoneNumber;
}
private PhoneNumberContact(Context context, Cursor cursor) throws IllegalArgumentException {
super(cursor);
try {
this.phoneNumber = PhoneNumberUtilWrapper.normalize(context,cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)));
} catch (NumberParseException | NullPointerException e) {
throw new IllegalArgumentException(e);
}
}
public static void load(Context context, OnPhoneContactsLoaded<PhoneNumberContact> callback) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
callback.onPhoneContactsLoaded(Collections.emptyList());
return;
}
final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
ContactsContract.Data.DISPLAY_NAME,
ContactsContract.Data.PHOTO_URI,
ContactsContract.Data.LOOKUP_KEY,
ContactsContract.CommonDataKinds.Phone.NUMBER};
final Cursor cursor;
try {
cursor = context.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, PROJECTION, null, null, null);
} catch (Exception e) {
callback.onPhoneContactsLoaded(Collections.emptyList());
return;
}
final HashMap<String, PhoneNumberContact> contacts = new HashMap<>();
while (cursor != null && cursor.moveToNext()) {
try {
final PhoneNumberContact contact = new PhoneNumberContact(context, cursor);
final PhoneNumberContact preexisting = contacts.get(contact.getPhoneNumber());
if (preexisting == null || preexisting.rating() < contact.rating()) {
contacts.put(contact.getPhoneNumber(), contact);
}
} catch (IllegalArgumentException e) {
Log.d(Config.LOGTAG, "unable to create phone contact");
}
}
if (cursor != null) {
cursor.close();
}
callback.onPhoneContactsLoaded(contacts.values());
}
}

View file

@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLHandshakeException;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.android.PhoneNumberContact;
import eu.siacs.conversations.crypto.sasl.Plain; import eu.siacs.conversations.crypto.sasl.Plain;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.AccountUtils;
@ -264,6 +265,14 @@ public class QuickConversationsService {
return false; return false;
} }
public void considerSync() {
PhoneNumberContact.load(service, contacts -> {
for(PhoneNumberContact c : contacts) {
Log.d(Config.LOGTAG, "Display Name=" + c.getDisplayName() + ", number=" + c.getPhoneNumber()+", uri="+c.getLookupUri());
}
});
}
public interface OnVerificationRequested { public interface OnVerificationRequested {
void onVerificationRequestFailed(int code); void onVerificationRequestFailed(int code);