/*
* Copyright (c) 2015 Ha Duy Trung
*
* 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 io.github.hidroh.materialistic;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Point;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Parcelable;
import android.support.annotation.AttrRes;
import android.support.annotation.DimenRes;
import android.support.annotation.NonNull;
import android.support.annotation.StyleRes;
import android.support.customtabs.CustomTabsIntent;
import android.support.customtabs.CustomTabsSession;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.util.Pair;
import android.support.v4.view.GravityCompat;
import android.support.v7.view.ContextThemeWrapper;
import android.text.Html;
import android.text.Layout;
import android.text.Spannable;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.style.ClickableSpan;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.webkit.WebSettings;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import io.github.hidroh.materialistic.annotation.PublicApi;
import io.github.hidroh.materialistic.data.HackerNewsClient;
import io.github.hidroh.materialistic.data.Item;
import io.github.hidroh.materialistic.data.WebItem;
import io.github.hidroh.materialistic.widget.PopupMenu;
@SuppressWarnings("WeakerAccess")
@PublicApi
public class AppUtils {
private static final String ABBR_YEAR = "y";
private static final String ABBR_WEEK = "w";
private static final String ABBR_DAY = "d";
private static final String ABBR_HOUR = "h";
private static final String ABBR_MINUTE = "m";
private static final String PLAY_STORE_URL = "market://details?id=" + BuildConfig.APPLICATION_ID;
private static final String FORMAT_HTML_COLOR = "%06X";
public static final int HOT_THRESHOLD_HIGH = 300;
public static final int HOT_THRESHOLD_NORMAL = 100;
static final int HOT_THRESHOLD_LOW = 10;
public static final int HOT_FACTOR = 3;
private static final String HOST_ITEM = "item";
private static final String HOST_USER = "user";
public static void openWebUrlExternal(Context context, WebItem item, String url, CustomTabsSession session) {
if (!hasConnection(context)) {
context.startActivity(new Intent(context, OfflineWebActivity.class)
.putExtra(OfflineWebActivity.EXTRA_URL, url));
return;
}
Intent intent = createViewIntent(context, item, url, session);
if (!HackerNewsClient.BASE_WEB_URL.contains(Uri.parse(url).getHost())) {
if (intent.resolveActivity(context.getPackageManager()) != null) {
context.startActivity(intent);
}
return;
}
List<ResolveInfo> activities = context.getPackageManager()
.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
ArrayList<Intent> intents = new ArrayList<>();
for (ResolveInfo info : activities) {
if (info.activityInfo.packageName.equalsIgnoreCase(context.getPackageName())) {
continue;
}
intents.add(createViewIntent(context, item, url, session)
.setPackage(info.activityInfo.packageName));
}
if (intents.isEmpty()) {
return;
}
if (intents.size() == 1) {
context.startActivity(intents.remove(0));
} else {
context.startActivity(Intent.createChooser(intents.remove(0),
context.getString(R.string.chooser_title))
.putExtra(Intent.EXTRA_INITIAL_INTENTS,
intents.toArray(new Parcelable[intents.size()])));
}
}
public static void setTextWithLinks(TextView textView, CharSequence html) {
textView.setText(html);
// TODO https://code.google.com/p/android/issues/detail?id=191430
//noinspection Convert2Lambda
textView.setOnTouchListener(new View.OnTouchListener() {
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
TextView widget = (TextView) v;
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] link = Spannable.Factory.getInstance()
.newSpannable(widget.getText())
.getSpans(off, off, ClickableSpan.class);
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
}
return true;
}
}
return false;
}
});
}
public static CharSequence fromHtml(String htmlText) {
return fromHtml(htmlText, false);
}
public static CharSequence fromHtml(String htmlText, boolean compact) {
if (TextUtils.isEmpty(htmlText)) {
return null;
}
CharSequence spanned;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//noinspection InlinedApi
spanned = Html.fromHtml(htmlText, compact ?
Html.FROM_HTML_MODE_COMPACT : Html.FROM_HTML_MODE_LEGACY);
} else {
//noinspection deprecation
spanned = Html.fromHtml(htmlText);
}
return trim(spanned);
}
public static Intent makeSendIntentChooser(Context context, Uri data) {
// use ACTION_SEND_MULTIPLE instead of ACTION_SEND to filter out
// share receivers that accept only EXTRA_TEXT but not EXTRA_STREAM
return Intent.createChooser(new Intent(Intent.ACTION_SEND_MULTIPLE)
.setType("text/plain")
.putParcelableArrayListExtra(Intent.EXTRA_STREAM,
new ArrayList<Uri>(){{add(data);}}),
context.getString(R.string.share_file));
}
public static void openExternal(@NonNull final Context context,
@NonNull PopupMenu popupMenu,
@NonNull View anchor,
@NonNull final WebItem item,
final CustomTabsSession session) {
if (TextUtils.isEmpty(item.getUrl()) ||
item.getUrl().startsWith(HackerNewsClient.BASE_WEB_URL)) {
openWebUrlExternal(context,
item, String.format(HackerNewsClient.WEB_ITEM_PATH, item.getId()),
session);
return;
}
popupMenu.create(context, anchor, GravityCompat.END)
.inflate(R.menu.menu_share)
.setOnMenuItemClickListener(menuItem -> {
openWebUrlExternal(context, item, menuItem.getItemId() == R.id.menu_article ?
item.getUrl() :
String.format(HackerNewsClient.WEB_ITEM_PATH, item.getId()), session);
return true;
})
.show();
}
public static void share(@NonNull final Context context,
@NonNull PopupMenu popupMenu,
@NonNull View anchor,
@NonNull final WebItem item) {
if (TextUtils.isEmpty(item.getUrl()) ||
item.getUrl().startsWith(HackerNewsClient.BASE_WEB_URL)) {
share(context, item.getDisplayedTitle(),
String.format(HackerNewsClient.WEB_ITEM_PATH, item.getId()));
return;
}
popupMenu.create(context, anchor, GravityCompat.END)
.inflate(R.menu.menu_share)
.setOnMenuItemClickListener(menuItem -> {
share(context, item.getDisplayedTitle(),
menuItem.getItemId() == R.id.menu_article ?
item.getUrl() :
String.format(HackerNewsClient.WEB_ITEM_PATH, item.getId()));
return true;
})
.show();
}
public static int getThemedResId(Context context, @AttrRes int attr) {
TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr});
final int resId = a.getResourceId(0, 0);
a.recycle();
return resId;
}
public static float getDimension(Context context, @StyleRes int styleResId, @AttrRes int attr) {
TypedArray a = context.getTheme().obtainStyledAttributes(styleResId, new int[]{attr});
float size = a.getDimension(0, 0);
a.recycle();
return size;
}
public static boolean isHackerNewsUrl(WebItem item) {
return !TextUtils.isEmpty(item.getUrl()) &&
item.getUrl().equals(String.format(HackerNewsClient.WEB_ITEM_PATH, item.getId()));
}
public static int getDimensionInDp(Context context, @DimenRes int dimenResId) {
return (int) (context.getResources().getDimension(dimenResId) /
context.getResources().getDisplayMetrics().density);
}
public static void restart(Activity activity, boolean transition) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
activity.recreate();
} else {
activity.finish();
if (transition) {
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
}
activity.startActivity(activity.getIntent());
}
}
public static String getAbbreviatedTimeSpan(long timeMillis) {
long span = Math.max(System.currentTimeMillis() - timeMillis, 0);
if (span >= DateUtils.YEAR_IN_MILLIS) {
return (span / DateUtils.YEAR_IN_MILLIS) + ABBR_YEAR;
}
if (span >= DateUtils.WEEK_IN_MILLIS) {
return (span / DateUtils.WEEK_IN_MILLIS) + ABBR_WEEK;
}
if (span >= DateUtils.DAY_IN_MILLIS) {
return (span / DateUtils.DAY_IN_MILLIS) + ABBR_DAY;
}
if (span >= DateUtils.HOUR_IN_MILLIS) {
return (span / DateUtils.HOUR_IN_MILLIS) + ABBR_HOUR;
}
return (span / DateUtils.MINUTE_IN_MILLIS) + ABBR_MINUTE;
}
public static boolean isOnWiFi(Context context) {
NetworkInfo activeNetwork = ((ConnectivityManager) context.getSystemService(
Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
return activeNetwork != null &&
activeNetwork.isConnectedOrConnecting() &&
activeNetwork.getType() == ConnectivityManager.TYPE_WIFI;
}
public static boolean hasConnection(Context context) {
NetworkInfo activeNetworkInfo = ((ConnectivityManager) context.getSystemService(
Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting();
}
@SuppressLint("MissingPermission")
public static Pair<String, String> getCredentials(Context context) {
String username = Preferences.getUsername(context);
if (TextUtils.isEmpty(username)) {
return null;
}
AccountManager accountManager = AccountManager.get(context);
Account[] accounts = accountManager.getAccountsByType(BuildConfig.APPLICATION_ID);
for (Account account : accounts) {
if (TextUtils.equals(username, account.name)) {
return Pair.create(username, accountManager.getPassword(account));
}
}
return null;
}
/**
* Displays UI to allow user to login
* If no accounts exist in user's device, regardless of login status, prompt to login again
* If 1 or more accounts in user's device, and already logged in, prompt to update password
* If 1 or more accounts in user's device, and logged out, show account chooser
* @param context activity context
* @param alertDialogBuilder dialog builder
*/
@SuppressLint("MissingPermission")
public static void showLogin(Context context, AlertDialogBuilder alertDialogBuilder) {
Account[] accounts = AccountManager.get(context).getAccountsByType(BuildConfig.APPLICATION_ID);
if (accounts.length == 0) { // no accounts, ask to login or re-login
context.startActivity(new Intent(context, LoginActivity.class));
} else if (!TextUtils.isEmpty(Preferences.getUsername(context))) { // stale account, ask to re-login
context.startActivity(new Intent(context, LoginActivity.class));
} else { // logged out, choose from existing accounts to log in
showAccountChooser(context, alertDialogBuilder, accounts);
}
}
@SuppressLint("MissingPermission")
public static void registerAccountsUpdatedListener(final Context context) {
AccountManager.get(context).addOnAccountsUpdatedListener(accounts -> {
String username = Preferences.getUsername(context);
if (TextUtils.isEmpty(username)) {
return;
}
for (Account account : accounts) {
if (TextUtils.equals(account.name, username)) {
return;
}
}
Preferences.setUsername(context, null);
}, null, true);
}
@SuppressWarnings("deprecation")
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void openPlayStore(Context context) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(PLAY_STORE_URL));
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY |
Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
} else {
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
}
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, R.string.no_playstore, Toast.LENGTH_SHORT).show();
}
}
@SuppressLint("MissingPermission")
public static void showAccountChooser(final Context context, AlertDialogBuilder alertDialogBuilder,
Account[] accounts) {
String username = Preferences.getUsername(context);
final String[] items = new String[accounts.length];
int checked = -1;
for (int i = 0; i < accounts.length; i++) {
String accountName = accounts[i].name;
items[i] = accountName;
if (TextUtils.equals(accountName, username)) {
checked = i;
}
}
int initialSelection = checked;
DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() {
private int selection = initialSelection;
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
Preferences.setUsername(context, items[selection]);
Toast.makeText(context,
context.getString(R.string.welcome, items[selection]),
Toast.LENGTH_SHORT)
.show();
dialog.dismiss();
break;
case DialogInterface.BUTTON_NEGATIVE:
Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(LoginActivity.EXTRA_ADD_ACCOUNT, true);
context.startActivity(intent);
dialog.dismiss();
break;
case DialogInterface.BUTTON_NEUTRAL:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
AccountManager.get(context).removeAccount(accounts[selection], null, null, null);
} else {
//noinspection deprecation
AccountManager.get(context).removeAccount(accounts[selection], null, null);
}
dialog.dismiss();
break;
default:
selection = which;
break;
}
}
};
alertDialogBuilder
.init(context)
.setTitle(R.string.choose_account)
.setSingleChoiceItems(items, checked, clickListener)
.setPositiveButton(android.R.string.ok, clickListener)
.setNegativeButton(R.string.add_account, clickListener)
.setNeutralButton(R.string.remove_account, clickListener)
.show();
}
public static void toggleFab(FloatingActionButton fab, boolean visible) {
CoordinatorLayout.LayoutParams p = (CoordinatorLayout.LayoutParams) fab.getLayoutParams();
if (visible) {
fab.show();
p.setBehavior(new ScrollAwareFABBehavior());
} else {
fab.hide();
p.setBehavior(null);
}
}
public static void toggleFabAction(FloatingActionButton fab, WebItem item, boolean commentMode) {
Context context = fab.getContext();
fab.setImageResource(commentMode ? R.drawable.ic_reply_white_24dp : R.drawable.ic_zoom_out_map_white_24dp);
fab.setOnClickListener(v -> {
if (commentMode) {
context.startActivity(new Intent(context, ComposeActivity.class)
.putExtra(ComposeActivity.EXTRA_PARENT_ID, item.getId())
.putExtra(ComposeActivity.EXTRA_PARENT_TEXT,
item instanceof Item ? ((Item) item).getText() : null));
} else {
LocalBroadcastManager.getInstance(context)
.sendBroadcast(new Intent(WebFragment.ACTION_FULLSCREEN)
.putExtra(WebFragment.EXTRA_FULLSCREEN, true));
}
});
}
public static String toHtmlColor(Context context, @AttrRes int colorAttr) {
return String.format(FORMAT_HTML_COLOR, 0xFFFFFF & ContextCompat.getColor(context,
AppUtils.getThemedResId(context, colorAttr)));
}
public static void toggleWebViewZoom(WebSettings webSettings, boolean enabled) {
webSettings.setSupportZoom(enabled);
webSettings.setBuiltInZoomControls(enabled);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
webSettings.setDisplayZoomControls(false);
}
}
public static void setStatusBarDim(Window window, boolean dim) {
setStatusBarColor(window, dim ? Color.TRANSPARENT :
ContextCompat.getColor(window.getContext(),
AppUtils.getThemedResId(window.getContext(), R.attr.colorPrimaryDark)));
}
public static void setStatusBarColor(Window window, int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.setStatusBarColor(color);
}
}
public static void navigate(int direction, AppBarLayout appBarLayout, Navigable navigable) {
switch (direction) {
case Navigable.DIRECTION_DOWN:
case Navigable.DIRECTION_RIGHT:
if (appBarLayout.getBottom() == 0) {
navigable.onNavigate(direction);
} else {
appBarLayout.setExpanded(false, true);
}
break;
default:
navigable.onNavigate(direction);
break;
}
}
public static int getDisplayHeight(Context context) {
Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
Point point = new Point();
display.getSize(point);
return point.y;
} else {
//noinspection deprecation
return display.getHeight();
}
}
public static LayoutInflater createLayoutInflater(Context context) {
return LayoutInflater.from(new ContextThemeWrapper(context,
Preferences.Theme.resolvePreferredTextSize(context)));
}
public static void share(Context context, String subject, String text) {
Intent intent = new Intent(Intent.ACTION_SEND)
.setType("text/plain")
.putExtra(Intent.EXTRA_SUBJECT, subject)
.putExtra(Intent.EXTRA_TEXT, !TextUtils.isEmpty(subject) ?
TextUtils.join(" - ", new String[]{subject, text}) : text);
if (intent.resolveActivity(context.getPackageManager()) != null) {
context.startActivity(intent);
}
}
public static Uri createItemUri(@NonNull String itemId) {
return new Uri.Builder()
.scheme(BuildConfig.APPLICATION_ID)
.authority(HOST_ITEM)
.path(itemId)
.build();
}
public static Uri createUserUri(@NonNull String userId) {
return new Uri.Builder()
.scheme(BuildConfig.APPLICATION_ID)
.authority(HOST_USER)
.path(userId)
.build();
}
public static String getDataUriId(@NonNull Intent intent, String altParamId) {
if (intent.getData() == null) {
return null;
}
if (TextUtils.equals(intent.getData().getScheme(), BuildConfig.APPLICATION_ID)) {
return intent.getData().getLastPathSegment();
} else { // web URI
return intent.getData().getQueryParameter(altParamId);
}
}
public static String wrapHtml(Context context, String html) {
return context.getString(R.string.html,
Preferences.Theme.getReadabilityTypeface(context),
toHtmlPx(context, Preferences.Theme.resolvePreferredReadabilityTextSize(context)),
AppUtils.toHtmlColor(context, android.R.attr.textColorPrimary),
AppUtils.toHtmlColor(context, android.R.attr.textColorLink),
TextUtils.isEmpty(html) ? context.getString(R.string.empty_text) : html,
toHtmlPx(context, context.getResources().getDimension(R.dimen.activity_vertical_margin)),
toHtmlPx(context, context.getResources().getDimension(R.dimen.activity_horizontal_margin)),
Preferences.getReadabilityLineHeight(context));
}
private static float toHtmlPx(Context context, @StyleRes int textStyleAttr) {
return toHtmlPx(context, AppUtils.getDimension(context, textStyleAttr, R.attr.contentTextSize));
}
private static float toHtmlPx(Context context, float dimen) {
return dimen / context.getResources().getDisplayMetrics().density;
}
private static CharSequence trim(CharSequence charSequence) {
if (TextUtils.isEmpty(charSequence)) {
return charSequence;
}
int end = charSequence.length() - 1;
while (Character.isWhitespace(charSequence.charAt(end))) {
end--;
}
return charSequence.subSequence(0, end + 1);
}
@NonNull
private static Intent createViewIntent(Context context, WebItem item, String url, CustomTabsSession session) {
if (Preferences.customChromeTabEnabled(context)) {
return new CustomTabsIntent.Builder(session)
.setToolbarColor(ContextCompat.getColor(context,
AppUtils.getThemedResId(context, R.attr.colorPrimary)))
.setShowTitle(true)
.enableUrlBarHiding()
.addDefaultShareMenuItem()
.addMenuItem(context.getString(R.string.comments),
PendingIntent.getActivity(context, 0,
new Intent(context, ItemActivity.class)
.putExtra(ItemActivity.EXTRA_ITEM, item)
.putExtra(ItemActivity.EXTRA_OPEN_COMMENTS, true),
PendingIntent.FLAG_ONE_SHOT))
.build()
.intent
.setData(Uri.parse(url));
} else {
return new Intent(Intent.ACTION_VIEW, Uri.parse(url));
}
}
@SuppressLint("InlinedApi")
public static Intent multiWindowIntent(Activity activity, Intent intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode()) {
intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT |
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
}
return intent;
}
public static void setTextAppearance(TextView textView, @StyleRes int textAppearance) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
textView.setTextAppearance(textAppearance);
} else {
//noinspection deprecation
textView.setTextAppearance(textView.getContext(), textAppearance);
}
}
static class SystemUiHelper {
private final Window window;
private final int originalUiFlags;
private boolean enabled = true;
SystemUiHelper(Window window) {
this.window = window;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
this.originalUiFlags = window.getDecorView().getSystemUiVisibility();
} else {
this.originalUiFlags = 0;
}
}
@SuppressLint("InlinedApi")
void setFullscreen(boolean fullscreen) {
if (!enabled) {
return;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return;
}
if (fullscreen) {
window.getDecorView().setSystemUiVisibility(originalUiFlags |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
} else {
window.getDecorView().setSystemUiVisibility(originalUiFlags);
}
}
void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
}