Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.messaging.util;
     18 
     19 import android.app.Activity;
     20 import android.content.Context;
     21 import android.content.ContextWrapper;
     22 import android.content.pm.ActivityInfo;
     23 import android.content.res.Configuration;
     24 import android.graphics.Color;
     25 import android.graphics.Rect;
     26 import android.graphics.drawable.Drawable;
     27 import android.support.annotation.NonNull;
     28 import android.support.annotation.Nullable;
     29 import android.support.v7.app.ActionBar;
     30 import android.support.v7.app.AppCompatActivity;
     31 import android.text.Html;
     32 import android.text.Spanned;
     33 import android.text.TextPaint;
     34 import android.text.TextUtils;
     35 import android.text.style.URLSpan;
     36 import android.view.Gravity;
     37 import android.view.Surface;
     38 import android.view.View;
     39 import android.view.View.OnLayoutChangeListener;
     40 import android.view.animation.Animation;
     41 import android.view.animation.Animation.AnimationListener;
     42 import android.view.animation.Interpolator;
     43 import android.view.animation.ScaleAnimation;
     44 import android.widget.RemoteViews;
     45 import android.widget.Toast;
     46 
     47 import com.android.messaging.Factory;
     48 import com.android.messaging.R;
     49 import com.android.messaging.ui.SnackBar;
     50 import com.android.messaging.ui.SnackBar.Placement;
     51 import com.android.messaging.ui.conversationlist.ConversationListActivity;
     52 import com.android.messaging.ui.SnackBarInteraction;
     53 import com.android.messaging.ui.SnackBarManager;
     54 import com.android.messaging.ui.UIIntents;
     55 
     56 import java.lang.reflect.Field;
     57 import java.util.List;
     58 
     59 public class UiUtils {
     60     /** MediaPicker transition duration in ms */
     61     public static final int MEDIAPICKER_TRANSITION_DURATION =
     62             getApplicationContext().getResources().getInteger(
     63                     R.integer.mediapicker_transition_duration);
     64     /** Short transition duration in ms */
     65     public static final int ASYNCIMAGE_TRANSITION_DURATION =
     66             getApplicationContext().getResources().getInteger(
     67                     R.integer.asyncimage_transition_duration);
     68     /** Compose transition duration in ms */
     69     public static final int COMPOSE_TRANSITION_DURATION =
     70             getApplicationContext().getResources().getInteger(
     71                     R.integer.compose_transition_duration);
     72     /** Generic duration for revealing/hiding a view */
     73     public static final int REVEAL_ANIMATION_DURATION =
     74             getApplicationContext().getResources().getInteger(
     75                     R.integer.reveal_view_animation_duration);
     76 
     77     public static final Interpolator DEFAULT_INTERPOLATOR = new CubicBezierInterpolator(
     78             0.4f, 0.0f, 0.2f, 1.0f);
     79 
     80     public static final Interpolator EASE_IN_INTERPOLATOR = new CubicBezierInterpolator(
     81             0.4f, 0.0f, 0.8f, 0.5f);
     82 
     83     public static final Interpolator EASE_OUT_INTERPOLATOR = new CubicBezierInterpolator(
     84             0.0f, 0.0f, 0.2f, 1f);
     85 
     86     /** Show a simple toast at the bottom */
     87     public static void showToastAtBottom(final int messageId) {
     88         UiUtils.showToastAtBottom(getApplicationContext().getString(messageId));
     89     }
     90 
     91     /** Show a simple toast at the bottom */
     92     public static void showToastAtBottom(final String message) {
     93         final Toast toast = Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG);
     94         toast.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0);
     95         toast.show();
     96     }
     97 
     98     /** Show a simple toast at the default position */
     99     public static void showToast(final int messageId) {
    100         final Toast toast = Toast.makeText(getApplicationContext(),
    101                 getApplicationContext().getString(messageId), Toast.LENGTH_LONG);
    102         toast.setGravity(Gravity.CENTER_HORIZONTAL, 0, 0);
    103         toast.show();
    104     }
    105 
    106     /** Show a simple toast at the default position */
    107     public static void showToast(final int pluralsMessageId, final int count) {
    108         final Toast toast = Toast.makeText(getApplicationContext(),
    109                 getApplicationContext().getResources().getQuantityString(pluralsMessageId, count),
    110                 Toast.LENGTH_LONG);
    111         toast.setGravity(Gravity.CENTER_HORIZONTAL, 0, 0);
    112         toast.show();
    113     }
    114 
    115     public static void showSnackBar(final Context context, @NonNull final View parentView,
    116             final String message, @Nullable final Runnable runnable, final int runnableLabel,
    117             @Nullable final List<SnackBarInteraction> interactions) {
    118         Assert.notNull(context);
    119         SnackBar.Action action = null;
    120         switch (runnableLabel) {
    121             case SnackBar.Action.SNACK_BAR_UNDO:
    122                 action = SnackBar.Action.createUndoAction(runnable);
    123                 break;
    124             case SnackBar.Action.SNACK_BAR_RETRY:
    125                 action =  SnackBar.Action.createRetryAction(runnable);
    126                 break;
    127             default :
    128                 break;
    129         }
    130 
    131         showSnackBarWithCustomAction(context, parentView, message, action, interactions,
    132                                         null /* placement */);
    133     }
    134 
    135     public static void showSnackBarWithCustomAction(final Context context,
    136             @NonNull final View parentView,
    137             @NonNull final String message,
    138             @NonNull final SnackBar.Action action,
    139             @Nullable final List<SnackBarInteraction> interactions,
    140             @Nullable final Placement placement) {
    141         Assert.notNull(context);
    142         Assert.isTrue(!TextUtils.isEmpty(message));
    143         Assert.notNull(action);
    144         SnackBarManager.get()
    145             .newBuilder(parentView)
    146             .setText(message)
    147             .setAction(action)
    148             .withInteractions(interactions)
    149             .withPlacement(placement)
    150             .show();
    151     }
    152 
    153     /**
    154      * Run the given runnable once after the next layout pass of the view.
    155      */
    156     public static void doOnceAfterLayoutChange(final View view, final Runnable runnable) {
    157         final OnLayoutChangeListener listener = new OnLayoutChangeListener() {
    158             @Override
    159             public void onLayoutChange(final View v, final int left, final int top, final int right,
    160                     final int bottom, final int oldLeft, final int oldTop, final int oldRight,
    161                     final int oldBottom) {
    162                 // Call the runnable outside the layout pass because very few actions are allowed in
    163                 // the layout pass
    164                 ThreadUtil.getMainThreadHandler().post(runnable);
    165                 view.removeOnLayoutChangeListener(this);
    166             }
    167         };
    168         view.addOnLayoutChangeListener(listener);
    169     }
    170 
    171     public static boolean isLandscapeMode() {
    172         return Factory.get().getApplicationContext().getResources().getConfiguration().orientation
    173                 == Configuration.ORIENTATION_LANDSCAPE;
    174     }
    175 
    176     private static Context getApplicationContext() {
    177         return Factory.get().getApplicationContext();
    178     }
    179 
    180     public static CharSequence commaEllipsize(
    181             final String text,
    182             final TextPaint paint,
    183             final int width,
    184             final String oneMore,
    185             final String more) {
    186         CharSequence ellipsized = TextUtils.commaEllipsize(
    187                 text,
    188                 paint,
    189                 width,
    190                 oneMore,
    191                 more);
    192         if (TextUtils.isEmpty(ellipsized)) {
    193             ellipsized = text;
    194         }
    195         return ellipsized;
    196     }
    197 
    198     /**
    199      * Reveals/Hides a view with a scale animation from view center.
    200      * @param view the view to animate
    201      * @param desiredVisibility desired visibility (e.g. View.GONE) for the animated view.
    202      * @param onFinishRunnable an optional runnable called at the end of the animation
    203      */
    204     public static void revealOrHideViewWithAnimation(final View view, final int desiredVisibility,
    205             @Nullable final Runnable onFinishRunnable) {
    206         final boolean needAnimation = view.getVisibility() != desiredVisibility;
    207         if (needAnimation) {
    208             final float fromScale = desiredVisibility == View.VISIBLE ? 0F : 1F;
    209             final float toScale = desiredVisibility == View.VISIBLE ? 1F : 0F;
    210             final ScaleAnimation showHideAnimation =
    211                     new ScaleAnimation(fromScale, toScale, fromScale, toScale,
    212                             ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
    213                             ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
    214             showHideAnimation.setDuration(REVEAL_ANIMATION_DURATION);
    215             showHideAnimation.setInterpolator(DEFAULT_INTERPOLATOR);
    216             showHideAnimation.setAnimationListener(new AnimationListener() {
    217                 @Override
    218                 public void onAnimationStart(final Animation animation) {
    219                 }
    220 
    221                 @Override
    222                 public void onAnimationRepeat(final Animation animation) {
    223                 }
    224 
    225                 @Override
    226                 public void onAnimationEnd(final Animation animation) {
    227                     if (onFinishRunnable != null) {
    228                         // Rather than running this immediately, we post it to happen next so that
    229                         // the animation will be completed so that the view can be detached from
    230                         // it's window.  Otherwise, we may leak memory.
    231                         ThreadUtil.getMainThreadHandler().post(onFinishRunnable);
    232                     }
    233                 }
    234             });
    235             view.clearAnimation();
    236             view.startAnimation(showHideAnimation);
    237             // We are playing a view Animation; unlike view property animations, we can commit the
    238             // visibility immediately instead of waiting for animation end.
    239             view.setVisibility(desiredVisibility);
    240         } else if (onFinishRunnable != null) {
    241             // Make sure onFinishRunnable is always executed.
    242             ThreadUtil.getMainThreadHandler().post(onFinishRunnable);
    243         }
    244     }
    245 
    246     public static Rect getMeasuredBoundsOnScreen(final View view) {
    247         final int[] location = new int[2];
    248         view.getLocationOnScreen(location);
    249         return new Rect(location[0], location[1],
    250                 location[0] + view.getMeasuredWidth(), location[1] + view.getMeasuredHeight());
    251     }
    252 
    253     public static void setStatusBarColor(final Activity activity, final int color) {
    254         if (OsUtil.isAtLeastL()) {
    255             // To achieve the appearance of an 80% opacity blend against a black background,
    256             // each color channel is reduced in value by 20%.
    257             final int blendedRed = (int) Math.floor(0.8 * Color.red(color));
    258             final int blendedGreen = (int) Math.floor(0.8 * Color.green(color));
    259             final int blendedBlue = (int) Math.floor(0.8 * Color.blue(color));
    260 
    261             activity.getWindow().setStatusBarColor(
    262                     Color.rgb(blendedRed, blendedGreen, blendedBlue));
    263         }
    264     }
    265 
    266     public static void lockOrientation(final Activity activity) {
    267         final int orientation = activity.getResources().getConfiguration().orientation;
    268         final int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
    269 
    270         // rotation tracks the rotation of the device from its natural orientation
    271         // orientation tracks whether the screen is landscape or portrait.
    272         // It is possible to have a rotation of 0 (device in its natural orientation) in portrait
    273         // (phone), or in landscape (tablet), so we have to check both values to determine what to
    274         // pass to setRequestedOrientation.
    275         if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) {
    276             if (orientation == Configuration.ORIENTATION_PORTRAIT) {
    277                 activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    278             } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
    279                 activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
    280             }
    281         } else if (rotation == Surface.ROTATION_180 || rotation == Surface.ROTATION_270) {
    282             if (orientation == Configuration.ORIENTATION_PORTRAIT) {
    283                 activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
    284             } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
    285                 activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
    286             }
    287         }
    288     }
    289 
    290     public static void unlockOrientation(final Activity activity) {
    291         activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
    292     }
    293 
    294     public static int getPaddingStart(final View view) {
    295         return OsUtil.isAtLeastJB_MR1() ? view.getPaddingStart() : view.getPaddingLeft();
    296     }
    297 
    298     public static int getPaddingEnd(final View view) {
    299         return OsUtil.isAtLeastJB_MR1() ? view.getPaddingEnd() : view.getPaddingRight();
    300     }
    301 
    302     public static boolean isRtlMode() {
    303         return OsUtil.isAtLeastJB_MR2() && Factory.get().getApplicationContext().getResources()
    304                 .getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
    305     }
    306 
    307     /**
    308      * Check if the activity needs to be redirected to permission check
    309      * @return true if {@link Activity#finish()} was called because redirection was performed
    310      */
    311     public static boolean redirectToPermissionCheckIfNeeded(final Activity activity) {
    312         if (!OsUtil.hasRequiredPermissions()) {
    313             UIIntents.get().launchPermissionCheckActivity(activity);
    314         } else {
    315             // No redirect performed
    316             return false;
    317         }
    318 
    319         // Redirect performed
    320         activity.finish();
    321         return true;
    322     }
    323 
    324     /**
    325      * Called to check if all conditions are nominal and a "go" for some action, such as deleting
    326      * a message, that requires this app to be the default app. This is also a precondition
    327      * required for sending a draft.
    328      * @return true if all conditions are nominal and we're ready to send a message
    329      */
    330     public static boolean isReadyForAction() {
    331         final PhoneUtils phoneUtils = PhoneUtils.getDefault();
    332 
    333         // Have all the conditions been met:
    334         // Supports SMS?
    335         // Has a preferred sim?
    336         // Is the default sms app?
    337         return phoneUtils.isSmsCapable() &&
    338                 phoneUtils.getHasPreferredSmsSim() &&
    339                 phoneUtils.isDefaultSmsApp();
    340     }
    341 
    342     /*
    343      * Removes all html markup from the text and replaces links with the the text and a text version
    344      * of the href.
    345      * @param htmlText HTML markup text
    346      * @return Sanitized string with link hrefs inlined
    347      */
    348     public static String stripHtml(final String htmlText) {
    349         final StringBuilder result = new StringBuilder();
    350         final Spanned markup = Html.fromHtml(htmlText);
    351         final String strippedText = markup.toString();
    352 
    353         final URLSpan[] links = markup.getSpans(0, markup.length() - 1, URLSpan.class);
    354         int currentIndex = 0;
    355         for (final URLSpan link : links) {
    356             final int spanStart = markup.getSpanStart(link);
    357             final int spanEnd = markup.getSpanEnd(link);
    358             if (spanStart > currentIndex) {
    359                 result.append(strippedText, currentIndex, spanStart);
    360             }
    361             final String displayText = strippedText.substring(spanStart, spanEnd);
    362             final String linkText = link.getURL();
    363             result.append(getApplicationContext().getString(R.string.link_display_format,
    364                     displayText, linkText));
    365             currentIndex = spanEnd;
    366         }
    367         if (strippedText.length() > currentIndex) {
    368             result.append(strippedText, currentIndex, strippedText.length());
    369         }
    370         return result.toString();
    371     }
    372 
    373     public static void setActionBarShadowVisibility(final AppCompatActivity activity, final boolean visible) {
    374         final ActionBar actionBar = activity.getSupportActionBar();
    375         actionBar.setElevation(visible ?
    376                 activity.getResources().getDimensionPixelSize(R.dimen.action_bar_elevation) :
    377                 0);
    378         final View actionBarView = activity.getWindow().getDecorView().findViewById(
    379                 android.support.v7.appcompat.R.id.decor_content_parent);
    380         if (actionBarView != null) {
    381             // AppCompatActionBar has one drawable Field, which is the shadow for the action bar
    382             // set the alpha on that drawable manually
    383             final Field[] fields = actionBarView.getClass().getDeclaredFields();
    384             try {
    385                 for (final Field field : fields) {
    386                     if (field.getType().equals(Drawable.class)) {
    387                         field.setAccessible(true);
    388                         final Drawable shadowDrawable = (Drawable) field.get(actionBarView);
    389                         if (shadowDrawable != null) {
    390                             shadowDrawable.setAlpha(visible ? 255 : 0);
    391                             actionBarView.invalidate();
    392                             return;
    393                         }
    394                     }
    395                 }
    396             } catch (final IllegalAccessException ex) {
    397                 // Not expected, we should avoid this via field.setAccessible(true) above
    398                 LogUtil.e(LogUtil.BUGLE_TAG, "Error setting shadow visibility", ex);
    399             }
    400         }
    401     }
    402 
    403     /**
    404      * Get the activity that's hosting the view, typically casting view.getContext() as an Activity
    405      * is sufficient, but sometimes the context is a context wrapper, in which case we need to case
    406      * the base context
    407      */
    408     public static Activity getActivity(final View view) {
    409         if (view == null) {
    410             return null;
    411         }
    412         return getActivity(view.getContext());
    413     }
    414 
    415     /**
    416      * Get the activity for the supplied context, typically casting context as an Activity
    417      * is sufficient, but sometimes the context is a context wrapper, in which case we need to case
    418      * the base context
    419      */
    420     public static Activity getActivity(final Context context) {
    421         if (context == null) {
    422             return null;
    423         }
    424         if (context instanceof Activity) {
    425             return (Activity) context;
    426         }
    427         if (context instanceof ContextWrapper) {
    428             return getActivity(((ContextWrapper) context).getBaseContext());
    429         }
    430 
    431         // We've hit a non-activity context such as an app-context
    432         return null;
    433     }
    434 
    435     public static RemoteViews getWidgetMissingPermissionView(final Context context) {
    436         return new RemoteViews(context.getPackageName(), R.layout.widget_missing_permission);
    437     }
    438 }
    439