how to get referrer using google track in android? - android

I want to implement install referrer track and want referrer parameter and store in back end database i have seen many exmple or question as on like Get Android Google Analytics referrer tag or Android Google Analytics Campaign tracking not appearing but not get a way i have generated links to and try the code
package SimpleDemo.ReferralTrack;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
public class ReferralReceiver extends BroadcastReceiver
{
#Override
public void onReceive(Context context, Intent intent)
{
// Workaround for Android security issue: http://code.google.com/p/android/issues/detail?id=16006
try
{
final Bundle extras = intent.getExtras();
if (extras != null) {
extras.containsKey(null);
}
}
catch (final Exception e) {
return;
}
Map<String, String> referralParams = new HashMap<String, String>();
// Return if this is not the right intent.
if (! intent.getAction().equals("com.android.vending.INSTALL_REFERRER")) { //$NON-NLS-1$
return;
}
String referrer = intent.getStringExtra("referrer"); //$NON-NLS-1$
if( referrer == null || referrer.length() == 0) {
return;
}
try
{ // Remove any url encoding
referrer = URLDecoder.decode(referrer, "x-www-form-urlencoded"); //$NON-NLS-1$
}
catch (UnsupportedEncodingException e) { return; }
// Parse the query string, extracting the relevant data
String[] params = referrer.split("&"); // $NON-NLS-1$
for (String param : params)
{
String[] pair = param.split("="); // $NON-NLS-1$
referralParams.put(pair[0], pair[1]);
}
ReferralReceiver.storeReferralParams(context, referralParams);
}
private final static String[] EXPECTED_PARAMETERS = {
"utm_source",
"utm_medium",
"utm_term",
"utm_content",
"utm_campaign"
};
private final static String PREFS_FILE_NAME = "ReferralParamsFile";
/*
* Stores the referral parameters in the app's sharedPreferences.
* Rewrite this function and retrieveReferralParams() if a
* different storage mechanism is preferred.
*/
public static void storeReferralParams(Context context, Map<String, String> params)
{
SharedPreferences storage = context.getSharedPreferences(ReferralReceiver.PREFS_FILE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = storage.edit();
for(String key : ReferralReceiver.EXPECTED_PARAMETERS)
{
String value = params.get(key);
if(value != null)
{
editor.putString(key, value);
}
}
editor.commit();
}
/*
* Returns a map with the Market Referral parameters pulled from the sharedPreferences.
*/
public static Map<String, String> retrieveReferralParams(Context context)
{
HashMap<String, String> params = new HashMap<String, String>();
SharedPreferences storage = context.getSharedPreferences(ReferralReceiver.PREFS_FILE_NAME, Context.MODE_PRIVATE);
for(String key : ReferralReceiver.EXPECTED_PARAMETERS)
{
String value = storage.getString(key, null);
if(value != null)
{
params.put(key, value);
}
}
return params;
}
}
After that i have try in my activity
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(DemoActivity.this);
String referrers1 =preferences.getString("ga_campaign", "0");
Map<String, String> retrieveReferralParams=ReferralReceiver.retrieveReferralParams(DemoActivity.this);
String referrers2= retrieveReferralParams.get("utm_source");
String referrers3= retrieveReferralParams.get("utm_medium");
String referrers4= retrieveReferralParams.get("utm_term");
String referrers5= retrieveReferralParams.get("utm_content");
String referrers6= retrieveReferralParams.get("utm_campaign");
tv.setText(referrers1+" "+referrers2+" "+referrers3+" "+referrers4+" "+referrers5+" "+referrers6+" ");
on button click but not get desired output
i want something like from
"https://play.google.com/store/apps/details?id=com.lifestreet.android.TestInstallationIntent&referrer=bb%3DAAAAAAAAAA&feature=search_result%22"
Ans
referrer=bb
any help me highly appreciated
Thanks in advance.

Not sure that Google lets you send arbitrary information. Try using the generator to create the url.
https://developers.google.com/analytics/devguides/collection/android/devguide#google-play-builder

I have had a similar issue. Found it to be lifecycle problem: the onReceive of the install_referrer receivers are invoked AFTER my app's onResume(), on the SAME main thread, so any attempt to read the referrer file during onResume() fails. Here is the logcat to prove it, this was 100% reproducible over and over on 2 devices using Android 4.2.1 and 4.4.2:
First, play store broadcasts the referrer to the package on a separate (store) process:
11-04 14:17:51.558: D/Finsky(1737): [1] ReferrerRebroadcaster.doBroadcastInstallReferrer: Delivered referrer for com.xxx.demo
The app onResume() - still no activation of the broadcast receivers! S
11-04 14:17:51.888: D/XXX Main Activity(22730): onResume
The app tries to read the referrer (which the receivers should have stored using getSharedPreferences.putString):
11-04 14:17:51.958: I/XXX(22730): Extracted install referrer:
Only now the receivers are invoked on the main thread and will shortly try to write the referrer to a file:
11-04 14:17:51.918: I/XXX(22730): Received install referrer: abcdefg
As you can see, onResume() has no chance to actually read the file, so the extraction yields nothing. However, if I close the app and reopen it, onResume is now able to find the file and the referrer string gets processed - but not on first launch :)
Hope this helps!

Google Analytics uses arbitrary constants in their SDK.
for campaing_source is &cs.

Related

Android - ExoPlayer 2 play DRM (widevine) offline

