📜 ⬆️ ⬇️

Tips and recipes for novice Android programmers

Good afternoon, dear habrayuzer.

In this article, I want to share my development experience for Android.
Requirements for the functionality of the product being developed gave rise to various technical tasks, among which were both trivial, chewed in a variety of blogs, and extremely ambiguous, with an unobvious decision. I ran into a lot of things unfamiliar to me as a .NET developer. Learned about the existence of tools that greatly simplify life. I think that every novice Androider goes a similar way. I could save up to a third of the time spent on developing, searching, and experimenting with such an article.

Therefore, in this post, I offer to your attention a collection of recipes and tips that will help you quickly and correctly create your application.

The article is intended for beginners, but experienced developers will be able to find useful points. It is assumed that you have read the basics of building Android applications, know about the wonderful StartAndroid resource and know how to do HelloWorld. At the same time, you do not have the experience of creating full-fledged applications, and you have just begun to address this drawback. For me, this was the first project for Android.
')

Start


My partner and I have long thought to create some interesting product for Google Play. One day, while reading another SMS with a taxi advertisement, the idea arose to create an application that would fight SMS spam. It seemed to us interesting, having practical application, relatively easy to implement for a small team.

Further, a set of specific requirements was developed and a set of tasks was formed that should be solved. The most interesting of them are:

The following points will be omitted in the article:

Project preparation


HelloWorld, and the template project for the Android application, we can already create. Now let's see what else we might need. Knowing the existence of these tools, it is easy to find on the Internet how to use them. After all, the main problem is not to understand how to use the tool, but to find out what it is.

1) ActionBarSherlock is necessary for the implementation of platform-independent ActionBar - the menu at the top of the screen. We download from the official site and import into Workspace as source. Just a library (jar file) will not be enough, as there is a known problem with the underloading of some resources by the library.

2) Import to the Workspace as source Google play services from the sdk \ extras \ google \ google_play_services \ libproject \ google-play-services_lib \ SDK. This is needed for billing and authorization.

3) Put the libraries in the lib folder of the project (before that we will find them on the Internet)
* acra.jar - for the implementation of the mechanism for sending reports about application crash : ACRA .
* android-support-v4.jar - to implement compatibility with older versions of Android.
* roboguice-2.0.jar, roboguice-sherlock-1.5.jar - for implementing Dependency Injection, if you like its implementation in roboguice.
* ormlite-core.jar, ormlite-android.jar is the popular lightweight ORM for the sqlite Android database .
* joda-time.jar - library for working with dates.
* jdom.jar, gson.jar - for working with JSON.
* checkout.jar - for billing (I chose this library, Checkout as more convenient than working directly with api).

Receiving and parsing CMC


Below I will provide a way that works on Android with a version lower than 4.4 (KitKat), because in this version of Google radically changed the approach to processing CMC. The description of working with KitKat will be added later when it will be implemented by me in the application.

To work with CMC, we need permission in the manifest:

<uses-permission android:name="android.permission.RECEIVE_SMS" /> <uses-permission android:name="android.permission.WRITE_SMS" /> <uses-permission android:name="android.permission.READ_SMS" /> 

Where
RECEIVE_SMS — Allow the application to receive the CMC.
READ_SMS - permission to read SMS from the phone’s memory. It would seem that we do not need it, but without this permission the recording does not work.
WRITE_SMS - permission to write CMC to the phone memory.

Create an event listener "accepted CMC" SmsBroadcastReceiver. It will be called when the phone receives an SMS and starts the execution of basic processes for processing SMS.

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; } 


It is very important that onReceive worked in less than 10 seconds. If the method takes control for a longer period, execution is interrupted and the event is given to other handlers in order of priority.
In my case, SuperMegaMethodForResolving checks for the presence of SMS in the contact list and in the local list of senders, which takes less than a second. Then control is given to the selected stream, and onReceive calls abortBroadcast, which prevents other handlers from receiving SMS (including the basic application for SMS).

