Commit 66d1b837 authored by Danny Baumann's avatar Danny Baumann

Jelly: Allow launching external apps

Change-Id: I5c372a9ca1599fba56cdf320a2f4c3ae2bcd85fd
parent 9d693cc8
......@@ -5,7 +5,7 @@ android {
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "org.lineageos.jelly"
minSdkVersion 23
minSdkVersion 24
targetSdkVersion 25
versionCode 1
versionName "1.0"
......
......@@ -18,7 +18,10 @@ package org.lineageos.jelly;
import android.Manifest;
import android.app.ActivityManager;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
......@@ -32,6 +35,7 @@ import android.net.http.HttpResponseCache;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.ResultReceiver;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.design.widget.BottomSheetDialog;
......@@ -47,6 +51,7 @@ import android.support.v7.view.menu.MenuBuilder;
import android.support.v7.view.menu.MenuPopupHelper;
import android.support.v7.widget.PopupMenu;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.GestureDetector;
......@@ -87,11 +92,28 @@ public class MainActivity extends WebViewExtActivity implements View.OnTouchList
private static final String PROVIDER = "org.lineageos.jelly.fileprovider";
private static final String EXTRA_INCOGNITO = "extra_incognito";
private static final String EXTRA_DESKTOP_MODE = "extra_desktop_mode";
private static final String EXTRA_URL = "extra_url";
public static final String EXTRA_URL = "extra_url";
private static final String STATE_KEY_THEME_COLOR = "theme_color";
private static final int STORAGE_PERM_REQ = 423;
private static final int LOCATION_PERM_REQ = 424;
public static final String ACTION_URL_RESOLVED = "org.lineageos.jelly.action.URL_RESOLVED";
private BroadcastReceiver mUrlResolvedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Intent resolvedIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT);
if (TextUtils.equals(getPackageName(), resolvedIntent.getPackage())) {
String url = intent.getStringExtra(EXTRA_URL);
mWebView.loadUrl(url);
} else {
startActivity(resolvedIntent);
}
ResultReceiver receiver = intent.getParcelableExtra(Intent.EXTRA_RESULT_RECEIVER);
receiver.send(RESULT_CANCELED, new Bundle());
}
};
private CoordinatorLayout mCoordinator;
private WebViewExt mWebView;
private ProgressBar mLoadingProgress;
......@@ -210,9 +232,16 @@ public class MainActivity extends WebViewExtActivity implements View.OnTouchList
}
}
@Override
protected void onStart() {
super.onStart();
registerReceiver(mUrlResolvedReceiver, new IntentFilter(ACTION_URL_RESOLVED));
}
@Override
protected void onStop() {
CookieManager.getInstance().flush();
unregisterReceiver(mUrlResolvedReceiver);
HttpResponseCache cache = HttpResponseCache.getInstalled();
if (cache != null) {
cache.flush();
......
......@@ -23,7 +23,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class UrlUtils {
private static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile(
public static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile(
"(?i)" + // switch on case insensitive matching
"(" + // begin group for schema
"(?:http|https|file|chrome):\\/\\/" +
......
......@@ -15,11 +15,18 @@
*/
package org.lineageos.jelly.webview;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.webkit.HttpAuthHandler;
......@@ -30,7 +37,16 @@ import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.lineageos.jelly.IntentFilterCompat;
import org.lineageos.jelly.MainActivity;
import org.lineageos.jelly.R;
import org.lineageos.jelly.utils.UrlUtils;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
class WebClient extends WebViewClient {
......@@ -43,19 +59,16 @@ class WebClient extends WebViewClient {
if (request.isForMainFrame()) {
WebViewExt webViewExt = (WebViewExt) view;
String url = request.getUrl().toString();
if (!url.startsWith("http")) {
Context context = view.getContext();
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(request.getUrl());
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Snackbar.make(view, context.getString(R.string.error_no_activity_found),
Snackbar.LENGTH_LONG).show();
}
boolean needsLookup = request.hasGesture()
|| !TextUtils.equals(url, webViewExt.getLastLoadedUrl());
if (!webViewExt.isIncognito()
&& needsLookup
&& !request.isRedirect()
&& startActivityForUrl(view, url)) {
return true;
} else if (!webViewExt.getRequestHeaders().isEmpty()) {
webViewExt.loadUrl(url);
webViewExt.followUrl(url);
return true;
}
}
......@@ -85,4 +98,102 @@ class WebClient extends WebViewClient {
(dialog, whichButton) -> handler.cancel())
.show();
}
private boolean startActivityForUrl(WebView view, String url) {
Intent intent;
Context context = view.getContext();
try {
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
} catch (URISyntaxException ex) {
return false;
}
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
intent.setSelector(null);
Matcher m = UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url);
if (m.matches()) {
Intent chooserIntent = makeHandlerChooserIntent(context, intent, url);
if (chooserIntent != null) {
intent = chooserIntent;
} else {
// There only are browsers for this URL, handle it ourselves
return false;
}
} else {
String packageName = intent.getPackage();
if (packageName != null
&& context.getPackageManager().resolveActivity(intent, 0) == null) {
// Explicit intent, but app is not installed - try to redirect to Play Store
Uri storeUri = Uri.parse("market://search?q=pname:" + packageName);
intent = new Intent(Intent.ACTION_VIEW, storeUri)
.addCategory(Intent.CATEGORY_BROWSABLE);
}
}
try {
context.startActivity(intent);
return true;
} catch (ActivityNotFoundException e) {
Snackbar.make(view, context.getString(R.string.error_no_activity_found),
Snackbar.LENGTH_LONG).show();
}
return false;
}
private Intent makeHandlerChooserIntent(Context context, Intent intent, String url) {
final PackageManager pm = context.getPackageManager();
final List<ResolveInfo> activities = pm.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_RESOLVED_FILTER);
if (activities == null || activities.isEmpty()) {
return null;
}
final ArrayList<Intent> chooserIntents = new ArrayList<>();
final String ourPackageName = context.getPackageName();
Collections.sort(activities, new ResolveInfo.DisplayNameComparator(pm));
for (ResolveInfo resolveInfo : activities) {
IntentFilter filter = resolveInfo.filter;
ActivityInfo info = resolveInfo.activityInfo;
if (!info.enabled || !info.exported) {
continue;
}
if (filter == null) {
continue;
}
if (IntentFilterCompat.filterIsBrowser(filter)
&& !TextUtils.equals(info.packageName, ourPackageName)) {
continue;
}
Intent targetIntent = new Intent(intent);
targetIntent.setPackage(info.packageName);
chooserIntents.add(targetIntent);
}
if (chooserIntents.isEmpty()) {
return null;
}
final Intent lastIntent = chooserIntents.remove(chooserIntents.size() - 1);
if (chooserIntents.isEmpty()) {
// there was only one, no need to show the chooser
return TextUtils.equals(lastIntent.getPackage(), ourPackageName) ? null : lastIntent;
}
Intent changeIntent = new Intent(MainActivity.ACTION_URL_RESOLVED)
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
.putExtra(MainActivity.EXTRA_URL, url);
PendingIntent pi = PendingIntent.getBroadcast(context, 0, changeIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT);
Intent chooserIntent = Intent.createChooser(lastIntent, null);
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS,
chooserIntents.toArray(new Intent[chooserIntents.size()]));
chooserIntent.putExtra(Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER, pi.getIntentSender());
return chooserIntent;
}
}
......@@ -50,6 +50,7 @@ public class WebViewExt extends WebView {
private boolean mIncognito;
private boolean mDesktopMode;
private String mLastLoadedUrl;
private final Map<String, String> mRequestHeaders = new ArrayMap<>();
private static final String HEADER_DNT = "DNT";
......@@ -68,6 +69,11 @@ public class WebViewExt extends WebView {
@Override
public void loadUrl(String url) {
mLastLoadedUrl = url;
followUrl(url);
}
void followUrl(String url) {
String fixedUrl = UrlUtils.smartUrlFilter(url);
if (fixedUrl != null) {
super.loadUrl(fixedUrl, mRequestHeaders);
......@@ -81,6 +87,10 @@ public class WebViewExt extends WebView {
}
}
public String getLastLoadedUrl() {
return mLastLoadedUrl;
}
private void setup() {
getSettings().setJavaScriptEnabled(PrefsUtils.getJavascript(mActivity));
getSettings().setJavaScriptCanOpenWindowsAutomatically(PrefsUtils.getJavascript(mActivity));
......
/*
* Copyright (C) 2017 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lineageos.jelly;
import android.content.IntentFilter;
public class IntentFilterCompat {
public static boolean filterIsBrowser(IntentFilter filter) {
return filter.handleAllWebDataURI();
}
}
/*
* Copyright (C) 2017 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lineageos.jelly;
import android.content.IntentFilter;
public class IntentFilterCompat {
public static boolean filterIsBrowser(IntentFilter filter) {
return filter.countDataAuthorities() == 0;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment