Java tutorial
/* -*- 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() { super.finish(); // 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 { slideOut(); } } 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; } }