It can be difficult to handle android archive dependency issues if you are working on a maven based android project due to gradle and maven’s default dependency type differences as “aar” amd “jar”. Let’s say there is an android archive(aar) library dependency which has a transitive “aar” dependency and you want to use this in your maven based android project. That’s fine, you have added your dependency in your pom like this :

<dependency>
  <groupId>com.theartofdev.edmodo</groupId>
  <artifactId>android-image-cropper</artifactId>
  <version>2.2.5</version>
</dependency>

But your packaging process is going to be failed. Firstly, you need to define your dependency type as “aar” as below.

<dependency>
  <groupId>com.theartofdev.edmodo</groupId>
  <artifactId>android-image-cropper</artifactId>
  <version>2.2.5</version>
  <type>aar</type>
</dependency>

But it’s not over, because above library has a transitive dependency called “appcompat-v7” which is also an “aar” library. Now we are in trouble, bacause we can not access to the pom file of “android-image-cropper” library and change it’s pom file. There is still something we can do in this situation. Simply exclude “appcompat-v7” dependency and include it manually by defining type as “aar.

<dependency>
  <groupId>com.theartofdev.edmodo</groupId>
  <artifactId>android-image-cropper</artifactId>
  <version>2.2.5</version>
  <type>aar</type>
    <exclusions>
      <exclusion>
        <groupId>com.android.support</groupId>
        <artifactId>appcompat-v7</artifactId>
      </exclusion>
    </exclusions>
</dependency>
<dependency>
  <groupId>com.android.support</groupId>
  <artifactId>appcompat-v7</artifactId>
  <version>23.2.1</version>
  <type>aar</type>
</dependency>

That’s all :)

If you use HockeyApp platform for tracking android native side crashes, you probably aware of NativeCrashManager which implemented by using apache network library and perfectly working until android sdk version 24. Google has removed apache network library related codes from it’s core android module and recommends HttpURLConnection usage as you can read details in the following page :

https://developer.android.com/about/versions/marshmallow/android-6.0-changes.html#behavior-apache-http-client

I have reimplemented the same logic using okhttp library and it’s tested on live with one of our tope games. Here it is :

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Date;
import java.util.UUID;

import android.net.Uri;
import com.squareup.okhttp.*;
import android.app.Activity;
import android.util.Log;

public class NativeCrashManager {

  private static final String DESCRIPTION_FILE_NAME = "description.txt";
  private static final String TAG = "HockeyApp";

  public static void handleDumpFiles(Activity activity, String identifier, String userId) {
    String[] filenames = searchForDumpFiles();
    for (String dumpFilename : filenames) {
      Log.d(TAG, "dumpFile " + dumpFilename);
      String logFilename = createLogFile();
      if (logFilename != null) {
        uploadDumpAndLog(activity, identifier, dumpFilename, logFilename, userId);
      }
    }
  }

  public static String createLogFile() {
    final Date now = new Date();

    try {
      // Create filename from a random uuid
      String filename = UUID.randomUUID().toString();
      String path = Constants.FILES_PATH + "/" + filename + ".faketrace";
      Log.d(TAG, "Writing unhandled exception to: " + path);

      // Write the stacktrace to disk
      BufferedWriter write = new BufferedWriter(new FileWriter(path));
      write.write("Package: " + Constants.APP_PACKAGE + "\n");
      write.write("Version: " + Constants.APP_VERSION + "\n");
      write.write("Android: " + Constants.ANDROID_VERSION + "\n");
      write.write("Manufacturer: " + Constants.PHONE_MANUFACTURER + "\n");
      write.write("Model: " + Constants.PHONE_MODEL + "\n");
      write.write("Date: " + now + "\n");
      write.write("\n");
      write.write("MinidumpContainer");
      write.flush();
      write.close();

      return filename + ".faketrace";
    }
    catch (Exception another) {
      Log.e(TAG, "File couldn't be write to path!!!", another);
    }

    return null;
  }

