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.ActionBarActivity;
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 */
showToastAtBottom(final int messageId)87     public static void showToastAtBottom(final int messageId) {
88         UiUtils.showToastAtBottom(getApplicationContext().getString(messageId));
89     }
90 
91     /** Show a simple toast at the bottom */
showToastAtBottom(final String message)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 */
showToast(final int messageId)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 */
showToast(final int pluralsMessageId, final int count)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 
showSnackBar(final Context context, @NonNull final View parentView, final String message, @Nullable final Runnable runnable, final int runnableLabel, @Nullable final List<SnackBarInteraction> interactions)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 
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)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      */
doOnceAfterLayoutChange(final View view, final Runnable runnable)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 
isLandscapeMode()171     public static boolean isLandscapeMode() {
172         return Factory.get().getApplicationContext().getResources().getConfiguration().orientation
173                 == Configuration.ORIENTATION_LANDSCAPE;
174     }
175 
getApplicationContext()176     private static Context getApplicationContext() {
177         return Factory.get().getApplicationContext();
178     }
179 
commaEllipsize( final String text, final TextPaint paint, final int width, final String oneMore, final String more)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      */
revealOrHideViewWithAnimation(final View view, final int desiredVisibility, @Nullable final Runnable onFinishRunnable)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 
getMeasuredBoundsOnScreen(final View view)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 
setStatusBarColor(final Activity activity, final int color)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 
lockOrientation(final Activity activity)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 
unlockOrientation(final Activity activity)290     public static void unlockOrientation(final Activity activity) {
291         activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
292     }
293 
getPaddingStart(final View view)294     public static int getPaddingStart(final View view) {
295         return OsUtil.isAtLeastJB_MR1() ? view.getPaddingStart() : view.getPaddingLeft();
296     }
297 
getPaddingEnd(final View view)298     public static int getPaddingEnd(final View view) {
299         return OsUtil.isAtLeastJB_MR1() ? view.getPaddingEnd() : view.getPaddingRight();
300     }
301 
isRtlMode()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      */
redirectToPermissionCheckIfNeeded(final Activity activity)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      */
isReadyForAction()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      */
stripHtml(final String htmlText)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 
setActionBarShadowVisibility(final ActionBarActivity activity, final boolean visible)373     public static void setActionBarShadowVisibility(final ActionBarActivity 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      */
getActivity(final View view)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      */
getActivity(final Context context)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 
getWidgetMissingPermissionView(final Context context)435     public static RemoteViews getWidgetMissingPermissionView(final Context context) {
436         return new RemoteViews(context.getPackageName(), R.layout.widget_missing_permission);
437     }
438 }
439