Files
tubestation/mobile/android/base/overlays/ui/ShareDialog.java
Michael Comella cd19012d07 Bug 1155855 - Don't override animations when opening the browser from the share overlay. r=liuche
When used to do our own animation when opening the browser from the
share overlay. That caused this bug: we didn't call `finish` until
`onAnimationEnd` but since `startActivity` was called, the application
was switched before `onAnimationEnd`, and thus `finish`, could be
called. When we returned to the share overlay, it was in an unexpected
state (`isAnimating` was true) and the user could no longer interact
with it, blocking access to the app the ShareOverlay was opened from.

We fix this by not doing our custom animations and just calling `finish`.

Note: in any case, overriding the animation when opening the browser
could be unintuitive to users because they might expect a consistent
app-switch animation throughout the system.
2015-08-28 17:32:23 -07:00

482 lines
18 KiB
Java

/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.overlays.ui;
import java.net.URISyntaxException;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Assert;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.LocalBrowserDB;
import org.mozilla.gecko.db.RemoteClient;
import org.mozilla.gecko.overlays.OverlayConstants;
import org.mozilla.gecko.overlays.service.OverlayActionService;
import org.mozilla.gecko.overlays.service.sharemethods.SendTab;
import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod;
import org.mozilla.gecko.sync.setup.activities.WebURLFinder;
import org.mozilla.gecko.mozglue.ContextUtils;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.TextView;
import android.widget.Toast;
/**
* A transparent activity that displays the share overlay.
*/
public class ShareDialog extends Locales.LocaleAwareActivity implements SendTabTargetSelectedListener {
private enum State {
DEFAULT,
DEVICES_ONLY // Only display the device list.
}
private static final String LOGTAG = "GeckoShareDialog";
/** Flag to indicate that we should always show the device list; specific to this release channel. **/
public static final String INTENT_EXTRA_DEVICES_ONLY =
AppConstants.ANDROID_PACKAGE_NAME + ".intent.extra.DEVICES_ONLY";
/** The maximum number of devices we'll show in the dialog when in State.DEFAULT. **/
private static final int MAXIMUM_INLINE_DEVICES = 2;
private State state;
private SendTabList sendTabList;
private OverlayDialogButton readingListButton;
private OverlayDialogButton bookmarkButton;
// The reading list drawable set from XML - we need this to reset state.
private Drawable readingListButtonDrawable;
private String url;
private String title;
// The override intent specified by SendTab (if any). See SendTab.java.
private Intent sendTabOverrideIntent;
// Flag set during animation to prevent animation multiple-start.
private boolean isAnimating;
// BroadcastReceiver to receive callbacks from ShareMethods which are changing state.
private final BroadcastReceiver uiEventListener = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
ShareMethod.Type originShareMethod = intent.getParcelableExtra(OverlayConstants.EXTRA_SHARE_METHOD);
switch (originShareMethod) {
case SEND_TAB:
handleSendTabUIEvent(intent);
break;
default:
throw new IllegalArgumentException("UIEvent broadcast from ShareMethod that isn't thought to support such broadcasts.");
}
}
};
/**
* Called when a UI event broadcast is received from the SendTab ShareMethod.
*/
protected void handleSendTabUIEvent(Intent intent) {
sendTabOverrideIntent = intent.getParcelableExtra(SendTab.OVERRIDE_INTENT);
RemoteClient[] remoteClientRecords = (RemoteClient[]) intent.getParcelableArrayExtra(SendTab.EXTRA_REMOTE_CLIENT_RECORDS);
// Escape hatch: we don't show the option to open this dialog in this state so this should
// never be run. However, due to potential inconsistencies in synced client state
// (e.g. bug 1122302 comment 47), we might fail.
if (state == State.DEVICES_ONLY &&
(remoteClientRecords == null || remoteClientRecords.length == 0)) {
Log.e(LOGTAG, "In state: " + State.DEVICES_ONLY + " and received 0 synced clients. Finishing...");
Toast.makeText(this, getResources().getText(R.string.overlay_no_synced_devices), Toast.LENGTH_SHORT)
.show();
finish();
return;
}
sendTabList.setSyncClients(remoteClientRecords);
if (state == State.DEVICES_ONLY ||
remoteClientRecords == null ||
remoteClientRecords.length <= MAXIMUM_INLINE_DEVICES) {
// Show the list of devices in-line.
sendTabList.switchState(SendTabList.State.LIST);
// The first item in the list has a unique style. If there are no items
// in the list, the next button appears to be the first item in the list.
//
// Note: a more thorough implementation would add this
// (and other non-ListView buttons) into a custom ListView.
if (remoteClientRecords == null || remoteClientRecords.length == 0) {
readingListButton.setBackgroundResource(
R.drawable.overlay_share_button_background_first);
}
return;
}
// Just show a button to launch the list of devices to choose from.
sendTabList.switchState(SendTabList.State.SHOW_DEVICES);
}
@Override
protected void onDestroy() {
// Remove the listener when the activity is destroyed: we no longer care.
// Note: The activity can be destroyed without onDestroy being called. However, this occurs
// only when the application is killed, something which also kills the registered receiver
// list, and the service, and everything else: so we don't care.
LocalBroadcastManager.getInstance(this).unregisterReceiver(uiEventListener);
super.onDestroy();
}
/**
* Show a toast indicating we were started with no URL, and then stop.
*/
private void abortDueToNoURL() {
Log.e(LOGTAG, "Unable to process shared intent. No URL found!");
// Display toast notifying the user of failure (most likely a developer who screwed up
// trying to send a share intent).
Toast toast = Toast.makeText(this, getResources().getText(R.string.overlay_share_no_url), Toast.LENGTH_SHORT);
toast.show();
finish();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.overlay_share_dialog);
LocalBroadcastManager.getInstance(this).registerReceiver(uiEventListener,
new IntentFilter(OverlayConstants.SHARE_METHOD_UI_EVENT));
// Send tab.
sendTabList = (SendTabList) findViewById(R.id.overlay_send_tab_btn);
// Register ourselves as both the listener and the context for the Adapter.
final SendTabDeviceListArrayAdapter adapter = new SendTabDeviceListArrayAdapter(this, this);
sendTabList.setAdapter(adapter);
sendTabList.setSendTabTargetSelectedListener(this);
bookmarkButton = (OverlayDialogButton) findViewById(R.id.overlay_share_bookmark_btn);
readingListButton = (OverlayDialogButton) findViewById(R.id.overlay_share_reading_list_btn);
readingListButtonDrawable = readingListButton.getBackground();
// Bookmark button
bookmarkButton = (OverlayDialogButton) findViewById(R.id.overlay_share_bookmark_btn);
bookmarkButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
addBookmark();
}
});
// Reading List button
readingListButton = (OverlayDialogButton) findViewById(R.id.overlay_share_reading_list_btn);
readingListButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
addToReadingList();
}
});
}
@Override
protected void onResume() {
super.onResume();
final Intent intent = getIntent();
state = intent.getBooleanExtra(INTENT_EXTRA_DEVICES_ONLY, false) ?
State.DEVICES_ONLY : State.DEFAULT;
// If the Activity is being reused, we need to reset the state. Ideally, we create a
// new instance for each call, but Android L breaks this (bug 1137928).
sendTabList.switchState(SendTabList.State.LOADING);
readingListButton.setBackgroundDrawable(readingListButtonDrawable);
// The URL is usually hiding somewhere in the extra text. Extract it.
final String extraText = ContextUtils.getStringExtra(intent, Intent.EXTRA_TEXT);
if (TextUtils.isEmpty(extraText)) {
abortDueToNoURL();
return;
}
final String pageUrl = new WebURLFinder(extraText).bestWebURL();
if (TextUtils.isEmpty(pageUrl)) {
abortDueToNoURL();
return;
}
// Have the service start any initialisation work that's necessary for us to show the correct
// UI. The results of such work will come in via the BroadcastListener.
Intent serviceStartupIntent = new Intent(this, OverlayActionService.class);
serviceStartupIntent.setAction(OverlayConstants.ACTION_PREPARE_SHARE);
startService(serviceStartupIntent);
// Start the slide-up animation.
getWindow().setWindowAnimations(0);
final Animation anim = AnimationUtils.loadAnimation(this, R.anim.overlay_slide_up);
findViewById(R.id.sharedialog).startAnimation(anim);
// If provided, we use the subject text to give us something nice to display.
// If not, we wing it with the URL.
// TODO: Consider polling Fennec databases to find better information to display.
final String subjectText = intent.getStringExtra(Intent.EXTRA_SUBJECT);
final String telemetryExtras = "title=" + (subjectText != null);
if (subjectText != null) {
((TextView) findViewById(R.id.title)).setText(subjectText);
}
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.SHARE_OVERLAY, telemetryExtras);
title = subjectText;
url = pageUrl;
// Set the subtitle text on the view and cause it to marquee if it's too long (which it will
// be, since it's a URL).
final TextView subtitleView = (TextView) findViewById(R.id.subtitle);
subtitleView.setText(pageUrl);
subtitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
subtitleView.setSingleLine(true);
subtitleView.setMarqueeRepeatLimit(5);
subtitleView.setSelected(true);
final View titleView = findViewById(R.id.title);
if (state == State.DEVICES_ONLY) {
bookmarkButton.setVisibility(View.GONE);
readingListButton.setVisibility(View.GONE);
titleView.setOnClickListener(null);
subtitleView.setOnClickListener(null);
return;
}
bookmarkButton.setVisibility(View.VISIBLE);
readingListButton.setVisibility(View.VISIBLE);
// Configure buttons.
final View.OnClickListener launchBrowser = new View.OnClickListener() {
@Override
public void onClick(View view) {
ShareDialog.this.launchBrowser();
}
};
titleView.setOnClickListener(launchBrowser);
subtitleView.setOnClickListener(launchBrowser);
final LocalBrowserDB browserDB = new LocalBrowserDB(getCurrentProfile());
setButtonState(url, browserDB);
}
@Override
protected void onNewIntent(final Intent intent) {
super.onNewIntent(intent);
// The intent returned by getIntent is not updated automatically.
setIntent(intent);
}
/**
* Sets the state of the bookmark/reading list buttons: they are disabled if the given URL is
* already in the corresponding list.
*/
private void setButtonState(final String pageURL, final LocalBrowserDB browserDB) {
new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
// Flags to hold the result
boolean isBookmark;
boolean isReadingListItem;
@Override
protected Void doInBackground() {
final ContentResolver contentResolver = getApplicationContext().getContentResolver();
isBookmark = browserDB.isBookmark(contentResolver, pageURL);
isReadingListItem = browserDB.getReadingListAccessor().isReadingListItem(contentResolver, pageURL);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
findViewById(R.id.overlay_share_bookmark_btn).setEnabled(!isBookmark);
findViewById(R.id.overlay_share_reading_list_btn).setEnabled(!isReadingListItem);
}
}.execute();
}
/**
* Helper method to get an overlay service intent populated with the data held in this dialog.
*/
private Intent getServiceIntent(ShareMethod.Type method) {
final Intent serviceIntent = new Intent(this, OverlayActionService.class);
serviceIntent.setAction(OverlayConstants.ACTION_SHARE);
serviceIntent.putExtra(OverlayConstants.EXTRA_SHARE_METHOD, (Parcelable) method);
serviceIntent.putExtra(OverlayConstants.EXTRA_URL, url);
serviceIntent.putExtra(OverlayConstants.EXTRA_TITLE, title);
return serviceIntent;
}
@Override
public void finish() {
finish(true);
}
private void finish(final boolean shouldOverrideAnimations) {
super.finish();
if (shouldOverrideAnimations) {
// Don't perform an activity-dismiss animation.
overridePendingTransition(0, 0);
}
}
/*
* Button handlers. Send intents to the background service responsible for processing requests
* on Fennec in the background. (a nice extensible mechanism for "doing stuff without properly
* launching Fennec").
*/
@Override
public void onSendTabActionSelected() {
// This requires an override intent.
Assert.isTrue(sendTabOverrideIntent != null);
startActivity(sendTabOverrideIntent);
finish();
}
@Override
public void onSendTabTargetSelected(String targetGUID) {
// targetGUID being null with no override intent should be an impossible state.
Assert.isTrue(targetGUID != null);
Intent serviceIntent = getServiceIntent(ShareMethod.Type.SEND_TAB);
// Currently, only one extra parameter is necessary (the GUID of the target device).
Bundle extraParameters = new Bundle();
// Future: Handle multiple-selection. Bug 1061297.
extraParameters.putStringArray(SendTab.SEND_TAB_TARGET_DEVICES, new String[] { targetGUID });
serviceIntent.putExtra(OverlayConstants.EXTRA_PARAMETERS, extraParameters);
startService(serviceIntent);
slideOut();
Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.SHARE_OVERLAY, "sendtab");
}
public void addToReadingList() {
startService(getServiceIntent(ShareMethod.Type.ADD_TO_READING_LIST));
slideOut();
Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.SHARE_OVERLAY, "reading_list");
}
public void addBookmark() {
startService(getServiceIntent(ShareMethod.Type.ADD_BOOKMARK));
slideOut();
Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.SHARE_OVERLAY, "bookmark");
}
public void launchBrowser() {
try {
// This can launch in the guest profile. Sorry.
final Intent i = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
i.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
startActivity(i);
} catch (URISyntaxException e) {
// Nothing much we can do.
} finally {
// Since we're changing apps, users expect the default app switch animations.
finish(false);
}
}
private String getCurrentProfile() {
return GeckoProfile.DEFAULT_PROFILE;
}
/**
* Slide the overlay down off the screen and destroy it.
*/
private void slideOut() {
if (isAnimating) {
return;
}
isAnimating = true;
Animation anim = AnimationUtils.loadAnimation(this, R.anim.overlay_slide_down);
findViewById(R.id.sharedialog).startAnimation(anim);
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
// Unused. I can haz Miranda method?
}
@Override
public void onAnimationEnd(Animation animation) {
// (bug 1132720) Hide the View so it doesn't flicker as the Activity closes.
ShareDialog.this.setVisible(false);
finish();
}
@Override
public void onAnimationRepeat(Animation animation) {
// Unused.
}
});
}
/**
* Close the dialog if back is pressed.
*/
@Override
public void onBackPressed() {
slideOut();
Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.SHARE_OVERLAY);
}
/**
* Close the dialog if the anything that isn't a button is tapped.
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
slideOut();
Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.SHARE_OVERLAY);
return true;
}
}