  public static void uploadDumpAndLog(final Activity activity, final String identifier, final String dumpFilename, final String logFilename) {
    new Thread() {
      @Override
      public void run() {
        try {

          OkHttpClient httpClient = new OkHttpClient();

          File dumpFile = new File(Constants.FILES_PATH, dumpFilename);
          File logFile = new File(Constants.FILES_PATH, logFilename);

          MultipartBuilder multipartBuilder = new MultipartBuilder()
                  .type(MultipartBuilder.FORM)
                  .addFormDataPart("attachment0", dumpFile.getName(), RequestBody.create(MediaType.parse("text/plain"), dumpFile))
                  .addFormDataPart("log", logFile.getName(), RequestBody.create(MediaType.parse("text/plain"), logFile))
                  ;

          RequestBody requestBody = multipartBuilder.build();
          Request request = new Request.Builder()
                  .url("https://rink.hockeyapp.net/api/2/apps/" + identifier + "/crashes/upload")
                  .post(requestBody)
                  .build();

          Response response = httpClient.newCall(request).execute();
          if (response.isSuccessful()) {
            Log.d(TAG,"result code : " + response.code());
            Log.d(TAG, "Upload response : " + response.message());
          } else {
            Log.d(TAG, "Failed to upload dumpFile caused by code : " + response.code());
          }

        }
        catch (Exception e) {
          Log.e(TAG, "Failed to upload dumpFile", e);
        }
        finally {
          activity.deleteFile(logFilename);
          activity.deleteFile(dumpFilename);
        }
      }

      private void fillCrashDescription(File descriptionFile) {
        FileWriter fileWriter = null;
        try {
          fileWriter = new FileWriter(descriptionFile);
          fileWriter.write(SessionLogManager.getSavedSessionData());
        } catch (IOException e) {
          Log.e(TAG, "description file error", e);
        } finally {
          if(fileWriter != null) {
            try {
              fileWriter.close();
            } catch (IOException e) {
              Log.e(TAG, "fileWriter couldn't be closed!", e);
            }
          }
        }
      }

    }.start();
  }

  private static String[] searchForDumpFiles() {
    if (Constants.FILES_PATH != null) {
      // Try to create the files folder if it doesn't exist
      File dir = new File(Constants.FILES_PATH + "/");
      boolean created = dir.mkdir();
      if (!created && !dir.exists()) {
        return new String[0];
      }

      // Filter for ".dmp" files
      FilenameFilter filter = new FilenameFilter() {
        public boolean accept(File dir, String name) {
          return name.endsWith(".dmp");
        }
      };
      return dir.list(filter);
    }
    else {
      Log.d(TAG, "Can't search for exception as file path is null.");
      return new String[0];
    }
  }
}

C++ language’s standard vector has a function called “find_if” provides search functionality depends on a boolean return typed function. Let’s say you have an integer vector and trying to find an even number :

#include <iostream>
#include <algorithm>
#include <vector>

bool IsEven (int i) {
	return ((i%2)==0);
}

int main () {
	std::vector<int> numberVector;

	numberVector.push_back(1);
	numberVector.push_back(3);
	numberVector.push_back(5);
	numberVector.push_back(6);
	numberVector.push_back(7);

	std::vector<int>::iterator it = std::find_if (numberVector.begin(), numberVector.end(), IsEven);
	printf("First even number : %d", *it);

	return 0;
}

But what if you want to pass an additional parameter to your search function and use lambda functions. Take a look to the following code snippet. We have a Popup object and a lambda function defines a search condition depending on name of the popup.

#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
#include "Popup.h"

int main () {

	std::vector<Popup*> popups;
	
	popups.push_back(new Popup("name1", "message1"));
	popups.push_back(new Popup("name2", "message2"));
	popups.push_back(new Popup("name3", "message3"));
	
	const char* popupName = "name2";

	std::vector<Popup*>::iterator it = std::find_if(popups.begin(), popups.end(), [popupName](Popup* popup)->bool {
		return strcmp(popup->getName(), popupName) == 0;
	});

	return 0;
}

It is so simple, lambda function’s scope holds the local constant “popupName”. Finally iterator “it” holds the result :)

Playing a custom sound for android push notifications is really simple with just one trick you have to know. If you are an impatient person who reads the last part of novels first, then move to the end of this post and get the trick :)

Notification Configuration

private Notification prepareNotification () {
    PendingIntent resultPendingIntent = PendingIntent.getActivity(context, 1, resultIntent, 0);
    Uri notificationSoundUri = prepareNotificationSoundUri(R.raw.yourSound);
    Notification.Builder builder =
                new Notification.Builder(context)
                        .setSmallIcon(R.drawable.icon)
                        .setContentTitle("Title")
                        .setContentText("Text")
                        .setContentIntent(resultPendingIntent)
                        .setDefaults(Notification.DEFAULT_ALL)
                        .setTicker("Ticker Text")
                        .setSound(notificationSoundUri);
    return builder.build();
}

private static Uri prepareNotificationSoundUri(int soundResourceId) {
    return Uri.parse("android.resource://" + context.getPackageName() + "/" + soundResourceId);
}

IF you support android sdk version lower than 16, then you should use NotificationCompat as follows :