I'm trying to implement offline DRM support for ExoPlayer 2 but I have some problems.
I found this conversation. There is some implementation for ExoPlayer 1.x and some steps how to work that implementation with ExoPlayer 2.x.
I have I problem with OfflineDRMSessionManager whitch implements DrmSessionManager. In that example is DrmSessionManager imported from ExoPlayer 1.x. If I import it from ExoPlayer 2 then I have a problems to compile it. I have a problem with #Override methods (open(), close(), ..) which are NOT in that new DrmSessionManager and there are some new methods: acquireSession(), ... .
With the latest release of ExoPlayer 2.2.0 , it provides this facility inbuilt in ExoPlayer. ExoPlayer has a helper class to download and refresh offline license keys. It should be the preferred way to do this.
OfflineLicenseHelper.java
/**
* Helper class to download, renew and release offline licenses. It utilizes {#link
* DefaultDrmSessionManager}.
*/
public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
You can access the latest code from the ExoPlayer repo
I created a sample application for Offline playback of DRM content.You can access it from here
As #TheJango explained, with the latest release of ExoPlayer 2.2.0 , it provides this facility inbuilt in ExoPlayer.
However, the OfflineLicenseHelper class was designed with some VOD use case in mind. Buy a movie, save the license (download method), download the movie, load the license in a DefaultDrmSessionManager and then setMode for playback.
Another use case could be that you want to make an online streaming system where different content is using the same license (e.g. Television) for quite some time (e.g. 24hours) more intelligent. So that it never downloads a license which it already has (Suppose your DRM system charges you per license request and there will be a lot of requests for the same license otherwise), the following approach can be used with ExoPlayer 2.2.0. It took me some time to get a working solution without modifying anything to the ExoPlayer source. I don't quite like the approach they've taken with the setMode() method which can only be called once. Previously DrmSessionManagers would work for multiple sessions (audio, video) and now they no longer work if licenses differ or come from different methods (DOWNLOAD, PLAYBACK, ...). Anyway, I introduced a new class CachingDefaultDrmSessionManager to replace the DefaultDrmSessionManager you are probably using. Internally it delegates to a DefaultDrmSessionManager.
package com.google.android.exoplayer2.drm;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.concurrent.atomic.AtomicBoolean;
import android.os.Handler;
import android.os.Looper;
import android.util.Base64;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
import java.util.HashMap;
import java.util.UUID;
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_DOWNLOAD;
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_QUERY;
public class CachingDefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T> {
private final SharedPreferences drmkeys;
public static final String TAG="CachingDRM";
private final DefaultDrmSessionManager<T> delegateDefaultDrmSessionManager;
private final UUID uuid;
private final AtomicBoolean pending = new AtomicBoolean(false);
private byte[] schemeInitD;
public interface EventListener {
void onDrmKeysLoaded();
void onDrmSessionManagerError(Exception e);
void onDrmKeysRestored();
void onDrmKeysRemoved();
}
public CachingDefaultDrmSessionManager(Context context, UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters, final Handler eventHandler, final EventListener eventListener) {
this.uuid = uuid;
DefaultDrmSessionManager.EventListener eventListenerInternal = new DefaultDrmSessionManager.EventListener() {
#Override
public void onDrmKeysLoaded() {
saveDrmKeys();
pending.set(false);
if (eventListener!=null) eventListener.onDrmKeysLoaded();
}
#Override
public void onDrmSessionManagerError(Exception e) {
pending.set(false);
if (eventListener!=null) eventListener.onDrmSessionManagerError(e);
}
#Override
public void onDrmKeysRestored() {
saveDrmKeys();
pending.set(false);
if (eventListener!=null) eventListener.onDrmKeysRestored();
}
#Override
public void onDrmKeysRemoved() {
pending.set(false);
if (eventListener!=null) eventListener.onDrmKeysRemoved();
}
};
delegateDefaultDrmSessionManager = new DefaultDrmSessionManager<T>(uuid, mediaDrm, callback, optionalKeyRequestParameters, eventHandler, eventListenerInternal);
drmkeys = context.getSharedPreferences("drmkeys", Context.MODE_PRIVATE);
}
final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public void saveDrmKeys() {
byte[] offlineLicenseKeySetId = delegateDefaultDrmSessionManager.getOfflineLicenseKeySetId();
if (offlineLicenseKeySetId==null) {
Log.i(TAG,"Failed to download offline license key");
} else {
Log.i(TAG,"Storing downloaded offline license key for "+bytesToHex(schemeInitD)+": "+bytesToHex(offlineLicenseKeySetId));
storeKeySetId(schemeInitD, offlineLicenseKeySetId);
}
}
#Override
public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) {
if (pending.getAndSet(true)) {
return delegateDefaultDrmSessionManager.acquireSession(playbackLooper, drmInitData);
}
// First check if we already have this license in local storage and if it's still valid.
DrmInitData.SchemeData schemeData = drmInitData.get(uuid);
schemeInitD = schemeData.data;
Log.i(TAG,"Request for key for init data "+bytesToHex(schemeInitD));
if (Util.SDK_INT < 21) {
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitD, C.WIDEVINE_UUID);
if (psshData == null) {
// Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
} else {
schemeInitD = psshData;
}
}
byte[] cachedKeySetId=loadKeySetId(schemeInitD);
if (cachedKeySetId!=null) {
//Load successful.
Log.i(TAG,"Cached key set found "+bytesToHex(cachedKeySetId));
if (!Arrays.equals(delegateDefaultDrmSessionManager.getOfflineLicenseKeySetId(), cachedKeySetId))
{
delegateDefaultDrmSessionManager.setMode(MODE_QUERY, cachedKeySetId);
}
} else {
Log.i(TAG,"No cached key set found ");
delegateDefaultDrmSessionManager.setMode(MODE_DOWNLOAD,null);
}
DrmSession<T> tDrmSession = delegateDefaultDrmSessionManager.acquireSession(playbackLooper, drmInitData);
return tDrmSession;
}
#Override
public void releaseSession(DrmSession<T> drmSession) {
pending.set(false);
delegateDefaultDrmSessionManager.releaseSession(drmSession);
}
public void storeKeySetId(byte[] initData, byte[] keySetId) {
String encodedInitData = Base64.encodeToString(initData, Base64.NO_WRAP);
String encodedKeySetId = Base64.encodeToString(keySetId, Base64.NO_WRAP);
drmkeys.edit()
.putString(encodedInitData, encodedKeySetId)
.apply();
}
public byte[] loadKeySetId(byte[] initData) {
String encodedInitData = Base64.encodeToString(initData, Base64.NO_WRAP);
String encodedKeySetId = drmkeys.getString(encodedInitData, null);
if (encodedKeySetId == null) return null;
return Base64.decode(encodedKeySetId, 0);
}
}
Here keys are persisted as Base64 encoded strings in local storage. Because for a typical DASH stream both audio and video renderers will request a license from the DrmSessionManager, possibly at the same time, the AtomicBoolean is used. If audio and or video would use different keys, I think this approach would fail.
Also I am not yet checking for expired keys here. Have a look at OfflineLicenseHelper to see how to deal with those.
#Pepa Zapletal, proceed with below changes to play in offline.
You can also see the updated answer here.
Changes are as follows :
Changed signature of the method private void onKeyResponse(Object response) to private void onKeyResponse(Object response, boolean offline)
Rather than sending the file manifest URI send stored file path to PlayerActivity.java.
Change MediaDrm.KEY_TYPE_STREAMING to MediaDrm.KEY_TYPE_OFFLINE in getKeyRequest().
In postKeyRequest() first check whether the key is stored or not, if key found then directly call onKeyResponse(key, true).
In onKeyResponse(), call restoreKeys() rather than calling provideKeyResponse().
The rest everything is same, now your file will be playing.
Major role : Here provideKeyResponse() and restoreKeys() are native methods which acts major role in getting the key and restoring the key.
provideKeyResponse() method which will return us the main License key in byte array if and only if the keyType is MediaDrm.KEY_TYPE_OFFLINE else this method will return us the empty byte array with which we can do nothing with that array.
restoreKeys() method will expect the key which is to be restored for the current session, so feed the key which we have already stored in local to this method and it will take care of it.
Note : First you have to somehow download the license key and store it somewhere in local device securely.
In my case first im playing the file online, so exoplayer will fetch the key that key i have stored in local. From second time onwards first it will check whether the key is stored or not, if key found it will skip the License key request and will the play the file.
Replace the methods and inner classes of StreamingDrmSessionManager.java with these things.
private void postKeyRequest() {
KeyRequest keyRequest;
try {
// check is key exist in local or not, if exist no need to
// make a request License server for the key.
byte[] keyFromLocal = Util.getKeyFromLocal();
if(keyFromLocal != null) {
onKeyResponse(keyFromLocal, true);
return;
}
keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData.data, schemeData.mimeType, MediaDrm.KEY_TYPE_OFFLINE, optionalKeyRequestParameters);
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
} catch (NotProvisionedException e) {
onKeysError(e);
}
}
private void onKeyResponse(Object response, boolean offline) {
if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
// This event is stale.
return;
}
if (response instanceof Exception) {
onKeysError((Exception) response);
return;
}
try {
// if we have a key and we want to play offline then call
// 'restoreKeys()' with the key which we have already stored.
// Here 'response' is the stored key.
if(offline) {
mediaDrm.restoreKeys(sessionId, (byte[]) response);
} else {
// Don't have any key in local, so calling 'provideKeyResponse()' to
// get the main License key and store the returned key in local.
byte[] bytes = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
Util.storeKeyInLocal(bytes);
}
state = STATE_OPENED_WITH_KEYS;
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
#Override
public void run() {
eventListener.onDrmKeysLoaded();
}
});
}
} catch (Exception e) {
onKeysError(e);
}
}
#SuppressLint("HandlerLeak")
private class PostResponseHandler extends Handler {
public PostResponseHandler(Looper looper) {
super(looper);
}
#Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_PROVISION:
onProvisionResponse(msg.obj);
break;
case MSG_KEYS:
// We don't have key in local so calling 'onKeyResponse()' with offline to 'false'.
onKeyResponse(msg.obj, false);
break;
}
}
}

