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
17package com.android.messaging.util;
18
19import android.app.Activity;
20import android.content.Context;
21import android.content.ContextWrapper;
22import android.content.pm.ActivityInfo;
23import android.content.res.Configuration;
24import android.graphics.Color;
25import android.graphics.Rect;
26import android.graphics.drawable.Drawable;
27import android.support.annotation.NonNull;
28import android.support.annotation.Nullable;
29import android.support.v7.app.ActionBar;
30import android.support.v7.app.ActionBarActivity;
31import android.text.Html;
32import android.text.Spanned;
33import android.text.TextPaint;
34import android.text.TextUtils;
35import android.text.style.URLSpan;
36import android.view.Gravity;
37import android.view.Surface;
38import android.view.View;
39import android.view.View.OnLayoutChangeListener;
40import android.view.animation.Animation;
41import android.view.animation.Animation.AnimationListener;
42import android.view.animation.Interpolator;
43import android.view.animation.ScaleAnimation;
44import android.widget.RemoteViews;
45import android.widget.Toast;
46
47import com.android.messaging.Factory;
48import com.android.messaging.R;
49import com.android.messaging.ui.SnackBar;
50import com.android.messaging.ui.SnackBar.Placement;
51import com.android.messaging.ui.conversationlist.ConversationListActivity;
52import com.android.messaging.ui.SnackBarInteraction;
53import com.android.messaging.ui.SnackBarManager;
54import com.android.messaging.ui.UIIntents;
55
56import java.lang.reflect.Field;
57import java.util.List;
58
59public 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 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     */
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