diff --git a/.gitignore b/.gitignore index b8cd4a42..c7b11955 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ build/ .idea release.properties crashlytics.properties +*.iml diff --git a/app/build.gradle b/app/build.gradle index 21ff49ba..988503fb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,10 +12,10 @@ apply plugin: 'android-apt' dependencies { apt "org.androidannotations:androidannotations:3.2" - compile "org.androidannotations:androidannotations-api:3.2" - compile 'com.android.support:appcompat-v7:18.0.+' - compile 'com.android.support:support-v4:21.0.+' - + compile 'org.androidannotations:androidannotations-api:3.2' + compile 'com.android.support:appcompat-v7:23.3.0' + compile 'com.android.support:support-v4:23.3.0' + compile 'com.android.support:customtabs:23.3.0' compile fileTree(dir: 'libs', include: ['*.jar']) } @@ -23,13 +23,14 @@ android { compileOptions { encoding "UTF-8" } - compileSdkVersion 19 - buildToolsVersion '19.1.0' + compileSdkVersion 23 + buildToolsVersion '23.0.2' + useLibrary 'org.apache.http.legacy' defaultConfig { applicationId "com.manuelmaly.hn" - minSdkVersion 8 - targetSdkVersion 19 + minSdkVersion 15 + targetSdkVersion 23 versionCode 25 versionName "1.9.15" } diff --git a/app/src/main/java/com/manuelmaly/hn/ArticleReaderActivity.java b/app/src/main/java/com/manuelmaly/hn/ArticleReaderActivity.java index 70647c28..8378d123 100644 --- a/app/src/main/java/com/manuelmaly/hn/ArticleReaderActivity.java +++ b/app/src/main/java/com/manuelmaly/hn/ArticleReaderActivity.java @@ -1,6 +1,7 @@ package com.manuelmaly.hn; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -24,6 +25,7 @@ import com.manuelmaly.hn.util.FontHelper; import com.manuelmaly.hn.util.SpotlightActivity; import com.manuelmaly.hn.util.ViewedUtils; +import com.manuelmaly.hn.util.CustomTabActivityHelper; import org.androidannotations.annotations.AfterViews; import org.androidannotations.annotations.EActivity; @@ -33,7 +35,7 @@ import java.net.URLEncoder; @EActivity(R.layout.article_activity) -public class ArticleReaderActivity extends ActionBarActivity { +public class ArticleReaderActivity extends ActionBarActivity implements CustomTabActivityHelper.CustomTabFallback { public static final int ACTIVITY_LOGIN = 137; @@ -265,6 +267,17 @@ private void setShowRefreshing(boolean showRefreshing) { } } + @Override + public void openUri(Activity activity, HNPost post, String overrideHtmlProvider) { + Intent i = new Intent(activity, ArticleReaderActivity_.class); + i.putExtra(ArticleReaderActivity.EXTRA_HNPOST, post); + if (overrideHtmlProvider != null) { + i.putExtra(ArticleReaderActivity.EXTRA_HTMLPROVIDER_OVERRIDE, + overrideHtmlProvider); + } + activity.startActivity(i); + } + private class HNReaderWebViewClient extends WebViewClient { @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { diff --git a/app/src/main/java/com/manuelmaly/hn/CommentsActivity.java b/app/src/main/java/com/manuelmaly/hn/CommentsActivity.java index e9a1c7f7..69960ca6 100644 --- a/app/src/main/java/com/manuelmaly/hn/CommentsActivity.java +++ b/app/src/main/java/com/manuelmaly/hn/CommentsActivity.java @@ -11,6 +11,7 @@ import android.os.Handler; import android.os.Looper; import android.os.Parcelable; +import android.support.customtabs.CustomTabsIntent; import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.text.Html; @@ -48,6 +49,7 @@ import com.manuelmaly.hn.util.FontHelper; import com.manuelmaly.hn.util.SpotlightActivity; import com.manuelmaly.hn.util.ViewedUtils; +import com.manuelmaly.hn.util.CustomTabActivityHelper; import org.androidannotations.annotations.AfterViews; import org.androidannotations.annotations.EActivity; @@ -144,6 +146,9 @@ public void onClick(View v) { CommentsActivity.this); MainActivity.openURLInBrowser(articleURL, CommentsActivity.this); + } else if (Settings.getHtmlViewer(CommentsActivity.this).equals( + getString(R.string.pref_htmlviewer_chromecustomtabs))) { + MainActivity.openURLInChromeCustomTabs(mPost, null, CommentsActivity.this); } else { openArticleReader(); } @@ -368,20 +373,11 @@ private void showCommentsSpotlight() { } private void openArticleReader() { - Intent intent = new Intent(this, ArticleReaderActivity_.class); - intent.putExtra(CommentsActivity.EXTRA_HNPOST, mPost); - if (getIntent().getStringExtra( - ArticleReaderActivity.EXTRA_HTMLPROVIDER_OVERRIDE) != null) { - intent.putExtra( - ArticleReaderActivity.EXTRA_HTMLPROVIDER_OVERRIDE, - getIntent().getStringExtra( - ArticleReaderActivity.EXTRA_HTMLPROVIDER_OVERRIDE)); - } - - startActivity(intent); - overridePendingTransition(android.R.anim.fade_in, - android.R.anim.fade_out); - finish(); + CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); + CustomTabsIntent customTabsIntent = builder.build(); + CustomTabActivityHelper.openCustomTab( + this, customTabsIntent, mPost, Settings.getHtmlProvider(CommentsActivity.this), + new ArticleReaderActivity()); } private void initCommentsHeader() { diff --git a/app/src/main/java/com/manuelmaly/hn/MainActivity.java b/app/src/main/java/com/manuelmaly/hn/MainActivity.java index 84849b2c..dc18736e 100644 --- a/app/src/main/java/com/manuelmaly/hn/MainActivity.java +++ b/app/src/main/java/com/manuelmaly/hn/MainActivity.java @@ -10,6 +10,7 @@ import com.manuelmaly.hn.task.ITaskFinishedHandler; import com.manuelmaly.hn.util.FileUtil; import com.manuelmaly.hn.util.FontHelper; +import com.manuelmaly.hn.util.CustomTabActivityHelper; import org.androidannotations.annotations.AfterViews; import org.androidannotations.annotations.Background; @@ -19,6 +20,7 @@ import android.app.Activity; import android.app.AlertDialog; +import android.app.PendingIntent; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; @@ -26,9 +28,12 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.database.DataSetObserver; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; +import android.support.customtabs.CustomTabsIntent; import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.util.Log; @@ -503,6 +508,9 @@ else if (Settings.getHtmlViewer(MainActivity.this).equals( openURLInBrowser( getArticleViewURL(getItem(position)), MainActivity.this); + } else if (Settings.getHtmlViewer(MainActivity.this).equals( + getString(R.string.pref_htmlviewer_chromecustomtabs))) { + openURLInChromeCustomTabs(getItem(position), null, MainActivity.this); } else { openPostInApp(getItem(position), null, MainActivity.this); @@ -721,6 +729,22 @@ public static void openURLInBrowser(String url, Activity a) { a.startActivity(browserIntent); } + public static void openURLInChromeCustomTabs(HNPost post, String overrideHtmlProvider, Activity a) { + Intent commentsIntent = new Intent(a, CommentsActivity_.class); + commentsIntent.putExtra(CommentsActivity.EXTRA_HNPOST, post); + PendingIntent pendingIntent = PendingIntent.getActivity(a, 1, commentsIntent, + PendingIntent.FLAG_CANCEL_CURRENT); + + Bitmap icon = BitmapFactory.decodeResource(a.getResources(), + R.drawable.ic_launcher); + + CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); + builder.setActionButton(icon, a.getString(R.string.comments), pendingIntent); + CustomTabsIntent customTabsIntent = builder.build(); + CustomTabActivityHelper.openCustomTab( + a, customTabsIntent, post, overrideHtmlProvider, new ArticleReaderActivity()); + } + public static void openPostInApp(HNPost post, String overrideHtmlProvider, Activity a) { Intent i = new Intent(a, ArticleReaderActivity_.class); diff --git a/app/src/main/java/com/manuelmaly/hn/util/CustomTabActivityHelper.java b/app/src/main/java/com/manuelmaly/hn/util/CustomTabActivityHelper.java new file mode 100644 index 00000000..05b65885 --- /dev/null +++ b/app/src/main/java/com/manuelmaly/hn/util/CustomTabActivityHelper.java @@ -0,0 +1,70 @@ +package com.manuelmaly.hn.util; + +// Copyright 2015 Google Inc. All Rights Reserved. +// +// 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. + +import android.app.Activity; +import android.net.Uri; +import android.support.customtabs.CustomTabsIntent; + +import com.manuelmaly.hn.model.HNPost; + +import java.util.List; + +/** + * This is a helper class to manage the connection to the Custom Tabs Service. + */ +public class CustomTabActivityHelper { + + /** + * 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 post The HNPost article that must be opened + * @param overrideHtmlProvider something something HTML + * @param fallback a CustomTabFallback to be used if Custom Tabs is not available. + */ + public static void openCustomTab(Activity activity, + CustomTabsIntent customTabsIntent, + HNPost post, + String overrideHtmlProvider, + CustomTabFallback fallback) { + String packageName = CustomTabsHelper.getPackageNameToUse(activity); + + // If we cant find a package name, it means theres no browser that supports + // Chrome Custom Tabs installed. So, we fallback to the webview + if (packageName == null) { + if (fallback != null) { + fallback.openUri(activity, post, overrideHtmlProvider); + } + } else { + customTabsIntent.intent.setPackage(packageName); + customTabsIntent.launchUrl(activity, Uri.parse(post.getURL())); + } + } + + /** + * 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 post The HNPost article that must be opened + * @param overrideHtmlProvider something something HTML + */ + void openUri(Activity activity, HNPost post, String overrideHtmlProvider); + } +} diff --git a/app/src/main/java/com/manuelmaly/hn/util/CustomTabsHelper.java b/app/src/main/java/com/manuelmaly/hn/util/CustomTabsHelper.java new file mode 100644 index 00000000..ac867c6a --- /dev/null +++ b/app/src/main/java/com/manuelmaly/hn/util/CustomTabsHelper.java @@ -0,0 +1,136 @@ +package com.manuelmaly.hn.util; + +// Copyright 2015 Google Inc. All Rights Reserved. +// +// 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. + +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.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class for Custom Tabs. + */ +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 final String ACTION_CUSTOM_TABS_CONNECTION = + "android.support.customtabs.action.CustomTabsService"; + + 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(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}; + } +} diff --git a/app/src/main/res/values/preference_values.xml b/app/src/main/res/values/preference_values.xml index dd65ea52..d225c622 100644 --- a/app/src/main/res/values/preference_values.xml +++ b/app/src/main/res/values/preference_values.xml @@ -29,11 +29,13 @@ View Articles within … The App + Chrome Custom Tab The App The System Browser @string/pref_htmlviewer_app + @string/pref_htmlviewer_chromecustomtabs @string/pref_htmlviewer_browser