Related
I am trying to create push notification for my cordova project. I followed the tutorial from this link http://devgirl.org/2012/10/25/tutorial-android-push-notifications-with-phonegap/ and created the plugin and I see the FCM is not sending back the registration id. I have added sender id in my application. Please help me to get this done. Thanks in advance.
GCMIntentService.java
import org.json.JSONException;
import org.json.JSONObject;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.google.android.gcm.GCMBaseIntentService;
#SuppressLint("NewApi")
public class GCMIntentService extends GCMBaseIntentService {
private static final String TAG = "GCMIntentService";
public GCMIntentService() {
super("GCMIntentService");
}
#Override
public void onRegistered(Context context, String regId) {
Log.v(TAG, "onRegistered: "+ regId);
JSONObject json;
try
{
json = new JSONObject().put("event", "registered");
json.put("regid", regId);
Log.v(TAG, "onRegistered: " + json.toString());
// Send this JSON data to the JavaScript application above EVENT should be set to the msg type
// In this case this is the registration ID
PushPlugin.sendJavascript( json );
}
catch( JSONException e)
{
// No message to the user is sent, JSON failed
Log.e(TAG, "onRegistered: JSON exception");
}
}
#Override
public void onUnregistered(Context context, String regId) {
Log.d(TAG, "onUnregistered - regId: " + regId);
}
#Override
protected void onMessage(Context context, Intent intent) {
Log.d(TAG, "onMessage - context: " + context);
// Extract the payload from the message
Bundle extras = intent.getExtras();
if (extras != null)
{
// if we are in the foreground, just surface the payload, else post it to the statusbar
if (PushPlugin.isInForeground()) {
extras.putBoolean("foreground", true);
createNotification(context, extras);
PushPlugin.sendExtras(extras);
}
else {
extras.putBoolean("foreground", false);
// Send a notification if there is a message
if (extras.getString("message") != null && extras.getString("message").length() != 0) {
createNotification(context, extras);
}
}
}
}
public void createNotification(Context context, Bundle extras)
{
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
String appName = getAppName(this);
Intent notificationIntent = new Intent(this, PushHandlerActivity.class);
notificationIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
notificationIntent.putExtra("pushBundle", extras);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
int defaults = Notification.DEFAULT_ALL;
if (extras.getString("defaults") != null) {
try {
defaults = Integer.parseInt(extras.getString("defaults"));
} catch (NumberFormatException e) {}
}
NotificationCompat.Builder mBuilder =
new NotificationCompat.Builder(context)
.setDefaults(defaults)
.setSmallIcon(context.getApplicationInfo().icon)
.setWhen(System.currentTimeMillis())
.setContentTitle(extras.getString("title"))
.setContentText(extras.getString("title"))
.setTicker(extras.getString("title"))
.setContentIntent(contentIntent)
.setAutoCancel(true);
String message = extras.getString("message");
if (message != null) {
mBuilder.setContentText(message);
} else {
mBuilder.setContentText("<missing message content>");
}
String msgcnt = extras.getString("msgcnt");
if (msgcnt != null) {
mBuilder.setNumber(Integer.parseInt(msgcnt));
}
int notId = 0;
try {
notId = Integer.parseInt(extras.getString("notId"));
}
catch(NumberFormatException e) {
Log.e(TAG, "Number format exception - Error parsing Notification ID: " + e.getMessage());
}
catch(Exception e) {
Log.e(TAG, "Number format exception - Error parsing Notification ID" + e.getMessage());
}
mNotificationManager.notify((String) appName, notId, mBuilder.build());
}
private static String getAppName(Context context)
{
CharSequence appName =
context
.getPackageManager()
.getApplicationLabel(context.getApplicationInfo());
return (String)appName;
}
#Override
public void onError(Context context, String errorId) {
Log.e(TAG, "onError - errorId: " + errorId);
}
}
PushPlugin.Java
import java.util.Iterator;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.api.Plugin;
import org.apache.cordova.api.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.google.android.gcm.GCMRegistrar;
import android.os.Bundle;
import android.util.Log;
public class PushPlugin extends Plugin {
public static final String TAG = "PushPlugin";
public static final String REGISTER = "register";
public static final String UNREGISTER = "unregister";
public static final String EXIT = "exit";
private static CordovaWebView gWebView;
private static String gECB;
private static String gSenderID;
private static Bundle gCachedExtras = null;
private static boolean gForeground = false;
#Override
public PluginResult execute(String action, JSONArray data, String CallbackContext) {
// TODO Auto-generated method stub
boolean result = false;
Log.v(TAG, "execute: action=" + action);
if (REGISTER.equals(action)) {
Log.v(TAG, "execute: data=" + data.toString());
try {
JSONObject jo = data.getJSONObject(0);
gWebView = this.webView;
Log.v(TAG, "execute: jo=" + jo.toString());
gECB = (String) jo.get("ecb");
gSenderID = (String) jo.get("senderID");
Log.v(TAG, "execute: ECB=" + gECB + " senderID=" + gSenderID);
GCMRegistrar.register(this.cordova.getActivity(), gSenderID);
result = true;
// callbackContext.success();
} catch (JSONException e) {
Log.e(TAG, "execute: Got JSON Exception " + e.getMessage());
result = false;
// callbackContext.error(e.getMessage());
}
if ( gCachedExtras != null) {
Log.v(TAG, "sending cached extras");
sendExtras(gCachedExtras);
gCachedExtras = null;
}
}else if (UNREGISTER.equals(action)) {
GCMRegistrar.unregister(this.cordova.getActivity());
Log.v(TAG, "UNREGISTER");
result = true;
// callbackContext.success();
} else {
result = false;
Log.e(TAG, "Invalid action : " + action);
//callbackContext.error("Invalid action : " + action);
}
return new PluginResult(PluginResult.Status.OK);
//return null;
}
public static void sendExtras(Bundle extras)
{
if (extras != null) {
if (gECB != null && gWebView != null) {
sendJavascript(convertBundleToJson(extras));
} else {
Log.v(TAG, "sendExtras: caching extras to send at a later time.");
gCachedExtras = extras;
}
}
}
private static JSONObject convertBundleToJson(Bundle extras)
{
try
{
JSONObject json;
json = new JSONObject().put("event", "message");
JSONObject jsondata = new JSONObject();
Iterator<String> it = extras.keySet().iterator();
while (it.hasNext())
{
String key = it.next();
Object value = extras.get(key);
// System data from Android
if (key.equals("from") || key.equals("collapse_key"))
{
json.put(key, value);
}
else if (key.equals("foreground"))
{
json.put(key, extras.getBoolean("foreground"));
}
else if (key.equals("coldstart"))
{
json.put(key, extras.getBoolean("coldstart"));
}
else
{
// Maintain backwards compatibility
if (key.equals("message") || key.equals("msgcnt") || key.equals("soundname"))
{
json.put(key, value);
}
if ( value instanceof String ) {
// Try to figure out if the value is another JSON object
String strValue = (String)value;
if (strValue.startsWith("{")) {
try {
JSONObject json2 = new JSONObject(strValue);
jsondata.put(key, json2);
}
catch (Exception e) {
jsondata.put(key, value);
}
// Try to figure out if the value is another JSON array
}
else if (strValue.startsWith("["))
{
try
{
JSONArray json2 = new JSONArray(strValue);
jsondata.put(key, json2);
}
catch (Exception e)
{
jsondata.put(key, value);
}
}
else
{
jsondata.put(key, value);
}
}
}
} // while
json.put("payload", jsondata);
Log.v(TAG, "extrasToJSON: " + json.toString());
return json;
}
catch( JSONException e)
{
Log.e(TAG, "extrasToJSON: JSON exception");
}
return null;
}
public static void sendJavascript(JSONObject _json) {
String _d = "javascript:" + gECB + "(" + _json.toString() + ")";
Log.v(TAG, "sendJavascript: " + _d);
if (gECB != null && gWebView != null) {
gWebView.sendJavascript(_d);
}
}
public static boolean isInForeground()
{
return gForeground;
}
public static boolean isActive()
{
return gWebView != null;
}
}
PushHandlerActivity.java
import android.app.Activity;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
public class PushHandlerActivity extends Activity
{
private static String TAG = "PushHandlerActivity";
/*
* this activity will be started if the user touches a notification that we own.
* We send it's data off to the push plugin for processing.
* If needed, we boot up the main activity to kickstart the application.
* #see android.app.Activity#onCreate(android.os.Bundle)
*/
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Log.v(TAG, "onCreate");
boolean isPushPluginActive = PushPlugin.isActive();
processPushBundle(isPushPluginActive);
finish();
if (!isPushPluginActive) {
forceMainActivityReload();
}
}
/**
* Takes the pushBundle extras from the intent,
* and sends it through to the PushPlugin for processing.
*/
private void processPushBundle(boolean isPushPluginActive)
{
Bundle extras = getIntent().getExtras();
if (extras != null) {
Bundle originalExtras = extras.getBundle("pushBundle");
originalExtras.putBoolean("foreground", false);
originalExtras.putBoolean("coldstart", !isPushPluginActive);
PushPlugin.sendExtras(originalExtras);
}
}
/**
* Forces the main activity to re-launch if it's unloaded.
*/
private void forceMainActivityReload()
{
PackageManager pm = getPackageManager();
Intent launchIntent = pm.getLaunchIntentForPackage(getApplicationContext().getPackageName());
startActivity(launchIntent);
}
#Override
protected void onResume() {
super.onResume();
final NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancelAll();
}
}
Manifeast file
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<permission
android:name="MyPackageName".push.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<activity android:name="MyPackageName".push.PushHandlerActivity"/>
<receiver android:name="MyPackageName".push.CordovaGCMBroadcastReceiver" android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="MyPackageName".push" />
</intent-filter>
</receiver>
<service android:name="MyPackageName".push.GCMIntentService" />
I saw some questions like this, but none of them solved my problem.
I'm starting a background service through an AlarmManager. Everytime the service starts, it checks if it has been disabled in SharedPreferences and, if not, it reschedule a new instance of itself and goes on, following these alternative paths:
if the user wants to use GPS, it waits for user's position and uses
it to call a REST endpoint;
if the user does not want to use GPS, it uses the position stored in prefs.
The result of the HTTP call (timeout: 30 seconds) is a JSONObject, which generates 0-n notifications (depending on how many "close objects" it finds).
My problem is: notifications, even if canceled by the user (sliding on them or opening them), often reappears, as if they were never shown. It should never happen, because the web service receives a list of excluded object ids that are updated every time.
Here the code:
ScannerService.java
package com.kiulomb.itascanner.service;
import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationManager;
import android.media.RingtoneManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.Volley;
import com.kiulomb.itascanner.R;
import com.kiulomb.itascanner.network.HTTPRequestManager;
import com.kiulomb.itascanner.network.HTTPResponseListener;
import com.kiulomb.itascanner.network.URLs;
import com.kiulomb.itascanner.pref.FilterPreferencesManager;
import com.kiulomb.itascanner.pref.NotificationsHistoryManager;
import com.kiulomb.itascanner.pref.PrefConstants;
import com.kiulomb.itascanner.utils.Haversine;
import com.kiulomb.itascanner.utils.MyConfiguration;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
public class ScannerService extends Service {
private static final String TAG = ScannerService.class.getSimpleName();
private boolean locationFound = false;
private boolean withoutLocation = false;
private LocationManager mLocationManager = null;
private final Timer myTimer = new Timer();
private final long TIMEOUT = 20000;
TimerTask myTask = new TimerTask() {
public void run() {
try {
Log.i(TAG, "Timeout is over, trying to stop service (location found? " + locationFound + ")");
if (!locationFound) {
stopSelf();
}
} catch (Exception e) {
Log.e(TAG, "Could not stop service after time: " + e.getMessage());
}
}
};
private LocationListener[] mLocationListeners = new LocationListener[] {
new LocationListener(LocationManager.GPS_PROVIDER),
new LocationListener(LocationManager.NETWORK_PROVIDER)
};
private boolean alreadySearching = false;
private class LocationListener implements android.location.LocationListener {
Location mLastLocation;
LocationListener(String provider) {
Log.i(TAG, "LocationListener is " + provider);
mLastLocation = new Location(provider);
}
#Override
public void onLocationChanged(final Location location) {
Log.i(TAG, "onLocationChanged: " + location);
if (withoutLocation) {
return;
}
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting();
if (location != null) {
if (isConnected) {
mLastLocation.set(location);
locationFound = true;
Log.i(TAG, "already searching? " + alreadySearching);
if (!alreadySearching) {
findClosest(location.getLatitude(), location.getLongitude());
}
alreadySearching = true;
} else {
Log.e(TAG, "no connectivity, ending service");
stopSelf();
}
} else {
Log.e(TAG, "no position, ending service");
stopSelf();
}
}
#Override
public void onProviderDisabled(String provider) {
Log.i(TAG, "onProviderDisabled: " + provider);
}
#Override
public void onProviderEnabled(String provider) {
Log.i(TAG, "onProviderEnabled: " + provider);
}
#Override
public void onStatusChanged(String provider, int status, Bundle extras) {
Log.i(TAG, "onStatusChanged: " + provider);
}
}
private void initializeLocationManager() {
Log.d(TAG, "initializeLocationManager");
if (mLocationManager == null) {
mLocationManager = (LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
}
}
#Override
public IBinder onBind(Intent arg0) {
return null;
}
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand");
// super.onStartCommand(intent, flags, startId);
return START_STICKY;
}
#Override
public void onCreate() {
Log.d(TAG, "onCreate");
SharedPreferences pref = getSharedPreferences(PrefConstants.PREF_APP_FILE, MODE_PRIVATE);
if (pref.getBoolean(PrefConstants.PREF_APP_SERVICE_ENABLED, PrefConstants.PREF_APP_SERVICE_ENABLED_DEFAULT)) {
Intent intent = new Intent(ScannerService.this, ScannerService.class);
PendingIntent pintent = PendingIntent.getService(ScannerService.this, 0, intent, 0);
AlarmManager alarm = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Calendar cal = Calendar.getInstance();
alarm.set(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis() + 60000, pintent); // or setExact() // TODO custom time
// alarm.setRepeating(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(), 60000, pintent);
if (!pref.getBoolean(PrefConstants.PREF_APP_SERVICE_CUSTOMCENTER, PrefConstants.PREF_APP_SERVICE_CUSTOMCENTER_DEFAULT)) {
// use GPS
initializeLocationManager();
try {
mLocationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
MyConfiguration.LOCATION_INTERVAL,
MyConfiguration.LOCATION_DISTANCE,
mLocationListeners[1]);
} catch (SecurityException ex) {
Log.e(TAG, "fail to request location update, ignore", ex);
} catch (IllegalArgumentException ex) {
Log.e(TAG, "network provider does not exist, " + ex.getMessage());
}
try {
mLocationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
MyConfiguration.LOCATION_INTERVAL,
MyConfiguration.LOCATION_DISTANCE,
mLocationListeners[0]);
} catch (SecurityException ex) {
Log.e(TAG, "fail to request location update, ignore", ex);
} catch (IllegalArgumentException ex) {
Log.e(TAG, "gps provider does not exist " + ex.getMessage());
}
} else {
withoutLocation = true;
// do not use GPS
String[] savedNotifCenter = pref.getString(PrefConstants.PREF_APP_SERVICE_CENTER, PrefConstants.PREF_APP_SERVICE_CENTER_DEFAULT).split(",");
double savedLat = Double.parseDouble(savedNotifCenter[0]);
double savedLng = Double.parseDouble(savedNotifCenter[1]);
locationFound = true; // prevent the service from stopping
findClosest(savedLat, savedLng);
}
} else {
stopSelf();
return;
}
/*if (isForeground(getPackageName())) {
Log.i(getClass().getSimpleName(), "application is in foreground, stopping service");
stopSelf();
return;
}*/
myTimer.schedule(myTask, TIMEOUT);
}
public boolean isForeground(String myPackage) {
ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
List<ActivityManager.RunningTaskInfo> runningTaskInfo = manager.getRunningTasks(1);
ComponentName componentInfo = runningTaskInfo.get(0).topActivity;
return componentInfo.getPackageName().equals(myPackage);
}
#Override
public void onDestroy() {
Log.d(TAG, "onDestroy");
super.onDestroy();
if (mLocationManager != null) {
for (LocationListener mLocationListener : mLocationListeners) {
try {
mLocationManager.removeUpdates(mLocationListener);
} catch (SecurityException se) {
Log.e(TAG, "security exception", se);
} catch (Exception ex) {
Log.e(TAG, "fail to remove location listeners, ignore", ex);
}
}
}
}
private void findClosest(final double lat, final double lng) {
new Thread(new Runnable() {
#Override
public void run() {
String url = URLs.buildURL(URLs.NOTIFICATIONS);
url += "?lat=" + lat;
url += "&lng=" + lng;
final SharedPreferences pref = getSharedPreferences(PrefConstants.PREF_APP_FILE, MODE_PRIVATE);
if (pref.contains(PrefConstants.PREF_APP_SERVICE_RADIUS)) {
url += "&radius=" + pref.getInt(PrefConstants.PREF_APP_SERVICE_RADIUS, PrefConstants.PREF_APP_SERVICE_RADIUS_DEFAULT);
}
url += "&limit=" + PrefConstants.PREF_APP_MAP_LIMIT_DEFAULT;
if (pref.contains(PrefConstants.PREF_APP_SERVICE_IV)) {
url += "&iv=" + pref.getInt(PrefConstants.PREF_APP_SERVICE_IV, PrefConstants.PREF_APP_SERVICE_IV_DEFAULT);
}
String exclusionsNumbers = getExcludedNumbersParam();
if (exclusionsNumbers.length() > 0) {
url += "&exNum=" + exclusionsNumbers;
}
final NotificationsHistoryManager notificationsHistoryManager = new NotificationsHistoryManager(ScannerService.this);
final List<Long> excludedIds = notificationsHistoryManager.getAlreadyFoundObjects();
String exclusionsIds = getExcludedIdsParam(excludedIds);
if (exclusionsIds.length() > 0) {
url += "&exId=" + exclusionsIds;
}
/*final long lastId = pref.getLong(PrefConstants.PREF_SERVICE_LAST_ID, 0L);
url += "&li=" + lastId;*/
final Context context = ScannerService.this;
HTTPRequestManager requestManager = new HTTPRequestManager(context, url, true, null, new HTTPResponseListener() {
#Override
public void onSuccess(JSONObject response) {
try {
JSONArray responseArray = response.getJSONArray("objects");
final String foundString = getString(R.string.found);
final String inCityString = getString(R.string.in_city);
final String expiringString = getString(R.string.expiring);
final DateFormat sdf = SimpleDateFormat.getTimeInstance();
final Resources res = getResources();
final String packageName = getPackageName();
final String mapsApiKey = getString(R.string.google_maps_key);
final boolean notifClickAutoCancel = pref.getBoolean(PrefConstants.PREF_APP_SERVICE_NOTIFCANCEL, PrefConstants.PREF_APP_SERVICE_NOTIFCANCEL_DEFAULT);
final boolean notifExpiredAutoCancel = pref.getBoolean(PrefConstants.PREF_APP_SERVICE_NOTIFCANCELEXPIRED, PrefConstants.PREF_APP_SERVICE_NOTIFCANCELEXPIRED_DEFAULT);
final boolean mapPicture = pref.getBoolean(PrefConstants.PREF_APP_SERVICE_MAPPICTURE, PrefConstants.PREF_APP_SERVICE_MAPPICTURE_DEFAULT);
final Locale defaultLocale = Locale.getDefault();
Calendar calendar = Calendar.getInstance();
// long maxId = lastId;
for (int i = 0; i < responseArray.length(); i++) {
try {
final MyEntity p = MyEntity.fromJSONLight(responseArray.getJSONObject(i));
// it should never happen, but notifications are shown many times :/
if (!excludedIds.contains(p.getId())) {
excludedIds.add(p.getId());
// maxId = Math.max(p.getId(), maxId);
final double iv = p.getIV();
final long expirationFixed = (p.getDisappearTime() - System.currentTimeMillis() - 2000);
final Calendar expirationTime = (Calendar) calendar.clone();
// now.add(Calendar.SECOND, (int) ((p.getDisappearTime() - System.currentTimeMillis() / 1000) - 2));
expirationTime.setTimeInMillis(expirationTime.getTimeInMillis() + expirationFixed);
final int distance = (int) Math.round(1000 * Haversine.distance(lat, lng, p.getLatitude(), p.getLongitude()));
String cityName = null;
Geocoder gcd = new Geocoder(context, defaultLocale);
List<Address> addresses = gcd.getFromLocation(p.getLatitude(), p.getLongitude(), 1);
if (addresses.size() > 0) {
cityName = addresses.get(0).getLocality();
}
final String cityNameParam = cityName;
new Thread(new Runnable() {
#Override
public void run() {
sendNotification((int) (p.getId()),
foundString + " " + p.getName() + (iv > 0 ? " " + iv + "%" : "") + (cityNameParam != null ? " " + inCityString + " " + cityNameParam : ""),
expiringString + " " + sdf.format(expirationTime.getTime()) + " - " + distance + "m" + (movesStringParam != null ? " (" + movesStringParam + ")" : ""),
p,
res,
packageName,
notifClickAutoCancel,
notifExpiredAutoCancel,
expirationFixed,
mapsApiKey,
mapPicture);
}
}).start();
}
} catch (Exception e) {
Log.e(TAG, "error", e);
}
}
notificationsHistoryManager.saveAlreadyFoundObjects(excludedIds);
stopSelf();
} catch (Exception e) {
Log.e(TAG, "error in reading JSONArray", e);
stopSelf();
}
}
#Override
public void onError(int errorCode) {
stopSelf();
}
});
RequestQueue requestQueue = Volley.newRequestQueue(context);
requestQueue.add(requestManager);
}
}).start();
}
private String getExcludedNumbersParam() {
String exclusionsNumbers = "";
List<Integer> excludedNumbers = new FilterPreferencesManager(ScannerService.this).getNotificationsExcludedNumbers();
int sizeNumbers = excludedNumbers.size();
for (int i = 0; i < sizeNumbers; i++) {
exclusionsNumbers += excludedNumbers.get(i);
if (i < sizeNumbers - 1) {
exclusionsNumbers += ",";
}
}
return exclusionsNumbers;
}
private String getExcludedIdsParam(List<Long> excludedIds) {
String exclusionsIds = "";
int sizeIds = excludedIds.size();
for (int i = 0; i < sizeIds; i++) {
exclusionsIds += excludedIds.get(i);
if (i < sizeIds - 1) {
exclusionsIds += ",";
}
}
return exclusionsIds;
}
private Locale locale = Locale.getDefault();
private void sendNotification(final int notificationId,
final String title,
final String message,
final MyEntity entity,
final Resources res,
final String packageName,
final boolean autoClickCancel,
final boolean autoExpiredCancel,
final long expirationFromNow,
final String mapsApiKey,
final boolean mapPicture) {
final double entityLat = entity.getLatitude();
final double entityLng = entity.getLongitude();
Intent mapIntent = null;
try {
String urlAddress = "http://maps.google.com/maps?q=" + entityLat + "," + entityLng;
mapIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlAddress));
} catch (Exception e) {
Log.e(TAG, "error in notification intent preparation", e);
}
PendingIntent pendingIntent = PendingIntent.getActivity(ScannerService.this, 0, mapIntent, PendingIntent.FLAG_CANCEL_CURRENT);
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
int drawable = res.getIdentifier("entity" + String.format(locale, "%04d", entity.getNumber()) + "big", "drawable", packageName);
final NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(ScannerService.this)
.setSmallIcon(drawable)
.setContentTitle(title)
.setContentText(message)
.setAutoCancel(autoClickCancel)
.setSound(defaultSoundUri)
.setPriority(Notification.PRIORITY_HIGH)
.setLights(ContextCompat.getColor(ScannerService.this, R.color.colorPrimary), 500, 2000);
if (mapPicture) {
String imageUrl = "https://maps.googleapis.com/maps/api/staticmap"
+ "?center=" + entityLat + "," + entityLng
+ "&zoom=14"
+ "&scale=false"
+ "&size=450x275"
+ "&maptype=roadmap"
+ "&key=" + mapsApiKey
+ "&format=jpg"
+ "&visual_refresh=true";
Log.i(getClass().getSimpleName(), "generated url for notification image: " + imageUrl);
Bitmap bmURL = getBitmapFromURL(imageUrl);
if (bmURL != null) {
notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(bmURL));
}
}
if (mapIntent != null) {
notificationBuilder.setContentIntent(pendingIntent);
}
if (autoExpiredCancel) {
Log.i(getClass().getSimpleName(), "setting notification timer for expiration, id: " + notificationId + ", expiring in " + expirationFromNow + "ms");
Timer timer = new Timer();
timer.schedule(new TimerTask() {
#Override
public void run() {
Log.i(getClass().getSimpleName(), "canceling notification expired, id: " + notificationId);
notificationManager.cancel(notificationId);
}
}, expirationFromNow);
}
}
// }
private Bitmap getBitmapFromURL(String strURL) {
try {
URL url = new URL(strURL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.connect();
InputStream input = connection.getInputStream();
return BitmapFactory.decodeStream(input);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
Notifications history manager
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class NotificationsHistoryManager {
private final static String PREF_FILE = "nh";
private final static String PREF_FOUND_KEY = "f";
private SharedPreferences pref;
public NotificationsHistoryManager(Context context) {
pref = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE);
}
public void saveAlreadyFoundObjects(List<Long> found) {
Set<String> idsString = new HashSet<>();
int size = found.size();
for (int i = Math.max(0, size - 200); i < size; i++) {
long f = found.get(i);
idsString.add(f + "");
}
pref.edit().putStringSet(PREF_FOUND_KEY, idsString).apply();
}
public List<Long> getAlreadyFoundObjects() {
List<Long> excluded = new ArrayList<>();
for (String id : pref.getStringSet(PREF_FOUND_KEY, new HashSet<String>())) {
try {
excluded.add(Long.parseLong(id));
} catch (Exception e) {
Log.e(getClass().getSimpleName(), "error in parsing string '" + id + "' to long id: " + e.getMessage());
}
}
return excluded;
}
public void clean() {
pref.edit().clear().apply();
}
}
Note: when MainActivity is started, it checks if an instance of the service is running and, if not, it schedules a new one with an AlarmManager. I thought it was the cause of the problem, but the service, as you see, checks every time what has already been notified and skip it.
I tried changing START_STICKY to NOT_STICKY, using preferences to handle duplicate IDs, synchronizing operations... I don't know what else to try. Please, help me :) If you need any more details, just ask.
Thank you!
To share what I found... I understood what the problem is.
Take a look at NotificationsHistoryManager: it uses, to save the list of found object, a Set (the only "list" object type available in SharedPreferences), saving only the last 200 objects found (old ones expires, so it is meaningless to keep them).
THE PROBLEM IS: SET ARE NOT ORDERED LIST. The 200 objects I save are not the LAST added, because when I read them from pref (getAlreadyFoundObjects()) they are written in a set, "randomly" ordered.
I had to change the way I stored them, creating a custom string (comma separated values), to be sure they are saved in the order I want.
Hope it helps someone.
I'm using Smack and Openfire for my XMPP connection/server. But I've run into the very common problem (apparently) of resource conflicts. Can anyone tell me the proper way of handling a conflict?
Openfire is set to always kick the original resource (it's a bespoke platform, not open to the public). But I still get the error and don't get a new connection. My XMPP class is below.
package com.goosesys.gaggle.services;
import java.util.Collection;
import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.ChatManagerListener;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.Roster.SubscriptionMode;
import org.jivesoftware.smack.RosterListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Presence.Type;
import com.google.gson.Gson;
import com.goosesys.gaggle.Globals;
import com.goosesys.gaggle.application.AppSettings;
import com.goosesys.gaggle.application.Utility;
import android.app.Service;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;
public class BackgroundXmppConnector extends Service
{
private ConnectionConfiguration acc;
private XMPPConnection xConnection;
private final IBinder mBinder = new XmppBinder();
private final Handler mHandler = new Handler();
private final int manualReconnectionTimer = (5 * 60 * 1000);
private static int mInterval1m = (2 * 60 * 1000);
private static int mInterval5m = (5 * 60 * 1000);
private static boolean bConnecting = false;
private static final Object connectLock = new Object();
private static final Object checkLock = new Object();
private final Runnable checkConnection = new Runnable()
{
#Override
public void run()
{
synchronized(checkLock)
{
Log.d("BXC", "Handler running - Checking connection");
checkConnectionStatus();
}
}
};
private final Runnable killConnection = new Runnable()
{
#Override
public void run()
{
synchronized(checkLock)
{
Log.d("BXC", "Killing connection and restarting");
// Manually disconnect and restart the connection every 5 minutes
if(xConnection != null)
xConnection.disconnect();
destroyConnectionAndRestart();
new LoginTask().execute();
mHandler.postDelayed(this, mInterval5m);
}
}
};
#Override
public void onCreate()
{
Log.i("BXC", "BackgroundXmppConnector Service has been created");
// Checks the connection state every 1 minute //
mHandler.postDelayed(checkConnection, mInterval1m);
mHandler.postDelayed(killConnection, mInterval5m);
}
#Override
public int onStartCommand(Intent intent, int flags, int startId)
{
Log.d("BXC", "Xmpp Connector Started");
new LoginTask().execute();
return Service.START_STICKY;
}
private void destroyConnectionAndRestart()
{
xConnection.disconnect();
xConnection = null;
Globals.backgroundXmppConnectorRunning = false;
bConnecting = false;
}
private void setupConnection()
{
Log.d("BXC", "Settting up XMPP connection");
try
{
if(!bConnecting && !Globals.backgroundXmppConnectorRunning)
{
acc = new ConnectionConfiguration(AppSettings.XMPP_SERVER_HOST,
AppSettings.XMPP_SERVER_PORT);
acc.setSecurityMode(SecurityMode.disabled);
acc.setSASLAuthenticationEnabled(false);
acc.setReconnectionAllowed(false);
acc.setSendPresence(true);
xConnection = new XMPPConnection(acc);
xConnection.addConnectionListener(new ConnectionListener()
{
#Override
public void connectionClosed()
{
Log.e("BXC", "Xmpp connection closed");
Globals.backgroundXmppConnectorRunning = false;
Globals.numberOfDisconnects += 1;
//destroyConnectionAndRestart();
Utility.writeToLog(getApplicationContext(), "Xmpp Connection closed - disconnected# (" + Globals.numberOfDisconnects + ")");
}
#Override
public void connectionClosedOnError(Exception e)
{
Log.e("BXC", "Xmpp connection closed with error: " + e);
Globals.backgroundXmppConnectorRunning = false;
Globals.numberOfDisconnectsOnError += 1;
// This is more than likely due to a conflict loop - it's best to disconnect and nullify
// our connection and let the software restart when it checks every 5 minutes
if(e.toString().toUpperCase().contains("CONFLICT"))
{
Log.e("BXC", "Conflict connection loop detected - Waiting");
}
Utility.writeToLog(getApplicationContext(), "Xmpp Connection closed with error [" + e + "] - disconnected# (" + Globals.numberOfDisconnectsOnError + ")");
}
#Override
public void reconnectingIn(int seconds)
{
Log.i("BXC", "Xmpp connection, reconnecting in " + seconds + " seconds");
Globals.backgroundXmppConnectorRunning = false;
bConnecting = true;
}
#Override
public void reconnectionFailed(Exception e)
{
Log.e("BXC", "Xmpp reconnection failed: " + e);
Globals.backgroundXmppConnectorRunning = false;
//destroyConnectionAndRestart();
Utility.writeToLog(getApplicationContext(), "Xmpp reConnection failed with error [" + e + "] - disconnected# (" + Globals.numberOfDisconnects + ")");
}
#Override
public void reconnectionSuccessful()
{
Log.i("BXC", "Xmpp reconnected successfully");
Globals.backgroundXmppConnectorRunning = true;
bConnecting = false;
}
});
}
else
{
Log.i("BXC", "Already in connecting state");
}
}
catch (Exception e)
{
Log.e("BXC", e.getMessage());
}
}
public boolean sendMessage(Intent intent)
{
if(xConnection != null && xConnection.isConnected())
{
String jsonObject;
Bundle extras = intent.getExtras();
if(extras != null)
{
jsonObject = extras.getString("MESSAGEDATA");
Message m = new Gson().fromJson(jsonObject, Message.class);
if(m != null)
{
sendMessage(m);
}
else
{
Log.e("BXC", "Message to send was/is null. Can't send.");
}
m = null;
jsonObject = null;
extras = null;
}
Log.i("BXC", "Sending Xmpp Packet");
return true;
}
return false;
}
/*
* Sends message to xmpp server - message packet in form of
*
* --------------------MESSAGE PACKET-------------------------
* TO
* -----------------------
* FROM
* -----------------------
* BODY
* TRANSACTION-------------------------------------------
* MessageType
* --------------------------------------------------
* TransactionObject
*/
private void sendMessage(Message m)
{
try
{
Log.d("BXC", "Sending transaction message to Xmpp Server");
xConnection.sendPacket(m);
//Toast.makeText(getApplicationContext(), "Packet sent to XMPP", Toast.LENGTH_LONG).show();
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
private void checkConnectionStatus()
{
Log.d("BXC", "Checking Xmpp connection status");
if(xConnection == null || xConnection.isAuthenticated() == false ||
xConnection.isConnected() == false || xConnection.isSocketClosed() ||
Globals.backgroundXmppConnectorRunning == false)
{
Log.e("BXC", "Connection to server is dead. Retrying");
Toast.makeText(getApplicationContext(), "Connection dead - retrying", Toast.LENGTH_SHORT).show();
destroyConnectionAndRestart();
new LoginTask().execute();
}
else
{
Log.i("BXC", "Connection appears to be valid");
Toast.makeText(getApplicationContext(), "Connection valid", Toast.LENGTH_SHORT).show();
}
}
// BINDER ////////////////////////////////////////////////////////////////////////////////
#Override
public IBinder onBind(Intent intent)
{
return mBinder;
}
// INTERNAL CLASSES //////////////////////////////////////////////////////////////////////
public class XmppBinder extends Binder
{
public BackgroundXmppConnector getService(){
return BackgroundXmppConnector.this;
}
}
private class LoginTask extends AsyncTask<Void, Void, Void>
{
#Override
protected Void doInBackground(Void... params)
{
// First ensure we've got a connection to work with first
if(Utility.hasActiveInternetConnection(getApplicationContext()) &&
((!bConnecting) || (!Globals.backgroundXmppConnectorRunning)))
{
try
{
//bConnecting = true;
Log.d("BXC", "Beginning connection");
synchronized(connectLock)
{
setupConnection();
xConnection.connect();
Log.i("BXC", "Login credentials: " + Utility.getAndroidID(getApplicationContext()) + " " + AppSettings.XMPP_KEYSTORE_PASSWORD);
xConnection.login(Utility.getAndroidID(getApplicationContext()), AppSettings.XMPP_KEYSTORE_PASSWORD);
xConnection.getChatManager().addChatListener(new ChatManagerListener(){
#Override
public void chatCreated(final Chat chat, boolean createdLocally)
{
if(!createdLocally)
{
// add chat listener //
chat.addMessageListener(new BackgroundMessageListener(getApplicationContext()));
}
}
});
Presence p = new Presence(Presence.Type.subscribe);
p.setStatus("Out and About");
xConnection.sendPacket(p);
Roster r = xConnection.getRoster();
r.setSubscriptionMode(SubscriptionMode.accept_all);
r.createEntry(AppSettings.BOT_NAME, "AbleBot", null);
r.addRosterListener(new RosterListener(){
#Override
public void entriesAdded(Collection<String> addresses)
{
for(String s : addresses)
{
Log.d("BXC", "Entries Added: " + s);
}
}
#Override
public void entriesDeleted(Collection<String> addresses)
{
for(String s : addresses)
{
Log.d("BXC", "Entries Deleted: " + s);
}
}
#Override
public void entriesUpdated(Collection<String> addresses)
{
for(String s : addresses)
{
Log.d("BXC", "Entries updated: " + s);
}
}
#Override
public void presenceChanged(Presence presence)
{
Log.d("BXC", "PresenceChanged: " + presence.getFrom());
}
});
}
}
catch(IllegalStateException ex)
{
Log.e("BXC", "IllegalStateException -->");
if(ex.getMessage().contains("Already logged in to server"))
{
Globals.backgroundXmppConnectorRunning = true;
}
else
{
Globals.backgroundXmppConnectorRunning = false;
Utility.writeExceptionToLog(getApplicationContext(), ex);
ex.printStackTrace();
}
}
catch(XMPPException ex)
{
Log.e("BXC", "XMPPException -->");
Globals.backgroundXmppConnectorRunning = false;
Utility.writeExceptionToLog(getApplicationContext(), ex);
ex.printStackTrace();
}
catch(NullPointerException ex)
{
Log.e("BXC", "NullPointerException -->");
Globals.backgroundXmppConnectorRunning = false;
Utility.writeExceptionToLog(getApplicationContext(), ex);
ex.printStackTrace();
}
catch(Exception ex)
{
Log.e("BXC", "Exception -->");
Globals.backgroundXmppConnectorRunning = false;
Utility.writeToLog(getApplicationContext(), ex.toString());
ex.printStackTrace();
}
return null;
}
else
{
Log.i("BXC", "No active internet data connection - will retry");
}
return null;
}
#Override
protected void onPostExecute(Void ignored)
{
if(xConnection != null)
{
if(xConnection.isConnected() && (!xConnection.isSocketClosed()))
{
Log.i("BXC", "Logged in to XMPP Server");
Globals.backgroundXmppConnectorRunning = true;
mHandler.postDelayed(checkConnection, mInterval1m);
}
else
{
Log.e("BXC", "Unable to log into XMPP Server.");
Globals.backgroundXmppConnectorRunning = false;
destroyConnectionAndRestart();
}
}
else
{
Log.e("BXC", "Xmpp Connection object is null");
Globals.backgroundXmppConnectorRunning = false;
}
}
}
}
From what I've read, openfire (when set to always kick) will always kick the original resource and then allow the new login to connect, but I'm not seeing this at all. Any help is greatly appreciated. Thanks.
The proper way to deal with resource conflicts is to not use a fixed resource unless you absolutely must. If your client specifies no resource when logging in, the server should assign it a randomly generated one which will never conflict.
There are very few reasons why you'd ever need to specify a fixed resource, as most clients hide the resource of your contacts anyway, and there are other reasons why having a new resource on every connection is advantageous (like avoiding a common bug with group chats getting out of sync because the group chat's server didn't realize the connecting user is actually a new session).
A big problem with fixed resources is reconnection loops, where, if the server is configured to kick the old conflicting resource, two clients kick each other repeatedly. You should make sure you don't automatically reconnect when receiving a resource conflict error.
From what I've read, openfire (when set to always kick) will always kick the original resource and then allow the new login to connect,…
Terminating the other connection with the same resource, ie. the previous one, is one option to deal with it. Most, if not all, XMPP servers provide this policy option regarding handling resource conflicts.
I can't comment on why setting Openfire to "Always kick" doesn't work for you, but it certainly does for me. And IIRC there are no current bug reports in Openfire that tell otherwise.
There is also another trivial approach: If you get a resource conflict on login(), simply specify a different resource: login(String username, String password, String resource).
Or even more trivial: If you don't care how what the resource String is, you can have the server auto assign one to you. Simply use 'null' as resource argument: login(user, password, null).
I am looking for and example of casting an image to chromecast in android. Oddly enough it doesn't seem like this is covered in the googlecast sample repositories. Does anyone have a simple implementation of this? I basically would like to click on an image in my app's photo gallery on my android device and have it cast to the screen.
One side question is, does the image need to be at a url? or is it possible to stream the image to the device? I appreciate the help in advance.
I've solved this without the CastCompanionLibrary, but based on google's CastHelloText-android sample. Basically what I did was:
encode an image into a base64 string and send it as a message to a custom receiver
modify the sample's receiver to receive a base64 string and set it as the image source.
upload and register my receiver and have the application use the generated application id
This is the code for the receiver:
<!DOCTYPE html>
<html>
<head>
<style>
img#androidImage {
height:auto;
width:100%;
}
</style>
<title>Cast Hello Text</title>
</head>
<body>
<img id="androidImage" src="" />
<script type="text/javascript" src="//www.gstatic.com/cast/sdk/libs/receiver/2.0.0/cast_receiver.js"></script>
<script type="text/javascript">
window.onload = function() {
cast.receiver.logger.setLevelValue(0);
window.castReceiverManager = cast.receiver.CastReceiverManager.getInstance();
console.log('Starting Receiver Manager');
// handler for the 'ready' event
castReceiverManager.onReady = function(event) {
console.log('Received Ready event: ' + JSON.stringify(event.data));
window.castReceiverManager.setApplicationState("Application status is ready...");
};
// handler for 'senderconnected' event
castReceiverManager.onSenderConnected = function(event) {
console.log('Received Sender Connected event: ' + event.data);
console.log(window.castReceiverManager.getSender(event.data).userAgent);
};
// handler for 'senderdisconnected' event
castReceiverManager.onSenderDisconnected = function(event) {
console.log('Received Sender Disconnected event: ' + event.data);
if (window.castReceiverManager.getSenders().length == 0) {
window.close();
}
};
// handler for 'systemvolumechanged' event
castReceiverManager.onSystemVolumeChanged = function(event) {
console.log('Received System Volume Changed event: ' + event.data['level'] + ' ' +
event.data['muted']);
};
// create a CastMessageBus to handle messages for a custom namespace
window.messageBus =
window.castReceiverManager.getCastMessageBus(
'urn:x-cast:com.google.cast.sample.helloworld');
// handler for the CastMessageBus message event
window.messageBus.onMessage = function(event) {
console.log('Message recieved');
var obj = JSON.parse(event.data)
console.log('Message type: ' + obj.type);
if (obj.type == "text") {
console.log('Skipping message: ' + obj.data);
}
if (obj.type == "image") {
var source = 'data:image/png;base64,'.concat(obj.data)
displayImage(source);
}
// inform all senders on the CastMessageBus of the incoming message event
// sender message listener will be invoked
window.messageBus.send(event.senderId, event.data);
}
// initialize the CastReceiverManager with an application status message
window.castReceiverManager.start({statusText: "Application is starting"});
console.log('Receiver Manager started');
};
function displayImage(source) {
console.log('received image');
document.getElementById("androidImage").src=source;
window.castReceiverManager.setApplicationState('image source changed');
};
</script>
</body>
</html>
Below is the modified MainActivity.java code. Don't forget to modify the app_id in string.xml once your receiver application is registered.
2 notes:
The sent messages are wrapped in a JSON object so I can filter out
the text messages.
The ENCODED_IMAGE_STRING variable isn't defined in this
example, you'll have to find an image and convert it to a base64 string yourself.
MainActivity.java:
package com.example.casthelloworld;
import java.io.IOException;
import java.util.ArrayList;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.speech.RecognizerIntent;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.app.MediaRouteActionProvider;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter;
import android.support.v7.media.MediaRouter.RouteInfo;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;
import com.google.android.gms.cast.ApplicationMetadata;
import com.google.android.gms.cast.Cast;
import com.google.android.gms.cast.Cast.ApplicationConnectionResult;
import com.google.android.gms.cast.Cast.MessageReceivedCallback;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Status;
/**
* Main activity to send messages to the receiver.
*/
public class MainActivity extends ActionBarActivity {
private static final String TAG = MainActivity.class.getSimpleName();
private static final int REQUEST_CODE = 1;
private MediaRouter mMediaRouter;
private MediaRouteSelector mMediaRouteSelector;
private MediaRouter.Callback mMediaRouterCallback;
private CastDevice mSelectedDevice;
private GoogleApiClient mApiClient;
private Cast.Listener mCastListener;
private ConnectionCallbacks mConnectionCallbacks;
private ConnectionFailedListener mConnectionFailedListener;
private HelloWorldChannel mHelloWorldChannel;
private boolean mApplicationStarted;
private boolean mWaitingForReconnect;
private String mSessionId;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ActionBar actionBar = getSupportActionBar();
actionBar.setBackgroundDrawable(new ColorDrawable(
android.R.color.transparent));
// When the user clicks on the button, use Android voice recognition to
// get text
Button voiceButton = (Button) findViewById(R.id.voiceButton);
voiceButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
startVoiceRecognitionActivity();
}
});
// When the user clicks on the button, use Android voice recognition to
// get text
Button yarrButton = (Button) findViewById(R.id.tmpButton);
yarrButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
castImage();
}
});
// Configure Cast device discovery
mMediaRouter = MediaRouter.getInstance(getApplicationContext());
mMediaRouteSelector = new MediaRouteSelector.Builder()
.addControlCategory(
CastMediaControlIntent.categoryForCast(getResources()
.getString(R.string.app_id))).build();
mMediaRouterCallback = new MyMediaRouterCallback();
}
private void castImage()
{
Log.d(TAG, "castImage()");
String image_string = createJsonMessage(MessageType.image, ENCODED_IMAGE_STRING);
sendMessage(image_string);
}
/**
* Android voice recognition
*/
private void startVoiceRecognitionActivity() {
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
intent.putExtra(RecognizerIntent.EXTRA_PROMPT,
getString(R.string.message_to_cast));
startActivityForResult(intent, REQUEST_CODE);
}
/*
* Handle the voice recognition response
*
* #see android.support.v4.app.FragmentActivity#onActivityResult(int, int,
* android.content.Intent)
*/
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
ArrayList<String> matches = data
.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
if (matches.size() > 0) {
Log.d(TAG, matches.get(0));
String message = createJsonMessage(MessageType.text, matches.get(0));
sendMessage(message);
}
}
super.onActivityResult(requestCode, resultCode, data);
}
#Override
protected void onResume() {
super.onResume();
// Start media router discovery
mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback,
MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
}
#Override
protected void onPause() {
if (isFinishing()) {
// End media router discovery
mMediaRouter.removeCallback(mMediaRouterCallback);
}
super.onPause();
}
#Override
public void onDestroy() {
teardown();
super.onDestroy();
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.main, menu);
MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item);
MediaRouteActionProvider mediaRouteActionProvider = (MediaRouteActionProvider) MenuItemCompat
.getActionProvider(mediaRouteMenuItem);
// Set the MediaRouteActionProvider selector for device discovery.
mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector);
return true;
}
/**
* Callback for MediaRouter events
*/
private class MyMediaRouterCallback extends MediaRouter.Callback {
#Override
public void onRouteSelected(MediaRouter router, RouteInfo info) {
Log.d(TAG, "onRouteSelected");
// Handle the user route selection.
mSelectedDevice = CastDevice.getFromBundle(info.getExtras());
launchReceiver();
}
#Override
public void onRouteUnselected(MediaRouter router, RouteInfo info) {
Log.d(TAG, "onRouteUnselected: info=" + info);
teardown();
mSelectedDevice = null;
}
}
/**
* Start the receiver app
*/
private void launchReceiver() {
try {
mCastListener = new Cast.Listener() {
#Override
public void onApplicationDisconnected(int errorCode) {
Log.d(TAG, "application has stopped");
teardown();
}
};
// Connect to Google Play services
mConnectionCallbacks = new ConnectionCallbacks();
mConnectionFailedListener = new ConnectionFailedListener();
Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions
.builder(mSelectedDevice, mCastListener);
mApiClient = new GoogleApiClient.Builder(this)
.addApi(Cast.API, apiOptionsBuilder.build())
.addConnectionCallbacks(mConnectionCallbacks)
.addOnConnectionFailedListener(mConnectionFailedListener)
.build();
mApiClient.connect();
} catch (Exception e) {
Log.e(TAG, "Failed launchReceiver", e);
}
}
/**
* Google Play services callbacks
*/
private class ConnectionCallbacks implements
GoogleApiClient.ConnectionCallbacks {
#Override
public void onConnected(Bundle connectionHint) {
Log.d(TAG, "onConnected");
if (mApiClient == null) {
// We got disconnected while this runnable was pending
// execution.
return;
}
try {
if (mWaitingForReconnect) {
mWaitingForReconnect = false;
// Check if the receiver app is still running
if ((connectionHint != null)
&& connectionHint
.getBoolean(Cast.EXTRA_APP_NO_LONGER_RUNNING)) {
Log.d(TAG, "App is no longer running");
teardown();
} else {
// Re-create the custom message channel
try {
Cast.CastApi.setMessageReceivedCallbacks(
mApiClient,
mHelloWorldChannel.getNamespace(),
mHelloWorldChannel);
} catch (IOException e) {
Log.e(TAG, "Exception while creating channel", e);
}
}
} else {
// Launch the receiver app
Cast.CastApi
.launchApplication(mApiClient,
getString(R.string.app_id), false)
.setResultCallback(
new ResultCallback<Cast.ApplicationConnectionResult>() {
#Override
public void onResult(
ApplicationConnectionResult result) {
Status status = result.getStatus();
Log.d(TAG,
"ApplicationConnectionResultCallback.onResult: statusCode "
+ status.getStatusCode());
if (status.isSuccess()) {
ApplicationMetadata applicationMetadata = result
.getApplicationMetadata();
mSessionId = result
.getSessionId();
String applicationStatus = result
.getApplicationStatus();
boolean wasLaunched = result
.getWasLaunched();
Log.d(TAG,
"application name: "
+ applicationMetadata
.getName()
+ ", status: "
+ applicationStatus
+ ", sessionId: "
+ mSessionId
+ ", wasLaunched: "
+ wasLaunched);
mApplicationStarted = true;
// Create the custom message
// channel
mHelloWorldChannel = new HelloWorldChannel();
try {
Cast.CastApi
.setMessageReceivedCallbacks(
mApiClient,
mHelloWorldChannel
.getNamespace(),
mHelloWorldChannel);
} catch (IOException e) {
Log.e(TAG,
"Exception while creating channel",
e);
}
// set the initial instructions
// on the receiver
String message = createJsonMessage(MessageType.text, getString(R.string.instructions));
sendMessage(message);
} else {
Log.e(TAG,
"application could not launch");
teardown();
}
}
});
}
} catch (Exception e) {
Log.e(TAG, "Failed to launch application", e);
}
}
#Override
public void onConnectionSuspended(int cause) {
Log.d(TAG, "onConnectionSuspended");
mWaitingForReconnect = true;
}
}
/**
* Google Play services callbacks
*/
private class ConnectionFailedListener implements
GoogleApiClient.OnConnectionFailedListener {
#Override
public void onConnectionFailed(ConnectionResult result) {
Log.e(TAG, "onConnectionFailed ");
teardown();
}
}
/**
* Tear down the connection to the receiver
*/
private void teardown() {
Log.d(TAG, "teardown");
if (mApiClient != null) {
if (mApplicationStarted) {
if (mApiClient.isConnected() || mApiClient.isConnecting()) {
try {
Cast.CastApi.stopApplication(mApiClient, mSessionId);
if (mHelloWorldChannel != null) {
Cast.CastApi.removeMessageReceivedCallbacks(
mApiClient,
mHelloWorldChannel.getNamespace());
mHelloWorldChannel = null;
}
} catch (IOException e) {
Log.e(TAG, "Exception while removing channel", e);
}
mApiClient.disconnect();
}
mApplicationStarted = false;
}
mApiClient = null;
}
mSelectedDevice = null;
mWaitingForReconnect = false;
mSessionId = null;
}
/**
* Send a text message to the receiver
*
* #param message
*/
private void sendMessage(String message) {
if (mApiClient != null && mHelloWorldChannel != null) {
try {
Cast.CastApi.sendMessage(mApiClient,
mHelloWorldChannel.getNamespace(), message)
.setResultCallback(new ResultCallback<Status>() {
#Override
public void onResult(Status result) {
if (!result.isSuccess()) {
Log.e(TAG, "Sending message failed");
}
}
});
} catch (Exception e) {
Log.e(TAG, "Exception while sending message", e);
}
} else {
Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT)
.show();
}
}
/**
* Custom message channel
*/
class HelloWorldChannel implements MessageReceivedCallback {
/**
* #return custom namespace
*/
public String getNamespace() {
return getString(R.string.namespace);
}
/*
* Receive message from the receiver app
*/
#Override
public void onMessageReceived(CastDevice castDevice, String namespace,
String message) {
Log.d(TAG, "onMessageReceived: " + message);
}
}
enum MessageType {
text,
image,
}
public static Bitmap getBitmapFromView(View view) {
//Define a bitmap with the same size as the view
Bitmap returnedBitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),Bitmap.Config.ARGB_8888);
//Bind a canvas to it
Canvas canvas = new Canvas(returnedBitmap);
//Get the view's background
Drawable bgDrawable =view.getBackground();
if (bgDrawable!=null)
//has background drawable, then draw it on the canvas
bgDrawable.draw(canvas);
else
//does not have background drawable, then draw white background on the canvas
canvas.drawColor(Color.WHITE);
// draw the view on the canvas
view.draw(canvas);
//return the bitmap
return returnedBitmap;
}
private static String createJsonMessage(MessageType type, String message)
{
return String.format("{\"type\":\"%s\", \"data\":\"%s\"}", type.toString(), message);
}
}
Since on Chromecast your application is running inside a web browser, you need to have an <img/> tag show the image. The src attribute of that tag should point to the image that you want to see and it has to be a url, so if your image is residing on your phone's local storage, you need to start a small web server in your mobile application to serve that image and communicate with the receiver what url it should point at (which would be the url at which your server is serving that image). these are all doable and you can use the CastCompanionLibrary, if you want, to communicate with your custom receiver; simply use the DataCastManager class instead of VideoCastManager.
May be my answer will be helpful for other developers, because I also did'nt found good solution and done it by myself.
For showing image via Google Cast on your device screen from your app you can create and start simply web server from your app which will process http requests with selected image name or id in URL.
Example:
public class MyWebServer {
private Activity activity;
private static ServerSocket httpServerSocket;
private static boolean isWebServerSunning;
public static final String drawableDelimiter = "pic-"
public MyWebServer(Activity activity) {
this.activity = activity;
}
public void stopWebServer() {
isWebServerSunning = false;
try {
if (httpServerSocket != null) {
httpServerSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void startWebServer() {
isWebServerSunning = true;
Thread webServerThread = new Thread(() -> {
Socket socket;
HttpResponseThread httpResponseThread;
try {
httpServerSocket = new ServerSocket(5050);
while (isWebServerSunning) {
socket = httpServerSocket.accept();
httpResponseThread = new HttpResponseThread(socket);
httpResponseThread.start();
}
} catch (Exception e) {
e.printStackTrace();
}
});
webServerThread.start();
}
private class HttpResponseThread extends Thread {
Socket clientSocket;
HttpResponseThread(Socket socket) {
this.clientSocket = socket;
}
#Override
public void run() {
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
OutputStream outputStream = clientSocket.getOutputStream();
) {
String input = bufferedReader.readLine();
if (input != null && !input.isEmpty() && input.contains("/") && input.contains(" ")) {
if (input.contains(drawableDelimiter)) {
String imageId = input.substring(input.indexOf("/") + 1, input.lastIndexOf(" ")).trim().split(drawableDelimiter)[1];
Bitmap bitmap = BitmapFactory.decodeResource(activity.getResources(), Integer.parseInt(imageId));
if (bitmap != null) {
ByteArrayOutputStream bitmapBytes = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, bitmapBytes);
outputStream.write("HTTP/1.0 200 OK\r\n".getBytes());
outputStream.write("Server: Apache/0.8.4\r\n".getBytes());
outputStream.write(("Content-Length: " + bitmapBytes.toByteArray().length + "\r\n").getBytes());
outputStream.write("\r\n".getBytes());
outputStream.write(bitmapBytes.toByteArray());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
And just start or stop your web server at when Google Cast will be useable or stopped.
MyWebServer myWebServer = new MyWebServer(this); // pass your activity here
myWebServer.startWebServer();
myWebServer.stopWebServer();
I have an activity that displays sensor values and sends them to a synchronous queue, from the onSensorChanged method. I have a timeout on the publish to queue so that the onSensorChanged method does not block, when the queue is blocking. I am then expecting that the onPause method will be called when the back button is pressed, however it is not and the screen just hangs without returning to the previous screen. Any idea why this is happening?
Btw, when the queue does not block (data is removed by the subscriber) then all works as expected, onPause is called when the back button is pressed.
public void onSensorChanged(SensorEvent event) {
TextView tvX = (TextView) findViewById(R.id.textViewRemLinAccX);
TextView tvY = (TextView) findViewById(R.id.textViewRemLinAccY);
TextView tvZ = (TextView) findViewById(R.id.textViewRemLinAccZ);
String x = String.format(format, event.values[0]);
String y = String.format(format, event.values[1]);
String z = String.format(format, event.values[2]);
tvX.setText(x);
tvY.setText(y);
tvZ.setText(z);
try {
if (btConnection.isRunning()) {
Log.i(TAG, "+++ queue values");
queue.offer(constructData(x, y, z), 1, TimeUnit.SECONDS);
}
} catch (InterruptedException e) {
Log.e(TAG, "+++ err " + e.toString());
}
}
If you are blocking the UI with other operations (when you execute something on the main thread), then the back button will not work, you should do any operation that takes time on a background thread.
This appears to be resolved though I am not quite sure how I did it, so have included the code for others to use. I did a lot of restructuring to ensure that connection resources were cleaned up properly and now the onPause, and onDestroy, methods are called when the back button is pressed.
Fyi, this activity opens a bluetooth connection and sends sensor data to another computer to be used to control a LEGO NXT robot.
package uk.co.moonsit.apps.sensors.remote;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import uk.co.moonsit.apps.sensors.R;
import uk.co.moonsit.bluetooth.BluetoothConnection;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.Menu;
import android.view.WindowManager;
import android.widget.TextView;
public class RemoteLinearAccelerationActivity extends Activity implements
SensorEventListener {
private static final String TAG = "RemoteLinearAccelerationActivity";
private BlockingQueue<String> queue;
private SensorManager mSensorManager;
private Sensor mLinAcc;
private String format = "%.3f";
private String type;
private BluetoothConnection btConnection;
private String delimiter = "|";
private String cr;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_remote_linear_acceleration);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
byte[] crb = new byte[1];
crb[0] = 13;
cr = new String(crb);
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
mLinAcc = mSensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION);
queue = new SynchronousQueue<String>();
type = "LinAcc";
btConnection = new BluetoothConnection(queue, "00001101-0000-1000-8000-00805F9B34FB", "<MAC address here>", "11", "28,13");
Log.i(TAG, "+++ onCreate ");
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.remote_linear_acceleration, menu);
return true;
}
#Override
public void onAccuracyChanged(Sensor arg0, int arg1) {
}
#Override
public void onSensorChanged(SensorEvent event) {
TextView tvX = (TextView) findViewById(R.id.textViewRemLinAccX);
TextView tvY = (TextView) findViewById(R.id.textViewRemLinAccY);
TextView tvZ = (TextView) findViewById(R.id.textViewRemLinAccZ);
String x = String.format(format, event.values[0]);
String y = String.format(format, event.values[1]);
String z = String.format(format, event.values[2]);
tvX.setText(x);
tvY.setText(y);
tvZ.setText(z);
try {
String msg = constructData(x, y, z);
if (btConnection.isRunning()) {
Log.i(TAG, "+++ queue values");
queue.offer(msg, 10, TimeUnit.SECONDS);
}
} catch (InterruptedException e) {
Log.e(TAG, "+++ err " + e.toString());
}
}
private String constructData(String x, String y, String z) {
StringBuilder sb = new StringBuilder();
sb.append(type + delimiter);
sb.append(x + delimiter);
sb.append(y + delimiter);
sb.append(z);
sb.append(cr);
return sb.toString();
}
#Override
protected void onPause() {
super.onPause();
Log.i(TAG, "+++ onPause unregisterListener ");
mSensorManager.unregisterListener(this);
}
#Override
protected void onResume() {
super.onResume();
Log.i(TAG, "+++ onResume registerListener ");
mSensorManager.registerListener(this, mLinAcc, SensorManager.SENSOR_DELAY_NORMAL);
Log.i(TAG, "+++ onResume start btConnection");
new Thread(btConnection).start();
}
#Override
protected void onDestroy() {
super.onDestroy();
Log.i(TAG, "+++ onDestroy closing btConnection");
btConnection.stop();
}
}
package uk.co.moonsit.bluetooth;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import uk.co.moonsit.messaging.BeginEndEnvelope;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
//import android.os.ParcelUuid;
import android.util.Log;
public class BluetoothConnection implements Runnable {
private static final String TAG = "BluetoothConnection";
private final BlockingQueue<String> queue;
private BluetoothAdapter mBluetoothAdapter;
private BluetoothDevice device;
private BluetoothSocket clientSocket;
private DataInputStream in = null;
private DataOutputStream out = null;
private String address;
private boolean isConnected = false;
private BeginEndEnvelope envelope;
private String uuid;
private boolean isRunning = true;
public BluetoothConnection(BlockingQueue<String> q, String ud, String a,
String start, String end) {
uuid = ud;
queue = q;
address = a;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
envelope = new BeginEndEnvelope(start, end);
}
private void getDevice() throws IOException {
device = mBluetoothAdapter.getRemoteDevice(address);
}
private void getSocket() throws IOException {
clientSocket = device.createRfcommSocketToServiceRecord(UUID.fromString(uuid));
mBluetoothAdapter.cancelDiscovery();
}
private boolean connect() {
if (!isConnected) {
Log.i(TAG, "+++ connecting");
try {
getSocket();
Log.i(TAG, "+++ b4 connect");
clientSocket.connect();
Log.i(TAG, "+++ connected");
isConnected = true;
in = new DataInputStream(clientSocket.getInputStream());
out = new DataOutputStream(clientSocket.getOutputStream());
Log.i(TAG, "+++ streams created");
} catch (IOException e) {
Long sleep = (long) 10000;
Log.e(TAG, "+++ connection failed, sleep for " + sleep);
try {
Thread.sleep(sleep);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
return isConnected;
}
public void run() {
try {
getDevice();
} catch (IOException e) {
Log.e(TAG, "+++ device error " + e.toString());
}
while (isRunning) {
try {
processData();
} catch (Exception e) {
Log.e(TAG, "+++ data error " + e.toString());
}
}
close();
Log.i(TAG, "+++ ending bluetooth run");
}
private void closeSocket() {
if (clientSocket != null)
try {
clientSocket.close();
Log.d(TAG, "+++ socket closed");
} catch (IOException e) {
e.printStackTrace();
}
}
private void closeStreams() {
if (in != null)
try {
in.close();
Log.d(TAG, "+++ input stream closed");
} catch (IOException e) {
Log.e(TAG, "+++ input stream not closed " + e.toString());
}
if (out != null)
try {
out.close();
Log.d(TAG, "+++ output stream closed");
} catch (IOException e) {
Log.e(TAG, "+++ output stream not closed " + e.toString());
}
}
private void close() {
closeStreams();
closeSocket();
isConnected = false;
}
public void stop() {
isRunning = false;
}
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
private void processData() throws Exception {
try {
String outData = null;
int timer = 0;
while (outData == null) {
if (!connect())
return;
Log.i(TAG, "+++ waiting on queue ");
outData = queue.poll(1, TimeUnit.SECONDS);// .take();
if (timer++ > 15) {
return;
}
}
envelope.sendMessage(outData, out);
String inData = envelope.receiveMessage(in);
Log.i(TAG, "+++ response " + inData);
} catch (Exception e) {
Log.e(TAG, "+++ processData error " + e.toString());
close();
throw e;
}
}
}