Skip to content
This repository has been archived by the owner on Apr 18, 2023. It is now read-only.

Commit

Permalink
initial commit.
Browse files Browse the repository at this point in the history
Note. Not working in this state. Portrait/Landscape transition must be done first.
  • Loading branch information
sebastianst committed Jul 4, 2013
0 parents commit b690524
Show file tree
Hide file tree
Showing 19 changed files with 567 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .gitignore
@@ -0,0 +1,10 @@
*.iml
*.iws
*.ipr
.idea/
.gradle/
local.properties

*/build/

*.apk
19 changes: 19 additions & 0 deletions Snapshare/build.gradle
@@ -0,0 +1,19 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.4'
}
}
apply plugin: 'android'

android {
compileSdkVersion 'rovo89:Xposed Bridge API:17'
buildToolsVersion "17.0.0"

defaultConfig {
minSdkVersion 14
targetSdkVersion 17
}
}
24 changes: 24 additions & 0 deletions Snapshare/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.cantab.stammler.snapshare"
android:versionCode="1"
android:versionName="1.0">

<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="17" />

<application android:allowBackup="true"
android:label="@string/app_name"
android:icon="@drawable/ic_launcher">
<meta-data android:value="true" android:name="xposedmodule"/>
<meta-data android:value="2.0*" android:name="xposedminversion"/>
<meta-data android:value="Lets you share arbitrary Images via Snapchat." android:name="xposeddescription"/>
<activity android:name=".ReceiveImageActivity"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
</application>

</manifest>
1 change: 1 addition & 0 deletions Snapshare/src/main/assets/xposed_init
@@ -0,0 +1 @@
net.cantab.stammler.snapshare.Snapshare
Binary file added Snapshare/src/main/ic_launcher-web.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,134 @@
package net.cantab.stammler.snapshare;

/**
* Created by sebastian on 6/26/13.
*
*/

import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.DisplayMetrics;
import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

import static android.graphics.Bitmap.createBitmap;

import static net.cantab.stammler.snapshare.Snapshare.LOG_TAG;

