blob: 6e0c16eb30147c0c5d623b8fe60f635708a7d286 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source 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 com.android.messaging.util;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import android.text.Html;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.URLSpan;
import android.view.Gravity;
import android.view.Surface;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.Interpolator;
import android.view.animation.ScaleAnimation;
import android.widget.RemoteViews;
import android.widget.Toast;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.ui.SnackBar;
import com.android.messaging.ui.SnackBar.Placement;
import com.android.messaging.ui.conversationlist.ConversationListActivity;
import com.android.messaging.ui.SnackBarInteraction;
import com.android.messaging.ui.SnackBarManager;
import com.android.messaging.ui.UIIntents;
import java.lang.reflect.Field;
import java.util.List;
public class UiUtils {
/** MediaPicker transition duration in ms */
public static final int MEDIAPICKER_TRANSITION_DURATION =
getApplicationContext().getResources().getInteger(
R.integer.mediapicker_transition_duration);
/** Short transition duration in ms */
public static final int ASYNCIMAGE_TRANSITION_DURATION =
getApplicationContext().getResources().getInteger(
R.integer.asyncimage_transition_duration);
/** Compose transition duration in ms */
public static final int COMPOSE_TRANSITION_DURATION =
getApplicationContext().getResources().getInteger(
R.integer.compose_transition_duration);
/** Generic duration for revealing/hiding a view */
public static final int REVEAL_ANIMATION_DURATION =
getApplicationContext().getResources().getInteger(
R.integer.reveal_view_animation_duration);
public static final Interpolator DEFAULT_INTERPOLATOR = new CubicBezierInterpolator(
0.4f, 0.0f, 0.2f, 1.0f);
public static final Interpolator EASE_IN_INTERPOLATOR = new CubicBezierInterpolator(
0.4f, 0.0f, 0.8f, 0.5f);
public static final Interpolator EASE_OUT_INTERPOLATOR = new CubicBezierInterpolator(
0.0f, 0.0f, 0.2f, 1f);
/** Show a simple toast at the bottom */
public static void showToastAtBottom(final int messageId) {
UiUtils.showToastAtBottom(getApplicationContext().getString(messageId));
}
/** Show a simple toast at the bottom */
public static void showToastAtBottom(final String message) {
final Toast toast = Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG);
toast.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0);
toast.show();
}
/** Show a simple toast at the default position */
public static void showToast(final int messageId) {
final Toast toast = Toast.makeText(getApplicationContext(),
getApplicationContext().getString(messageId), Toast.LENGTH_LONG);
toast.setGravity(Gravity.CENTER_HORIZONTAL, 0, 0);
toast.show();
}
/** Show a simple toast at the default position */
public static void showToast(final int pluralsMessageId, final int count) {
final Toast toast = Toast.makeText(getApplicationContext(),
getApplicationContext().getResources().getQuantityString(pluralsMessageId, count),
Toast.LENGTH_LONG);
toast.setGravity(Gravity.CENTER_HORIZONTAL, 0, 0);
toast.show();
}
public static void showSnackBar(final Context context, @NonNull final View parentView,
final String message, @Nullable final Runnable runnable, final int runnableLabel,
@Nullable final List<SnackBarInteraction> interactions) {
Assert.notNull(context);
SnackBar.Action action = null;
switch (runnableLabel) {
case SnackBar.Action.SNACK_BAR_UNDO:
action = SnackBar.Action.createUndoAction(runnable);
break;
case SnackBar.Action.SNACK_BAR_RETRY:
action = SnackBar.Action.createRetryAction(runnable);
break;
default :
break;
}
showSnackBarWithCustomAction(context, parentView, message, action, interactions,
null /* placement */);
}
public static void showSnackBarWithCustomAction(final Context context,
@NonNull final View parentView,
@NonNull final String message,
@NonNull final SnackBar.Action action,
@Nullable final List<SnackBarInteraction> interactions,
@Nullable final Placement placement) {
Assert.notNull(context);
Assert.isTrue(!TextUtils.isEmpty(message));
Assert.notNull(action);
SnackBarManager.get()
.newBuilder(parentView)
.setText(message)
.setAction(action)
.withInteractions(interactions)
.withPlacement(placement)
.show();
}
/**
* Run the given runnable once after the next layout pass of the view.
*/
public static void doOnceAfterLayoutChange(final View view, final Runnable runnable) {
final OnLayoutChangeListener listener = new OnLayoutChangeListener() {
@Override
public void onLayoutChange(final View v, final int left, final int top, final int right,
final int bottom, final int oldLeft, final int oldTop, final int oldRight,
final int oldBottom) {
// Call the runnable outside the layout pass because very few actions are allowed in
// the layout pass
ThreadUtil.getMainThreadHandler().post(runnable);
view.removeOnLayoutChangeListener(this);
}
};
view.addOnLayoutChangeListener(listener);
}
public static boolean isLandscapeMode() {
return Factory.get().getApplicationContext().getResources().getConfiguration().orientation
== Configuration.ORIENTATION_LANDSCAPE;
}
private static Context getApplicationContext() {
return Factory.get().getApplicationContext();
}
public static CharSequence commaEllipsize(
final String text,
final TextPaint paint,
final int width,
final String oneMore,
final String more) {
CharSequence ellipsized = TextUtils.commaEllipsize(
text,
paint,
width,
oneMore,
more);
if (TextUtils.isEmpty(ellipsized)) {
ellipsized = text;
}
return ellipsized;
}
/**
* Reveals/Hides a view with a scale animation from view center.
* @param view the view to animate
* @param desiredVisibility desired visibility (e.g. View.GONE) for the animated view.
* @param onFinishRunnable an optional runnable called at the end of the animation
*/
public static void revealOrHideViewWithAnimation(final View view, final int desiredVisibility,
@Nullable final Runnable onFinishRunnable) {
final boolean needAnimation = view.getVisibility() != desiredVisibility;
if (needAnimation) {
final float fromScale = desiredVisibility == View.VISIBLE ? 0F : 1F;
final float toScale = desiredVisibility == View.VISIBLE ? 1F : 0F;
final ScaleAnimation showHideAnimation =
new ScaleAnimation(fromScale, toScale, fromScale, toScale,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
showHideAnimation.setDuration(REVEAL_ANIMATION_DURATION);
showHideAnimation.setInterpolator(DEFAULT_INTERPOLATOR);
showHideAnimation.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationStart(final Animation animation) {
}
@Override
public void onAnimationRepeat(final Animation animation) {
}
@Override
public void onAnimationEnd(final Animation animation) {
if (onFinishRunnable != null) {
// Rather than running this immediately, we post it to happen next so that
// the animation will be completed so that the view can be detached from
// it's window. Otherwise, we may leak memory.
ThreadUtil.getMainThreadHandler().post(onFinishRunnable);
}
}
});
view.clearAnimation();
view.startAnimation(showHideAnimation);
// We are playing a view Animation; unlike view property animations, we can commit the
// visibility immediately instead of waiting for animation end.
view.setVisibility(desiredVisibility);
} else if (onFinishRunnable != null) {
// Make sure onFinishRunnable is always executed.
ThreadUtil.getMainThreadHandler().post(onFinishRunnable);
}
}
public static Rect getMeasuredBoundsOnScreen(final View view) {
final int[] location = new int[2];
view.getLocationOnScreen(location);
return new Rect(location[0], location[1],
location[0] + view.getMeasuredWidth(), location[1] + view.getMeasuredHeight());
}
public static void setStatusBarColor(final Activity activity, final int color) {
if (OsUtil.isAtLeastL()) {
// To achieve the appearance of an 80% opacity blend against a black background,
// each color channel is reduced in value by 20%.
final int blendedRed = (int) Math.floor(0.8 * Color.red(color));
final int blendedGreen = (int) Math.floor(0.8 * Color.green(color));
final int blendedBlue = (int) Math.floor(0.8 * Color.blue(color));
activity.getWindow().setStatusBarColor(
Color.rgb(blendedRed, blendedGreen, blendedBlue));
}
}
public static void lockOrientation(final Activity activity) {
final int orientation = activity.getResources().getConfiguration().orientation;
final int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
// rotation tracks the rotation of the device from its natural orientation
// orientation tracks whether the screen is landscape or portrait.
// It is possible to have a rotation of 0 (device in its natural orientation) in portrait
// (phone), or in landscape (tablet), so we have to check both values to determine what to
// pass to setRequestedOrientation.
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) {
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
} else if (rotation == Surface.ROTATION_180 || rotation == Surface.ROTATION_270) {
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
} else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
}
}
}
public static void unlockOrientation(final Activity activity) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
}
public static int getPaddingStart(final View view) {
return OsUtil.isAtLeastJB_MR1() ? view.getPaddingStart() : view.getPaddingLeft();
}
public static int getPaddingEnd(final View view) {
return OsUtil.isAtLeastJB_MR1() ? view.getPaddingEnd() : view.getPaddingRight();
}
public static boolean isRtlMode() {
return OsUtil.isAtLeastJB_MR2() && Factory.get().getApplicationContext().getResources()
.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
}
/**
* Check if the activity needs to be redirected to permission check
* @return true if {@link Activity#finish()} was called because redirection was performed
*/
public static boolean redirectToPermissionCheckIfNeeded(final Activity activity) {
if (!OsUtil.hasRequiredPermissions()) {
UIIntents.get().launchPermissionCheckActivity(activity);
} else {
// No redirect performed
return false;
}
// Redirect performed
activity.finish();
return true;
}
/**
* Called to check if all conditions are nominal and a "go" for some action, such as deleting
* a message, that requires this app to be the default app. This is also a precondition
* required for sending a draft.
* @return true if all conditions are nominal and we're ready to send a message
*/
public static boolean isReadyForAction() {
final PhoneUtils phoneUtils = PhoneUtils.getDefault();
// Have all the conditions been met:
// Supports SMS?
// Has a preferred sim?
// Is the default sms app?
return phoneUtils.isSmsCapable() &&
phoneUtils.getHasPreferredSmsSim() &&
phoneUtils.isDefaultSmsApp();
}
/*
* Removes all html markup from the text and replaces links with the the text and a text version
* of the href.
* @param htmlText HTML markup text
* @return Sanitized string with link hrefs inlined
*/
public static String stripHtml(final String htmlText) {
final StringBuilder result = new StringBuilder();
final Spanned markup = Html.fromHtml(htmlText);
final String strippedText = markup.toString();
final URLSpan[] links = markup.getSpans(0, markup.length() - 1, URLSpan.class);
int currentIndex = 0;
for (final URLSpan link : links) {
final int spanStart = markup.getSpanStart(link);
final int spanEnd = markup.getSpanEnd(link);
if (spanStart > currentIndex) {
result.append(strippedText, currentIndex, spanStart);
}
final String displayText = strippedText.substring(spanStart, spanEnd);
final String linkText = link.getURL();
result.append(getApplicationContext().getString(R.string.link_display_format,
displayText, linkText));
currentIndex = spanEnd;
}
if (strippedText.length() > currentIndex) {
result.append(strippedText, currentIndex, strippedText.length());
}
return result.toString();
}
public static void setActionBarShadowVisibility(final AppCompatActivity activity, final boolean visible) {
final ActionBar actionBar = activity.getSupportActionBar();
actionBar.setElevation(visible ?
activity.getResources().getDimensionPixelSize(R.dimen.action_bar_elevation) :
0);
final View actionBarView = activity.getWindow().getDecorView().findViewById(
androidx.appcompat.R.id.decor_content_parent);
if (actionBarView != null) {
// AppCompatActionBar has one drawable Field, which is the shadow for the action bar
// set the alpha on that drawable manually
final Field[] fields = actionBarView.getClass().getDeclaredFields();
try {
for (final Field field : fields) {
if (field.getType().equals(Drawable.class)) {
field.setAccessible(true);
final Drawable shadowDrawable = (Drawable) field.get(actionBarView);
if (shadowDrawable != null) {
shadowDrawable.setAlpha(visible ? 255 : 0);
actionBarView.invalidate();
return;
}
}
}
} catch (final IllegalAccessException ex) {
// Not expected, we should avoid this via field.setAccessible(true) above
LogUtil.e(LogUtil.BUGLE_TAG, "Error setting shadow visibility", ex);
}
}
}
/**
* Get the activity that's hosting the view, typically casting view.getContext() as an Activity
* is sufficient, but sometimes the context is a context wrapper, in which case we need to case
* the base context
*/
public static Activity getActivity(final View view) {
if (view == null) {
return null;
}
return getActivity(view.getContext());
}
/**
* Get the activity for the supplied context, typically casting context as an Activity
* is sufficient, but sometimes the context is a context wrapper, in which case we need to case
* the base context
*/
public static Activity getActivity(final Context context) {
if (context == null) {
return null;
}
if (context instanceof Activity) {
return (Activity) context;
}
if (context instanceof ContextWrapper) {
return getActivity(((ContextWrapper) context).getBaseContext());
}
// We've hit a non-activity context such as an app-context
return null;
}
public static RemoteViews getWidgetMissingPermissionView(final Context context) {
return new RemoteViews(context.getPackageName(), R.layout.widget_missing_permission);
}
}