/* * 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 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 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); } }