I'm trying to implement offline DRM support for ExoPlayer 2 but I have some problems.
I found this conversation. There is some implementation for ExoPlayer 1.x and some steps how to work that implementation with ExoPlayer 2.x.
I have I problem with OfflineDRMSessionManager whitch implements DrmSessionManager. In that example is DrmSessionManager imported from ExoPlayer 1.x. If I import it from ExoPlayer 2 then I have a problems to compile it. I have a problem with #Override methods (open(), close(), ..) which are NOT in that new DrmSessionManager and there are some new methods: acquireSession(), ... .
With the latest release of ExoPlayer 2.2.0 , it provides this facility inbuilt in ExoPlayer. ExoPlayer has a helper class to download and refresh offline license keys. It should be the preferred way to do this.
OfflineLicenseHelper.java
/**
* Helper class to download, renew and release offline licenses. It utilizes {#link
* DefaultDrmSessionManager}.
*/
public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
You can access the latest code from the ExoPlayer repo
I created a sample application for Offline playback of DRM content.You can access it from here
As #TheJango explained, with the latest release of ExoPlayer 2.2.0 , it provides this facility inbuilt in ExoPlayer.
However, the OfflineLicenseHelper class was designed with some VOD use case in mind. Buy a movie, save the license (download method), download the movie, load the license in a DefaultDrmSessionManager and then setMode for playback.
Another use case could be that you want to make an online streaming system where different content is using the same license (e.g. Television) for quite some time (e.g. 24hours) more intelligent. So that it never downloads a license which it already has (Suppose your DRM system charges you per license request and there will be a lot of requests for the same license otherwise), the following approach can be used with ExoPlayer 2.2.0. It took me some time to get a working solution without modifying anything to the ExoPlayer source. I don't quite like the approach they've taken with the setMode() method which can only be called once. Previously DrmSessionManagers would work for multiple sessions (audio, video) and now they no longer work if licenses differ or come from different methods (DOWNLOAD, PLAYBACK, ...). Anyway, I introduced a new class CachingDefaultDrmSessionManager to replace the DefaultDrmSessionManager you are probably using. Internally it delegates to a DefaultDrmSessionManager.
package com.google.android.exoplayer2.drm;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.concurrent.atomic.AtomicBoolean;
import android.os.Handler;
import android.os.Looper;
import android.util.Base64;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
import java.util.HashMap;
import java.util.UUID;
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_DOWNLOAD;
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_QUERY;
public class CachingDefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T> {
private final SharedPreferences drmkeys;
public static final String TAG="CachingDRM";
private final DefaultDrmSessionManager<T> delegateDefaultDrmSessionManager;
private final UUID uuid;
private final AtomicBoolean pending = new AtomicBoolean(false);
private byte[] schemeInitD;
public interface EventListener {
void onDrmKeysLoaded();
void onDrmSessionManagerError(Exception e);
void onDrmKeysRestored();
void onDrmKeysRemoved();
}
public CachingDefaultDrmSessionManager(Context context, UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters, final Handler eventHandler, final EventListener eventListener) {
this.uuid = uuid;
DefaultDrmSessionManager.EventListener eventListenerInternal = new DefaultDrmSessionManager.EventListener() {
#Override
public void onDrmKeysLoaded() {
saveDrmKeys();
pending.set(false);
if (eventListener!=null) eventListener.onDrmKeysLoaded();
}
#Override
public void onDrmSessionManagerError(Exception e) {
pending.set(false);
if (eventListener!=null) eventListener.onDrmSessionManagerError(e);
}
#Override
public void onDrmKeysRestored() {
saveDrmKeys();
pending.set(false);
if (eventListener!=null) eventListener.onDrmKeysRestored();
}
#Override
public void onDrmKeysRemoved() {
pending.set(false);
if (eventListener!=null) eventListener.onDrmKeysRemoved();
}
};
delegateDefaultDrmSessionManager = new DefaultDrmSessionManager<T>(uuid, mediaDrm, callback, optionalKeyRequestParameters, eventHandler, eventListenerInternal);
drmkeys = context.getSharedPreferences("drmkeys", Context.MODE_PRIVATE);
}
final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public void saveDrmKeys() {
byte[] offlineLicenseKeySetId = delegateDefaultDrmSessionManager.getOfflineLicenseKeySetId();
if (offlineLicenseKeySetId==null) {
Log.i(TAG,"Failed to download offline license key");
} else {
Log.i(TAG,"Storing downloaded offline license key for "+bytesToHex(schemeInitD)+": "+bytesToHex(offlineLicenseKeySetId));
storeKeySetId(schemeInitD, offlineLicenseKeySetId);
}
}
#Override
public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) {
if (pending.getAndSet(true)) {
return delegateDefaultDrmSessionManager.acquireSession(playbackLooper, drmInitData);
}
// First check if we already have this license in local storage and if it's still valid.
DrmInitData.SchemeData schemeData = drmInitData.get(uuid);
schemeInitD = schemeData.data;
Log.i(TAG,"Request for key for init data "+bytesToHex(schemeInitD));
if (Util.SDK_INT < 21) {
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitD, C.WIDEVINE_UUID);
if (psshData == null) {
// Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
} else {
schemeInitD = psshData;
}
}
byte[] cachedKeySetId=loadKeySetId(schemeInitD);
if (cachedKeySetId!=null) {
//Load successful.
Log.i(TAG,"Cached key set found "+bytesToHex(cachedKeySetId));
if (!Arrays.equals(delegateDefaultDrmSessionManager.getOfflineLicenseKeySetId(), cachedKeySetId))
{
delegateDefaultDrmSessionManager.setMode(MODE_QUERY, cachedKeySetId);
}
} else {
Log.i(TAG,"No cached key set found ");
delegateDefaultDrmSessionManager.setMode(MODE_DOWNLOAD,null);
}
DrmSession<T> tDrmSession = delegateDefaultDrmSessionManager.acquireSession(playbackLooper, drmInitData);
return tDrmSession;
}
#Override
public void releaseSession(DrmSession<T> drmSession) {
pending.set(false);
delegateDefaultDrmSessionManager.releaseSession(drmSession);
}
public void storeKeySetId(byte[] initData, byte[] keySetId) {
String encodedInitData = Base64.encodeToString(initData, Base64.NO_WRAP);
String encodedKeySetId = Base64.encodeToString(keySetId, Base64.NO_WRAP);
drmkeys.edit()
.putString(encodedInitData, encodedKeySetId)
.apply();
}
public byte[] loadKeySetId(byte[] initData) {
String encodedInitData = Base64.encodeToString(initData, Base64.NO_WRAP);
String encodedKeySetId = drmkeys.getString(encodedInitData, null);
if (encodedKeySetId == null) return null;
return Base64.decode(encodedKeySetId, 0);
}
}
Here keys are persisted as Base64 encoded strings in local storage. Because for a typical DASH stream both audio and video renderers will request a license from the DrmSessionManager, possibly at the same time, the AtomicBoolean is used. If audio and or video would use different keys, I think this approach would fail.
Also I am not yet checking for expired keys here. Have a look at OfflineLicenseHelper to see how to deal with those.
#Pepa Zapletal, proceed with below changes to play in offline.
You can also see the updated answer here.
Changes are as follows :
Changed signature of the method private void onKeyResponse(Object response) to private void onKeyResponse(Object response, boolean offline)
Rather than sending the file manifest URI send stored file path to PlayerActivity.java.
Change MediaDrm.KEY_TYPE_STREAMING to MediaDrm.KEY_TYPE_OFFLINE in getKeyRequest().
In postKeyRequest() first check whether the key is stored or not, if key found then directly call onKeyResponse(key, true).
In onKeyResponse(), call restoreKeys() rather than calling provideKeyResponse().
The rest everything is same, now your file will be playing.
Major role : Here provideKeyResponse() and restoreKeys() are native methods which acts major role in getting the key and restoring the key.
provideKeyResponse() method which will return us the main License key in byte array if and only if the keyType is MediaDrm.KEY_TYPE_OFFLINE else this method will return us the empty byte array with which we can do nothing with that array.
restoreKeys() method will expect the key which is to be restored for the current session, so feed the key which we have already stored in local to this method and it will take care of it.
Note : First you have to somehow download the license key and store it somewhere in local device securely.
In my case first im playing the file online, so exoplayer will fetch the key that key i have stored in local. From second time onwards first it will check whether the key is stored or not, if key found it will skip the License key request and will the play the file.
Replace the methods and inner classes of StreamingDrmSessionManager.java with these things.
private void postKeyRequest() {
KeyRequest keyRequest;
try {
// check is key exist in local or not, if exist no need to
// make a request License server for the key.
byte[] keyFromLocal = Util.getKeyFromLocal();
if(keyFromLocal != null) {
onKeyResponse(keyFromLocal, true);
return;
}
keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData.data, schemeData.mimeType, MediaDrm.KEY_TYPE_OFFLINE, optionalKeyRequestParameters);
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
} catch (NotProvisionedException e) {
onKeysError(e);
}
}
private void onKeyResponse(Object response, boolean offline) {
if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
// This event is stale.
return;
}
if (response instanceof Exception) {
onKeysError((Exception) response);
return;
}
try {
// if we have a key and we want to play offline then call
// 'restoreKeys()' with the key which we have already stored.
// Here 'response' is the stored key.
if(offline) {
mediaDrm.restoreKeys(sessionId, (byte[]) response);
} else {
// Don't have any key in local, so calling 'provideKeyResponse()' to
// get the main License key and store the returned key in local.
byte[] bytes = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
Util.storeKeyInLocal(bytes);
}
state = STATE_OPENED_WITH_KEYS;
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
#Override
public void run() {
eventListener.onDrmKeysLoaded();
}
});
}
} catch (Exception e) {
onKeysError(e);
}
}
#SuppressLint("HandlerLeak")
private class PostResponseHandler extends Handler {
public PostResponseHandler(Looper looper) {
super(looper);
}
#Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_PROVISION:
onProvisionResponse(msg.obj);
break;
case MSG_KEYS:
// We don't have key in local so calling 'onKeyResponse()' with offline to 'false'.
onKeyResponse(msg.obj, false);
break;
}
}
}
Related
I am trying to find a way to have a tablet basically auto-accept/give permission to accept an incoming Bluetooth Share file transferred from a laptop to my Nexus Android device. Since the Android bluetooth system itself does not support this feature, I was wondering if this could be done programmatically using an Android application that listened for the notification and accepted it for me.
MUCH EASIER WAY
If you have a rooted device and use XPosed Framework, your goal can be achieved much easier.
You Need not implement your own bluetooth server nor kill the original BT service, which are very bothering!!!
xposed tutorial link.
Try this code.
import android.util.*;
import de.robv.android.xposed.*;
import de.robv.android.xposed.callbacks.XC_LoadPackage.*;
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
public class Tutorial implements IXposedHookLoadPackage
{
private String TAG="TUTORIAL";
public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
if (!lpparam.packageName.equals("com.android.bluetooth"))
{
Log.i(TAG,"Not: "+lpparam.packageName);
return;
}
Log.i(TAG,"Yes "+lpparam.packageName);
findAndHookMethod("com.android.bluetooth.opp.BluetoothOppManager", lpparam.classLoader, "isWhitelisted", String.class,new XC_MethodHook() {
#Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Log.v(TAG,"HOOK DONE");
param.setResult(true); /* you can compare the sender address(String) with your computer and determine if you return true or just allow the original method to be called after this returns.*/
}
});
}
}
I tested and it works fine:)
Links
Dropbox link of the auto accepting app
Dropbox link of the project files (zip)
Xposed apk site
Towelroot site to root your phone
Background(Original answer)
As I commented above, you bay be able to, and I tried and succeeded in blocking (though not receiving) with this code.
import android.util.*;
import de.robv.android.xposed.*;
import de.robv.android.xposed.callbacks.XC_LoadPackage.*;
import java.io.*;
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
public class Tutorial implements IXposedHookLoadPackage
{
private String TAG="TUTORIAL";
public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
if (!lpparam.packageName.equals("com.android.bluetooth"))
{
Log.i(TAG,"Not: "+lpparam.packageName);
return;
}
Log.i(TAG,"Yes "+lpparam.packageName);
findAndHookMethod("com.android.bluetooth.opp.BluetoothOppService", lpparam.classLoader, "startSocketListener", new XC_MethodHook() {
#Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Log.v(TAG,"HOOK DONE");
param.setResult(null);
}
});
}
}
The code above hooks the method startListenerSocket() of com.android.bluetooth.BluetoothOppService and prevents the original method from being called by the line param.setResult(null);
Refer to here to see the full code of com.android.bluetooth.BluetoothOppService.java and you will understand the operation.
And the code you can start from is shown below.
import android.util.*;
import de.robv.android.xposed.*;
import de.robv.android.xposed.callbacks.XC_LoadPackage.*;
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
public class Tutorial implements IXposedHookLoadPackage
{
private String TAG="TUTORIAL";
public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
if (!lpparam.packageName.equals("com.android.bluetooth"))
{
Log.i(TAG,"Not: "+lpparam.packageName);
return;
}
Log.i(TAG,"Yes "+lpparam.packageName);
findAndHookMethod("com.android.bluetooth.opp.BluetoothOppObexServerSession", lpparam.classLoader, "onPut", new XC_MethodHook() {
#Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Log.v(TAG,"HOOK DONE");
Class c=param.thisObject.getClass();
}
});
}
}
This code hooks the onPut method of com.android.bluetooth. BluetoothOppObexServerSession linked here. I either am newbie to xposed framework but I hope my answer helped.
I had the same issues you asked and partially solved the problem by implementing my custom OBEX server and manually / programmatically(with ps|grep and su kill pid) killing the native BluetoothOppService. But I will either try the idea of hooking and directly executing my code.
And to help you customize OBEX server session I post my implementation below.
#Override
public int onPut(Operation op)
{
if (D)
{
Log.d(TAG, "onPut " + op.toString());
}
HeaderSet request;
String name, mimeType;
Long length;
String extension=null;// type;
int obexResponse = ResponseCodes.OBEX_HTTP_OK;
String destination;
if (mTransport instanceof BluetoothObexTransport)
{
destination = ((BluetoothObexTransport) mTransport).getRemoteAddress();
}
else
{
destination = "FF:FF:FF:00:00:00";
}
boolean isWhitelisted =IsWhitelisted(destination);
try
{
boolean preReject = false;
request = op.getReceivedHeader();
if (V)
{
// Constants.logHeader(request);
}
name = (String) request.getHeader(HeaderSet.NAME);
length = (Long) request.getHeader(HeaderSet.LENGTH);
mimeType = (String) request.getHeader(HeaderSet.TYPE);
if (length == 0)
{
if (D)
{
Log.w(TAG, "length is 0, reject the transfer");
}
preReject = true;
obexResponse = ResponseCodes.OBEX_HTTP_LENGTH_REQUIRED;
}
if (name == null || name.isEmpty())
{
if (D)
{
Log.w(TAG, "name is null or empty, reject the transfer");
}
preReject = true;
obexResponse = ResponseCodes.OBEX_HTTP_BAD_REQUEST;
}
int dotIndex = name.lastIndexOf(".");
if (dotIndex > 0)
{
extension = name.substring(dotIndex + 1).toLowerCase();
}
// Reject policy: anything outside the "white list" plus unspecified
// MIME Types. Also reject everything in the "black list".
// if (!preReject && (mimeType == null || (!isWhitelisted && !Constants.mimeTypeMatches(
// mimeType, Constants.ACCEPTABLE_SHARE_INBOUND_TYPES))
// || Constants.mimeTypeMatches(mimeType,
// Constants.UNACCEPTABLE_SHARE_INBOUND_TYPES))) {
// if (D) {
// Log.w(TAG, "mimeType is null or in unacceptable list, reject the transfer");
// }
// preReject = true;
// obexResponse = ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE;
// }
if (preReject && obexResponse != ResponseCodes.OBEX_HTTP_OK)
{
// some bad implemented client won't send disconnect
return obexResponse;
}
}
catch (IOException e)
{
Log.e(TAG, "get getReceivedHeaders error " + e);
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
}
int status = receiveFile(destination, name, extension, length, op);
/*
* TODO map status to obex response code
*/
if (status != BluetoothShare.STATUS_SUCCESS)
{
obexResponse = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
}
Log.d(TAG, "MIME TYPE)" + mimeType);
return obexResponse;
}
I just removed some rejecting codes from the original one.
Also to look at my full code please refer to my git repository.
I also thank the contributors to the android project!
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 am developing a chat application and done with it. Now I want to implement video chat also.
After research a lot I decided to go with "WebRTC" library.
What I have done?
1) Able to run AppRtcDemo at local server and Its working fine between browsers.
Reference : http://www.webrtc.org/reference/getting-started
2) Able to build Android AppRtcDemo.But when I run it say "Cross origin does not support".
After research I found in webrtc discussion that to resolve this issue I need to set-up own turn server.
3) So I install latest rfc5766TurnServer recommended by webrtc. I got success to run turn server.
Reference : http://code.google.com/p/rfc5766-turn-server/
I do following changes to ApprtcDemo (web) and (Android) to work with my Turn server
1) apprtc.py
Replace:
turn_url = 'https://computeengineondemand.appspot.com/'
turn_url = turn_url + 'turn?' + 'username=' + user + '&key=4080218913'
With point to my turn server:
turn_url = 'http://192.168.5.85:3478/?service=turn&username=biraj'
2) index.html
Replace:
var pcConfig = {{ pc_config|safe }};
With:
var pcConfig = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}, {"url":"turn:biraj#192.168.5.85:3479", "credential":"0x5b04123c3eec4cf0be64ab909bb2ff5b"}]};
Android
1)AppRTCDemoActivity.java
Replace:
roomInput.setText("https://apprtc.appspot.com/?r=");
With my local apprtc server:
roomInput.setText("http://192.168.5.86:8080/?r=");
2) AppRTCClient.java
In private PeerConnection.IceServer requestTurnServer(String url){} function
Replace:
connection.addRequestProperty("origin", "https://apprtc.appspot.com");
With:
connection.addRequestProperty("origin", "http://192.168.5.86:8080");
3) /assets/channel.html
Replace:
<script src="https://apprtc.appspot.com/_ah/channel/jsapi"></script>
With:
<script src="http://192.168.5.86:8080/_ah/channel/jsapi"></script>
Now my question is why this is working between browsers but not between android AppRtcDemo and browser.
When I run AppRtcDemo on android after doing above changes local camera preview is started at right-top corner and message prompt "waiting for ICEcandidates" then nothing happens.
Thanks in advance.
Thanks to All for supporting my question.After long rocky ride with ApprtcDemo I got success and it works fine.I am posting the solution.
Find the "GAEChannelClient.java" java file.
and do change as below.
/*
* libjingle
* Copyright 2013, Google Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.appspot.apprtc;
import java.io.InputStream;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.util.Log;
import android.webkit.ConsoleMessage;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
/**
* Java-land version of Google AppEngine's JavaScript Channel API:
* https://developers.google.com/appengine/docs/python/channel/javascript
*
* Requires a hosted HTML page that opens the desired channel and dispatches JS
* on{Open,Message,Close,Error}() events to a global object named
* "androidMessageHandler".
*/
public class GAEChannelClient {
private static final String TAG = "GAEChannelClient";
private WebView webView;
private final ProxyingMessageHandler proxyingMessageHandler;
/**
* Callback interface for messages delivered on the Google AppEngine
* channel.
*
* Methods are guaranteed to be invoked on the UI thread of |activity|
* passed to GAEChannelClient's constructor.
*/
public interface MessageHandler {
public void onOpen();
public void onMessage(String data);
public void onClose();
public void onError(int code, String description);
}
/** Asynchronously open an AppEngine channel. */
#SuppressLint("SetJavaScriptEnabled")
public GAEChannelClient(Activity activity, String token, MessageHandler handler) {
webView = new WebView(activity);
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setAllowFileAccessFromFileURLs(true); // Maybe you
// don't
// need this
// rule
webView.getSettings().setAllowUniversalAccessFromFileURLs(true);
webView.setWebChromeClient(new WebChromeClient() { // Purely for
// debugging.
public boolean onConsoleMessage(ConsoleMessage msg) {
Log.d(TAG, "console: " + msg.message() + " at " + msg.sourceId() + ":" + msg.lineNumber());
return false;
}
});
webView.setWebViewClient(new WebViewClient() { // Purely for debugging.
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
Log.e(TAG, "JS error: " + errorCode + " in " + failingUrl + ", desc: " + description);
}
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
System.out.println("HI");
return super.shouldOverrideUrlLoading(view, url);
}
});
proxyingMessageHandler = new ProxyingMessageHandler(activity, handler, token);
webView.addJavascriptInterface(proxyingMessageHandler, "androidMessageHandler");
// webView.loadUrl("file:///android_asset/channel.html");
try {
InputStream is = activity.getAssets().open("channel.html");
StringBuilder builder = new StringBuilder();
byte[] buffer = new byte[1024];
while (is.read(buffer) != -1) {
builder.append(new String(buffer));
}
is.close();
String str = builder.toString();
webView.loadDataWithBaseURL("http://192.168.5.86:8080", str, "text/html", "utf-8", null);
} catch (Exception e) {
e.printStackTrace();
}
}
/** Close the connection to the AppEngine channel. */
public void close() {
if (webView == null) {
return;
}
proxyingMessageHandler.disconnect();
webView.removeJavascriptInterface("androidMessageHandler");
webView.loadUrl("about:blank");
webView = null;
}
// Helper class for proxying callbacks from the Java<->JS interaction
// (private, background) thread to the Activity's UI thread.
private static class ProxyingMessageHandler {
private final Activity activity;
private final MessageHandler handler;
private final boolean[] disconnected = { false };
private final String token;
public ProxyingMessageHandler(Activity activity, MessageHandler handler, String token) {
this.activity = activity;
this.handler = handler;
this.token = token;
}
public void disconnect() {
disconnected[0] = true;
}
private boolean disconnected() {
return disconnected[0];
}
#JavascriptInterface
public String getToken() {
return token;
}
#JavascriptInterface
public void onOpen() {
System.out.println("GAEClient : Open" );
activity.runOnUiThread(new Runnable() {
public void run() {
if (!disconnected()) {
handler.onOpen();
}
}
});
}
#JavascriptInterface
public void onMessage(final String data) {
System.out.println("GAEClient : Message : " +data );
activity.runOnUiThread(new Runnable() {
public void run() {
if (!disconnected()) {
handler.onMessage(data);
}
}
});
}
#JavascriptInterface
public void onClose() {
System.out.println("GAEClient : Close" );
activity.runOnUiThread(new Runnable() {
public void run() {
if (!disconnected()) {
handler.onClose();
}
}
});
}
#JavascriptInterface
public void onError(final int code, final String description) {
System.out.println("GAEClient : Erroe : " + description);
activity.runOnUiThread(new Runnable() {
public void run() {
if (!disconnected()) {
handler.onError(code, description);
}
}
});
}
}
}
Channel.html in assets folder
<html>
<head>
<script src="http://192.168.5.86:8080/_ah/channel/jsapi"></script>
</head>
<!--
Helper HTML that redirects Google AppEngine's Channel API to a JS object named
|androidMessageHandler|, which is expected to be injected into the WebView
rendering this page by an Android app's class such as AppRTCClient.
-->
<body onbeforeunload="closeSocket()" onload="openSocket()">
<script type="text/javascript">
var token = androidMessageHandler.getToken();
if (!token)
throw "Missing/malformed token parameter: [" + token + "]";
var channel = null;
var socket = null;
function openSocket() {
channel = new goog.appengine.Channel(token);
socket = channel.open({
'onopen': function() { androidMessageHandler.onOpen(); },
'onmessage': function(msg) { androidMessageHandler.onMessage(msg.data); },
'onclose': function() { androidMessageHandler.onClose(); },
'onerror': function(err) { androidMessageHandler.onError(err.code, err.description); }
});
}
function closeSocket() {
socket.close();
}
</script>
</body>
</html>
Sadly I don't know if you have done these things:
Use the SAME stun and turn server on every application (wether PC or mobile).
Are you even sending the ICE candidates between the applications (I think you do, but just to verify).
Are you sure that the STUN/TURN url are the ones that give the error, as I can't believe that these things do about cross-origins (they shouldn't, as you are just connecting from a client to a server. Cross origin is mostly 'used' on web pages that load data from an external source. You aren't allowed to do that from an XHR). I am really thinking it has something to do with https://apprtc.appspot.com/_ah/channel/jsapi, as this is a good example of cross origin stuff.
What if you open the web page that is working in your chrome browser on mobile? What does it do then? (note that you can connect your phone to your pc to have the full developer tools chrome has. Chrome runs on your android device but you can see the devtools on your pc).
If you can provide me these answers I might be able to help you. Try to revert all those changes and just use google's TURN server, but only make that https://apprtc.appspot.com/_ah/channel/jsapi file local.
EDIT: I see you found your answer. Would you mind to share it?
I'm developing an Android app that uses the Dropbox Sync API to upload files. I have already created the app on Dropbox, gotten the APP_KEY and the APP_SECRET. I have included all the necessary libraries, set the proper keys in my activity code and the Manifest. My app is similar to the HelloDropbox sample provided in the documentation, but when I click on the "Link to Dropbox" button which is supposed to display a place to enter my dropbox credentials, nothing happens. Here's the source code:
package com.diamondtrust66.helix.player;
import java.io.File;
import java.io.IOException;
import java.util.List;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.dropbox.client2.DropboxAPI;
import com.dropbox.sync.android.DbxAccountManager;
import com.dropbox.sync.android.DbxFile;
import com.dropbox.sync.android.DbxFileInfo;
import com.dropbox.sync.android.DbxFileSystem;
import com.dropbox.sync.android.DbxPath;
public class HelixPlayer extends Activity {
private static final String appKey = "1234-my-key";
private static final String appSecret = "1234-my-secret";
private static final int REQUEST_LINK_TO_DBX = 0;
private TextView mTestOutput;
private Button mLinkButton;
private DbxAccountManager mDbxAcctMgr;
private DropboxAPI<?> mDBApi;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_helix_player);
mTestOutput = (TextView) findViewById(R.id.test_output);
mLinkButton = (Button) findViewById(R.id.link_button);
mLinkButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
onClickLinkToDropbox();
}
});
mDbxAcctMgr = DbxAccountManager.getInstance(getApplicationContext(), appKey, appSecret);
}
#Override
protected void onResume() {
super.onResume();
if (mDbxAcctMgr.hasLinkedAccount()) {
showLinkedView();
doDropboxTest();
} else {
showUnlinkedView();
}
}
private void showLinkedView() {
mLinkButton.setVisibility(View.GONE);
mTestOutput.setVisibility(View.VISIBLE);
}
private void showUnlinkedView() {
mLinkButton.setVisibility(View.VISIBLE);
mTestOutput.setVisibility(View.GONE);
}
private void onClickLinkToDropbox() {
mDbxAcctMgr.startLink((Activity)this, REQUEST_LINK_TO_DBX);
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_LINK_TO_DBX) {
if (resultCode == Activity.RESULT_OK) {
doDropboxTest();
} else {
Toast.makeText(getApplicationContext(), "FAILURE", Toast.LENGTH_LONG).show();
mTestOutput.setText("Link to Dropbox failed or was cancelled.");
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void doDropboxTest() {
try {
final String TEST_DATA = "Hello Dropbox";
final String TEST_FILE_NAME = "be like that.mp3";
DbxPath testPath = new DbxPath(DbxPath.ROOT, TEST_FILE_NAME);
// Create DbxFileSystem for synchronized file access.
DbxFileSystem dbxFs = DbxFileSystem.forAccount(mDbxAcctMgr.getLinkedAccount());
// Print the contents of the root folder. This will block until we can
// sync metadata the first time.
List<DbxFileInfo> infos = dbxFs.listFolder(DbxPath.ROOT);
mTestOutput.setText("\nContents of app folder:\n");
for (DbxFileInfo info : infos) {
mTestOutput.append(" " + info.path + ", " + info.modifiedTime + '\n');
}
// Create a test file only if it doesn't already exist.
if (!dbxFs.exists(testPath)) {
DbxFile testFile = dbxFs.create(testPath);
try {
File myFile = new File("/mnt/sdcard/alarms/be like that.mp3");
//testFile.writeString(TEST_DATA);
testFile.writeFromExistingFile(myFile, false);
} finally {
testFile.close();
}
mTestOutput.append("\nCreated new file '" + testPath + "'.\n");
}
// Read and print the contents of test file. Since we're not making
// any attempt to wait for the latest version, this may print an
// older cached version. Use getSyncStatus() and/or a listener to
// check for a new version.
/*if (dbxFs.isFile(testPath)) {
String resultData;
DbxFile testFile = dbxFs.open(testPath);
try {
resultData = testFile.readString();
} finally {
testFile.close();
}
mTestOutput.append("\nRead file '" + testPath + "' and got data:\n " + resultData);
} else if (dbxFs.isFolder(testPath)) {
mTestOutput.append("'" + testPath.toString() + "' is a folder.\n");
}*/
} catch (IOException e) {
mTestOutput.setText("Dropbox test failed: " + e);
}
}
}
Are you able to run the unmodified Hello Dropbox example on the same emulator/device where you're experiencing this problem? You can try replacing the app key/secret in the sample with your own as well. If those also fail, it may be there's something wrong with the configuration of your device which is keeping the API from launching a browser to complete authentication. If the example works, but your app doesn't, then I'd suspect something misconfigured there. Can you check with a log statement whether your call to startLink() is actually happening? Do you see anything appear in LogCat after that point?
The best way to debug this further might be to open a suppot ticket. Use the API Support link here: https://www.dropbox.com/developers
I ran into the same problem, the startLink() was doing nothing when I tried to use the same dropbox app credentials I was using with another android app my device had installed (although not running), but it didn't work. So you have two options: Uninstall any other android app using the same credentials OR create another dropbox app and renew the set of app/pass keys. Only then the Dropbox Login Dialog appears.
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.