QuickContactActivity.java revision d9eab4da8b9946b76a2091e5cce6c858663ef1bb
1/* 2 * Copyright (C) 2009 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.contacts.quickcontact; 18 19import android.app.Activity; 20import android.app.Fragment; 21import android.app.FragmentManager; 22import android.app.LoaderManager.LoaderCallbacks; 23import android.content.ActivityNotFoundException; 24import android.content.ContentUris; 25import android.content.Context; 26import android.content.Intent; 27import android.content.Loader; 28import android.content.pm.PackageManager; 29import android.graphics.Rect; 30import android.graphics.drawable.Drawable; 31import android.net.Uri; 32import android.os.Bundle; 33import android.os.Handler; 34import android.provider.ContactsContract.CommonDataKinds.Email; 35import android.provider.ContactsContract.CommonDataKinds.Phone; 36import android.provider.ContactsContract.CommonDataKinds.SipAddress; 37import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 38import android.provider.ContactsContract.CommonDataKinds.Website; 39import android.provider.ContactsContract.Contacts; 40import android.provider.ContactsContract.QuickContact; 41import android.provider.ContactsContract.RawContacts; 42import android.support.v13.app.FragmentPagerAdapter; 43import android.support.v4.view.ViewPager; 44import android.support.v4.view.ViewPager.SimpleOnPageChangeListener; 45import android.text.TextUtils; 46import android.util.Log; 47import android.view.MotionEvent; 48import android.view.View; 49import android.view.View.OnClickListener; 50import android.view.ViewGroup; 51import android.view.WindowManager; 52import android.widget.HorizontalScrollView; 53import android.widget.ImageButton; 54import android.widget.ImageView; 55import android.widget.RelativeLayout; 56import android.widget.TextView; 57import android.widget.Toast; 58 59import com.android.contacts.common.Collapser; 60import com.android.contacts.R; 61import com.android.contacts.model.AccountTypeManager; 62import com.android.contacts.model.Contact; 63import com.android.contacts.model.ContactLoader; 64import com.android.contacts.model.RawContact; 65import com.android.contacts.common.model.account.AccountType; 66import com.android.contacts.model.dataitem.DataItem; 67import com.android.contacts.common.model.dataitem.DataKind; 68import com.android.contacts.model.dataitem.EmailDataItem; 69import com.android.contacts.model.dataitem.ImDataItem; 70import com.android.contacts.common.util.Constants; 71import com.android.contacts.util.DataStatus; 72import com.android.contacts.util.ImageViewDrawableSetter; 73import com.android.contacts.util.SchedulingUtils; 74import com.android.contacts.common.util.StopWatch; 75import com.google.common.base.Preconditions; 76import com.google.common.collect.Lists; 77 78import java.util.HashMap; 79import java.util.HashSet; 80import java.util.List; 81import java.util.Set; 82 83// TODO: Save selected tab index during rotation 84 85/** 86 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads 87 * data asynchronously, and then shows a popup with details centered around 88 * {@link Intent#getSourceBounds()}. 89 */ 90public class QuickContactActivity extends Activity { 91 private static final String TAG = "QuickContact"; 92 93 private static final boolean TRACE_LAUNCH = false; 94 private static final String TRACE_TAG = "quickcontact"; 95 private static final int POST_DRAW_WAIT_DURATION = 60; 96 private static final boolean ENABLE_STOPWATCH = false; 97 98 99 @SuppressWarnings("deprecation") 100 private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY; 101 102 private Uri mLookupUri; 103 private String[] mExcludeMimes; 104 private List<String> mSortedActionMimeTypes = Lists.newArrayList(); 105 106 private FloatingChildLayout mFloatingLayout; 107 108 private View mPhotoContainer; 109 private ViewGroup mTrack; 110 private HorizontalScrollView mTrackScroller; 111 private View mSelectedTabRectangle; 112 private View mLineAfterTrack; 113 114 private ImageView mOpenDetailsImage; 115 private ImageButton mOpenDetailsPushLayerButton; 116 private ViewPager mListPager; 117 118 private ContactLoader mContactLoader; 119 120 private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter(); 121 122 /** 123 * Keeps the default action per mimetype. Empty if no default actions are set 124 */ 125 private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>(); 126 127 /** 128 * Set of {@link Action} that are associated with the aggregate currently 129 * displayed by this dialog, represented as a map from {@link String} 130 * MIME-type to a list of {@link Action}. 131 */ 132 private ActionMultiMap mActions = new ActionMultiMap(); 133 134 /** 135 * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types. 136 * 137 * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog, 138 * in the order specified here.</p> 139 * 140 * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order 141 * specified here.</p> 142 * 143 * <p>The rest go between them, in the order in the array.</p> 144 */ 145 private static final List<String> LEADING_MIMETYPES = Lists.newArrayList( 146 Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE); 147 148 /** See {@link #LEADING_MIMETYPES}. */ 149 private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList( 150 StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE); 151 152 /** Id for the background loader */ 153 private static final int LOADER_ID = 0; 154 155 private StopWatch mStopWatch = ENABLE_STOPWATCH 156 ? StopWatch.start("QuickContact") : StopWatch.getNullStopWatch(); 157 158 @Override 159 protected void onCreate(Bundle icicle) { 160 mStopWatch.lap("c"); // create start 161 super.onCreate(icicle); 162 163 mStopWatch.lap("sc"); // super.onCreate 164 165 if (TRACE_LAUNCH) android.os.Debug.startMethodTracing(TRACE_TAG); 166 167 // Parse intent 168 final Intent intent = getIntent(); 169 170 Uri lookupUri = intent.getData(); 171 172 // Check to see whether it comes from the old version. 173 if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) { 174 final long rawContactId = ContentUris.parseId(lookupUri); 175 lookupUri = RawContacts.getContactLookupUri(getContentResolver(), 176 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 177 } 178 179 mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri"); 180 181 mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); 182 183 mStopWatch.lap("i"); // intent parsed 184 185 mContactLoader = (ContactLoader) getLoaderManager().initLoader( 186 LOADER_ID, null, mLoaderCallbacks); 187 188 mStopWatch.lap("ld"); // loader started 189 190 // Show QuickContact in front of soft input 191 getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 192 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 193 194 setContentView(R.layout.quickcontact_activity); 195 196 mStopWatch.lap("l"); // layout inflated 197 198 mFloatingLayout = (FloatingChildLayout) findViewById(R.id.floating_layout); 199 mTrack = (ViewGroup) findViewById(R.id.track); 200 mTrackScroller = (HorizontalScrollView) findViewById(R.id.track_scroller); 201 mOpenDetailsImage = (ImageView) findViewById(R.id.contact_details_image); 202 mOpenDetailsPushLayerButton = (ImageButton) findViewById(R.id.open_details_push_layer); 203 mListPager = (ViewPager) findViewById(R.id.item_list_pager); 204 mSelectedTabRectangle = findViewById(R.id.selected_tab_rectangle); 205 mLineAfterTrack = findViewById(R.id.line_after_track); 206 207 mFloatingLayout.setOnOutsideTouchListener(new View.OnTouchListener() { 208 @Override 209 public boolean onTouch(View v, MotionEvent event) { 210 handleOutsideTouch(); 211 return true; 212 } 213 }); 214 215 final OnClickListener openDetailsClickHandler = new OnClickListener() { 216 @Override 217 public void onClick(View v) { 218 final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri); 219 mContactLoader.cacheResult(); 220 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 221 startActivity(intent); 222 close(false); 223 } 224 }; 225 mOpenDetailsPushLayerButton.setOnClickListener(openDetailsClickHandler); 226 mListPager.setAdapter(new ViewPagerAdapter(getFragmentManager())); 227 mListPager.setOnPageChangeListener(new PageChangeListener()); 228 229 final Rect sourceBounds = intent.getSourceBounds(); 230 if (sourceBounds != null) { 231 mFloatingLayout.setChildTargetScreen(sourceBounds); 232 } 233 234 // find and prepare correct header view 235 mPhotoContainer = findViewById(R.id.photo_container); 236 setHeaderNameText(R.id.name, R.string.missing_name); 237 238 mStopWatch.lap("v"); // view initialized 239 240 SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() { 241 @Override 242 public void run() { 243 mFloatingLayout.fadeInBackground(); 244 } 245 }); 246 247 mStopWatch.lap("cf"); // onCreate finished 248 } 249 250 private void handleOutsideTouch() { 251 if (mFloatingLayout.isContentFullyVisible()) { 252 close(true); 253 } 254 } 255 256 private void close(boolean withAnimation) { 257 // cancel any pending queries 258 getLoaderManager().destroyLoader(LOADER_ID); 259 260 if (withAnimation) { 261 mFloatingLayout.fadeOutBackground(); 262 final boolean animated = mFloatingLayout.hideContent(new Runnable() { 263 @Override 264 public void run() { 265 // Wait until the final animation frame has been drawn, otherwise 266 // there is jank as the framework transitions to the next Activity. 267 SchedulingUtils.doAfterDraw(mFloatingLayout, new Runnable() { 268 @Override 269 public void run() { 270 // Unfortunately, we need to also use postDelayed() to wait a moment 271 // for the frame to be drawn, else the framework's activity-transition 272 // animation will kick in before the final frame is available to it. 273 // This seems unavoidable. The problem isn't merely that there is no 274 // post-draw listener API; if that were so, it would be sufficient to 275 // call post() instead of postDelayed(). 276 new Handler().postDelayed(new Runnable() { 277 @Override 278 public void run() { 279 finish(); 280 } 281 }, POST_DRAW_WAIT_DURATION); 282 } 283 }); 284 } 285 }); 286 if (!animated) { 287 // If we were in the wrong state, simply quit (this can happen for example 288 // if the user pushes BACK before anything has loaded) 289 finish(); 290 } 291 } else { 292 finish(); 293 } 294 } 295 296 @Override 297 public void onBackPressed() { 298 close(true); 299 } 300 301 /** Assign this string to the view if it is not empty. */ 302 private void setHeaderNameText(int id, int resId) { 303 setHeaderNameText(id, getText(resId)); 304 } 305 306 /** Assign this string to the view if it is not empty. */ 307 private void setHeaderNameText(int id, CharSequence value) { 308 final View view = mPhotoContainer.findViewById(id); 309 if (view instanceof TextView) { 310 if (!TextUtils.isEmpty(value)) { 311 ((TextView)view).setText(value); 312 } 313 } 314 } 315 316 /** 317 * Check if the given MIME-type appears in the list of excluded MIME-types 318 * that the most-recent caller requested. 319 */ 320 private boolean isMimeExcluded(String mimeType) { 321 if (mExcludeMimes == null) return false; 322 for (String excludedMime : mExcludeMimes) { 323 if (TextUtils.equals(excludedMime, mimeType)) { 324 return true; 325 } 326 } 327 return false; 328 } 329 330 /** 331 * Handle the result from the ContactLoader 332 */ 333 private void bindData(Contact data) { 334 final ResolveCache cache = ResolveCache.getInstance(this); 335 final Context context = this; 336 337 mOpenDetailsImage.setVisibility(isMimeExcluded(Contacts.CONTENT_ITEM_TYPE) ? View.GONE 338 : View.VISIBLE); 339 340 mDefaultsMap.clear(); 341 342 mStopWatch.lap("sph"); // Start photo setting 343 344 final ImageView photoView = (ImageView) mPhotoContainer.findViewById(R.id.photo); 345 mPhotoSetter.setupContactPhoto(data, photoView); 346 347 mStopWatch.lap("ph"); // Photo set 348 349 for (RawContact rawContact : data.getRawContacts()) { 350 for (DataItem dataItem : rawContact.getDataItems()) { 351 final String mimeType = dataItem.getMimeType(); 352 final AccountType accountType = rawContact.getAccountType(this); 353 final DataKind dataKind = AccountTypeManager.getInstance(this) 354 .getKindOrFallback(accountType, mimeType); 355 356 // Skip this data item if MIME-type excluded 357 if (isMimeExcluded(mimeType)) continue; 358 359 final long dataId = dataItem.getId(); 360 final boolean isPrimary = dataItem.isPrimary(); 361 final boolean isSuperPrimary = dataItem.isSuperPrimary(); 362 363 if (dataKind != null) { 364 // Build an action for this data entry, find a mapping to a UI 365 // element, build its summary from the cursor, and collect it 366 // along with all others of this MIME-type. 367 final Action action = new DataAction(context, dataItem, dataKind); 368 final boolean wasAdded = considerAdd(action, cache, isSuperPrimary); 369 if (wasAdded) { 370 // Remember the default 371 if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) { 372 mDefaultsMap.put(mimeType, action); 373 } 374 } 375 } 376 377 // Handle Email rows with presence data as Im entry 378 final DataStatus status = data.getStatuses().get(dataId); 379 if (status != null && dataItem instanceof EmailDataItem) { 380 final EmailDataItem email = (EmailDataItem) dataItem; 381 final ImDataItem im = ImDataItem.createFromEmail(email); 382 if (dataKind != null) { 383 final DataAction action = new DataAction(context, im, dataKind); 384 action.setPresence(status.getPresence()); 385 considerAdd(action, cache, isSuperPrimary); 386 } 387 } 388 } 389 } 390 391 mStopWatch.lap("e"); // Entities inflated 392 393 // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources) 394 for (List<Action> actionChildren : mActions.values()) { 395 Collapser.collapseList(actionChildren); 396 } 397 398 mStopWatch.lap("c"); // List collapsed 399 400 setHeaderNameText(R.id.name, data.getDisplayName()); 401 402 // All the mime-types to add. 403 final Set<String> containedTypes = new HashSet<String>(mActions.keySet()); 404 mSortedActionMimeTypes.clear(); 405 // First, add LEADING_MIMETYPES, which are most common. 406 for (String mimeType : LEADING_MIMETYPES) { 407 if (containedTypes.contains(mimeType)) { 408 mSortedActionMimeTypes.add(mimeType); 409 containedTypes.remove(mimeType); 410 } 411 } 412 413 // Add all the remaining ones that are not TRAILING 414 for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) { 415 if (!TRAILING_MIMETYPES.contains(mimeType)) { 416 mSortedActionMimeTypes.add(mimeType); 417 containedTypes.remove(mimeType); 418 } 419 } 420 421 // Then, add TRAILING_MIMETYPES, which are least common. 422 for (String mimeType : TRAILING_MIMETYPES) { 423 if (containedTypes.contains(mimeType)) { 424 containedTypes.remove(mimeType); 425 mSortedActionMimeTypes.add(mimeType); 426 } 427 } 428 429 mStopWatch.lap("mt"); // Mime types initialized 430 431 // Add buttons for each mimetype 432 mTrack.removeAllViews(); 433 for (String mimeType : mSortedActionMimeTypes) { 434 final View actionView = inflateAction(mimeType, cache, mTrack); 435 mTrack.addView(actionView); 436 } 437 438 mStopWatch.lap("mt"); // Buttons added 439 440 final boolean hasData = !mSortedActionMimeTypes.isEmpty(); 441 mTrackScroller.setVisibility(hasData ? View.VISIBLE : View.GONE); 442 mSelectedTabRectangle.setVisibility(hasData ? View.VISIBLE : View.GONE); 443 mLineAfterTrack.setVisibility(hasData ? View.VISIBLE : View.GONE); 444 mListPager.setVisibility(hasData ? View.VISIBLE : View.GONE); 445 } 446 447 /** 448 * Consider adding the given {@link Action}, which will only happen if 449 * {@link PackageManager} finds an application to handle 450 * {@link Action#getIntent()}. 451 * @param action the action to handle 452 * @param resolveCache cache of applications that can handle actions 453 * @param front indicates whether to add the action to the front of the list 454 * @return true if action has been added 455 */ 456 private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front) { 457 if (resolveCache.hasResolve(action)) { 458 mActions.put(action.getMimeType(), action, front); 459 return true; 460 } 461 return false; 462 } 463 464 /** 465 * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values. 466 * Will use the icon provided by the {@link DataKind}. 467 */ 468 private View inflateAction(String mimeType, ResolveCache resolveCache, ViewGroup root) { 469 final CheckableImageView typeView = (CheckableImageView) getLayoutInflater().inflate( 470 R.layout.quickcontact_track_button, root, false); 471 472 List<Action> children = mActions.get(mimeType); 473 typeView.setTag(mimeType); 474 final Action firstInfo = children.get(0); 475 476 // Set icon and listen for clicks 477 final CharSequence descrip = resolveCache.getDescription(firstInfo); 478 final Drawable icon = resolveCache.getIcon(firstInfo); 479 typeView.setChecked(false); 480 typeView.setContentDescription(descrip); 481 typeView.setImageDrawable(icon); 482 typeView.setOnClickListener(mTypeViewClickListener); 483 484 return typeView; 485 } 486 487 private CheckableImageView getActionViewAt(int position) { 488 return (CheckableImageView) mTrack.getChildAt(position); 489 } 490 491 @Override 492 public void onAttachFragment(Fragment fragment) { 493 final QuickContactListFragment listFragment = (QuickContactListFragment) fragment; 494 listFragment.setListener(mListFragmentListener); 495 } 496 497 private LoaderCallbacks<Contact> mLoaderCallbacks = 498 new LoaderCallbacks<Contact>() { 499 @Override 500 public void onLoaderReset(Loader<Contact> loader) { 501 } 502 503 @Override 504 public void onLoadFinished(Loader<Contact> loader, Contact data) { 505 mStopWatch.lap("lf"); // onLoadFinished 506 if (isFinishing()) { 507 close(false); 508 return; 509 } 510 if (data.isError()) { 511 // This shouldn't ever happen, so throw an exception. The {@link ContactLoader} 512 // should log the actual exception. 513 throw new IllegalStateException("Failed to load contact", data.getException()); 514 } 515 if (data.isNotFound()) { 516 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri()); 517 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 518 Toast.LENGTH_LONG).show(); 519 close(false); 520 return; 521 } 522 523 bindData(data); 524 525 mStopWatch.lap("bd"); // bindData finished 526 527 if (TRACE_LAUNCH) android.os.Debug.stopMethodTracing(); 528 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 529 Log.d(Constants.PERFORMANCE_TAG, "QuickContact shown"); 530 } 531 532 // Data bound and ready, pull curtain to show. Put this on the Handler to ensure 533 // that the layout passes are completed 534 SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() { 535 @Override 536 public void run() { 537 mFloatingLayout.showContent(new Runnable() { 538 @Override 539 public void run() { 540 mContactLoader.upgradeToFullContact(); 541 } 542 }); 543 } 544 }); 545 mStopWatch.stopAndLog(TAG, 0); 546 mStopWatch = StopWatch.getNullStopWatch(); // We're done with it. 547 } 548 549 @Override 550 public Loader<Contact> onCreateLoader(int id, Bundle args) { 551 if (mLookupUri == null) { 552 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early"); 553 } 554 return new ContactLoader(getApplicationContext(), mLookupUri, false); 555 } 556 }; 557 558 /** A type (e.g. Call/Addresses was clicked) */ 559 private final OnClickListener mTypeViewClickListener = new OnClickListener() { 560 @Override 561 public void onClick(View view) { 562 final CheckableImageView actionView = (CheckableImageView)view; 563 final String mimeType = (String) actionView.getTag(); 564 int index = mSortedActionMimeTypes.indexOf(mimeType); 565 mListPager.setCurrentItem(index, true); 566 } 567 }; 568 569 private class ViewPagerAdapter extends FragmentPagerAdapter { 570 public ViewPagerAdapter(FragmentManager fragmentManager) { 571 super(fragmentManager); 572 } 573 574 @Override 575 public Fragment getItem(int position) { 576 QuickContactListFragment fragment = new QuickContactListFragment(); 577 final String mimeType = mSortedActionMimeTypes.get(position); 578 final List<Action> actions = mActions.get(mimeType); 579 fragment.setActions(actions); 580 return fragment; 581 } 582 583 @Override 584 public int getCount() { 585 return mSortedActionMimeTypes.size(); 586 } 587 } 588 589 private class PageChangeListener extends SimpleOnPageChangeListener { 590 @Override 591 public void onPageSelected(int position) { 592 final CheckableImageView actionView = getActionViewAt(position); 593 mTrackScroller.requestChildRectangleOnScreen(actionView, 594 new Rect(0, 0, actionView.getWidth(), actionView.getHeight()), false); 595 renderSelectedRectangle(position, 0); 596 } 597 598 @Override 599 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 600 renderSelectedRectangle(position, positionOffset); 601 } 602 603 private void renderSelectedRectangle(int position, float positionOffset) { 604 final RelativeLayout.LayoutParams layoutParams = 605 (RelativeLayout.LayoutParams) mSelectedTabRectangle.getLayoutParams(); 606 final int width = layoutParams.width; 607 layoutParams.leftMargin = (int) ((position + positionOffset) * width); 608 mSelectedTabRectangle.setLayoutParams(layoutParams); 609 } 610 } 611 612 private final QuickContactListFragment.Listener mListFragmentListener = 613 new QuickContactListFragment.Listener() { 614 @Override 615 public void onOutsideClick() { 616 // If there is no background, we want to dismiss, because to the user it seems 617 // like he had touched outside. If the ViewPager is solid however, those taps 618 // must be ignored 619 final boolean isTransparent = mListPager.getBackground() == null; 620 if (isTransparent) handleOutsideTouch(); 621 } 622 623 @Override 624 public void onItemClicked(final Action action, final boolean alternate) { 625 final Runnable startAppRunnable = new Runnable() { 626 @Override 627 public void run() { 628 try { 629 startActivity(alternate ? action.getAlternateIntent() : action.getIntent()); 630 } catch (ActivityNotFoundException e) { 631 Toast.makeText(QuickContactActivity.this, R.string.quickcontact_missing_app, 632 Toast.LENGTH_SHORT).show(); 633 } 634 635 close(false); 636 } 637 }; 638 // Defer the action to make the window properly repaint 639 new Handler().post(startAppRunnable); 640 } 641 }; 642} 643