- PVSM.RU - https://www.pvsm.ru -
Добрый день, уважаемые читатели.
В данной статье я хочу поделиться своим опытом разработки под Android.
Требования к функционалу разрабатываемого продукта породили различные технические задачи, среди которых были как тривиальные, разжеванные во множестве блогов, так и крайне неоднозначные, с неочевидным решением. Я столкнулся с массой вещей, незнакомых мне, как .NET разработчику. Узнал о существовании инструментов, которые значительно упрощают жизнь. Думаю, что каждый начинающий андроидовец проходит похожий путь. Я мог бы сэкономить до трети времени, потраченного на разработку, поиск и эксперименты, имея такую статью.
Поэтому в данном посте я предлагаю вашему вниманию сборник рецептов и советов, которые помогут быстрей и правильней создать Ваше приложение.
Статья предназначена для начинающих, но и опытные разработчики смогут найти полезные моменты. Предполагается, что вы прочитали основы построения Android приложений, знаете о замечательном ресурсе StartAndroid [1] и умеете делать HelloWorld. При этом опыта создания полноценных приложений у вас нет, и вы только что занялись устранением этого недостатка. Для меня же это был первый проект под Android.
Мы с напарником давно подумывали создать какой-нибудь интересный продукт для Google Play. В один прекрасный день, при прочтении очередной СМС с рекламой такси, возникла идея создать приложение, которое будет бороться с СМС спамом. Это показалось нам интересным, имеющим практическое применение, относительно несложным в реализации для небольшой команды.
Далее был выработан набор конкретных требований и сформирован набор задач, которые надо решить. Самые интересные из них:
Опущены в статье будут следующие моменты:
HelloWorld, и шаблонный проект Android приложения мы создавать уже умеем. Теперь посмотрим, что нам может понадобиться еще. Зная о существовании этих инструментов, легко найти в интернете, как их применять. Ведь основная проблема — это не понять, как использовать инструмент, а узнать, что он вообще есть.
1) ActionBarSherlock [10] необходим для реализации платформонезависимого ActionBar — меню вверху экрана. Качаем с официального сайта и импортируем в Workspace в виде исходников. Просто библиотеки (jar файла) будет недостаточно, так как есть известная проблема с недогрузкой некоторых ресурсов библиотекой.
2) Импортируем в Workspace в виде исходников Google play services из SDK sdkextrasgooglegoogle_play_serviceslibprojectgoogle-play-services_lib. Это понадобится для биллинга и авторизации.
3) Положим в папку lib проекта библиотечки (перед этим найдем их в интернете)
* acra.jar — для реализации механизма отправки отчетов о краше приложения: ACRA [11].
* android-support-v4.jar — для реализации совместимости со старыми версиями Android.
* roboguice-2.0.jar, roboguice-sherlock-1.5.jar — для реализации Dependency Injection, если понравится реализация его в roboguice.
* ormlite-core.jar, ormlite-android.jar — популярная «легковесная» ORM для sqlite базы Android [12].
* joda-time.jar — библиотека для работы с датами.
* jdom.jar, gson.jar — для работы с JSON.
* checkout.jar — для биллинга (выбрал эту библиотечку, Checkout [13] как более удобную, чем работа непосредственно с api).
Ниже я приведу способ, работающий на Android с версией ниже 4.4 (KitKat), посколько в данной версии Google радикально поменял подход к обработке CMC. Описание работы с KitKat добавлю позже, когда это будет реализовано мной в приложении.
Для работы с CMC нам нужно разрешение в манифесте:
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.WRITE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
где
RECEIVE_SMS — разрешение приложению получать CMC.
READ_SMS — разрешение на чтение смс из памяти телефона. Казалось бы, оно нам не нужно, но без этого разрешения не работает запись.
WRITE_SMS — разрешение на запись CMC в память телефона.
Создадим прослушиватель события «принято CMC» SmsBroadcastReceiver. Он будет вызываться в момент получения телефоном СМС и запускать выполнение основных процессов по обработке СМС.
//BroadcastReceiver обязательный предок прослушивателей системных событий
public class SmsBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//Интент - входной объект с данными, передаваемый всем прослушивателям
Bundle bundle = intent.getExtras();
//Извлекаем из словаря некий pdus - в нем информация о СМС
Object[] pdus = (Object[]) bundle.get("pdus");
if (pdus.length == 0) {
return; // Что то пошло не так
}
// читаем CMC
Sms sms = SmsFromPdus(pdus, context);
// определям, спам ли
Boolean isClearFromSpam = SuperMegaMethodForResolving Spam(sms, context);
if (!isClearFromSpam) {
// если это спам - прекращаем обработку CMC системой
abortBroadcast();
return;
}
}
private Sms SmsFromPdus(Object[] pdus, Context context) {
Sms sms = new Sms();
for (int i = 0; i < pdus.length; i++) {
SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) pdus[i]);
sms.Text += smsMessage.getMessageBody(); //соберем весь текст (CMC может быть "многостраничной")
}
SmsMessage first = SmsMessage.createFromPdu((byte[]) pdus[0]);//из первой страницы получим
sms.SenderId = first.getOriginatingAddress(); //отправителя
Date receiveDate = new Date(first.getTimestampMillis()); //дату
sms.RecieveDate = receiveDate;
sms.Status = first.getStatus(); //статус (новое, прочтено, доставлено)
return sms;
}
}
public class Sms{
public String SenderId;
public String Text;
public Date RecieveDate;
public int Status;
}
Очень важно, чтобы onReceive отрабатывал менее чем за 10 секунд. Если метод захватывает управление на больший срок, исполнение прерывается и событие отдается другим обработчикам в порядке приоритета.
В моем случае в SuperMegaMethodForResolving происходит проверка на наличие СМС в списке контактов и в локальном списке отправителей, что занимает менее секунды. Затем управление отдается в выделенный поток, а onReceive вызывает abortBroadcast, что не дает другим обработчикам получить СМС (в том числе базовому приложению для СМС).
После нам необходимо подписать SmsBroadcastReceiver на событие приёма CMC. Для этого в блоке application манифеста объявим прослушиватель события android.provider.Telephony.SMS_RECEIVED с именем SmsBroadcastReceiver, который слушает системное событие SMS_RECEIVED и имеет приоритет 2147483631. Приоритет может быть до 2^31. При этом Google не рекомендует использовать значения больше 999. Но многие приложения их используют, а мы хотим, чтобы антиспам перехватывал CMC до того, как оно будет прочтено, например, приложением Contacts+. Это приложение запрашивает самый высокий из известных мне приоритетов.
<receiver
android:name="su.Jalapeno.AntiSpam.SystemService.SmsBroadcastReceiver"
android:enabled="true"
android:exported="true" >
<intent-filter android:priority="2147483631" >
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
Запуск приложения после включения устройства может понадобиться для, например, уведомления пользователя о существовании еще непроверенных подозрительных СМС посредством нотификации.
Для автозагрузки потребуется разрешение в манифесте:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
где
RECEIVE_BOOT_COMPLETED — разрешение слушать событие «загрузка»
Создадим прослушиватель события «загрузка» ServiceBroadcastReceiver.
public class ServiceBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//Сделаем что-нибудь
}
}
В нем мы будем выполнять необходимые нам действия после включения телефона.
Затем потребуется подписать ServiceBroadcastReceiver на событие загрузки телефона. Для этого в блоке application манифеста объявим прослушиватель события android.intent.action.BOOT_COMPLETED с именем SmsBroadcastReceiver.
<receiver
android:name="su.Jalapeno.AntiSpam.SystemService.ServiceBroadcastReceiver"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
Итак, у нас есть CMC, и надо связаться с сервером. Дополним манифест следующим образом:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
где
INTERNET — разрешение отправлять веб запрос.
ACCESS_NETWORK_STATE — разрешение на чтение статуса сети (подключен или нет 3g или wifi).
Осуществление запросов тривиально, для них используем базовый Android http client: org.apache.http.client.HttpClient.
Делаем все так, как описано здесь [12].
Для хранения настроек приложение мы не используем app.config, *.ini или БД приложения, поскольку Android предоставляет нам механизм SharedPreferences [14].
Это отняло у меня больше всего времени, из-за несистематизированности и неполноты информации, включая документацию Google. Итак, наша задача — получить от Google через приложение подписанный токен с информацией о пользователе и секретными сведениями о приложении. Это даст нам основание полагать, что токен сгенерирован не злоумышленником. Для решения этой задачи используем механизм CrossClientAuth [15].
Необходимо сделать следующее:
1) Получить сертификат приложения и подписать им приложение. Это просто осуществить в Eclipse с помощью мастера. Нажмем правой кнопкой мыши на проекте в Package Explorer -> Android tools -> Export signed application package. Мастер предложит создать новое хранилище сертификата, сгенерировать сертификат и поместить его в хранилище, защищенное указанным нами паролем. Не забудем сохранить хеш сертификата, поскольку он в дальнейшем понадобится.
2) Создать проект в консоли Google [16]. Затем открыть созданный проект и перейти на вкладку Api & auth -> Credentials в панели слева. Здесь необходимо создать пару Client Id для работы серверной части и Android клиента. Жмем Create new Client ID, нам нужен Client ID for Android application.
Заполняем, как указано на скриншоте, указав корректное имя пакета и отпечаток сертификата. После завершения создания получим табличку с информацией и сгенерированным для нас «CLIENT ID». Он понадобится нам на сервере.
Затем создаем новый Client Id типа Web application. В моем случае адреса можно указать произвольно, так как у нас не будет взаимодействия по http с веб ресурсами Google. В результате получим новый CLIENT ID, он уже понадобится на клиенте.
3) Клиентский код в полном виде можно найти в интернете, например GoogleAuthUtil [17]. Отмечу только ключевые моменты: как правильно составить Scope, и откуда взять для него Id
//выбор аккаунта
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_PICK_ACCOUNT) {
if (resultCode == RESULT_OK) {
Email = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
getUsername();
}
}
super.onActivityResult(requestCode, resultCode, data);
}
private void pickUserAccount() {
String[] accountTypes = new String[] { "com.google" };
Intent intent = AccountPicker.newChooseAccountIntent(null, null, accountTypes, false, null, null, null, null);
startActivityForResult(intent, REQUEST_CODE_PICK_ACCOUNT);
}
//Код получения токена (по хитрому обернутый Try catch и в фоновом потоке
//WEB_CLIENT_ID из Client ID for web application
final private String WEB_CLIENT_ID = "1999999-aaaaaaaaaaaaaaaaaaaaaaaaa.apps.googleusercontent.com";
//"Область" применения Client id. Тут указано что мы хотим аутентификацию
String SCOPE = String.format("audience:server:client_id:%s", WEB_CLIENT_ID);
//Вот и токен
String token = GoogleAuthUtil.getToken(_activity, Email, SCOPE);
Осталось передать токен серверу
4) Серверный код для проверки токена
Используем Microsoft.IdentityModel.Tokens.JWT Nuget [18]. Нижеприведенный код позволяет получить GoogleId юзера и его Email.
public string GetUserIdByJwt(string jwt, out string userEmail)
{
userEmail = string.Empty;
string userId = null;
//Секретный Client ID веб сервиса (Client ID for web application)
string audience = "111111111111111111-aaaaaaaaaaaaaaaaaaaaa.apps.googleusercontent.com";
//Секретный Client ID приложения (Client ID for Android application)
string azp = "1111111111111-aaaaaaaaaaaaaaaaaaaaaaaa.apps.googleusercontent.com";
var tokenHandler = new JWTSecurityTokenHandler();
SecurityToken securityToken = tokenHandler.ReadToken(jwt);
var jwtSecurityToken = securityToken as JWTSecurityToken;
userEmail = GetClaimValue(jwtSecurityToken, "email");
userId = GetClaimValue(jwtSecurityToken, "id");
var validationParameters =
new TokenValidationParameters()
{
AllowedAudience = audience,
ValidIssuer = "accounts.google.com",
ValidateExpiration = true,
//с либами для токенов некоторая неразбериха.
//По какой то причине не удалось заставить проверять подпись Google
//в токене средствами Microsoft.IdentityModel
ValidateSignature = false,
};
try
{
//Выкинет Exception, если токен не валидный
ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwtSecurityToken, validationParameters);
//Сверим, что наши Client Id совпадают с токеновскими
bool allGood = ValidateClaim(jwtSecurityToken, "azp", azp) && ValidateClaim(jwtSecurityToken, "aud", audience);
if (!allGood)
{
userId = null;
}
}
catch
{
userId = null;
}
return userId;
}
//Сверим значение в Claim с ожидаемым
private static bool ValidateClaim(JWTSecurityToken securityToken, string type, string value)
{
string claim = GetClaimValue(securityToken, type);
if (claim == null)
return false;
return claim == value;
}
//Получим значение из Claim (по сути KeyValuePair)
private static string GetClaimValue(JWTSecurityToken securityToken, string type)
{
var claim = securityToken.Claims.SingleOrDefault(x => x.Type == type);
if (claim == null)
return null;
return claim.Value;
}
Для начала необходимо в девелоперской консоли Google [19] у проекта приложения на вкладке КОНТЕНТ ДЛЯ ПРОДАЖИ создать нужные товары. Клиент будем писать на базе примеров Checkout [13]. Здесь приведу выдержки из своего кода, касающиеся биллинга, для более полного понимания библиотеки Checkout.
public class MyApplication extends Application {
private static final Products products = Products.create().add(IN_APP,
asList("Ид вашего товара из консоли гугл", "Ид вашего товара2 из консоли гугл"));
private final Billing billing = new Billing(this, new Billing.Configuration() {
@Nonnull
@Override
public String getPublicKey() {
String base64EncodedPublicKey = "ЛИЦЕНЗИОННЫЙ КЛЮЧ ДЛЯ ЭТОГО ПРИЛОЖЕНИЯ, который вы можете взять на вкладке СЛУЖБЫ И API консоли разработчика";
return base64EncodedPublicKey;
}
@Nullable
@Override
public Cache getCache() {
return Billing.newCache();
}
});
@Nonnull
private final Checkout checkout = Checkout.forApplication(billing, products);
@Nonnull
private static MyApplication instance;
public MyApplication() {
instance = this;
}
@Override
public void onCreate() {
super.onCreate();
billing.connect();
}
@Nonnull
public static MyApplication get() {
return instance;
}
@Nonnull
public Checkout getCheckout() {
return checkout;
}
}
public class BillingActivity extends RoboSherlockActivity {
private Sku _skuAccess;
@Nonnull
protected final ActivityCheckout checkout = Checkout.forActivity(this,
MyApplication.get().getCheckout());
@Nonnull
protected Inventory inventory;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_skuAccess = null;
_activity = this;
checkout.start();
checkout.createPurchaseFlow(new PurchaseListener());
inventory = checkout.loadInventory();
inventory.whenLoaded(new InventoryLoadedListener());
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
checkout.onActivityResult(requestCode, resultCode, data);
}
@Override
protected void onDestroy() {
checkout.stop();
checkout.destroyPurchaseFlow();
super.onDestroy();
}
@Nonnull
public ActivityCheckout getCheckout() {
return checkout;
}
public void Buy(View view) {
purchase(_skuAccess);
}
private void purchase(@Nonnull final Sku sku) {
boolean billingSupported = checkout.isBillingSupported(IN_APP);
if (!billingSupported) {
return;
}
checkout.whenReady(new Checkout.ListenerAdapter() {
@Override
public void onReady(@Nonnull BillingRequests requests) {
requests.purchase(sku, null, checkout.getPurchaseFlow());
}
});
}
private class PurchaseListener extends BaseRequestListener<Purchase> {
@Override
public void onSuccess(@Nonnull Purchase purchase) {
onPurchased();
}
private void onPurchased() {
//перегрузим инвентарь - и после загрузки инвентаря проверим покупку и обработаем это в приложении
inventory.load().whenLoaded(new InventoryLoadedListener());
}
@Override
public void onError(int response, @Nonnull Exception ex) {
// it is possible that our data is not synchronized with data on
// Google Play => need to handle some errors
if (response == ResponseCodes.ITEM_ALREADY_OWNED) {
onPurchased();
} else {
super.onError(response, ex);
}
}
}
private class InventoryLoadedListener implements Inventory.Listener {
private String _purchaseOrderId;
@Override
public void onLoaded(@Nonnull Inventory.Products products) {
final Inventory.Product product = products.get(IN_APP);
if (product.isSupported()) {
boolean isPurchased = InspectPurchases(product);
//Делаем что-нибудь
}
}
private boolean InspectPurchases(Product product) {
List<Sku> skus = product.getSkus();
Sku sku = skus.get(0); //допустим один товар
final Purchase purchase = product.getPurchaseInState(sku,
Purchase.State.PURCHASED);
boolean isPurchased = purchase != null
&& !TextUtils.isEmpty(purchase.token);
if (isPurchased) {
//Может уже куплено?
return true;
}
else {
//если нет - запомним товар для покупки
_skuAccess = sku;
return false;
}
}
}
private abstract class BaseRequestListener<Req> implements
RequestListener<Req> {
@Override
public void onError(int response, @Nonnull Exception ex) {
}
}
}
На этом пока все. Если Вам захочется подробней разобрать какой-либо аспект — напишите и я дополню статью. Надеюсь, пост поможет начинающим Android программистам наломать меньше граблей, не наступать на дрова и сэкономить время.
Автор: alexus_ru
Источник [20]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android/71389
Ссылки в тексте:
[1] StartAndroid: http://startandroid.ru/ru/uroki/vse-uroki-spiskom
[2] Подготовка проекта: #Prepare
[3] Получение и разбор CMC: #Sms
[4] Автозапуск приложения при загрузке телефона: #Autorun
[5] Осуществление веб запросов: #Web
[6] Хранение данных в локальной БД: #Sqlite
[7] Хранение настроек: #Preference
[8] Авторизация посредством токена Google: #Token
[9] Работа с механизмом покупок: #Billing
[10] ActionBarSherlock: http://actionbarsherlock.com/
[11] ACRA: https://github.com/ACRA/acra/wiki/BasicSetup
[12] ORM для sqlite базы Android: http://habrahabr.ru/post/143431/
[13] Checkout: http://habrahabr.ru/post/233265/
[14] SharedPreferences: http://startandroid.ru/ru/uroki/vse-uroki-spiskom/73-urok-33-hranenie-dannyh-preferences.html
[15] CrossClientAuth: https://developers.google.com/accounts/docs/CrossClientAuth
[16] консоли Google: https://console.developers.google.com/project
[17] GoogleAuthUtil: http://developer.android.com/reference/com/google/android/gms/auth/GoogleAuthUtil.html
[18] Nuget: https://www.nuget.org/packages/System.IdentityModel.Tokens.Jwt/
[19] консоли Google: https://play.google.com/apps/publish/
[20] Источник: http://habrahabr.ru/post/240045/
Нажмите здесь для печати.