After we need to subscribe SmsBroadcastReceiver to the CMC reception event. To do this, in the application manifest block, we will declare an event listener android.provider.Telephony.SMS_RECEIVED with the name SmsBroadcastReceiver, which listens for the SMS_RECEIVED system event and has priority 2147483631. Priority can be up to 2 ^ 31. At the same time, Google does not recommend using values ​​greater than 999. But many applications use them, and we want antispam to intercept the CMC before it is read, for example, by the Contacts + application. This application requests the highest priority I know.

 <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> 

Autostart application when the phone boots


Starting the application after turning on the device may be necessary for, for example, notifying the user of the existence of still untested suspicious SMS through notification.

Autoloading will require permission in the manifest:

 <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> 

Where
RECEIVE_BOOT_COMPLETED - permission to listen to the event "download"

Create a listener for the “boot” event of the ServiceBroadcastReceiver.

 public class ServiceBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // - } } 

In it, we will perform the actions we need after turning on the phone.

Then you need to subscribe the ServiceBroadcastReceiver to the phone download event. To do this, in the application manifest block, we will declare an event listener android.intent.action.BOOT_COMPLETED with the name 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> 

Making web requests


So, we have a CMC, and we need to contact the server. Let's add the manifesto as follows:

 <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> 

Where
INTERNET - permission to send a web request.
ACCESS_NETWORK_STATE - permission to read the network status (connected or not 3g or wifi).

Execution of requests is trivial, for them we use the basic Android http client: org.apache.http.client.HttpClient.

Data storage in a local database


We do everything as described here .

Storage settings


We do not use app.config, * .ini or the application database to store the application settings, since Android provides us with the SharedPreferences mechanism.

Google Token Authorization


It took me the most time because of the lack of systematization and incomplete information, including Google documentation. So, our task is to get a signed token from Google through the application with information about the user and secret information about the application. This will give us reason to believe that the token was not generated by an attacker. To solve this problem, we use the CrossClientAuth mechanism.

You must do the following:
1) Get the certificate of the application and sign the application for them. This is easy to implement in Eclipse using the wizard. Right-click on the project in the Package Explorer -> Android tools -> Export signed application package. The wizard prompts you to create a new certificate store, generate a certificate, and place it in the store, protected by the password specified by us. Do not forget to save the certificate hash, since it will be needed later.

2) Create a project in the Google console . Then open the created project and go to the Api & auth -> Credentials tab in the left pane. Here you need to create a pair of Client Id for the server side and Android client. Click Create new Client ID, we need Client ID for Android application.



Fill in as indicated on the screenshot, indicating the correct package name and certificate thumbprint. After the creation is completed, we will receive a label with the information and the generated for us “CLIENT ID”. He will need us on the server.

Then we create a new Client Id of type Web application. In my case, the addresses can be specified arbitrarily, since we will not have http interaction with Google’s web resources. As a result, we will get a new CLIENT ID, it will already be needed on the client.

3) The client code in its full form can be found on the Internet, for example GoogleAuthUtil . I will note only the key points: how to make Scope correctly, and where to get for it Id

Code
 //  @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); 


It remains to transfer the token to the server.

4) Server code to verify the token
Use Microsoft.IdentityModel.Tokens.JWT Nuget . The code below allows you to get GoogleId user and his Email.

Code
 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; } 


Work with the shopping mechanism


First you need in the developer console Google at the application project on the CONTENT FOR SALE tab to create the necessary goods. We will write the client based on Checkout examples. Here I will give excerpts from my code regarding billing, for a more complete understanding of the Checkout library.

Application Class Changes
 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; } } 


Activate with purchase
 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) { } } } 


Conclusion


That's all for now. If you want to analyze any aspect in more detail - write and I will add the article. I hope the post will help novice Android programmers to break less rakes, do not step on the wood and save time. You can try out the finished application here .

Source: https://habr.com/ru/post/240045/


All Articles