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