Use UserSingleton with null context (silent push notification)

I'm currently working on GCM notifications, especially on silent push notification. For the moment, I receive the silent notification, even when the app is closed (which is the aim of my notification). The problem is the code I am trying to execute when receiving this silent push notification :
private void sendPingNotification() {
mWebServiceCoordinator = new WebServiceCoordinator(UserSingleton.getContext(), this);
mWebServiceCoordinator.fetchSessionConnectionData(
UserSingleton.getInstance().getLogin(),
UserSingleton.getInstance().getPassword(),
UserSingleton.getInstance().getDeviceId(),
UserSingleton.getInstance().getDeviceType(),
UserSingleton.getInstance().getAvailability());
}
As you can see, I want to send the UserSingleton's login, password, etc... to my server.
First, I create a new WebServiceCoordinator, which takes the Context of UserSingleton in parameter. And this is where there is a problem : as the application is totally closed, there is not any Context !
Here is the function trying to get the Context in my UserSingleton :
public static Context getContext() {
if (context == null) {
if (MainActivity.isRunning) {
context = MainActivity.getContext();
} else {
Log.e(LOG_TAG, "Unable to get the context");
}
}
return context;
}
This function is perfectly working when the application is running. But as the application is closed and nothing is running, even not MainActivity, the value of context returned by the function is still null
Thus, the next part of my sendPingNotification() doesn't work...
I'm pretty sure there is no way to get a context when an application isn't running (it's logical), but how can I still use my UserSingleton methods ? Because they need a Context to work, for exemple, the getSharedValue() method :
public String getSharedValue(String key, String defaultValue) {
final SharedPreferences data_r = UserSingleton.getContext().getSharedPreferences(SHARED_KEY, Context.MODE_PRIVATE);
return data_r.getString(key, defaultValue);
}
Any ideas ? Thanks
------ EDIT ------
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
public class MyGcmListenerService extends GcmListenerService implements WebServiceCoordinator.Listener {
private static final String TAG = "MyGcmListenerService";
private WebServiceCoordinator mWebServiceCoordinator;
/**
* Called when message is received.
*
* #param from SenderID of the sender.
* #param data Data bundle containing message data as key/value pairs.
* For Set of keys use data.keySet().
*/
#Override
public void onMessageReceived(String from, Bundle data) {
String type = data.getString("type");
String title = data.getString("title");
String message = data.getString("message");
Log.d(TAG, "Notification from " + from + " : (" + type + ")" + " " + message);
if (type.equals("appointment_details")) {
String nom = data.getString("nom");
String prenom = data.getString("prenom");
String mail = data.getString("mail");
String telephone = data.getString("telephone");
String state = data.getString("state");
Integer stateId = Integer.parseInt(data.getString("stateId"));
String date = data.getString("date");
String token = data.getString("token");
String length = data.getString("length");
sendDetailsRendezvousNotification(title, message, nom, prenom, mail, telephone, state, stateId, date, token, length);
}
else if (type.equals("appointment"))
sendNormalNotification(title, message);
else if (type.equals("newCall"))
sendNewCallNotification();
else if (type.equals("ping"))
sendPingNotification();
else if (type.equals("normal"))
sendNormalNotification(title, message);
}
private void sendPingNotification() {
mWebServiceCoordinator = new WebServiceCoordinator(UserSingleton.getContext(), this);
mWebServiceCoordinator.fetchSessionConnectionData(UserSingleton.getInstance().getLogin(),
UserSingleton.getInstance().getPassword(),
UserSingleton.getInstance().getDeviceId(),
UserSingleton.getInstance().getDeviceType(),
UserSingleton.getInstance().getAvailability());
}
}
Ok im pretty sure you can use getApplicationContext to get the context inside a gcm service
mWebServiceCoordinator = new WebServiceCoordinator(getApplicationContext(), this);
So I edited my sendPingNotification() method as following :
private void sendPingNotification() {
mWebServiceCoordinator = new WebServiceCoordinator(getApplicationContext(), this);
UserSingleton.setContext(getApplicationContext());
mWebServiceCoordinator.fetchSessionConnectionData(UserSingleton.getInstance().getLogin(),
UserSingleton.getInstance().getPassword(),
UserSingleton.getInstance().getDeviceId(),
UserSingleton.getInstance().getDeviceType(),
UserSingleton.getInstance().getAvailability());
}
I added a setContext() method in UserSingleton, so that I can set the context of my UserSingleton as getApplicationContext(). The context isn't null anymore and getContext() returns a valid Context. Everything is working ! Thanks to you two.

Get crash report in google analytics

I want to receive notification regarding crash report due to uncaught exception in my google analytics for my android app. I followed the steps given in https://developers.google.com/analytics/devguides/collection/android/v4/exceptions#parsing
but still I dont receive any crash report. I had a runtime exception when my app runs. I added the code for ga_reportUncaughtException as true:
true
in my analytics.xml. Is there anything else I need to add in order to get hit in google analytics account. Please help!
There is an open issue in Analytics. I'm experiencing the same behavior but on real devices from API 10 to 19.
https://code.google.com/p/analytics-issues/issues/detail?id=443
EDIT1: Removed question, just to answer the question described.
EDIT2: I tried to capture and send the exceptions using the Analytics ExceptionBuilder, but it didn't work.
It looks like the report is sent (at least LogCat is showing that the crash is reported), but it is not processed by Analytics.
While Google replies to the issue, I'm using this workaround. I guess it is not the best solution and the code can be improved, but it works for me:
I created a custom dimension in Analytics following this steps https://support.google.com/analytics/answer/2709829?hl=en
In my App, I created a custom exception handler, using the Analytics ExceptionReporter class. When an exception is caught, I get the stack trace and truncate it to 150 Bytes (Actually I'm getting only the first line of the stack and truncate it to 150 chars. I'm assuming that 1Char = 1 Byte). I have to truncate it, because it is the Max Lenght allowed by Analytics when sending custom dimensions values.
The stack trace is stored in a Shared Preference instead of being sent. I tried to send it directly, but it does not work once the App has crashed.
package com.company.package;
import java.lang.Thread.UncaughtExceptionHandler;
import android.content.Context;
import com.google.android.gms.analytics.ExceptionParser;
import com.google.android.gms.analytics.ExceptionReporter;
import com.google.android.gms.analytics.GoogleAnalytics;
import com.google.android.gms.analytics.HitBuilders;
import com.google.android.gms.analytics.Tracker;
public class GoogleAnalyticsTracker {
private static Tracker mTracker;
private static GoogleAnalytics mGa;
private Context mContext;
public GoogleAnalyticsTracker(Context context, int resource) {
mContext = context;
mGa = GoogleAnalytics.getInstance(context);
mTracker = getTracker(resource);
Thread.setDefaultUncaughtExceptionHandler(new AnalyticsExceptionReporter(mTracker,
Thread.getDefaultUncaughtExceptionHandler(), context));
}
synchronized Tracker getTracker(int xmlResource) {
return mGa.newTracker(xmlResource);
}
public void sendScreenLabel(String screenLabel) {
mTracker.setScreenName(screenLabel);
mTracker.send(new HitBuilders.AppViewBuilder().build());
}
public void sendCustomDimension(int index, String value) {
mTracker.send(new HitBuilders.AppViewBuilder().setCustomDimension(index, value).build());
}
private class AnalyticsExceptionReporter extends ExceptionReporter {
public AnalyticsExceptionReporter(Tracker tracker, UncaughtExceptionHandler originalHandler, Context context) {
super(tracker, originalHandler, context);
setExceptionParser(new AnalyticsExceptionParser());
}
#Override
public void uncaughtException(Thread t, Throwable e) {
String exceptionDescription = getExceptionParser().getDescription(t.getName(), e);
//Add code to store the exception stack trace in shared preferences
super.uncaughtException(t, e);
}
}
private class AnalyticsExceptionParser implements ExceptionParser {
#Override
public String getDescription(String arg0, Throwable arg1) {
StringBuilder exceptionFirsLine = new StringBuilder();
for (StackTraceElement element : arg1.getStackTrace()) {
exceptionFirsLine.append(element.toString());
break;
}
//150 Bytes is the maximum allowed by Analytics for custom dimensions values. Assumed that 1 Byte = 1 Character (UTF-8)
String exceptionDescription = exceptionFirsLine.toString();
if(exceptionDescription.length() > 150)
exceptionDescription = exceptionDescription.substring(0, 149);
return exceptionDescription;
}
}
}
In the MainActivity when OnStart(), I check if there is any stored stack trace in the shared preferences. If so, I send the custom dimension and clear the shared preference.
#Override
protected void onStart() {
super.onStart();
String exception = getExceptionFromSharedPreferences(this);
if(exception != null && !exception.isEmpty()) {
MainApplication.googleAnalyticsTracker.sendCustomDimension(1, exception);
}
clearExceptionFromSharedPreferences(this);
}
Finally I created a custom report in Analytics
EDIT 3:
I realized that I was sending only the fileName and lineNumber, but not the ExceptionName and the origin of the Exception in my package. I have improved the answer by adding code to also send that info.
private class AnalyticsExceptionParser implements ExceptionParser {
#Override
public String getDescription(String arg0, Throwable arg1) {
String exceptionDescription = getExceptionInfo(arg1, "", true) + getCauseExceptionInfo(arg1.getCause());
//150 Bytes is the maximum allowed by Analytics for custom dimensions values. Assumed that 1 Byte = 1 Character (UTF-8)
if(exceptionDescription.length() > 150)
exceptionDescription = exceptionDescription.substring(0, 150);
return exceptionDescription;
}
}
//#endregion
//#region PRIVATE METHODS
private String getCauseExceptionInfo(Throwable t) {
String causeDescription = "";
while(t != null && causeDescription.isEmpty()) {
causeDescription = getExceptionInfo(t, "com.myPackageName", false);
t = t.getCause();
}
return causeDescription;
}
private String getExceptionInfo(Throwable t, String packageName, boolean includeExceptionName) {
String exceptionName = "";
String fileName = "";
String lineNumber = "";
for (StackTraceElement element : t.getStackTrace()) {
String className = element.getClassName().toString().toLowerCase();
if(packageName.isEmpty() || (!packageName.isEmpty() && className.contains(packageName))){
exceptionName = includeExceptionName ? t.toString() : "";
fileName = element.getFileName();
lineNumber = String.valueOf(element.getLineNumber());
return exceptionName + "#" + fileName + ":" + lineNumber;
}
}
return "";
}
From my experience you need to understand two things about crashes and exceptions in Google Analytics:
1) Only basic information is stored - Google Analytics will only save the name of the exception and the location (Code file and line number) where the exception was thrown. No information beyond that will be accessible to you on GA. This is definitely not ideal and if you wish to track the actual content of your exceptions (mainly the call stack), use Google Play or implement your own solution.
2) Exceptions are not real-time. Exception information is collected and updated maybe once a day, so if you're experimenting with exceptions and you don't see them immediately, just give it time.

