package com.gh4a.utils; import android.Manifest; import android.annotation.TargetApi; import android.app.Dialog; import android.app.DownloadManager; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.pm.PackageManager; import android.content.res.TypedArray; import android.graphics.Color; import android.net.ConnectivityManager; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v4.view.ViewCompat; import android.support.v7.app.AlertDialog; import android.support.v7.widget.RecyclerView; import android.text.Editable; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; import android.view.ActionMode; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewTreeObserver; import android.view.inputmethod.InputMethodManager; import android.widget.EdgeEffect; import android.widget.EditText; import android.widget.MultiAutoCompleteTextView; import android.widget.TextView; import android.widget.Toast; import com.gh4a.BaseActivity; import com.gh4a.Gh4Application; import com.gh4a.R; import com.gh4a.widget.IssueLabelSpan; import org.eclipse.egit.github.core.Label; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List; public class UiUtils { public static final LinkMovementMethod CHECKING_LINK_METHOD = new LinkMovementMethod() { @Override public boolean onTouchEvent(@NonNull TextView widget, @NonNull Spannable buffer, @NonNull MotionEvent event) { try { return super.onTouchEvent(widget, buffer, event); } catch (ActivityNotFoundException e) { Toast.makeText(widget.getContext(), R.string.link_not_openable, Toast.LENGTH_LONG) .show(); return true; } catch (SecurityException e) { // some apps have intent filters set for the VIEW action for // internal, non-exported activities // -> ignore return true; } } }; public static void hideImeForView(View view) { if (view == null) { return; } InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } public static void showImeForView(View view) { if (view == null) { return; } InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } public static int textColorForBackground(Context context, int backgroundColor) { int red = Color.red(backgroundColor); int green = Color.green(backgroundColor); int blue = Color.blue(backgroundColor); int luminance = Math.round(0.213F * red + 0.715F * green + 0.072F * blue); if (luminance >= 128) { return ContextCompat.getColor(context, R.color.label_fg_dark); } return ContextCompat.getColor(context, R.color.label_fg_light); } public static void trySetListOverscrollColor(RecyclerView view, int color) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { RecyclerViewEdgeColorHelper helper = (RecyclerViewEdgeColorHelper) view.getTag(R.id.EdgeColorHelper); if (helper == null) { helper = new RecyclerViewEdgeColorHelper(view); view.setTag(R.id.EdgeColorHelper, helper); } helper.setColor(color); } } @TargetApi(21) private static class RecyclerViewEdgeColorHelper implements ViewTreeObserver.OnGlobalLayoutListener { private final RecyclerView mView; private int mColor; private EdgeEffect mTopEffect, mBottomEffect; private Object mLastTopEffect, mLastBottomEffect; private static Method sTopEnsureMethod, sBottomEnsureMethod; private static Field sTopEffectField, sBottomEffectField; public RecyclerViewEdgeColorHelper(RecyclerView view) { mView = view; mColor = 0; mView.getViewTreeObserver().addOnGlobalLayoutListener(this); } public void setColor(int color) { mColor = color; applyIfPossible(); } @Override public void onGlobalLayout() { applyIfPossible(); } private void applyIfPossible() { if (!ensureStaticMethodsAndFields()) { return; } try { Object topEffect = sTopEffectField.get(mView); Object bottomEffect = sBottomEffectField.get(mView); if (topEffect == null || bottomEffect == null || topEffect != mLastTopEffect || bottomEffect != mLastBottomEffect) { sTopEnsureMethod.invoke(mView); sBottomEnsureMethod.invoke(mView); mLastTopEffect = sTopEffectField.get(mView); mLastBottomEffect = sBottomEffectField.get(mView); final Field edgeField = mLastTopEffect.getClass().getDeclaredField("mEdgeEffect"); edgeField.setAccessible(true); mTopEffect = (EdgeEffect) edgeField.get(mLastTopEffect); mBottomEffect = (EdgeEffect) edgeField.get(mLastBottomEffect); } } catch (IllegalAccessException | InvocationTargetException | NoSuchFieldException e) { mTopEffect = mBottomEffect = null; } applyColor(mTopEffect); applyColor(mBottomEffect); } private void applyColor(EdgeEffect effect) { if (effect != null) { final int alpha = Color.alpha(effect.getColor()); effect.setColor(Color.argb(alpha, Color.red(mColor), Color.green(mColor), Color.blue(mColor))); } } private boolean ensureStaticMethodsAndFields() { if (sTopEnsureMethod != null && sBottomEnsureMethod != null) { return true; } try { sTopEnsureMethod = RecyclerView.class.getDeclaredMethod("ensureTopGlow"); sTopEnsureMethod.setAccessible(true); sBottomEnsureMethod = RecyclerView.class.getDeclaredMethod("ensureBottomGlow"); sBottomEnsureMethod.setAccessible(true); sTopEffectField = RecyclerView.class.getDeclaredField("mTopGlow"); sTopEffectField.setAccessible(true); sBottomEffectField = RecyclerView.class.getDeclaredField("mBottomGlow"); sBottomEffectField.setAccessible(true); return true; } catch (NoSuchMethodException | NoSuchFieldException e) { // ignored } return false; } } public static int mixColors(int startColor, int endColor, float fraction) { // taken from ArgbEvaluator.evaluate int startA = (startColor >> 24) & 0xff; int startR = (startColor >> 16) & 0xff; int startG = (startColor >> 8) & 0xff; int startB = startColor & 0xff; int endA = (endColor >> 24) & 0xff; int endR = (endColor >> 16) & 0xff; int endG = (endColor >> 8) & 0xff; int endB = endColor & 0xff; return ((startA + (int)(fraction * (endA - startA))) << 24) | ((startR + (int)(fraction * (endR - startR))) << 16) | ((startG + (int)(fraction * (endG - startG))) << 8) | ((startB + (int)(fraction * (endB - startB)))); } public static boolean canViewScrollUp(View view) { if (view == null) { return false; } return ViewCompat.canScrollVertically(view, -1); } public static Context makeHeaderThemedContext(Context context) { int themeResId = resolveDrawable(context, R.attr.headerTheme); if (themeResId != 0) { return new ContextThemeWrapper(context, themeResId); } return context; } public static Dialog createProgressDialog(Context context, @StringRes int messageResId) { View content = LayoutInflater.from(context).inflate(R.layout.progress_dialog, null); TextView message = (TextView) content.findViewById(R.id.message); message.setText(messageResId); return new AlertDialog.Builder(context) .setView(content) .create(); } public static int resolveDrawable(Context context, int styledAttributeId) { TypedArray a = context.obtainStyledAttributes(new int[] { styledAttributeId }); int resource = a.getResourceId(0, 0); a.recycle(); return resource; } public static int resolveColor(Context context, int styledAttributeId) { TypedArray a = context.obtainStyledAttributes(new int[] { styledAttributeId }); int color = a.getColor(0, 0); a.recycle(); return color; } private static void enqueueDownload(Context context, Uri uri, String fileName, String description, String mimeType, String mediaType, boolean wifiOnly) { final DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); DownloadManager.Request request = new DownloadManager.Request(uri); request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); request.setDescription(description); if (mediaType != null) { request.addRequestHeader("Accept", mediaType); } if (mimeType != null) { request.setMimeType(mimeType); } if (wifiOnly) { request.setAllowedOverMetered(false); } request.setAllowedOverRoaming(false); dm.enqueue(request); } public static void enqueueDownloadWithPermissionCheck(final BaseActivity activity, final String url, final String mimeType, final String fileName, final String description, final String mediaType) { final ActivityCompat.OnRequestPermissionsResultCallback cb = new ActivityCompat.OnRequestPermissionsResultCallback() { @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { enqueueDownload(activity, url, mimeType, fileName, description, mediaType); } } }; activity.requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, cb, R.string.download_permission_rationale); } private static void enqueueDownload(final Context context, String url, final String mimeType, final String fileName, final String description, final String mediaType) { if (url == null) { return; } final Uri uri = Uri.parse(url).buildUpon() .appendQueryParameter("access_token", Gh4Application.get().getAuthToken()) .build(); if (!downloadNeedsWarning(context)) { enqueueDownload(context, uri, fileName, description, mimeType, mediaType, false); return; } DialogInterface.OnClickListener buttonListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { boolean wifiOnly = which == DialogInterface.BUTTON_NEUTRAL; enqueueDownload(context, uri, fileName, description, mimeType, mediaType, wifiOnly); } }; new AlertDialog.Builder(context) .setTitle(R.string.download_mobile_warning_title) .setMessage(R.string.download_mobile_warning_message) .setPositiveButton(R.string.download_now_button, buttonListener) .setNeutralButton(R.string.download_wifi_button, buttonListener) .setNegativeButton(R.string.cancel, null) .show(); } public static boolean downloadNeedsWarning(Context context) { ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); return cm.isActiveNetworkMetered(); } public static abstract class EmptinessWatchingTextWatcher implements TextWatcher { public EmptinessWatchingTextWatcher(EditText editor) { afterTextChanged(editor.getText()); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { onIsEmpty(s == null || s.length() == 0); } public abstract void onIsEmpty(boolean isEmpty); } public static class ButtonEnableTextWatcher extends EmptinessWatchingTextWatcher { private View mView; public ButtonEnableTextWatcher(EditText editor, View view) { super(editor); mView = view; afterTextChanged(editor.getText()); } @Override public void onIsEmpty(boolean isEmpty) { if (mView != null) { mView.setEnabled(!isEmpty); } } } public static CharSequence getSelectedText(TextView view) { int min = 0; int max = view.length(); if (view.isFocused()) { int selectionStart = view.getSelectionStart(); int selectionEnd = view.getSelectionEnd(); min = Math.max(0, Math.min(selectionStart, selectionEnd)); max = Math.max(0, Math.max(selectionStart, selectionEnd)); } return view.getText().subSequence(min, max); } public static abstract class QuoteActionModeCallback implements ActionMode.Callback { private final TextView mView; public QuoteActionModeCallback(TextView view) { mView = view; } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { mode.getMenuInflater().inflate(R.menu.comment_selection_menu, menu); return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { if (item.getItemId() != R.id.quote) { return false; } onTextQuoted(UiUtils.getSelectedText(mView)); mode.finish(); return true; } @Override public void onDestroyActionMode(ActionMode mode) { } public abstract void onTextQuoted(CharSequence text); } public static class WhitespaceTokenizer implements MultiAutoCompleteTextView.Tokenizer { @Override public int findTokenStart(CharSequence text, int cursor) { while (cursor > 0 && !Character.isWhitespace(text.charAt(cursor - 1))) { cursor--; } return cursor; } @Override public int findTokenEnd(CharSequence text, int cursor) { int len = text.length(); while (cursor < len) { if (Character.isWhitespace(text.charAt(cursor))) { return cursor; } else { cursor++; } } return len; } @Override public CharSequence terminateToken(CharSequence text) { return text; } } @NonNull public static SpannableStringBuilder formatLabelList(Context context, List<Label> labels) { SpannableStringBuilder builder = new SpannableStringBuilder(); for (Label label : labels) { int pos = builder.length(); IssueLabelSpan span = new IssueLabelSpan(context, label, true); builder.append(label.getName()); builder.setSpan(span, pos, pos + label.getName().length(), 0); } return builder; } }