public class ReceiveImageActivity extends Activity {
private static final int JPEG_QUALITY = 85;

@Override
public void onCreate (Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Get intent, action and MIME type
Intent intent = getIntent();
String action = intent.getAction();
String type = intent.getType();
Uri imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (Intent.ACTION_SEND.equals(action) && type.startsWith("image/")) {
Log.d(LOG_TAG, "Received Image share of type " + type
+ "\nand URI " + imageUri.toString()
+ "\nCalling hooked Snapchat with same Intent.");

//InputStream iStream;
ByteArrayOutputStream oStream = new ByteArrayOutputStream();
try {
/*iStream = getContentResolver().openInputStream(imageUri);
oStream = new ByteArrayOutputStream(iStream.available());
Log.d("Snapshare", "iStream.available(): " + iStream.available());
int byteSize = IOUtils.copy(iStream, oStream);
Log.v("Snapshare", "Image size: " + byteSize/1024 + " kB");*/
/*TODO use BitmapFactory with inSampleSize magic to avoid using too much memory,
see http://developer.android.com/training/displaying-bitmaps/load-bitmap.html#load-bitmap */
Bitmap bitmap = MediaStore.Images.Media.getBitmap(this.getContentResolver(), imageUri);
int width = bitmap.getWidth();
int height = bitmap.getHeight();
Log.d(LOG_TAG, "Original image w x h: " + width + " x " + height);
// Portrait images have to be rotated 90 degrees clockwise for Snapchat to be displayed correctly
if (height > width) {
Log.d(LOG_TAG, "Portrait image detected, rotating 90 degrees clockwise.");
Matrix matrix = new Matrix();
matrix.setRotate(270);
bitmap = createBitmap(bitmap, 0, 0, width, height, matrix, true);
// resetting width and height
width = bitmap.getWidth();
height = bitmap.getHeight();
}

/* Scaling and cropping mayhem
Snapchat will break if the image sent does not fit into the
DisplayMetrics.widthPixels x DisplayMetrics.heightPixels rectangle (Display rectangle)
and it will scale the image up if the Display rectangle is larger than the image,
ignoring the image's ratio.
So, we sample the image down such that the Display rectangle fits into it and touches one side.
Then we crop the picture to that rectangle */
DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
int dWidth = dm.widthPixels;
int dHeight = dm.heightPixels;
Log.d(LOG_TAG, "Display metrics w x h: " + dWidth + " x " + dHeight);
// DisplayMetrics' values depend on the phone's tilt, so we normalize them to landscape mode
if (dHeight > dWidth) {
Log.d(LOG_TAG, "Normalizing display metrics to landscape mode.");
int temp = dWidth;
dWidth = dHeight;
dHeight = temp;
}
/* If the image properly covers the Display rectangle, we mark it as a "large" image
and are going to scale it down. We make this distinction because we don't wanna
scale the image up if it is smaller than the Display rectangle. */
boolean largeImage = ((width > dWidth) & (height > dHeight));
Log.d(LOG_TAG, "Large image? " + largeImage);
int imageToDisplayRatio = width * dHeight - height * dWidth;
if (imageToDisplayRatio > 0) {
// i.e., width/height > dWidth/dHeight, so have to crop from left and right:
int newWidth = (dWidth * height / dHeight);
Log.d(LOG_TAG, "New width after cropping left & right: " + newWidth);
bitmap = createBitmap(bitmap, (width - newWidth) / 2, 0, newWidth, height);

} else if (imageToDisplayRatio < 0) {
// i.e., width/height < dWidth/dHeight, so have to crop from top and bottom:
int newHeight = (dHeight * width / dWidth);
Log.d(LOG_TAG, "New height after cropping top & bottom: " + newHeight);
bitmap = createBitmap(bitmap, 0, (height - newHeight) / 2, width, newHeight);
}
if (largeImage) {
Log.d(LOG_TAG, "Scaling down.");
bitmap = Bitmap.createScaledBitmap(bitmap, dWidth, dHeight, true);
}
/// Scaling and cropping finished, ready to pass to Snapchat

bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, oStream);
Log.d(LOG_TAG, "outputStream size: " + oStream.size()/1024 + " kB = " + oStream.size() + " B");
byte [] imageData = oStream.toByteArray();
int dataLength = imageData.length;
Log.d(LOG_TAG, "byte array size: " + dataLength/1024 + " kB = " + dataLength + " B");
bitmap.recycle();
intent.putExtra("imageData", imageData);
intent.putExtra("dataLength", dataLength);
intent.setComponent(ComponentName.unflattenFromString("com.snapchat.android/.LandingPageActivity"));
startActivity(intent);
} catch (FileNotFoundException e) {
Log.e(LOG_TAG, "File not found!", e);
} catch (IOException e) {
Log.e(LOG_TAG, "IO Error!", e);
}
}
//call finish at the end to close the wrapper
finish();
}
}
//TODO video sharing:
// intent.putExtra("videoFile", imageUri.toString()); LATER for video share. should be a file:// string, pointing to a .mp4 file
@@ -0,0 +1,94 @@
package net.cantab.stammler.snapshare;

import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.ViewGroup;

import java.lang.reflect.InvocationTargetException;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

import static de.robv.android.xposed.XposedHelpers.callMethod;
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import static de.robv.android.xposed.XposedHelpers.getObjectField;
import static de.robv.android.xposed.XposedHelpers.newInstance;

/**
* Created by sebastian on 6/26/13.
*
*/

public class Snapshare implements IXposedHookLoadPackage {
public static final String LOG_TAG = "Snapshare";

public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
if (!lpparam.packageName.equals("com.snapchat.android"))
return;
else
XposedBridge.log("Snapshare: Snapchat load detected.");

findAndHookMethod("com.snapchat.android.LandingPageActivity", lpparam.classLoader, "onCreate", Bundle.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Object thiz = param.thisObject;
//Log.i(LOG_TAG, "Invoking super.onCreate()");
//Activity.class.getMethod("onCreate", Bundle.class).invoke(thiz, param.args[0]);
Intent intent = (Intent) callSuperMethod(thiz, "getIntent");
String type = intent.getType();
Log.d(LOG_TAG, "intent type: " + type);
//Log.i(LOG_TAG, "param.args[0] toString():" + ((Bundle) param.args[0]).toString());
/*if (type != null) {
if (type.startsWith("image/")) {
intent.setComponent(ComponentName.unflattenFromString("com.snapchat.android/.SnapPreviewActivity"));
//callSuperMethod(thiz, "startActivity", intent);
thiz.getClass().getMethod("startActivity", Intent.class).invoke(thiz, intent);
callSuperMethod(thiz, "finish");
}
}*/
int dataLength = intent.getIntExtra("dataLength", -1);
Log.d(LOG_TAG, "data length: " + dataLength);
if (dataLength > 0) {
Bitmap imageBitmap = BitmapFactory.decodeByteArray(intent.getByteArrayExtra("imageData"), 0, dataLength);
Class SnapCapturedEventClass = Class.forName("com.snapchat.android.util.eventbus.SnapCapturedEvent", true, thiz.getClass().getClassLoader());
Object captureEvent = newInstance(SnapCapturedEventClass, imageBitmap);
callMethod(thiz, "onSnapCaptured", captureEvent);
}
//callSuperMethod(thiz, "finish");
}
});
findAndHookMethod("com.snapchat.android.SnapPreviewFragment", lpparam.classLoader, "onCreateView", LayoutInflater.class, ViewGroup.class, Bundle.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Object thiz = param.thisObject;
Activity curActivity = (Activity) callSuperMethod(thiz, "getActivity");
Log.d(LOG_TAG, "Fr> Current Activity: " + curActivity.getLocalClassName());
Intent intent = curActivity.getIntent();
Log.d(LOG_TAG, "Fr> Intent type: " + intent.getType());
Bitmap bitmap = (Bitmap) getObjectField(thiz, "mImageBitmap");
Log.d(LOG_TAG, "Fr> Width: " + bitmap.getWidth() + ", Height: " + bitmap.getHeight());
DisplayMetrics dm = (DisplayMetrics) getObjectField(thiz, "mDisplayMetrics");
Log.d(LOG_TAG, "Fr> SnapPreviewActivity Display Metrics w x h: " + dm.widthPixels + " x " + dm.heightPixels);
}
});
}
/*
* callSuperMethod()
*
* XposedHelpers.callMethod() cannot call methods of the super class of an object, because it
* uses getDeclaredMethods(). So we have to implement this little helper, which should work
* similar to callMethod(). Furthermore, the exceptions from getMethod() are passed on.
* See http://forum.xda-developers.com/showpost.php?p=42598280&postcount=1753
*/
private Object callSuperMethod(Object obj, String methodName) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
return obj.getClass().getMethod(methodName).invoke(obj);
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions Snapshare/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Snapshare</string>
</resources>
20 changes: 20 additions & 0 deletions Snapshare/src/main/res/values/styles.xml
@@ -0,0 +1,20 @@
<resources>

<!--
Base application theme, dependent on API level. This theme is replaced
by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
-->
<style name="AppBaseTheme" parent="android:Theme">
<!--
Theme customizations available in newer API levels can go in
res/values-vXX/styles.xml, while customizations related to
backward-compatibility can go here.
-->
</style>

<!-- Application theme. -->
<style name="AppTheme" parent="AppBaseTheme">
<!-- All customizations that are NOT specific to a particular API-level can go here. -->
</style>

</resources>
1 change: 1 addition & 0 deletions build.gradle
@@ -0,0 +1 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
Binary file added gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
6 changes: 6 additions & 0 deletions gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=http\://services.gradle.org/distributions/gradle-1.6-bin.zip

0 comments on commit b690524

Please sign in to comment.