How to get Category for each App on device on Android?

I've got an Android app which scans for all Apps installed on the device and then reports this to a server (it's an MDM agent). Any suggestions on how to get the Category of the App? Everyone has a different list of Categories, but basically something like Game, Entertainment, Tools/Utilities, etc.
From what I can tell there is nothing related to Category stored on the device itself. I was thinking of using the android market API to search for the application in the market and use the Category value returned by the search. Not sure how successful this will be finding a match. Any suggestions on how best to do this?
Any suggestions on a different approach?
Thanks in advance.
mike
I know that this is an old post, but for anyone still looking for this, API level 26 (O) has added categories to android.content.pm.ApplicationInfo.
From the docs https://developer.android.com/reference/android/content/pm/ApplicationInfo#category:
public int category
The category of this app. Categories are used to cluster multiple apps together into meaningful groups, such as when summarizing battery, network, or disk usage. Apps should only define this value when they fit well into one of the specific categories.
Set from the R.attr.appCategory attribute in the manifest. If the manifest doesn't define a category, this value may have been provided by the installer via PackageManager#setApplicationCategoryHint(String, int).
Value is CATEGORY_UNDEFINED, CATEGORY_GAME, CATEGORY_AUDIO, CATEGORY_VIDEO, CATEGORY_IMAGE, CATEGORY_SOCIAL, CATEGORY_NEWS, CATEGORY_MAPS, or CATEGORY_PRODUCTIVITY
One can now do something like:
PackageManager pm = context.getPackageManager();
ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName, 0);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
int appCategory = applicationInfo.category;
String categoryTitle = (String) ApplicationInfo.getCategoryTitle(context, appCategory)
// ...
}
if you get for each application its package name, you could ask directly to play store which category an app belongs, parsing html response page with this library:
org.jsoup.jsoup1.8.3
Here's a snippet to solve your problem:
public class MainActivity extends AppCompatActivity {
public final static String GOOGLE_URL = "https://play.google.com/store/apps/details?id=";
public static final String ERROR = "error";
...
private class FetchCategoryTask extends AsyncTask<Void, Void, Void> {
private final String TAG = FetchCategoryTask.class.getSimpleName();
private PackageManager pm;
private ActivityUtil mActivityUtil;
#Override
protected Void doInBackground(Void... errors) {
String category;
pm = getPackageManager();
List<ApplicationInfo> packages = pm.getInstalledApplications(PackageManager.GET_META_DATA);
Iterator<ApplicationInfo> iterator = packages.iterator();
while (iterator.hasNext()) {
ApplicationInfo packageInfo = iterator.next();
String query_url = GOOGLE_URL + packageInfo.packageName;
Log.i(TAG, query_url);
category = getCategory(query_url);
// store category or do something else
}
return null;
}
private String getCategory(String query_url) {
boolean network = mActivityUtil.isNetworkAvailable();
if (!network) {
//manage connectivity lost
return ERROR;
} else {
try {
Document doc = Jsoup.connect(query_url).get();
Element link = doc.select("span[itemprop=genre]").first();
return link.text();
} catch (Exception e) {
return ERROR;
}
}
}
}
}
You could make these queries in an AsyncTask, or in a service. Hope that you find it helpful.
I also faced the same issue. The solution for the above query is stated below.
Firstly, download the Jsoup library or download the jar file.
or
Add this to your build.gradle(Module: app) implementation 'org.jsoup:jsoup:1.11.3'
private class FetchCategoryTask extends AsyncTask<Void, Void, Void> {
private final String TAG = FetchCategoryTask.class.getSimpleName();
private PackageManager pm;
//private ActivityUtil mActivityUtil;
#Override
protected Void doInBackground(Void... errors) {
String category;
pm = getPackageManager();
List<ApplicationInfo> packages = pm.getInstalledApplications(PackageManager.GET_META_DATA);
Iterator<ApplicationInfo> iterator = packages.iterator();
// while (iterator.hasNext()) {
// ApplicationInfo packageInfo = iterator.next();
String query_url = "https://play.google.com/store/apps/details?id=com.imo.android.imoim"; //GOOGLE_URL + packageInfo.packageName;
Log.i(TAG, query_url);
category = getCategory(query_url);
Log.e("CATEGORY", category);
// store category or do something else
//}
return null;
}
private String getCategory(String query_url) {
try {
Document doc = Jsoup.connect(query_url).get();
Elements link = doc.select("a[class=\"hrTbp R8zArc\"]");
return link.text();
} catch (Exception e) {
Log.e("DOc", e.toString());
}
}
}
In return, you will get Application Company Name and category of the application
I made a Kotlin solution based on the answer from #Ankit Kumar Singh.
This solution maps the category to an enum, in case you want to do other things than just show it.
import kotlinx.coroutines.*
import org.jsoup.Jsoup
import javax.inject.Inject
import javax.inject.Singleton
class AppCategoryService {
companion object {
private const val APP_URL = "https://play.google.com/store/apps/details?id="
private const val CAT_SIZE = 9
private const val CATEGORY_STRING = "category/"
}
suspend fun fetchCategory(packageName: String): AppCategory {
val url = "$APP_URL$packageName&hl=en" //https://play.google.com/store/apps/details?id=com.example.app&hl=en
val categoryRaw = parseAndExtractCategory(url) ?: return AppCategory.OTHER
return AppCategory.fromCategoryName(categoryRaw)
}
#Suppress("BlockingMethodInNonBlockingContext")
private suspend fun parseAndExtractCategory(url: String): String? = withContext(Dispatchers.IO) {
return#withContext try {
val text = Jsoup.connect(url).get()?.select("a[itemprop=genre]") ?: return#withContext null
val href = text.attr("abs:href")
if (href != null && href.length > 4 && href.contains(CATEGORY_STRING)) {
getCategoryTypeByHref(href)
} else {
null
}
} catch (e: Throwable) {
null
}
}
private fun getCategoryTypeByHref(href: String) = href.substring(href.indexOf(CATEGORY_STRING) + CAT_SIZE, href.length)
}
And here is the enum with all the possible values at of this moment in time:
// Note: Enum name matches API value and should not be changed
enum class AppCategory {
OTHER,
ART_AND_DESIGN,
AUTO_AND_VEHICLES,
BEAUTY,
BOOKS_AND_REFERENCE,
BUSINESS,
COMICS,
COMMUNICATION,
DATING,
EDUCATION,
ENTERTAINMENT,
EVENTS,
FINANCE,
FOOD_AND_DRINK,
HEALTH_AND_FITNESS,
HOUSE_AND_HOME,
LIBRARIES_AND_DEMO,
LIFESTYLE,
MAPS_AND_NAVIGATION,
MEDICAL,
MUSIC_AND_AUDIO,
NEWS_AND_MAGAZINES,
PARENTING,
PERSONALIZATION,
PHOTOGRAPHY,
PRODUCTIVITY,
SHOPPING,
SOCIAL,
SPORTS,
TOOLS,
TRAVEL_AND_LOCAL,
VIDEO_PLAYERS,
WEATHER,
GAMES;
companion object {
private val map = values().associateBy(AppCategory::name)
private const val CATEGORY_GAME_STRING = "GAME_" // All games start with this prefix
fun fromCategoryName(name: String): AppCategory {
if (name.contains(CATEGORY_GAME_STRING)) return GAMES
return map[name.toUpperCase(Locale.ROOT)] ?: OTHER
}
}
}
private fun getCategory(){
val GOOGLE_URL = "https://play.google.com/store/apps/details?id=com.google.android.deskclock"
lifecycleScope.launch(Dispatchers.IO) {
val doc: Document = Jsoup.connect(GOOGLE_URL).get()
val index = doc.body().data().indexOf("applicationCategory")
val simpleString = doc.body().data().subSequence(index,index+100)
val data = simpleString.split(":")[1].split(",")[0]
Log.e("DATA-->",data.toString())
}
}
You can use below AsyncTask for extract Android app category from playStore by using app package id.
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.util.Log;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.io.IOException;
public class GetAppCategory extends AsyncTask<String, Void, String> {
//Main URL for each app on Play Store
public static final String APP_URL = "https://play.google.com/store/apps/details?id=";
//Use below String if extracting 'CATEGORY' from href tag.
private final String CATEGORY_STRING = "category/";
private final int cat_size = 9;
/*Use below String for identified 'GAME' apps, which must start with this prefix.
Here I only display 'Games' as category for all Games app instead of showing their subCategory also*/
private final String CATEGORY_GAME_STRING = "GAME_";
//Name of the app package for which you want to get category.
private String packageName = null;
private PackageManager pm = null;
//Activity or Application context as per requirement.
private Context appContext;
/* You can add default system app OR non play store app package name here as comma seprated for ignore them
and set their category directly 'Others' OR anythings you wish. */
private final String extractionApps = "com.android.providers.downloads.ui, com.android.contacts," +
" com.android.gallery3d, com.android.vending, com.android.calculator2, com.android.calculator," +
" com.android.deskclock, com.android.messaging, com.android.settings, com.android.stk";
//Class level TAG, use for Logging.
private final String TAG = "GetAppCategory";
/**
* #param packageName: package name of the app, you want to extract category.
* #param appContext: Activity/Application level Context ap per requirement.
*/
public GetAppCategory(String packageName, Context appContext) {
this.packageName = packageName;
this.appContext = appContext;
}
#Override
protected String doInBackground(String... params) {
try {
pm = appContext.getPackageManager();
if (packageName != null && packageName.length() > 1) {
if (packageName.contains("package:")) {
packageName = packageName.replace("package:", "");
}
/**
* Mathod used for parse play store html page and extract category from their.
*/
String appCategoryType = parseAndExtractCategory(packageName);
Log.i(TAG, "package :" + packageName);
Log.i(TAG, "APP_CATEGORY: " + appCategoryType);
}
} catch (Exception e) {
//TODO:: Handle Exception
e.printStackTrace();
} finally {
//TODO::
}
return null;
}
#Override
protected void onPostExecute(String result) {
}
/**
* #param packageName
* #return
*/
private String parseAndExtractCategory(String packageName) {
//You can pass hl={language_code} for get category in some other langauage also other than English.
//String url = APP_URL + packageName + "&hl=" + appContext.getString(R.string.app_lang);
String url = APP_URL + packageName + "&hl=en"; //{https://play.google.com/store/apps/details?id=com.example.app&hl=en}
String appCategoryType = null;
String appName = null;
try {
if (!extractionApps.contains(packageName)) {
Document doc = null;
try {
doc = Jsoup.connect(url).get();
if (doc != null) {
//TODO: START_METHOD_1
//Extract category String from a <anchor> tag value directly.
//NOTE: its return sub category text, for apps with multiple sub category.
//Comment this method {METHOD_1}, if you wish to extract category by href value.
Element CATEGORY_SUB_CATEGORY = doc.select("a[itemprop=genre]").first();
if (CATEGORY_SUB_CATEGORY != null) {
appCategoryType = CATEGORY_SUB_CATEGORY.text();
}
//TODO: END_METHOD_1
//TODO: START_METHOD_2
// Use below code only if you wist to extract category by href value.
//Its return parent or Main Category Text for all app.
//Comment this method {METHOD_2}, If you wihs to extract category from a<anchor> value.
if (appCategoryType == null || appCategoryType.length() < 1) {
Elements text = doc.select("a[itemprop=genre]");
if (text != null) {
if (appCategoryType == null || appCategoryType.length() < 2) {
String href = text.attr("abs:href");
if (href != null && href.length() > 4 && href.contains(CATEGORY_STRING)) {
appCategoryType = getCategoryTypeByHref(href);
}
}
}
}
//TODO: END_METHOD_2
if (appCategoryType != null && appCategoryType.length() > 1) {
/**
* Ger formatted category String by removing special character.
*/
appCategoryType = replaceSpecialCharacter(appCategoryType);
}
}
} catch (IOException e) {
//appCategoryType = appContext.getString(R.string.category_others);
appCategoryType = "OTHERS";
//TODO:: Handle Exception
e.printStackTrace();
}
} else {
//appCategoryType = appContext.getString(R.string.category_others);
appCategoryType = "OTHERS";
}
} catch (Exception e) {
//TODO:: Handle Exception
e.printStackTrace();
}
return appCategoryType;
}
/**
* #param href
* #return
*/
private String getCategoryTypeByHref(String href) {
String appCategoryType = null;
try {
appCategoryType = href.substring((href.indexOf(CATEGORY_STRING) + cat_size), href.length());
if (appCategoryType != null && appCategoryType.length() > 1) {
if (appCategoryType.contains(CATEGORY_GAME_STRING)) {
//appCategoryType = appContext.getString(R.string.games);
appCategoryType = "GAMES";
}
}
} catch (Exception e) {
//TODO:: Handle Exception
e.printStackTrace();
}
return appCategoryType;
}
/**
* #param appCategoryType
* #return: formatted String
*/
private String replaceSpecialCharacter(String appCategoryType) {
try {
//Find and Replace '&' with '&' in category Text
if (appCategoryType.contains("&")) {
appCategoryType = appCategoryType.replace("&", " & ");
}
//Find and Replace '_AND_' with ' & ' in category Text
if (appCategoryType.contains("_AND_")) {
appCategoryType = appCategoryType.replace("_AND_", " & ");
}
//Find and Replace '_' with ' ' <space> in category Text
if (appCategoryType.contains("_")) {
appCategoryType = appCategoryType.replace("_", " ");
}
} catch (Exception e) {
//TODO:: Handle Exception
e.printStackTrace();
}
return appCategoryType;
}
}
It's requires jsoup library for parsing the html page. you can find it here org.jsoup.jsoup1.11.1
Probably a bit late, but the problem is still here.
The OP has the advantage because of sending those results to the API (here I assume that the API is managed by the OP or his API colleagues at least).
So, for anyone with the similar problem I'd suggest following:
Collect all the package names you're interested in from device.
Send that data to the your API
API should extract package names and try to read results from its cache / db...
For those packages that do not exist in cache / db make "market API" call and extract category - save it to the db / cache for reuse in this iteration.
When all requests (to cache / db and market API) are completed do whatever you like with the results.
Things to consider:
When multiple users try to query your API for a same package name and you don't have a category for that package in your cache / db...
Do 1 request to "market API" for packagex and update packagex in your cache / db to "waiting for results" state - next request should either get a "waiting for results" or a result that "market API" returned.
One should also consider a fallback for possible "market API" fails (market API not working, not a google play app, or something similar). This decision is basically tied to your domain and the business trend that you're trying to catch will force a decision about this for you. If you're really into getting this category stuff sorted out you could pipeline this fallback to human decision and update your API db / cache for packagex accordingly.
put up a nice API that would handle these and similar scenarios gracefully then one could probably even commercialize it up to a certain extent and "market API endpoint" - AKA play store package details page. That page would lose a big part of it's fake users :)

