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.
Related
According this thread on stackoverflow it should be possible to manage notification from outside main/UI thread. And it actually is. I'm creating notification in SyncAdapter to notify user that background sync started and updating upload progress and after upload is finished I'm canceling notification after some defined timeout. My problem is that notification auto cancelling is not predictable. Sometimes it auto cancels itself ok, sometimes it is visible until next sync.
Here is the whole Adapter:
package com.marianhello.bgloc.sync;
import android.accounts.Account;
import android.app.NotificationManager;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.app.NotificationCompat;
import com.marianhello.bgloc.Config;
import com.marianhello.bgloc.HttpPostService;
import com.marianhello.bgloc.UploadingCallback;
import com.marianhello.bgloc.data.ConfigurationDAO;
import com.marianhello.bgloc.data.DAOFactory;
import com.marianhello.logging.LoggerManager;
import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
/**
* Handle the transfer of data between a server and an
* app, using the Android sync adapter framework.
*/
public class SyncAdapter extends AbstractThreadedSyncAdapter implements UploadingCallback {
private static final int NOTIFICATION_ID = 1;
ContentResolver contentResolver;
private ConfigurationDAO configDAO;
private NotificationManager notifyManager;
private BatchManager batchManager;
private org.slf4j.Logger log;
/**
* Set up the sync adapter
*/
public SyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
log = LoggerManager.getLogger(SyncAdapter.class);
/*
* If your app uses a content resolver, get an instance of it
* from the incoming Context
*/
contentResolver = context.getContentResolver();
configDAO = DAOFactory.createConfigurationDAO(context);
batchManager = new BatchManager(this.getContext());
notifyManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
}
/**
* Set up the sync adapter. This form of the
* constructor maintains compatibility with Android 3.0
* and later platform versions
*/
public SyncAdapter(
Context context,
boolean autoInitialize,
boolean allowParallelSyncs) {
super(context, autoInitialize, allowParallelSyncs);
log = LoggerManager.getLogger(SyncAdapter.class);
/*
* If your app uses a content resolver, get an instance of it
* from the incoming Context
*/
contentResolver = context.getContentResolver();
configDAO = DAOFactory.createConfigurationDAO(context);
batchManager = new BatchManager(this.getContext());
notifyManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
}
/*
* Specify the code you want to run in the sync adapter. The entire
* sync adapter runs in a background thread, so you don't have to set
* up your own background processing.
*/
#Override
public void onPerformSync(
Account account,
Bundle extras,
String authority,
ContentProviderClient provider,
SyncResult syncResult) {
Config config = null;
try {
config = configDAO.retrieveConfiguration();
} catch (JSONException e) {
log.error("Error retrieving config: {}", e.getMessage());
}
if (config == null) return;
log.debug("Sync request: {}", config.toString());
if (config.hasUrl() || config.hasSyncUrl()) {
Long batchStartMillis = System.currentTimeMillis();
File file = null;
try {
file = batchManager.createBatch(batchStartMillis);
} catch (IOException e) {
log.error("Failed to create batch: {}", e.getMessage());
}
if (file == null) {
log.info("Nothing to sync");
return;
}
log.info("Syncing batchStartMillis: {}", batchStartMillis);
String url = config.hasSyncUrl() ? config.getSyncUrl() : config.getUrl();
HashMap<String, String> httpHeaders = new HashMap<String, String>();
httpHeaders.putAll(config.getHttpHeaders());
httpHeaders.put("x-batch-id", String.valueOf(batchStartMillis));
if (uploadLocations(file, url, httpHeaders)) {
log.info("Batch sync successful");
batchManager.setBatchCompleted(batchStartMillis);
if (file.delete()) {
log.info("Batch file has been deleted: {}", file.getAbsolutePath());
} else {
log.warn("Batch file has not been deleted: {}", file.getAbsolutePath());
}
} else {
log.warn("Batch sync failed due server error");
syncResult.stats.numIoExceptions++;
}
}
}
private boolean uploadLocations(File file, String url, HashMap httpHeaders) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext());
builder.setOngoing(true);
builder.setContentTitle("Syncing locations");
builder.setContentText("Sync in progress");
builder.setSmallIcon(android.R.drawable.ic_dialog_info);
notifyManager.notify(NOTIFICATION_ID, builder.build());
try {
int responseCode = HttpPostService.postJSON(url, file, httpHeaders, this);
if (responseCode == HttpURLConnection.HTTP_OK) {
builder.setContentText("Sync completed");
} else {
builder.setContentText("Sync failed due server error");
}
return responseCode == HttpURLConnection.HTTP_OK;
} catch (IOException e) {
log.warn("Error uploading locations: {}", e.getMessage());
builder.setContentText("Sync failed: " + e.getMessage());
} finally {
builder.setOngoing(false);
builder.setProgress(0, 0, false);
builder.setAutoCancel(true);
notifyManager.notify(NOTIFICATION_ID, builder.build());
Handler h = new Handler(Looper.getMainLooper());
long delayInMilliseconds = 5000;
h.postDelayed(new Runnable() {
public void run() {
notifyManager.cancel(NOTIFICATION_ID);
}
}, delayInMilliseconds);
}
return false;
}
public void uploadListener(int progress) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext());
builder.setOngoing(true);
builder.setContentTitle("Syncing locations");
builder.setContentText("Sync in progress");
builder.setSmallIcon(android.R.drawable.ic_dialog_info);
builder.setProgress(100, progress, false);
notifyManager.notify(NOTIFICATION_ID, builder.build());
}
}
The whole project is OSS so full source code is available. To get bigger picture also HttpPostService.java might interesting.
I think your issue is the following: you post notification cancel on UI thread, but in parallel you post updates on background thread. There is race condition between cancellation and the last update(s) - sometimes cancellation is the last command that notification manager gets, and sometimes it receives additional update(s) after cancellation (which makes him pop up the notification again).
Why do you post cancellation on the main thread in the first place? Just check the status in uploadListener(int) and decide whether you want to update the notification or cancel it...
I've found solution to my problem in this stackoverflow thread.
When I changed NOTIFICATION_ID from 1 to [RANDOM_NUMBER], it magically started working. I assume that 1 is somehow reserved, although there is no note in any documentation...
An of course make sure you use the same NOTIFICATION_ID to cancel:
notificationManager.cancel(NOTIFICATION_ID);
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.
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 :)
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.
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