From ae2cefd6a59623e2f32cc206ecb5f69d68fce811 Mon Sep 17 00:00:00 2001 From: vanitasvitae Date: Sun, 11 Sep 2016 12:35:16 +0200 Subject: [PATCH] Use customTabs to open external links. This is a very basic implementation --- app/build.gradle | 1 + .../activity/MainActivity.java | 38 ++++- .../ui/CustomWebViewClient.java | 8 +- .../CustomTabHelpers/BrowserFallback.java | 18 +++ .../CustomTabActivityHelper.java | 149 ++++++++++++++++++ .../CustomTabHelpers/CustomTabsHelper.java | 121 ++++++++++++++ 6 files changed, 331 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/github/dfa/diaspora_android/util/CustomTabHelpers/BrowserFallback.java create mode 100644 app/src/main/java/com/github/dfa/diaspora_android/util/CustomTabHelpers/CustomTabActivityHelper.java create mode 100644 app/src/main/java/com/github/dfa/diaspora_android/util/CustomTabHelpers/CustomTabsHelper.java diff --git a/app/build.gradle b/app/build.gradle index 105da4da..b9be8f48 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,6 +48,7 @@ dependencies { compile 'com.jakewharton:butterknife:8.0.1' compile 'info.guardianproject.netcipher:netcipher:2.0.0-alpha1' compile 'info.guardianproject.netcipher:netcipher-webkit:2.0.0-alpha1' + compile "com.android.support:customtabs:24.2.0" apt 'com.jakewharton:butterknife-compiler:8.0.1' } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/activity/MainActivity.java b/app/src/main/java/com/github/dfa/diaspora_android/activity/MainActivity.java index eb66c894..c9f7a68b 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/activity/MainActivity.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/activity/MainActivity.java @@ -41,6 +41,7 @@ import android.os.Handler; import android.os.StrictMode; import android.provider.MediaStore; import android.support.annotation.NonNull; +import android.support.customtabs.CustomTabsIntent; import android.support.design.widget.AppBarLayout; import android.support.design.widget.NavigationView; import android.support.design.widget.Snackbar; @@ -79,6 +80,8 @@ import com.github.dfa.diaspora_android.data.PodUserProfile; import com.github.dfa.diaspora_android.listener.WebUserProfileChangedListener; import com.github.dfa.diaspora_android.ui.ContextMenuWebView; import com.github.dfa.diaspora_android.ui.CustomWebViewClient; +import com.github.dfa.diaspora_android.util.CustomTabHelpers.BrowserFallback; +import com.github.dfa.diaspora_android.util.CustomTabHelpers.CustomTabActivityHelper; import com.github.dfa.diaspora_android.util.DiasporaUrlHelper; import com.github.dfa.diaspora_android.util.Helpers; import com.github.dfa.diaspora_android.util.Log; @@ -111,6 +114,7 @@ public class MainActivity extends AppCompatActivity public static final int REQUEST_CODE__ACCESS_EXTERNAL_STORAGE = 124; public static final String ACTION_OPEN_URL = "com.github.dfa.diaspora_android.MainActivity.open_url"; + public static final String ACTION_OPEN_EXTERNAL_URL = "com.github.dfa.diaspora_android.MainActivity.open_external_url"; public static final String ACTION_CHANGE_ACCOUNT = "com.github.dfa.diaspora_android.MainActivity.change_account"; public static final String ACTION_CLEAR_CACHE = "com.github.dfa.diaspora_android.MainActivity.clear_cache"; public static final String ACTION_UPDATE_TITLE_FROM_URL = "com.github.dfa.diaspora_android.MainActivity.set_title"; @@ -123,6 +127,7 @@ public class MainActivity extends AppCompatActivity private ValueCallback imageUploadFilePathCallbackNew; private ValueCallback imageUploadFilePathCallbackOld; private String mCameraPhotoPath; + private CustomTabActivityHelper customTabActivityHelper; private WebSettings webSettings; private AppSettings appSettings; private DiasporaUrlHelper urls; @@ -186,6 +191,7 @@ public class MainActivity extends AppCompatActivity podUserProfile.setCallbackHandler(uiHandler); podUserProfile.setListener(this); urls = new DiasporaUrlHelper(appSettings); + customTabActivityHelper = new CustomTabActivityHelper(); if (appSettings.isProxyEnabled()) { if (!setProxy(appSettings.getProxyHost(), appSettings.getProxyPort())) { @@ -203,7 +209,7 @@ public class MainActivity extends AppCompatActivity Log.i(App.TAG, "MainActivity.setupUI()"); boolean newWebView = (webView == null); if(newWebView) { - Log.v(App.TAG, "Webview was null. Create new one."); + Log.v(App.TAG, "WebView was null. Create new one."); View webviewHolder = getLayoutInflater().inflate(R.layout.webview, this.contentLayout, false); webView = (ContextMenuWebView) webviewHolder.findViewById(R.id.webView); ((LinearLayout)webView.getParent()).removeView(webView); @@ -727,11 +733,40 @@ public class MainActivity extends AppCompatActivity } }; + private final BroadcastReceiver brOpenExternalLink = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String url = intent.getStringExtra(EXTRA_URL); + if(url != null) { + CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(); + if(Build.VERSION.SDK_INT >= 23) { + intentBuilder.setToolbarColor(getResources().getColor(R.color.colorPrimary, getTheme())); + } else { + intentBuilder.setToolbarColor(getResources().getColor(R.color.colorPrimary)); + } + CustomTabActivityHelper.openCustomTab(MainActivity.this, intentBuilder.build(), Uri.parse(url), new BrowserFallback()); + } + } + }; + + @Override + protected void onStart() { + super.onStart(); + customTabActivityHelper.bindCustomTabsService(this); + } + + @Override + protected void onStop() { + super.onStop(); + customTabActivityHelper.unbindCustomTabsService(this); + } + @Override protected void onPause() { Log.v(App.TAG, "MainActivity.onPause()"); Log.v(App.TAG, "Unregister BroadcastReceivers"); LocalBroadcastManager.getInstance(this).unregisterReceiver(brSetTitle); + LocalBroadcastManager.getInstance(this).unregisterReceiver(brOpenExternalLink); super.onPause(); } @@ -741,6 +776,7 @@ public class MainActivity extends AppCompatActivity super.onResume(); Log.v(App.TAG, "Register BroadcastReceivers"); LocalBroadcastManager.getInstance(this).registerReceiver(brSetTitle, new IntentFilter(ACTION_UPDATE_TITLE_FROM_URL)); + LocalBroadcastManager.getInstance(this).registerReceiver(brOpenExternalLink, new IntentFilter(ACTION_OPEN_EXTERNAL_URL)); } @Override diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/CustomWebViewClient.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/CustomWebViewClient.java index 36150888..9a7832ac 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/ui/CustomWebViewClient.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/CustomWebViewClient.java @@ -20,11 +20,13 @@ package com.github.dfa.diaspora_android.ui; import android.content.Intent; import android.net.Uri; +import android.support.v4.content.LocalBroadcastManager; import android.webkit.CookieManager; import android.webkit.WebView; import android.webkit.WebViewClient; import com.github.dfa.diaspora_android.App; +import com.github.dfa.diaspora_android.activity.MainActivity; public class CustomWebViewClient extends WebViewClient { private final App app; @@ -37,9 +39,9 @@ public class CustomWebViewClient extends WebViewClient { public boolean shouldOverrideUrlLoading(WebView view, String url) { if (!url.contains(app.getSettings().getPodDomain())) { - Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - app.getApplicationContext().startActivity(i); + Intent i = new Intent(MainActivity.ACTION_OPEN_EXTERNAL_URL); + i.putExtra(MainActivity.EXTRA_URL, url); + LocalBroadcastManager.getInstance(app.getApplicationContext()).sendBroadcast(i); return true; } return false; diff --git a/app/src/main/java/com/github/dfa/diaspora_android/util/CustomTabHelpers/BrowserFallback.java b/app/src/main/java/com/github/dfa/diaspora_android/util/CustomTabHelpers/BrowserFallback.java new file mode 100644 index 00000000..a4bdd930 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/CustomTabHelpers/BrowserFallback.java @@ -0,0 +1,18 @@ +package com.github.dfa.diaspora_android.util.CustomTabHelpers; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; + +/** + * Adapted from https://medium.com/ribot-labs/exploring-chrome-customs-tabs-on-android-ef427effe2f4 + */ + +public class BrowserFallback implements CustomTabActivityHelper.CustomTabFallback { + @Override + public void openUri(Activity activity, Uri uri) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(uri); + activity.startActivity(intent); + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/util/CustomTabHelpers/CustomTabActivityHelper.java b/app/src/main/java/com/github/dfa/diaspora_android/util/CustomTabHelpers/CustomTabActivityHelper.java new file mode 100644 index 00000000..9e14c748 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/CustomTabHelpers/CustomTabActivityHelper.java @@ -0,0 +1,149 @@ +package com.github.dfa.diaspora_android.util.CustomTabHelpers; + +import android.app.Activity; +import android.content.ComponentName; +import android.net.Uri; +import android.os.Bundle; +import android.support.customtabs.CustomTabsClient; +import android.support.customtabs.CustomTabsIntent; +import android.support.customtabs.CustomTabsServiceConnection; +import android.support.customtabs.CustomTabsSession; +import android.util.Log; + +import java.util.List; + +/** + * Adapted from https://medium.com/ribot-labs/exploring-chrome-customs-tabs-on-android-ef427effe2f4 + */ + +public class CustomTabActivityHelper { + private CustomTabsSession mCustomTabsSession; + private CustomTabsClient mClient; + private CustomTabsServiceConnection mConnection; + private ConnectionCallback mConnectionCallback; + + /** + * Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView + * + * @param activity The host activity + * @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available + * @param uri the Uri to be opened + * @param fallback a CustomTabFallback to be used if Custom Tabs is not available + */ + public static void openCustomTab(Activity activity, + CustomTabsIntent customTabsIntent, + Uri uri, + CustomTabFallback fallback) { + String packageName = CustomTabsHelper.getPackageNameToUse(activity); + + //If we cant find a package name, it means there's no browser that supports + //Chrome Custom Tabs installed. So, we fallback to the webview + if (packageName == null) { + if (fallback != null) { + fallback.openUri(activity, uri); + } + } else { + customTabsIntent.intent.setPackage(packageName); + customTabsIntent.launchUrl(activity, uri); + } + } + + /** + * Unbinds the Activity from the Custom Tabs Service + * @param activity the activity that is connected to the service + */ + public void unbindCustomTabsService(Activity activity) { + if (mConnection == null) return; + activity.unbindService(mConnection); + mClient = null; + mCustomTabsSession = null; + } + + /** + * Creates or retrieves an exiting CustomTabsSession + * + * @return a CustomTabsSession + */ + public CustomTabsSession getSession() { + if (mClient == null) { + mCustomTabsSession = null; + } else if (mCustomTabsSession == null) { + mCustomTabsSession = mClient.newSession(null); + } + return mCustomTabsSession; + } + + /** + * Register a Callback to be called when connected or disconnected from the Custom Tabs Service + * @param connectionCallback + */ + public void setConnectionCallback(ConnectionCallback connectionCallback) { + this.mConnectionCallback = connectionCallback; + } + + /** + * Binds the Activity to the Custom Tabs Service + * @param activity the activity to be binded to the service + */ + public void bindCustomTabsService(Activity activity) { + if (mClient != null) return; + + String packageName = CustomTabsHelper.getPackageNameToUse(activity); + if (packageName == null) return; + mConnection = new CustomTabsServiceConnection() { + @Override + public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) { + mClient = client; + mClient.warmup(0L); + if (mConnectionCallback != null) mConnectionCallback.onCustomTabsConnected(); + //Initialize a session as soon as possible. + getSession(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mClient = null; + if (mConnectionCallback != null) mConnectionCallback.onCustomTabsDisconnected(); + } + }; + CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection); + } + + public boolean mayLaunchUrl(Uri uri, Bundle extras, List otherLikelyBundles) { + if (mClient == null) return false; + + CustomTabsSession session = getSession(); + if (session == null) return false; + + return session.mayLaunchUrl(uri, extras, otherLikelyBundles); + } + + /** + * A Callback for when the service is connected or disconnected. Use those callbacks to + * handle UI changes when the service is connected or disconnected + */ + public interface ConnectionCallback { + /** + * Called when the service is connected + */ + void onCustomTabsConnected(); + + /** + * Called when the service is disconnected + */ + void onCustomTabsDisconnected(); + } + + /** + * To be used as a fallback to open the Uri when Custom Tabs is not available + */ + public interface CustomTabFallback { + /** + * + * @param activity The Activity that wants to open the Uri + * @param uri The uri to be opened by the fallback + */ + void openUri(Activity activity, Uri uri); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/github/dfa/diaspora_android/util/CustomTabHelpers/CustomTabsHelper.java b/app/src/main/java/com/github/dfa/diaspora_android/util/CustomTabHelpers/CustomTabsHelper.java new file mode 100644 index 00000000..417c62a7 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/CustomTabHelpers/CustomTabsHelper.java @@ -0,0 +1,121 @@ +package com.github.dfa.diaspora_android.util.CustomTabHelpers; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.support.customtabs.CustomTabsService; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class for Custom Tabs. Adapted from https://medium.com/ribot-labs/exploring-chrome-customs-tabs-on-android-ef427effe2f4 + */ +public class CustomTabsHelper { + private static final String TAG = "CustomTabsHelper"; + static final String STABLE_PACKAGE = "com.android.chrome"; + static final String BETA_PACKAGE = "com.chrome.beta"; + static final String DEV_PACKAGE = "com.chrome.dev"; + static final String LOCAL_PACKAGE = "com.google.android.apps.chrome"; + private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE = + "android.support.customtabs.extra.KEEP_ALIVE"; + + private static String sPackageNameToUse; + + private CustomTabsHelper() {} + + /** + * Goes through all apps that handle VIEW intents and have a warmup service. Picks + * the one chosen by the user if there is one, otherwise makes a best effort to return a + * valid package name. + * + * This is not threadsafe. + * + * @param context {@link Context} to use for accessing {@link PackageManager}. + * @return The package name recommended to use for connecting to custom tabs related components. + */ + public static String getPackageNameToUse(Context context) { + if (sPackageNameToUse != null) return sPackageNameToUse; + + PackageManager pm = context.getPackageManager(); + // Get default VIEW intent handler. + Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); + ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0); + String defaultViewHandlerPackageName = null; + if (defaultViewHandlerInfo != null) { + defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName; + } + + // Get all apps that can handle VIEW intents. + List resolvedActivityList = pm.queryIntentActivities(activityIntent, 0); + List packagesSupportingCustomTabs = new ArrayList<>(); + for (ResolveInfo info : resolvedActivityList) { + Intent serviceIntent = new Intent(); + serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION); + serviceIntent.setPackage(info.activityInfo.packageName); + if (pm.resolveService(serviceIntent, 0) != null) { + packagesSupportingCustomTabs.add(info.activityInfo.packageName); + } + } + + // Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents + // and service calls. + if (packagesSupportingCustomTabs.isEmpty()) { + sPackageNameToUse = null; + } else if (packagesSupportingCustomTabs.size() == 1) { + sPackageNameToUse = packagesSupportingCustomTabs.get(0); + } else if (!TextUtils.isEmpty(defaultViewHandlerPackageName) + && !hasSpecializedHandlerIntents(context, activityIntent) + && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) { + sPackageNameToUse = defaultViewHandlerPackageName; + } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) { + sPackageNameToUse = STABLE_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) { + sPackageNameToUse = BETA_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) { + sPackageNameToUse = DEV_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) { + sPackageNameToUse = LOCAL_PACKAGE; + } + return sPackageNameToUse; + } + + /** + * Used to check whether there is a specialized handler for a given intent. + * @param intent The intent to check with. + * @return Whether there is a specialized handler for the given intent. + */ + private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) { + try { + PackageManager pm = context.getPackageManager(); + List handlers = pm.queryIntentActivities( + intent, + PackageManager.GET_RESOLVED_FILTER); + if (handlers == null || handlers.size() == 0) { + return false; + } + for (ResolveInfo resolveInfo : handlers) { + IntentFilter filter = resolveInfo.filter; + if (filter == null) continue; + if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue; + if (resolveInfo.activityInfo == null) continue; + return true; + } + } catch (RuntimeException e) { + Log.e(TAG, "Runtime exception while getting specialized handlers"); + } + return false; + } + + /** + * @return All possible chrome package names that provide custom tabs feature. + */ + public static String[] getPackages() { + return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE}; + } +}