├── res
├── raw
│ └── apps.yaml
├── drawable-hdpi
│ └── u_turn.png
├── drawable-mdpi
│ └── u_turn.png
├── drawable-xhdpi
│ └── u_turn.png
├── drawable-xxhdpi
│ └── u_turn.png
├── values
│ └── strings.xml
└── layout
│ └── main.xml
├── yaml
├── screenshot_1.png
├── screenshot_2.png
├── u_turn_promo.png
├── u_turn_original.gif
├── u_turn_play_store.png
├── libs
└── snakeyaml-1.10-android.jar
├── .gitignore
├── local.properties
├── project.properties
├── ant.properties
├── generate_manifest.py
├── src
└── org
│ └── snarfed
│ └── android
│ └── openinapp
│ ├── TwitterWebIntent.java
│ └── Handler.java
├── app_manifests
├── com.silentlabs.android.mobilequeue_AndroidManifest.xml
├── com.hulu.plus_AndroidManifest.xml
├── org.wordpress.android_AndroidManifest.xml
├── com.opentable_AndroidManifest.xml
├── com.goodreads_AndroidManifest.xml
├── com.instagram.android_AndroidManifest.xml
├── intent_filters
├── com.github.mobile_AndroidManifest.xml
├── com.android.calendar_AndroidManifest.xml
├── com.google.android.calendar_AndroidManifest.xml
├── com.twitter.android_AndroidManifest.xml
└── com.google.android.apps.plus_AndroidManifest.xml
├── README.md
├── AndroidManifest.xml
├── test_links.html
└── apps.yaml
/res/raw/apps.yaml:
--------------------------------------------------------------------------------
1 | ../../apps.yaml
--------------------------------------------------------------------------------
/yaml:
--------------------------------------------------------------------------------
1 | ../../google_appengine/lib/yaml/lib/yaml
--------------------------------------------------------------------------------
/screenshot_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snarfed/open-in-app/HEAD/screenshot_1.png
--------------------------------------------------------------------------------
/screenshot_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snarfed/open-in-app/HEAD/screenshot_2.png
--------------------------------------------------------------------------------
/u_turn_promo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snarfed/open-in-app/HEAD/u_turn_promo.png
--------------------------------------------------------------------------------
/u_turn_original.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snarfed/open-in-app/HEAD/u_turn_original.gif
--------------------------------------------------------------------------------
/u_turn_play_store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snarfed/open-in-app/HEAD/u_turn_play_store.png
--------------------------------------------------------------------------------
/res/drawable-hdpi/u_turn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snarfed/open-in-app/HEAD/res/drawable-hdpi/u_turn.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/u_turn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snarfed/open-in-app/HEAD/res/drawable-mdpi/u_turn.png
--------------------------------------------------------------------------------
/res/drawable-xhdpi/u_turn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snarfed/open-in-app/HEAD/res/drawable-xhdpi/u_turn.png
--------------------------------------------------------------------------------
/libs/snakeyaml-1.10-android.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snarfed/open-in-app/HEAD/libs/snakeyaml-1.10-android.jar
--------------------------------------------------------------------------------
/res/drawable-xxhdpi/u_turn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snarfed/open-in-app/HEAD/res/drawable-xxhdpi/u_turn.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
2 |
3 | # Package Files #
4 | *.jar
5 | *.war
6 | *.ear
7 |
8 | # Directories
9 | bin
10 | gen
11 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Open Link in App
4 |
5 |
--------------------------------------------------------------------------------
/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/local.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must *NOT* be checked into Version Control Systems,
5 | # as it contains information specific to your local configuration.
6 |
7 | # location of the SDK. This is only used by Ant
8 | # For customization when using a Version Control System, please read the
9 | # header note.
10 | sdk.dir=/Users/ryan/android-sdk
11 |
--------------------------------------------------------------------------------
/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system edit
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 | #
10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
12 |
13 | # Project target.
14 | target=android-17
15 |
--------------------------------------------------------------------------------
/ant.properties:
--------------------------------------------------------------------------------
1 | # This file is used to override default values used by the Ant build system.
2 | #
3 | # This file must be checked into Version Control Systems, as it is
4 | # integral to the build system of your project.
5 |
6 | # This file is only used by the Ant script.
7 |
8 | # You can use this to override default values such as
9 | # 'source.dir' for the location of your java source folder and
10 | # 'out.dir' for the location of your output folder.
11 |
12 | # You can also use it define how the release builds are signed by declaring
13 | # the following properties:
14 | # 'key.store' for the location of your keystore and
15 | # 'key.alias' for the name of the key to use.
16 | # The password will be asked during the build when you use the 'release' target.
17 |
18 | key.store=/Users/ryan/.keystore
19 | key.alias=ryan_barrett
20 |
--------------------------------------------------------------------------------
/generate_manifest.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Generates AndroidManifest.xml based on apps.yaml."""
3 |
4 | import datetime
5 | import yaml
6 |
7 | CONFIG_FILE = 'apps.yaml'
8 |
9 |
10 | def main():
11 | with open(CONFIG_FILE) as f:
12 | config = yaml.load(f)
13 |
14 | schemes = config['schemes']
15 | data = []
16 | for app in config['apps']:
17 | assert 'hosts' in app, 'app %s is missing hosts field' % app['name']
18 | data_elements = []
19 |
20 | for host in app['hosts']:
21 | for scheme in schemes:
22 | prefixes = app.get('prefixes', [])
23 | patterns = app.get('patterns', [])
24 | if not prefixes and not patterns:
25 | prefixes = ['/']
26 |
27 | for tag, values in ('pathPrefix', prefixes), ('pathPattern', patterns):
28 | for value in values:
29 | # To catch links in Chrome, must include scheme, host, and
30 | # either pathPrefix or pathPattern.
31 | # http://stackoverflow.com/questions/17706667
32 | data.append(
33 | '' %
34 | (scheme, host, tag, value))
35 |
36 | print """\
37 |
38 |
41 |
45 |
46 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | %s
57 |
58 |
59 |
60 |
61 |
62 | """ % (datetime.datetime.now(), '\n'.join(data))
63 |
64 |
65 | if __name__ == "__main__":
66 | main()
67 |
--------------------------------------------------------------------------------
/src/org/snarfed/android/openinapp/TwitterWebIntent.java:
--------------------------------------------------------------------------------
1 | package org.snarfed.android.openinapp;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 | import android.net.Uri;
7 | import android.util.Log;
8 |
9 | // This activity is currently unused. The official Twitter app handles web
10 | // intent URLs, but it redirects them to the browser. It doesn't have native
11 | // intents for handling retweeting, favorites, etc. anyway. :/
12 |
13 | // Test command line: adb -d shell am start -d [link]
14 |
15 | // test links from https://dev.twitter.com/docs/intents :
16 | // http://twitter.com/intent/tweet?url=https://twitter.com/intent/tweet?url=http%3A%2F%2Fen.wikipedia.org%2Fwiki%2FIn_Watermelon_Sugar&in_reply_to=62862594515546112&via=twicodeer&text=foo%20bar&hashtags=baz,baj
17 | // http://twitter.com/intent/favorite?tweet_id=62862594515546112
18 | // http://twitter.com/intent/retweet?tweet_id=62862594515546112
19 | // http://twitter.com/intent/user?screen_name=alwaysmikegomez
20 |
21 | public class TwitterWebIntent extends Activity {
22 | static final String TAG = "OpenLinkInApp.TwitterWebIntent";
23 |
24 | @Override
25 | public void onCreate(Bundle savedInstanceState) {
26 | super.onCreate(savedInstanceState);
27 |
28 | Uri uri = getIntent().getData();
29 | if (uri == null || uri.getPath() == null) {
30 | Log.i(TAG, "No URI in intent! Exiting.");
31 | finish();
32 | return;
33 | }
34 |
35 | // http://wiki.akosma.com/IPhone_URL_Schemes#Twitter
36 | // http://omgwtfgames.com/2012/01/android-intents-captured-by-various-twitter-clients/
37 | Intent intent = null;
38 | if (uri.getPath().equals("/intent/tweet")) {
39 | intent = new Intent(Intent.ACTION_SEND);
40 | intent.setType("text/plain");
41 |
42 | StringBuilder builder = new StringBuilder();
43 | String text = uri.getQueryParameter("text");
44 | if (text != null) {
45 | builder.append(text);
46 | }
47 |
48 | String hashtags = uri.getQueryParameter("hashtags");
49 | if (hashtags != null) {
50 | for (String tag : hashtags.split(",")) {
51 | builder.append(" #" + tag);
52 | }
53 | }
54 |
55 | String url = uri.getQueryParameter("url");
56 | if (url != null) {
57 | builder.append(url);
58 | }
59 |
60 | String via = uri.getQueryParameter("via");
61 | if (via != null) {
62 | builder.append(" via @" + via);
63 | }
64 |
65 | intent.putExtra(Intent.EXTRA_TEXT, text);
66 | Log.i(TAG, "Redirecting " + uri + " to ACTION_SEND with text/plain: " + text);
67 |
68 | } else {
69 | intent = new Intent(getIntent());
70 | Log.i(TAG, "Unknown path " + uri.getPath() + " , resending original intent.");
71 | }
72 |
73 | startActivity(intent);
74 | finish();
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app_manifests/com.silentlabs.android.mobilequeue_AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Open Link in App
2 | ================
3 |
4 | Ever click on a link to Facebook, Twitter, or anywhere else, and it opens in the
5 | browser instead of the native app? Me too. No fun. This app intercepts those
6 | links and sends them to their native app.
7 |
8 | (See the
9 | [Play Store listing](https://play.google.com/store/apps/details?id=org.snarfed.android.openinapp)
10 | and [blog post announcement](http://snarfed.org/2013-07-16_open-link-in-app).)
11 |
12 | To use, after you click on link, select Open Link in App from the chooser dialog
13 | box. If you don't see the dialog box on a link that you think should work, go to
14 | Settings => Apps => Browser or Chrome => Clear defaults.
15 |
16 | I'm not actively working on this right now, but if you're technical, it's pretty
17 | straightforward to add a new app, and I happily accept pull requests.
18 |
19 | License: this project is placed in the public domain.
20 |
21 |
22 | Related work
23 | ===
24 | [Tasomaniac](http://www.tasomaniac.com)'s
25 | [Open Link With...](https://play.google.com/store/apps/details?id=com.tasomaniac.openwith)
26 | is similar, but only works when you explicitly share a URL.
27 |
28 | [Intrications](http://www.intrications.com)'s
29 | [Browser Intercept - Share URL](https://play.google.com/store/apps/details?id=com.intrications.android.sharebrowser)
30 | lets you share a text URL instead of opening it in a browser.
31 |
32 | And others...
33 |
34 |
35 | Development notes
36 | ===
37 |
38 | This app is heavily data-driven. The external apps to integrate with are defined
39 | in apps.yaml. generate_manifest.py uses that file at compile time to generate
40 | AndroidManifest.xml, and the app reads it at runtime to determine how to handle
41 | and redirect intents.
42 |
43 | The Python YAML library is PyYAML, grossly hacked as a symlink to App Engine's
44 | version in ~/google_appengine/ so that I don't have to check it into the repo.
45 | The Java YAML library is SnakeYAML, checked into libs/ as a jar.
46 |
47 | Test command line to open URL with ACTION_VIEW intent:
48 | adb -d shell am start -d [link]
49 |
50 | Lots of apps' AndroidManifest.xml manifest files are in app_manifests/.
51 | To extract an app's manifest:
52 | - "Back up" the app with
53 | [Astro File Manager](https://play.google.com/store/apps/details?id=com.metago.astro)
54 | - adb pull the apk from /sdcard/backups/apps
55 | - extract AndroidManifest.xml with
56 | [apktool](http://code.google.com/p/android-apktool/): apktool decode FILE.apk
57 |
58 | For an intent filter to catch taps on links in Chrome for Android, you have to
59 | include scheme, host, *and* either pathPrefix or pathPattern in the intent
60 | filter's data element: http://stackoverflow.com/questions/17706667
61 |
62 | Oddly, this also seems to "unlock" other apps' intent filters for browser link
63 | taps too. Goodreads, for example, doesn't normally handle browser link taps, but
64 | it does when Open Link in App is installed. Odd.
65 |
66 | Unfortunately, pathPattern is a very limited subset of regexp: only . and * are
67 | supported. That's not enough for some of the URI pattern matching we need. In
68 | these cases, we overspecify a prefix or pattern and do the rest of the filtering
69 | at runtime.
70 |
--------------------------------------------------------------------------------
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/test_links.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
16 |
17 | Facebook
18 |
25 |
26 | Twitter
27 |
33 |
34 | Instagram
35 |
42 |
43 | GitHub
44 | http://github.com/snarfed
45 | http://www.github.com/snarfed/open-in-app
46 | http://github.com/snarfed/open-in-app/commit/a8865ac8d7bd13667287943a8b9e81b8eb970629
47 | http://github.com/snarfed/facebook-atom/issues/1
48 | https://github.com/rogerhu/mockfacebook/pull/22
49 | https://gist.github.com/6002797
50 | https://gist.github.com/JakeWharton/6002797
51 | Should fail gracefully:
52 | https://github.com/snarfed/open-in-app/blob/master/build.xml
53 | http://github.com/snarfed/facebook-atom/issues
54 | http://github.com/rogerhu/mockfacebook/pulls
55 | https://gist.github.com/JakeWharton
56 |
57 |
58 | Hulu
59 |
63 |
64 | Goodreads
65 |
73 |
74 | Quip
75 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/app_manifests/com.hulu.plus_AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/src/org/snarfed/android/openinapp/Handler.java:
--------------------------------------------------------------------------------
1 | package org.snarfed.android.openinapp;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 | import java.util.Map;
6 | import java.util.regex.Matcher;
7 | import java.util.regex.Pattern;
8 |
9 | import android.app.Activity;
10 | import android.content.ActivityNotFoundException;
11 | import android.content.Intent;
12 | import android.content.pm.ResolveInfo;
13 | import android.os.Bundle;
14 | import android.os.Parcelable;
15 | import android.net.Uri;
16 | import android.util.Log;
17 | import android.widget.Toast;
18 |
19 | import org.yaml.snakeyaml.Yaml;
20 |
21 | public class Handler extends Activity {
22 | static final String TAG = "OpenLinkInApp";
23 |
24 | @Override
25 | public void onCreate(Bundle savedInstanceState) {
26 | super.onCreate(savedInstanceState);
27 |
28 | Uri uri = getIntent().getData();
29 | if (uri == null || uri.getPath() == null) {
30 | Log.e(TAG, "No URI in intent!");
31 | finish();
32 | return;
33 | }
34 |
35 | // Read config file and find the app for this host
36 | Map config = (Map)new Yaml().load(
37 | getResources().openRawResource(R.raw.apps));
38 | Map app = null;
39 | for (Map a : (List