private Notification prepareNotification () {
    PendingIntent resultPendingIntent = PendingIntent.getActivity(context, 1, resultIntent, 0);
    Uri notificationSoundUri = prepareNotificationSoundUri(R.raw.yourSound);
    NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
                    .setSmallIcon(R.drawable.icon)
                    .setContentTitle("Title")
                    .setContentText("Text")
                    .setContentIntent(resultPendingIntent)
                    .setDefaults(Notification.DEFAULT_ALL)
                    .setTicker("Ticker Text")
                    .setSound(notificationSoundUri);
    return builder.build();
}

private static Uri prepareNotificationSoundUri(int soundResourceId) {
    return Uri.parse("android.resource://" + context.getPackageName() + "/" + soundResourceId);
}

That’s all. You sent your push notification to your device but couldn’t hear the sound you expect. Now, the mighty trick you should apply is ignoring default sound on notification builder object as follows :

builder.setDefaults(~Notification.DEFAULT_SOUND);

Now you are ready to play your custom sound ;)

If you have used custom notification in your android project, you already aware of RemoteViews and content field of android notification. In this blog post, I have explained how to use custom notifications with OneSignal android SDK integration.

Requirements

Approximately a month ago OneSignal has started to support custom push notifications. You need to use version 2.4.0 or newer. You can use OneSignalSDK.jar directly in your project.

Action

You have prepared a layout xml file in your res/layout folder. Now you need to extend NotificationExtenderService and implement “onNotificationProcessing” method.

import android.support.v4.app.NotificationCompat;
import android.widget.RemoteViews;
import com.onesignal.NotificationExtenderService;
import com.onesignal.OSNotificationDisplayedResult;
import com.onesignal.OSNotificationPayload;

public class OneSignalNotificationExtender extends NotificationExtenderService {

    @Override
    protected boolean onNotificationProcessing(final OSNotificationPayload osNotificationPayload) {

        if(osNotificationPayload.additionalData != null && osNotificationPayload.additionalData.has(CUSTOM_LAYOUT)) {
            final OverrideSettings overrideSettings = new OverrideSettings();
            overrideSettings.extender = new NotificationCompat.Extender() {
                @Override
                public NotificationCompat.Builder extend(NotificationCompat.Builder builder) {
                    RemoteViews customNotification = new RemoteViews(getPackageName(),R.layout.custom_notification);
                    customNotification.setTextViewText(R.id.push_title, osNotificationPayload.title);
                    customNotification.setTextViewText(R.id.push_text, osNotificationPayload.message);
                    builder.setStyle(null);
                    return builder.setContent(customNotification);
                }
            };
            displayNotification(overrideSettings);
            return true;
        }
        return false;
    }
}

Most important part of this implementation is setting style of builder to “null”. In default implementation of OneSignal, style of notification builder is set as BigTextStyle. If your push notification is on most top of your notification bar, it’s expanded version is shown and your custom layout is hidden. By setting style to null, you replace the style and also say that you will use custom layout for your push notifications.

Returning true in onNotificationProcessing method means that you want to disable default one signal notification manager’s behaviour and you also prevent duplicate push messages. I have used “customLayout” named additional parameter for my push notifications sending from One Signal for differentiating normal and custom push notifications. If there is no “customLayout” parameter in push notification, returning false will do the trick and one signal default notification manager will show the default push notification.

One last thing to do is defining your extender service in manifest file.

<application
...
...
>
<service
	android:name="net.peakgames.Game.OneSignalNotificationExtender"
	android:exported="false">
	<intent-filter>
		<action android:name="com.onesignal.NotificationExtender" />
	</intent-filter>
</service>
...
...
</application>

Example layout

You can use the following layout file by replacing your own images.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
>

    <ImageView
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:id="@+id/pushBack"
            android:layout_gravity="center_horizontal"
            android:src="@drawable/push_back"
            android:baselineAlignBottom="false" android:scaleType="fitXY"
    />

    <ImageView
            android:id="@+id/pushIcon"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_margin="8dp"
            android:contentDescription="@string/app_name"
            android:src="@drawable/icon_notification"
    />

    <ImageView
            android:id="@+id/pushTiles"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:contentDescription="@string/app_name"
            android:src="@drawable/push_tas"
            android:layout_centerVertical="true"
            android:layout_alignParentRight="true"
            android:layout_alignParentEnd="true"/>

    <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_alignParentTop="true"
            android:layout_toRightOf="@+id/pushIcon"
            android:layout_toEndOf="@id/pushIcon"
            android:layout_toLeftOf="@+id/pushTiles"
            android:layout_toStartOf="@+id/pushTiles"
            android:gravity="center_vertical">
        <TextView
                android:id="@+id/push_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text=""
                android:textColor="#000000"
                android:textSize="17sp"
        />
        <TextView
                android:id="@+id/push_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text=""
                android:textColor="#414141"
                android:maxLines="2"
                android:minLines="1"
                android:textSize="14sp"

        />
    </LinearLayout>

</RelativeLayout>