Статьи

Перехватить входящие SMS на Android

На прошлой неделе я говорил об использовании SMS для активации вашего приложения, что является довольно мощным способом проверки учетной записи пользователя. Я оставил пару вещей, хотя. Одной из таких вещей является возможность автоматически получать входящие SMS. Это возможно только на Android, но это довольно круто для пользователей, так как избавляет от необходимости набирать текст активации.

Приемник вещания

Для того, чтобы получить входящее SMS, нам нужен широковещательный приемник, который представляет собой отдельный класс Android, который получает определенный тип события. Это часто сбивает с толку разработчиков, которые иногда получают класс impl из широковещательного приемника … Это ошибка …

Хитрость в том, что вы можете просто поместить любой собственный класс Android в каталог native/android . Он будет скомпилирован с остальной частью нативного кода и «просто работает». Поэтому я поместил этот класс в native/android/com/codename1/sms/intercept :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.codename1.sms.intercept;
 
import android.content.*;
import android.os.Bundle;
import android.telephony.*;
import com.codename1.io.Log;
 
public class SMSListener extends BroadcastReceiver {
 
    @Override
    public void onReceive(Context cntxt, Intent intent) {
        if(intent.getAction().equals("android.provider.Telephony.SMS_RECEIVED")) {
            Bundle bundle = intent.getExtras();
            SmsMessage[] msgs = null;
            if (bundle != null){
                try{
                    Object[] pdus = (Object[]) bundle.get("pdus");
                    msgs = new SmsMessage[pdus.length];
                    for(int i=0; i<msgs.length; i++){
                        msgs[i] = SmsMessage.createFromPdu((byte[])pdus[i]);
                        String msgBody = msgs[i].getMessageBody();
                        SMSCallback.smsReceived(msgBody);
                    }
                } catch(Exception e) {
                    Log.e(e);
                    SMSCallback.smsReceiveError(e);
                }
            }
        }
    }
}

Приведенный выше код является довольно стандартным нативным кодом Android, это просто обратный вызов, в котором большая часть логики похожа на нативный код Android, упомянутый в этом вопросе stackoverflow .

Но есть еще кое-что, что нам нужно сделать. Чтобы реализовать это изначально, нам нужно зарегистрировать разрешение и получателя в файле manifest.xml как описано в этом вопросе. Вот как выглядел их родной манифест:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="utf-8"?>
    package="com.bulsy.smstalk1">
    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    <uses-permission android:name="android.permission.READ_SMS" />
    <uses-permission android:name="android.permission.SEND_SMS"/>
    <uses-permission android:name="android.permission.READ_CONTACTS" />
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <receiver android:name="com.bulsy.smstalk1.SmsListener"
               android:enabled="true"
               android:permission="android.permission.BROADCAST_SMS"
               android:exported="true">
            <intent-filter android:priority="2147483647">//this doesnt work
                <category android:name="android.intent.category.DEFAULT" />
                <action android:name="android.provider.Telephony.SMS_RECEIVED" />
            </intent-filter>
        </receiver>
    </application>
</manifest>

Нам нужны только разрешение на вещание XML и разрешение XML. Оба выполнимы через подсказки сборки. Первый довольно прост:

1
android.xpermissions=<uses-permission android:name="android.permission.RECEIVE_SMS" />

Последнее не намного сложнее, обратите внимание, я взял несколько строк и сделал их одной строкой для удобства:

1
android.xapplication=<receiver android:name="com.codename1.sms.intercept.SMSListener"  android:enabled="true" android:permission="android.permission.BROADCAST_SMS"  android:exported="true">                    <intent-filter android:priority="2147483647"><category android:name="android.intent.category.DEFAULT" />        <action android:name="android.provider.Telephony.SMS_RECEIVED" />                 </intent-filter>             </receiver>

Вот это красиво отформатировано:

1
2
3
4
5
6
7
8
9
<receiver android:name="com.codename1.sms.intercept.SMSListener"
              android:enabled="true"
              android:permission="android.permission.BROADCAST_SMS"
              android:exported="true">
                   <intent-filter android:priority="2147483647">
                          <category android:name="android.intent.category.DEFAULT" />
                          <action android:name="android.provider.Telephony.SMS_RECEIVED" />
                   </intent-filter>
</receiver>

Прослушивание и разрешения

Вы заметите, что они не включают в себя фактическую привязку или запросы на разрешение, которые вы ожидаете для чего-то подобного. Для этого нам нужен собственный интерфейс.

Нативный пример переполнения стека ограничил слушателя в действии, но здесь мы хотим, чтобы код приложения решал, когда нам следует связать прослушивание:

1
2
3
4
public interface NativeSMSInterceptor extends NativeInterface {
    public void bindSMSListener();
    public void unbindSMSListener();
}

Это легко!

Обратите внимание, что isSupported() возвращает false для всех других ОС, поэтому нам не нужно спрашивать, является ли это «Android», мы можем просто использовать isSupported() .

