• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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.launcher3;
18 
19 import android.app.WallpaperManager;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.SharedPreferences;
24 import android.content.pm.ApplicationInfo;
25 import android.content.pm.PackageInfo;
26 import android.content.pm.PackageManager;
27 import android.content.pm.PackageManager.NameNotFoundException;
28 import android.content.pm.ResolveInfo;
29 import android.content.res.Resources;
30 import android.graphics.Bitmap;
31 import android.graphics.Matrix;
32 import android.graphics.Paint;
33 import android.graphics.Rect;
34 import android.graphics.RectF;
35 import android.os.Build;
36 import android.os.Bundle;
37 import android.os.DeadObjectException;
38 import android.os.Handler;
39 import android.os.Message;
40 import android.os.PowerManager;
41 import android.os.TransactionTooLargeException;
42 import android.text.Spannable;
43 import android.text.SpannableString;
44 import android.text.TextUtils;
45 import android.text.style.TtsSpan;
46 import android.util.DisplayMetrics;
47 import android.util.Log;
48 import android.util.Pair;
49 import android.util.TypedValue;
50 import android.view.View;
51 
52 import com.android.launcher3.config.FeatureFlags;
53 
54 import java.io.ByteArrayOutputStream;
55 import java.io.Closeable;
56 import java.io.IOException;
57 import java.lang.reflect.InvocationTargetException;
58 import java.lang.reflect.Method;
59 import java.util.Collection;
60 import java.util.HashSet;
61 import java.util.Locale;
62 import java.util.concurrent.Executor;
63 import java.util.concurrent.LinkedBlockingQueue;
64 import java.util.concurrent.ThreadPoolExecutor;
65 import java.util.concurrent.TimeUnit;
66 import java.util.regex.Matcher;
67 import java.util.regex.Pattern;
68 
69 /**
70  * Various utilities shared amongst the Launcher's classes.
71  */
72 public final class Utilities {
73 
74     private static final String TAG = "Launcher.Utilities";
75 
76     private static final Pattern sTrimPattern =
77             Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$");
78 
79     private static final int[] sLoc0 = new int[2];
80     private static final int[] sLoc1 = new int[2];
81     private static final float[] sPoint = new float[2];
82     private static final Matrix sMatrix = new Matrix();
83     private static final Matrix sInverseMatrix = new Matrix();
84 
85     public static final boolean ATLEAST_P =
86             Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
87 
88     public static final boolean ATLEAST_OREO_MR1 =
89             Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1;
90 
91     public static final boolean ATLEAST_OREO =
92             Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
93 
94     public static final boolean ATLEAST_NOUGAT_MR1 =
95             Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1;
96 
97     public static final boolean ATLEAST_NOUGAT =
98             Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
99 
100     public static final boolean ATLEAST_MARSHMALLOW =
101             Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
102 
103     public static final boolean ATLEAST_LOLLIPOP_MR1 =
104             Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1;
105 
106     public static final int SINGLE_FRAME_MS = 16;
107 
108     /**
109      * Indicates if the device has a debug build. Should only be used to store additional info or
110      * add extra logging and not for changing the app behavior.
111      */
112     public static final boolean IS_DEBUG_DEVICE = Build.TYPE.toLowerCase().contains("debug")
113             || Build.TYPE.toLowerCase().equals("eng");
114 
115     // An intent extra to indicate the horizontal scroll of the wallpaper.
116     public static final String EXTRA_WALLPAPER_OFFSET = "com.android.launcher3.WALLPAPER_OFFSET";
117 
118     public static final int COLOR_EXTRACTION_JOB_ID = 1;
119     public static final int WALLPAPER_COMPAT_JOB_ID = 2;
120 
121     // These values are same as that in {@link AsyncTask}.
122     private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
123     private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
124     private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
125     private static final int KEEP_ALIVE = 1;
126     /**
127      * An {@link Executor} to be used with async task with no limit on the queue size.
128      */
129     public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
130             CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
131             TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
132 
isPropertyEnabled(String propertyName)133     public static boolean isPropertyEnabled(String propertyName) {
134         return Log.isLoggable(propertyName, Log.VERBOSE);
135     }
136 
137     /**
138      * Given a coordinate relative to the descendant, find the coordinate in a parent view's
139      * coordinates.
140      *
141      * @param descendant The descendant to which the passed coordinate is relative.
142      * @param ancestor The root view to make the coordinates relative to.
143      * @param coord The coordinate that we want mapped.
144      * @param includeRootScroll Whether or not to account for the scroll of the descendant:
145      *          sometimes this is relevant as in a child's coordinates within the descendant.
146      * @return The factor by which this descendant is scaled relative to this DragLayer. Caution
147      *         this scale factor is assumed to be equal in X and Y, and so if at any point this
148      *         assumption fails, we will need to return a pair of scale factors.
149      */
getDescendantCoordRelativeToAncestor( View descendant, View ancestor, int[] coord, boolean includeRootScroll)150     public static float getDescendantCoordRelativeToAncestor(
151             View descendant, View ancestor, int[] coord, boolean includeRootScroll) {
152         sPoint[0] = coord[0];
153         sPoint[1] = coord[1];
154 
155         float scale = 1.0f;
156         View v = descendant;
157         while(v != ancestor && v != null) {
158             // For TextViews, scroll has a meaning which relates to the text position
159             // which is very strange... ignore the scroll.
160             if (v != descendant || includeRootScroll) {
161                 sPoint[0] -= v.getScrollX();
162                 sPoint[1] -= v.getScrollY();
163             }
164 
165             v.getMatrix().mapPoints(sPoint);
166             sPoint[0] += v.getLeft();
167             sPoint[1] += v.getTop();
168             scale *= v.getScaleX();
169 
170             v = (View) v.getParent();
171         }
172 
173         coord[0] = Math.round(sPoint[0]);
174         coord[1] = Math.round(sPoint[1]);
175         return scale;
176     }
177 
178     /**
179      * Inverse of {@link #getDescendantCoordRelativeToAncestor(View, View, int[], boolean)}.
180      */
mapCoordInSelfToDescendant(View descendant, View root, int[] coord)181     public static void mapCoordInSelfToDescendant(View descendant, View root, int[] coord) {
182         sMatrix.reset();
183         View v = descendant;
184         while(v != root) {
185             sMatrix.postTranslate(-v.getScrollX(), -v.getScrollY());
186             sMatrix.postConcat(v.getMatrix());
187             sMatrix.postTranslate(v.getLeft(), v.getTop());
188             v = (View) v.getParent();
189         }
190         sMatrix.postTranslate(-v.getScrollX(), -v.getScrollY());
191         sMatrix.invert(sInverseMatrix);
192 
193         sPoint[0] = coord[0];
194         sPoint[1] = coord[1];
195         sInverseMatrix.mapPoints(sPoint);
196         coord[0] = Math.round(sPoint[0]);
197         coord[1] = Math.round(sPoint[1]);
198     }
199 
200     /**
201      * Utility method to determine whether the given point, in local coordinates,
202      * is inside the view, where the area of the view is expanded by the slop factor.
203      * This method is called while processing touch-move events to determine if the event
204      * is still within the view.
205      */
pointInView(View v, float localX, float localY, float slop)206     public static boolean pointInView(View v, float localX, float localY, float slop) {
207         return localX >= -slop && localY >= -slop && localX < (v.getWidth() + slop) &&
208                 localY < (v.getHeight() + slop);
209     }
210 
getCenterDeltaInScreenSpace(View v0, View v1)211     public static int[] getCenterDeltaInScreenSpace(View v0, View v1) {
212         v0.getLocationInWindow(sLoc0);
213         v1.getLocationInWindow(sLoc1);
214 
215         sLoc0[0] += (v0.getMeasuredWidth() * v0.getScaleX()) / 2;
216         sLoc0[1] += (v0.getMeasuredHeight() * v0.getScaleY()) / 2;
217         sLoc1[0] += (v1.getMeasuredWidth() * v1.getScaleX()) / 2;
218         sLoc1[1] += (v1.getMeasuredHeight() * v1.getScaleY()) / 2;
219         return new int[] {sLoc1[0] - sLoc0[0], sLoc1[1] - sLoc0[1]};
220     }
221 
scaleRectFAboutCenter(RectF r, float scale)222     public static void scaleRectFAboutCenter(RectF r, float scale) {
223         if (scale != 1.0f) {
224             float cx = r.centerX();
225             float cy = r.centerY();
226             r.offset(-cx, -cy);
227             r.left = r.left * scale;
228             r.top = r.top * scale ;
229             r.right = r.right * scale;
230             r.bottom = r.bottom * scale;
231             r.offset(cx, cy);
232         }
233     }
234 
scaleRectAboutCenter(Rect r, float scale)235     public static void scaleRectAboutCenter(Rect r, float scale) {
236         if (scale != 1.0f) {
237             int cx = r.centerX();
238             int cy = r.centerY();
239             r.offset(-cx, -cy);
240             scaleRect(r, scale);
241             r.offset(cx, cy);
242         }
243     }
244 
scaleRect(Rect r, float scale)245     public static void scaleRect(Rect r, float scale) {
246         if (scale != 1.0f) {
247             r.left = (int) (r.left * scale + 0.5f);
248             r.top = (int) (r.top * scale + 0.5f);
249             r.right = (int) (r.right * scale + 0.5f);
250             r.bottom = (int) (r.bottom * scale + 0.5f);
251         }
252     }
253 
insetRect(Rect r, Rect insets)254     public static void insetRect(Rect r, Rect insets) {
255         r.left = Math.min(r.right, r.left + insets.left);
256         r.top = Math.min(r.bottom, r.top + insets.top);
257         r.right = Math.max(r.left, r.right - insets.right);
258         r.bottom = Math.max(r.top, r.bottom - insets.bottom);
259     }
260 
shrinkRect(Rect r, float scaleX, float scaleY)261     public static float shrinkRect(Rect r, float scaleX, float scaleY) {
262         float scale = Math.min(Math.min(scaleX, scaleY), 1.0f);
263         if (scale < 1.0f) {
264             int deltaX = (int) (r.width() * (scaleX - scale) * 0.5f);
265             r.left += deltaX;
266             r.right -= deltaX;
267 
268             int deltaY = (int) (r.height() * (scaleY - scale) * 0.5f);
269             r.top += deltaY;
270             r.bottom -= deltaY;
271         }
272         return scale;
273     }
274 
mapRange(float value, float min, float max)275     public static float mapRange(float value, float min, float max) {
276         return min + (value * (max - min));
277     }
278 
isSystemApp(Context context, Intent intent)279     public static boolean isSystemApp(Context context, Intent intent) {
280         PackageManager pm = context.getPackageManager();
281         ComponentName cn = intent.getComponent();
282         String packageName = null;
283         if (cn == null) {
284             ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
285             if ((info != null) && (info.activityInfo != null)) {
286                 packageName = info.activityInfo.packageName;
287             }
288         } else {
289             packageName = cn.getPackageName();
290         }
291         if (packageName != null) {
292             try {
293                 PackageInfo info = pm.getPackageInfo(packageName, 0);
294                 return (info != null) && (info.applicationInfo != null) &&
295                         ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0);
296             } catch (NameNotFoundException e) {
297                 return false;
298             }
299         } else {
300             return false;
301         }
302     }
303 
304     /*
305      * Finds a system apk which had a broadcast receiver listening to a particular action.
306      * @param action intent action used to find the apk
307      * @return a pair of apk package name and the resources.
308      */
findSystemApk(String action, PackageManager pm)309     static Pair<String, Resources> findSystemApk(String action, PackageManager pm) {
310         final Intent intent = new Intent(action);
311         for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) {
312             if (info.activityInfo != null &&
313                     (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
314                 final String packageName = info.activityInfo.packageName;
315                 try {
316                     final Resources res = pm.getResourcesForApplication(packageName);
317                     return Pair.create(packageName, res);
318                 } catch (NameNotFoundException e) {
319                     Log.w(TAG, "Failed to find resources for " + packageName);
320                 }
321             }
322         }
323         return null;
324     }
325 
326     /**
327      * Compresses the bitmap to a byte array for serialization.
328      */
flattenBitmap(Bitmap bitmap)329     public static byte[] flattenBitmap(Bitmap bitmap) {
330         // Try go guesstimate how much space the icon will take when serialized
331         // to avoid unnecessary allocations/copies during the write.
332         int size = bitmap.getWidth() * bitmap.getHeight() * 4;
333         ByteArrayOutputStream out = new ByteArrayOutputStream(size);
334         try {
335             bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
336             out.flush();
337             out.close();
338             return out.toByteArray();
339         } catch (IOException e) {
340             Log.w(TAG, "Could not write bitmap");
341             return null;
342         }
343     }
344 
345     /**
346      * Trims the string, removing all whitespace at the beginning and end of the string.
347      * Non-breaking whitespaces are also removed.
348      */
trim(CharSequence s)349     public static String trim(CharSequence s) {
350         if (s == null) {
351             return null;
352         }
353 
354         // Just strip any sequence of whitespace or java space characters from the beginning and end
355         Matcher m = sTrimPattern.matcher(s);
356         return m.replaceAll("$1");
357     }
358 
359     /**
360      * Calculates the height of a given string at a specific text size.
361      */
calculateTextHeight(float textSizePx)362     public static int calculateTextHeight(float textSizePx) {
363         Paint p = new Paint();
364         p.setTextSize(textSizePx);
365         Paint.FontMetrics fm = p.getFontMetrics();
366         return (int) Math.ceil(fm.bottom - fm.top);
367     }
368 
369     /**
370      * Convenience println with multiple args.
371      */
println(String key, Object... args)372     public static void println(String key, Object... args) {
373         StringBuilder b = new StringBuilder();
374         b.append(key);
375         b.append(": ");
376         boolean isFirstArgument = true;
377         for (Object arg : args) {
378             if (isFirstArgument) {
379                 isFirstArgument = false;
380             } else {
381                 b.append(", ");
382             }
383             b.append(arg);
384         }
385         System.out.println(b.toString());
386     }
387 
isRtl(Resources res)388     public static boolean isRtl(Resources res) {
389         return res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
390     }
391 
392     /**
393      * Returns true if the intent is a valid launch intent for a launcher activity of an app.
394      * This is used to identify shortcuts which are different from the ones exposed by the
395      * applications' manifest file.
396      *
397      * @param launchIntent The intent that will be launched when the shortcut is clicked.
398      */
isLauncherAppTarget(Intent launchIntent)399     public static boolean isLauncherAppTarget(Intent launchIntent) {
400         if (launchIntent != null
401                 && Intent.ACTION_MAIN.equals(launchIntent.getAction())
402                 && launchIntent.getComponent() != null
403                 && launchIntent.getCategories() != null
404                 && launchIntent.getCategories().size() == 1
405                 && launchIntent.hasCategory(Intent.CATEGORY_LAUNCHER)
406                 && TextUtils.isEmpty(launchIntent.getDataString())) {
407             // An app target can either have no extra or have ItemInfo.EXTRA_PROFILE.
408             Bundle extras = launchIntent.getExtras();
409             return extras == null || extras.keySet().isEmpty();
410         }
411         return false;
412     }
413 
dpiFromPx(int size, DisplayMetrics metrics)414     public static float dpiFromPx(int size, DisplayMetrics metrics){
415         float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT;
416         return (size / densityRatio);
417     }
pxFromDp(float size, DisplayMetrics metrics)418     public static int pxFromDp(float size, DisplayMetrics metrics) {
419         return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
420                 size, metrics));
421     }
pxFromSp(float size, DisplayMetrics metrics)422     public static int pxFromSp(float size, DisplayMetrics metrics) {
423         return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
424                 size, metrics));
425     }
426 
createDbSelectionQuery(String columnName, Iterable<?> values)427     public static String createDbSelectionQuery(String columnName, Iterable<?> values) {
428         return String.format(Locale.ENGLISH, "%s IN (%s)", columnName, TextUtils.join(", ", values));
429     }
430 
isBootCompleted()431     public static boolean isBootCompleted() {
432         return "1".equals(getSystemProperty("sys.boot_completed", "1"));
433     }
434 
getSystemProperty(String property, String defaultValue)435     public static String getSystemProperty(String property, String defaultValue) {
436         try {
437             Class clazz = Class.forName("android.os.SystemProperties");
438             Method getter = clazz.getDeclaredMethod("get", String.class);
439             String value = (String) getter.invoke(null, property);
440             if (!TextUtils.isEmpty(value)) {
441                 return value;
442             }
443         } catch (Exception e) {
444             Log.d(TAG, "Unable to read system properties");
445         }
446         return defaultValue;
447     }
448 
449     /**
450      * Ensures that a value is within given bounds. Specifically:
451      * If value is less than lowerBound, return lowerBound; else if value is greater than upperBound,
452      * return upperBound; else return value unchanged.
453      */
boundToRange(int value, int lowerBound, int upperBound)454     public static int boundToRange(int value, int lowerBound, int upperBound) {
455         return Math.max(lowerBound, Math.min(value, upperBound));
456     }
457 
458     /**
459      * @see #boundToRange(int, int, int).
460      */
boundToRange(float value, float lowerBound, float upperBound)461     public static float boundToRange(float value, float lowerBound, float upperBound) {
462         return Math.max(lowerBound, Math.min(value, upperBound));
463     }
464 
465     /**
466      * Wraps a message with a TTS span, so that a different message is spoken than
467      * what is getting displayed.
468      * @param msg original message
469      * @param ttsMsg message to be spoken
470      */
wrapForTts(CharSequence msg, String ttsMsg)471     public static CharSequence wrapForTts(CharSequence msg, String ttsMsg) {
472         SpannableString spanned = new SpannableString(msg);
473         spanned.setSpan(new TtsSpan.TextBuilder(ttsMsg).build(),
474                 0, spanned.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
475         return spanned;
476     }
477 
478     /**
479      * Replacement for Long.compare() which was added in API level 19.
480      */
longCompare(long lhs, long rhs)481     public static int longCompare(long lhs, long rhs) {
482         return lhs < rhs ? -1 : (lhs == rhs ? 0 : 1);
483     }
484 
getPrefs(Context context)485     public static SharedPreferences getPrefs(Context context) {
486         return context.getSharedPreferences(
487                 LauncherFiles.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE);
488     }
489 
getDevicePrefs(Context context)490     public static SharedPreferences getDevicePrefs(Context context) {
491         return context.getSharedPreferences(
492                 LauncherFiles.DEVICE_PREFERENCES_KEY, Context.MODE_PRIVATE);
493     }
494 
isPowerSaverPreventingAnimation(Context context)495     public static boolean isPowerSaverPreventingAnimation(Context context) {
496         if (ATLEAST_P) {
497             // Battery saver mode no longer prevents animations.
498             return false;
499         }
500         PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
501         return powerManager.isPowerSaveMode();
502     }
503 
isWallpaperAllowed(Context context)504     public static boolean isWallpaperAllowed(Context context) {
505         if (ATLEAST_NOUGAT) {
506             try {
507                 WallpaperManager wm = context.getSystemService(WallpaperManager.class);
508                 return (Boolean) wm.getClass().getDeclaredMethod("isSetWallpaperAllowed")
509                         .invoke(wm);
510             } catch (Exception e) { }
511         }
512         return true;
513     }
514 
closeSilently(Closeable c)515     public static void closeSilently(Closeable c) {
516         if (c != null) {
517             try {
518                 c.close();
519             } catch (IOException e) {
520                 if (FeatureFlags.IS_DOGFOOD_BUILD) {
521                     Log.d(TAG, "Error closing", e);
522                 }
523             }
524         }
525     }
526 
527     /**
528      * Returns true if {@param original} contains all entries defined in {@param updates} and
529      * have the same value.
530      * The comparison uses {@link Object#equals(Object)} to compare the values.
531      */
containsAll(Bundle original, Bundle updates)532     public static boolean containsAll(Bundle original, Bundle updates) {
533         for (String key : updates.keySet()) {
534             Object value1 = updates.get(key);
535             Object value2 = original.get(key);
536             if (value1 == null) {
537                 if (value2 != null) {
538                     return false;
539                 }
540             } else if (!value1.equals(value2)) {
541                 return false;
542             }
543         }
544         return true;
545     }
546 
547     /** Returns whether the collection is null or empty. */
isEmpty(Collection c)548     public static boolean isEmpty(Collection c) {
549         return c == null || c.isEmpty();
550     }
551 
isBinderSizeError(Exception e)552     public static boolean isBinderSizeError(Exception e) {
553         return e.getCause() instanceof TransactionTooLargeException
554                 || e.getCause() instanceof DeadObjectException;
555     }
556 
getOverrideObject(Class<T> clazz, Context context, int resId)557     public static <T> T getOverrideObject(Class<T> clazz, Context context, int resId) {
558         String className = context.getString(resId);
559         if (!TextUtils.isEmpty(className)) {
560             try {
561                 Class<?> cls = Class.forName(className);
562                 return (T) cls.getDeclaredConstructor(Context.class).newInstance(context);
563             } catch (ClassNotFoundException | InstantiationException | IllegalAccessException
564                     | ClassCastException | NoSuchMethodException | InvocationTargetException e) {
565                 Log.e(TAG, "Bad overriden class", e);
566             }
567         }
568 
569         try {
570             return clazz.newInstance();
571         } catch (InstantiationException|IllegalAccessException e) {
572             throw new RuntimeException(e);
573         }
574     }
575 
576     /**
577      * Returns a HashSet with a single element. We use this instead of Collections.singleton()
578      * because HashSet ensures all operations, such as remove, are supported.
579      */
singletonHashSet(T elem)580     public static <T> HashSet<T> singletonHashSet(T elem) {
581         HashSet<T> hashSet = new HashSet<>(1);
582         hashSet.add(elem);
583         return hashSet;
584     }
585 
586     /**
587      * Utility method to post a runnable on the handler, skipping the synchronization barriers.
588      */
postAsyncCallback(Handler handler, Runnable callback)589     public static void postAsyncCallback(Handler handler, Runnable callback) {
590         Message msg = Message.obtain(handler, callback);
591         msg.setAsynchronous(true);
592         handler.sendMessage(msg);
593     }
594 }
595