Get Android Google Analytics referrer tag

We're planning to use Google Analytics to track ad click-through referrals, through the Android Market, to our application.
According to the Google Documentation the referrer tag comes through via an intent, and is automatically recorded by the Google Analytics library.
That's great, but we need to extract that referral tag for our own internal analytics. The documentation is shy on details about how to grab it out of the initial launch intent, and instructions on how to simulate this before going live.
Does anyone have experience with this?
I went ahead and published a dead pixel finder app to play with snooping on the intent. For some reason, when I registered two different broadcast receivers (ie com.google.android.apps.analytics.AnalyticsReceiver and my own), I never received it on my own.
So instead, I registered only my own receiver, process the information, and pass it along to Google Analytics. Don't know how kosher this is, but it works. Code follows.
public class ZSGoogleInterceptor extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
Bundle extras = intent.getExtras();
String referrerString = extras.getString("referrer");
// Next line uses my helper function to parse a query (eg "a=b&c=d") into key-value pairs
HashMap<String, String> getParams = Utility.getHashMapFromQuery(referrerString);
String source = getParams.get("utm_campaign");
if (source != null) {
SharedPreferences preferences = context.getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
Editor preferencesEditor = preferences.edit();
preferencesEditor.putString("ga_campaign", source);
preferencesEditor.commit();
}
// Pass along to google
AnalyticsReceiver receiver = new AnalyticsReceiver();
receiver.onReceive(context, intent);
}
}
Then, when your application is actually launched, you can pull the value back out of the shared preferences and pass it along with user signup or whatever. I used the campaign tag for my purposes, but you can grab any parameters you want out of the referrer string created with this tool.
#DougW 's answer updated for Analytics SDK 4
https://developers.google.com/analytics/devguides/collection/android/v4/campaigns
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import com.google.android.gms.analytics.CampaignTrackingReceiver;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Created by dave on 15-05-05.
*/
public class ReferrerReceiver extends BroadcastReceiver {
public static final String REFERRER = "REF";
public static final String UTM_CAMPAIGN = "utm_campaign";
public static final String UTM_SOURCE = "utm_source";
public static final String UTM_MEDIUM = "utm_medium";
public static final String UTM_TERM = "utm_term";
public static final String UTM_CONTENT = "utm_content";
private final String[] sources = {
UTM_CAMPAIGN, UTM_SOURCE, UTM_MEDIUM, UTM_TERM, UTM_CONTENT
};
#Override
public void onReceive(Context context, Intent intent) {
Bundle extras = intent.getExtras();
String referrerString = extras.getString("referrer");
try {
Map<String, String> getParams = getHashMapFromQuery(referrerString);
SharedPreferences preferences = context
.getSharedPreferences(REFERRER, Context.MODE_PRIVATE);
SharedPreferences.Editor preferencesEditor = preferences.edit();
for (String sourceType : sources) {
String source = getParams.get(sourceType);
if (source != null) {
preferencesEditor.putString(sourceType, source);
}
}
preferencesEditor.commit();
} catch (UnsupportedEncodingException e) {
Log.e("Referrer Error", e.getMessage());
} finally {
// Pass along to google
CampaignTrackingReceiver receiver = new CampaignTrackingReceiver();
receiver.onReceive(context, intent);
}
}
public static Map<String, String> getHashMapFromQuery(String query)
throws UnsupportedEncodingException {
Map<String, String> query_pairs = new LinkedHashMap<String, String>();
String[] pairs = query.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
query_pairs.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"),
URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
}
return query_pairs;
}
}
In you manifest file:
<service android:name="com.google.android.gms.analytics.CampaignTrackingService" />
<receiver android:name="com.google.android.gms.analytics.CampaignTrackingReceiver" />
<!-- Make sure this points to the location of Referrer Receiver in your package -->
<receiver android:name=".ReferrerReceiver" android:exported="true">
<intent-filter>
<action android:name="com.android.vending.INSTALL_REFERRER" />
</intent-filter>
</receiver>
Check at:
Get referrer after installing app from Android Market
for the solutions.
Tobia

Categories

Resources