Реализация тоже довольно проста:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.codename1.sms.intercept;
 
import android.Manifest;
import android.content.IntentFilter;
import com.codename1.impl.android.AndroidNativeUtil;
 
public class NativeSMSInterceptorImpl {
    private SMSListener smsListener;
    public void bindSMSListener() {
        if(AndroidNativeUtil.checkForPermission(Manifest.permission.RECEIVE_SMS, "We can automatically enter the SMS code for you")) { (1)
            smsListener = new SMSListener();
            IntentFilter filter = new IntentFilter();
            filter.addAction("android.provider.Telephony.SMS_RECEIVED");
            AndroidNativeUtil.getActivity().registerReceiver(smsListener, filter); (2)
        }
    }
 
    public void unbindSMSListener() {
        AndroidNativeUtil.getActivity().unregisterReceiver(smsListener);
    }
 
    public boolean isSupported() {
        return true;
    }
}
1 Это вызовет запрос разрешения на Android 6 и новее. Даже если разрешение объявлено в XML, этого недостаточно для 6+. Обратите внимание, что даже когда вы работаете на Android 6, вам все равно нужно объявлять разрешения в XML!
2 Здесь мы на самом деле привязываем слушателя, это позволяет нам получать одно SMS, а не прослушивать каждое SMS, приходящее через

Callbacks

До сих пор код не очень удобен в использовании, поэтому давайте немного абстрагироваться. Но сначала нам нужно реализовать класс обратного вызова, в который отправляются SMS и сообщения об ошибках из кода выше:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.codename1.sms.intercept; (1)
 
import com.codename1.util.FailureCallback;
import com.codename1.util.SuccessCallback;
import static com.codename1.ui.CN.*;
 
/**
 * This is an internal class, it's package protect to hide that
 */
class SMSCallback {
    static SuccessCallback<String> onSuccess;
    static FailureCallback onFail;
 
    public static void smsReceived(String sms) {
        if(onSuccess != null) {
            SuccessCallback<String> s = onSuccess;
            onSuccess = null;
            onFail = null;
            SMSInterceptor.unbindListener();
            callSerially(() -> s.onSucess(sms)); (2)
        }
    }
 
    public static void smsReceiveError(Exception err) {
        if(onFail != null) {
            FailureCallback f = onFail;
            onFail = null;
            SMSInterceptor.unbindListener();
            onSuccess = null;
            callSerially(() -> f.onError(null, err, 1, err.toString()));
        } else {
            if(onSuccess != null) {
                SMSInterceptor.unbindListener();
                onSuccess = null;
            }
        }
    }
}
1 Обратите внимание, что пакет совпадает с собственным кодом и другими классами. Это позволяет классу обратного вызова быть защищенным пакетом, поэтому он не предоставляется через API (класс не имеет модификатора public)
2 Мы упаковываем обратный вызов в вызове последовательно, чтобы соответствовать соглашению Codename One об использовании EDT по умолчанию. Вызов, вероятно, поступит в собственный поток Android, поэтому имеет смысл его нормализовать и не подвергать собственный поток Android пользовательскому коду

Простой API

Последняя часть головоломки — простой API, который может обернуть все это, а также скрыть тот факт, что это специфично для Android. Мы рассмотрим полный API в последней части, но пока это API уровня пользователя, который скрывает собственный интерфейс. Использование такого класса, как правило, является хорошей практикой, поскольку оно позволяет нам гибко работать с реальным базовым интерфейсом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.codename1.sms.intercept;
 
import com.codename1.system.NativeLookup;
import com.codename1.util.FailureCallback;
import com.codename1.util.SuccessCallback;
 
/**
 * This is a high level abstraction of the native classes and callbacks rolled into one.
 */
public class SMSInterceptor {
    private static NativeSMSInterceptor nativeImpl;
 
    private static NativeSMSInterceptor get() {
        if(nativeImpl == null) {
            nativeImpl = NativeLookup.create(NativeSMSInterceptor.class);
            if(!nativeImpl.isSupported()) {
                nativeImpl = null;
            }
        }
        return nativeImpl;
    }
 
    public static boolean isSupported() {
        return get() != null;
    }
 
    public static void grabNextSMS(SuccessCallback<String> onSuccess) {
        SMSCallback.onSuccess = onSuccess;
        get().bindSMSListener();
    }
 
    static void unbindListener() {
        get().unbindSMSListener();
    }
}

В следующий раз

В следующий раз я заверну все это с пользовательским интерфейсом и упакую все в простой в использовании cn1lib.

Некоторые из вещей, которые я затронул здесь, могут быть немного «волосатыми» с точки зрения использования нативного интерфейса, поэтому, если что-то не понятно, просто спросите в комментариях.

Смотреть оригинальную статью здесь: СОВЕТ: Перехватывать входящие SMS на Android

Мнения, высказанные участниками Java Code Geeks, являются их собственными.