Bug 1044947 - Frontend for share handler. r=rnewman
@ -372,6 +372,21 @@
|
||||
</provider>
|
||||
|
||||
#ifdef MOZ_ANDROID_SHARE_OVERLAY
|
||||
<!-- Share overlay activity -->
|
||||
<activity android:name="org.mozilla.gecko.overlays.ui.ShareDialog"
|
||||
android:label="@string/overlay_share_header"
|
||||
android:theme="@style/ShareOverlayActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|mcc|mnc|locale|layoutDirection"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<!-- Service to handle requests from overlays. -->
|
||||
<service android:name="org.mozilla.gecko.overlays.service.OverlayActionService" />
|
||||
#endif
|
||||
|
@ -90,6 +90,23 @@
|
||||
to display browser chrome. -->
|
||||
<!ENTITY locale_system_default "System default">
|
||||
|
||||
<!-- Localization note (overlay_share_bookmark_btn_label) : This string is
|
||||
used in the share overlay menu to select an action. It is the verb
|
||||
"to bookmark", not the noun "a bookmark". -->
|
||||
<!ENTITY overlay_share_bookmark_btn_label "Bookmark">
|
||||
<!ENTITY overlay_share_reading_list_btn_label "Add to Reading List">
|
||||
<!ENTITY overlay_share_header "Send to &brandShortName;">
|
||||
<!ENTITY overlay_share_send_other "Send to other devices">
|
||||
|
||||
<!-- Localization note (overlay_share_send_tab_btn_label) : Used on the
|
||||
share overlay menu to represent the "Send Tab" action when the user
|
||||
either has not set up Sync, or has no other devices to send a tab
|
||||
to. -->
|
||||
<!ENTITY overlay_share_send_tab_btn_label "Send to another device">
|
||||
<!ENTITY overlay_share_no_url "No link found in this share">
|
||||
<!ENTITY overlay_share_retry "Try again">
|
||||
<!ENTITY overlay_share_select_device "Select device">
|
||||
|
||||
<!ENTITY pref_category_search3 "Search">
|
||||
<!ENTITY pref_category_search_summary "Customize your search providers">
|
||||
<!ENTITY pref_category_display "Display">
|
||||
|
@ -488,7 +488,11 @@ if CONFIG['MOZ_ANDROID_SHARE_OVERLAY']:
|
||||
'overlays/service/sharemethods/ParcelableClientRecord.java',
|
||||
'overlays/service/sharemethods/SendTab.java',
|
||||
'overlays/service/sharemethods/ShareMethod.java',
|
||||
'overlays/ui/OverlayToastHelper.java'
|
||||
'overlays/ui/OverlayToastHelper.java',
|
||||
'overlays/ui/SendTabDeviceListArrayAdapter.java',
|
||||
'overlays/ui/SendTabList.java',
|
||||
'overlays/ui/SendTabTargetSelectedListener.java',
|
||||
'overlays/ui/ShareDialog.java',
|
||||
]
|
||||
|
||||
gbjar.sources += sync_java_files
|
||||
|
@ -48,7 +48,7 @@ public class OverlayConstants {
|
||||
// The optional extra Parcelable parameters for a ShareMethod.
|
||||
public static final String EXTRA_PARAMETERS = "EXTRA";
|
||||
|
||||
// The extra field key used for holding one or more share method names (See above).
|
||||
// The extra field key used for holding the ShareMethod.Type we wish to use for an operation.
|
||||
public static final String EXTRA_SHARE_METHOD = "SHARE_METHOD";
|
||||
|
||||
/*
|
||||
|
@ -29,21 +29,48 @@ public class OverlayToastHelper {
|
||||
* @param isTransient Should a retry button be presented?
|
||||
* @param retryListener Listener to fire when the retry button is pressed.
|
||||
*/
|
||||
public static void showFailureToast(Context context, String failureMessage, boolean isTransient, View.OnClickListener retryListener) {
|
||||
showToast(context, failureMessage, isTransient, retryListener);
|
||||
public static void showFailureToast(Context context, String failureMessage, View.OnClickListener retryListener) {
|
||||
showToast(context, failureMessage, false, retryListener);
|
||||
}
|
||||
public static void showFailureToast(Context context, String failureMessage, boolean isTransient) {
|
||||
showFailureToast(context, failureMessage, isTransient, null);
|
||||
public static void showFailureToast(Context context, String failureMessage) {
|
||||
showFailureToast(context, failureMessage, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a toast indicating a successful share.
|
||||
* @param successMesssage Message to show in the toast.
|
||||
* @param successMessage Message to show in the toast.
|
||||
*/
|
||||
public static void showSuccessToast(Context context, String successMesssage) {
|
||||
showToast(context, successMesssage, false, null);
|
||||
public static void showSuccessToast(Context context, String successMessage) {
|
||||
showToast(context, successMessage, true, null);
|
||||
}
|
||||
|
||||
private static void showToast(Context context, String message, boolean withRetry, View.OnClickListener retryListener) {
|
||||
private static void showToast(Context context, String message, boolean success, View.OnClickListener retryListener) {
|
||||
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
|
||||
View layout = inflater.inflate(R.layout.overlay_share_toast, null);
|
||||
|
||||
TextView text = (TextView) layout.findViewById(R.id.overlay_toast_message);
|
||||
text.setText(message);
|
||||
|
||||
if (retryListener == null) {
|
||||
// Hide the retry button.
|
||||
layout.findViewById(R.id.overlay_toast_separator).setVisibility(View.GONE);
|
||||
layout.findViewById(R.id.overlay_toast_retry_btn).setVisibility(View.GONE);
|
||||
} else {
|
||||
// Set up the button to perform a retry.
|
||||
Button retryBtn = (Button) layout.findViewById(R.id.overlay_toast_retry_btn);
|
||||
retryBtn.setOnClickListener(retryListener);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
// Hide the happy green tick.
|
||||
text.setCompoundDrawables(null, null, null, null);
|
||||
}
|
||||
|
||||
Toast toast = new Toast(context);
|
||||
toast.setGravity(Gravity.CENTER_VERTICAL | Gravity.BOTTOM, 0, 0);
|
||||
toast.setDuration(Toast.LENGTH_SHORT);
|
||||
toast.setView(layout);
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,178 @@
|
||||
/* 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 android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.Assert;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.overlays.service.sharemethods.ParcelableClientRecord;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.mozilla.gecko.overlays.ui.SendTabList.*;
|
||||
|
||||
public class SendTabDeviceListArrayAdapter extends ArrayAdapter<ParcelableClientRecord> {
|
||||
private static final String LOGTAG = "GeckoSendTabAdapter";
|
||||
|
||||
private State currentState;
|
||||
|
||||
// String to display when in a "button-like" special state. Instead of using a
|
||||
// ParcelableClientRecord we override the rendering using this string.
|
||||
private String dummyRecordName;
|
||||
|
||||
private final SendTabTargetSelectedListener listener;
|
||||
|
||||
private Collection<ParcelableClientRecord> records;
|
||||
|
||||
// The AlertDialog to show in the event the record is pressed while in the SHOW_DEVICES state.
|
||||
// This will show the user a prompt to select a device from a longer list of devices.
|
||||
private AlertDialog dialog;
|
||||
|
||||
public SendTabDeviceListArrayAdapter(Context context, SendTabTargetSelectedListener aListener, int textViewResourceId) {
|
||||
super(context, textViewResourceId);
|
||||
|
||||
listener = aListener;
|
||||
|
||||
// We do this manually and avoid multiple notifications when doing compound operations.
|
||||
setNotifyOnChange(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of the contents of this adapter were it in the LIST state.
|
||||
* Useful for determining the "real" contents of the adapter.
|
||||
*/
|
||||
public ParcelableClientRecord[] toArray() {
|
||||
return records.toArray(new ParcelableClientRecord[records.size()]);
|
||||
}
|
||||
|
||||
public void setClientRecordList(Collection<ParcelableClientRecord> clientRecordList) {
|
||||
records = clientRecordList;
|
||||
updateRecordList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the contents of the Adapter are synchronised with the `records` field. This may not
|
||||
* be the case if records has recently changed, or if we have experienced a state change.
|
||||
*/
|
||||
public void updateRecordList() {
|
||||
if (currentState != State.LIST) {
|
||||
return;
|
||||
}
|
||||
|
||||
clear();
|
||||
|
||||
setNotifyOnChange(false); // So we don't notify for each add.
|
||||
if (AppConstants.Versions.feature11Plus) {
|
||||
addAll(records);
|
||||
} else {
|
||||
for (ParcelableClientRecord record : records) {
|
||||
add(record);
|
||||
}
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(final int position, View convertView, ViewGroup parent) {
|
||||
final Context context = getContext();
|
||||
|
||||
// Reuse View objects if they exist.
|
||||
TextView row = (TextView) convertView;
|
||||
if (row == null) {
|
||||
row = (TextView) View.inflate(context, R.layout.overlay_share_send_tab_item, null);
|
||||
}
|
||||
|
||||
if (currentState != State.LIST) {
|
||||
// If we're in a special "Button-like" state, use the override string and a generic icon.
|
||||
row.setText(dummyRecordName);
|
||||
row.setCompoundDrawablesWithIntrinsicBounds(R.drawable.overlay_send_tab_icon, 0, 0, 0);
|
||||
}
|
||||
|
||||
// If we're just a button to launch the dialog, set the listener and abort.
|
||||
if (currentState == State.SHOW_DEVICES) {
|
||||
row.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
dialog.show();
|
||||
}
|
||||
});
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
// The remaining states delegate to the SentTabTargetSelectedListener.
|
||||
final String listenerGUID;
|
||||
|
||||
ParcelableClientRecord clientRecord = getItem(position);
|
||||
if (currentState == State.LIST) {
|
||||
row.setText(clientRecord.name);
|
||||
row.setCompoundDrawablesWithIntrinsicBounds(getImage(clientRecord), 0, 0, 0);
|
||||
|
||||
listenerGUID = clientRecord.guid;
|
||||
} else {
|
||||
listenerGUID = null;
|
||||
}
|
||||
|
||||
row.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
listener.onSendTabTargetSelected(listenerGUID);
|
||||
}
|
||||
});
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private static int getImage(ParcelableClientRecord record) {
|
||||
if ("mobile".equals(record.type)) {
|
||||
return R.drawable.sync_mobile;
|
||||
}
|
||||
|
||||
return R.drawable.sync_desktop;
|
||||
}
|
||||
|
||||
public void switchState(State newState) {
|
||||
if (currentState == newState) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentState = newState;
|
||||
|
||||
switch (newState) {
|
||||
case LIST:
|
||||
updateRecordList();
|
||||
break;
|
||||
case NONE:
|
||||
showDummyRecord(getContext().getResources().getString(R.string.overlay_share_send_tab_btn_label));
|
||||
break;
|
||||
case SHOW_DEVICES:
|
||||
showDummyRecord(getContext().getResources().getString(R.string.overlay_share_send_other));
|
||||
break;
|
||||
default:
|
||||
Assert.isTrue(false, "Unexpected state transition: " + newState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the dummy override string to the given value and clear the list.
|
||||
*/
|
||||
private void showDummyRecord(String name) {
|
||||
dummyRecordName = name;
|
||||
clear();
|
||||
add(null);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setDialog(AlertDialog aDialog) {
|
||||
dialog = aDialog;
|
||||
}
|
||||
}
|
170
mobile/android/base/overlays/ui/SendTabList.java
Normal file
@ -0,0 +1,170 @@
|
||||
/* 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 android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ListView;
|
||||
import org.mozilla.gecko.Assert;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.overlays.service.sharemethods.ParcelableClientRecord;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.mozilla.gecko.overlays.ui.SendTabList.State.LIST;
|
||||
import static org.mozilla.gecko.overlays.ui.SendTabList.State.LOADING;
|
||||
import static org.mozilla.gecko.overlays.ui.SendTabList.State.NONE;
|
||||
import static org.mozilla.gecko.overlays.ui.SendTabList.State.SHOW_DEVICES;
|
||||
|
||||
/**
|
||||
* The SendTab button has a few different states depending on the available devices (and whether
|
||||
* we've loaded them yet...)
|
||||
*
|
||||
* Initially, the view resembles a disabled button. (the LOADING state)
|
||||
* Once state is loaded from Sync's database, we know how many devices the user may send their tab
|
||||
* to.
|
||||
*
|
||||
* If there are no targets, the user was found to not have a Sync account, or their Sync account is
|
||||
* in a state that prevents it from being able to send a tab, we enter the NONE state and display
|
||||
* a generic button which launches an appropriate activity to fix the situation when tapped (such
|
||||
* as the set up Sync wizard).
|
||||
*
|
||||
* If the number of targets does not MAX_INLINE_SYNC_TARGETS, we present a button for each of them.
|
||||
* (the LIST state)
|
||||
*
|
||||
* Otherwise, we enter the SHOW_DEVICES state, in which we display a "Send to other devices" button
|
||||
* that takes the user to a menu for selecting a target device from their complete list of many
|
||||
* devices.
|
||||
*/
|
||||
public class SendTabList extends ListView {
|
||||
private static final String LOGTAG = "SendTabList";
|
||||
|
||||
// The maximum number of target devices to show in the main list. Further devices are available
|
||||
// from a secondary menu.
|
||||
public static final int MAXIMUM_INLINE_ELEMENTS = 2;
|
||||
|
||||
private SendTabDeviceListArrayAdapter clientListAdapter;
|
||||
|
||||
// Listener to fire when a share target is selected (either directly or via the prompt)
|
||||
private SendTabTargetSelectedListener listener;
|
||||
|
||||
private State currentState = LOADING;
|
||||
|
||||
/**
|
||||
* Enum defining the states this view may occupy.
|
||||
*/
|
||||
public enum State {
|
||||
// State when no sync targets exist (a generic "Send to Firefox Sync" button which launches
|
||||
// an activity to set it up)
|
||||
NONE,
|
||||
|
||||
// As NONE, but disabled. Initial state. Used until we get information from Sync about what
|
||||
// we really want.
|
||||
LOADING,
|
||||
|
||||
// A list of devices to share to.
|
||||
LIST,
|
||||
|
||||
// A single button prompting the user to select a device to share to.
|
||||
SHOW_DEVICES
|
||||
}
|
||||
|
||||
public SendTabList(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public SendTabList(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAdapter(ListAdapter adapter) {
|
||||
Assert.isTrue(adapter instanceof SendTabDeviceListArrayAdapter);
|
||||
|
||||
clientListAdapter = (SendTabDeviceListArrayAdapter) adapter;
|
||||
super.setAdapter(adapter);
|
||||
}
|
||||
|
||||
public void setSendTabTargetSelectedListener(SendTabTargetSelectedListener aListener) {
|
||||
listener = aListener;
|
||||
}
|
||||
|
||||
public void switchState(State state) {
|
||||
if (state == currentState) {
|
||||
return;
|
||||
}
|
||||
|
||||
clientListAdapter.switchState(state);
|
||||
if (state == SHOW_DEVICES) {
|
||||
clientListAdapter.setDialog(getDialog());
|
||||
}
|
||||
}
|
||||
|
||||
public void setSyncClients(ParcelableClientRecord[] clients) {
|
||||
if (clients == null) {
|
||||
clients = new ParcelableClientRecord[0];
|
||||
}
|
||||
|
||||
int size = clients.length;
|
||||
if (size == 0) {
|
||||
// Just show a button to set up sync (or whatever).
|
||||
switchState(NONE);
|
||||
return;
|
||||
}
|
||||
|
||||
clientListAdapter.setClientRecordList(Arrays.asList(clients));
|
||||
|
||||
if (size <= MAXIMUM_INLINE_ELEMENTS) {
|
||||
// Show the list of devices inline.
|
||||
switchState(LIST);
|
||||
return;
|
||||
}
|
||||
|
||||
// Just show a button to launch the list of devices to choose one from.
|
||||
switchState(SHOW_DEVICES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an AlertDialog listing all devices, allowing the user to select the one they want.
|
||||
* Used when more than MAXIMUM_INLINE_ELEMENTS devices are found (to avoid displaying them all
|
||||
* inline and looking crazy.
|
||||
*/
|
||||
public AlertDialog getDialog() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
||||
|
||||
final ParcelableClientRecord[] records = clientListAdapter.toArray();
|
||||
final String[] dialogElements = new String[records.length];
|
||||
|
||||
for (int i = 0; i < records.length; i++) {
|
||||
dialogElements[i] = records[i].name;
|
||||
}
|
||||
|
||||
builder.setTitle(R.string.overlay_share_select_device)
|
||||
.setItems(dialogElements, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int index) {
|
||||
listener.onSendTabTargetSelected(records[index].guid);
|
||||
}
|
||||
});
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent scrolling of this ListView.
|
||||
*/
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent ev) {
|
||||
if (ev.getAction() == MotionEvent.ACTION_MOVE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.dispatchTouchEvent(ev);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Interface for classes that wish to listen for the selection of an element from a SendTabList.
|
||||
*/
|
||||
public interface SendTabTargetSelectedListener {
|
||||
/**
|
||||
* Called when a row in the SendTabList is clicked.
|
||||
*
|
||||
* @param targetGUID The GUID of the ClientRecord the element represents (if any, otherwise null)
|
||||
*/
|
||||
public void onSendTabTargetSelected(String targetGUID);
|
||||
}
|
284
mobile/android/base/overlays/ui/ShareDialog.java
Normal file
@ -0,0 +1,284 @@
|
||||
/* -*- 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 android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
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;
|
||||
|
||||
import org.mozilla.gecko.Assert;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.overlays.OverlayConstants;
|
||||
import org.mozilla.gecko.overlays.service.OverlayActionService;
|
||||
import org.mozilla.gecko.overlays.service.sharemethods.ParcelableClientRecord;
|
||||
import org.mozilla.gecko.overlays.service.sharemethods.SendTab;
|
||||
import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod;
|
||||
import org.mozilla.gecko.LocaleAware;
|
||||
import org.mozilla.gecko.sync.setup.activities.WebURLFinder;
|
||||
|
||||
/**
|
||||
* A transparent activity that displays the share overlay.
|
||||
*/
|
||||
public class ShareDialog extends LocaleAware.LocaleAwareActivity implements SendTabTargetSelectedListener {
|
||||
private static final String LOGTAG = "GeckoShareDialog";
|
||||
|
||||
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);
|
||||
|
||||
SendTabList sendTabList = (SendTabList) findViewById(R.id.overlay_send_tab_btn);
|
||||
|
||||
ParcelableClientRecord[] clientrecords = (ParcelableClientRecord[]) intent.getParcelableArrayExtra(SendTab.EXTRA_CLIENT_RECORDS);
|
||||
sendTabList.setSyncClients(clientrecords);
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
getWindow().setWindowAnimations(0);
|
||||
|
||||
Intent intent = getIntent();
|
||||
|
||||
// The URL is usually hiding somewhere in the extra text. Extract it.
|
||||
String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
String pageUrl = new WebURLFinder(extraText).bestWebURL();
|
||||
|
||||
if (TextUtils.isEmpty(pageUrl)) {
|
||||
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();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setContentView(R.layout.overlay_share_dialog);
|
||||
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(uiEventListener,
|
||||
new IntentFilter(OverlayConstants.SHARE_METHOD_UI_EVENT));
|
||||
|
||||
// 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);
|
||||
|
||||
// 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.
|
||||
String subjectText = intent.getStringExtra(Intent.EXTRA_SUBJECT);
|
||||
if (subjectText != null) {
|
||||
((TextView) findViewById(R.id.title)).setText(subjectText);
|
||||
}
|
||||
|
||||
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).
|
||||
TextView subtitleView = (TextView) findViewById(R.id.subtitle);
|
||||
subtitleView.setText(pageUrl);
|
||||
subtitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
|
||||
subtitleView.setSingleLine(true);
|
||||
subtitleView.setMarqueeRepeatLimit(5);
|
||||
subtitleView.setSelected(true);
|
||||
|
||||
// Start the slide-up animation.
|
||||
Animation anim = AnimationUtils.loadAnimation(this, R.anim.overlay_slide_up);
|
||||
findViewById(R.id.sharedialog).startAnimation(anim);
|
||||
|
||||
// Add button event listeners.
|
||||
|
||||
findViewById(R.id.overlay_share_bookmark_btn).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
addBookmark();
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.overlay_share_reading_list_btn).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
addToReadingList();
|
||||
}
|
||||
});
|
||||
|
||||
// Send tab.
|
||||
SendTabList sendTabList = (SendTabList) findViewById(R.id.overlay_send_tab_btn);
|
||||
|
||||
// Register ourselves as both the listener and the context for the Adapter.
|
||||
SendTabDeviceListArrayAdapter adapter = new SendTabDeviceListArrayAdapter(this, this, R.layout.sync_list_item);
|
||||
sendTabList.setAdapter(adapter);
|
||||
sendTabList.setSendTabTargetSelectedListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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").
|
||||
*/
|
||||
|
||||
public void sendTab(String targetGUID) {
|
||||
// If an override intent has been set, dispatch it.
|
||||
if (sendTabOverrideIntent != null) {
|
||||
startActivity(sendTabOverrideIntent);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendTabTargetSelected(String targetGUID) {
|
||||
sendTab(targetGUID);
|
||||
}
|
||||
|
||||
public void addToReadingList() {
|
||||
startService(getServiceIntent(ShareMethod.Type.ADD_TO_READING_LIST));
|
||||
slideOut();
|
||||
}
|
||||
|
||||
public void addBookmark() {
|
||||
startService(getServiceIntent(ShareMethod.Type.ADD_BOOKMARK));
|
||||
slideOut();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {
|
||||
// Unused.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the dialog if back is pressed.
|
||||
*/
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
slideOut();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the dialog if the anything that isn't a button is tapped.
|
||||
*/
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
slideOut();
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_longAnimTime"
|
||||
android:fromYDelta="0"
|
||||
android:toYDelta="100%p" />
|
9
mobile/android/base/resources/anim/overlay_slide_up.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_longAnimTime"
|
||||
android:fromYDelta="100%p"
|
||||
android:toYDelta="0" />
|
After Width: | Height: | Size: 652 B |
BIN
mobile/android/base/resources/drawable-hdpi/overlay_check.png
Normal file
After Width: | Height: | Size: 391 B |
After Width: | Height: | Size: 340 B |
After Width: | Height: | Size: 325 B |
After Width: | Height: | Size: 477 B |
BIN
mobile/android/base/resources/drawable-mdpi/overlay_check.png
Normal file
After Width: | Height: | Size: 308 B |
After Width: | Height: | Size: 336 B |
After Width: | Height: | Size: 307 B |
After Width: | Height: | Size: 755 B |
BIN
mobile/android/base/resources/drawable-xhdpi/overlay_check.png
Normal file
After Width: | Height: | Size: 422 B |
After Width: | Height: | Size: 410 B |
After Width: | Height: | Size: 410 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 951 B |
BIN
mobile/android/base/resources/drawable-xxhdpi/overlay_check.png
Normal file
After Width: | Height: | Size: 509 B |
After Width: | Height: | Size: 451 B |
After Width: | Height: | Size: 460 B |
117
mobile/android/base/resources/layout/overlay_share_dialog.xml
Normal file
@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- 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/. -->
|
||||
|
||||
<!-- Serves to position the content on the screen (bottom, centered) and provide the drop-shadow -->
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/sharedialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="15dp"
|
||||
android:layout_marginRight="15dp"
|
||||
android:layout_marginBottom="-12dp"
|
||||
android:paddingTop="30dp"
|
||||
android:layout_gravity="bottom|center"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/share_overlay_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/share_overlay_background">
|
||||
|
||||
<!-- Header -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="0dp"
|
||||
android:background="@color/background_light"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="15dp"
|
||||
android:paddingLeft="15dp"
|
||||
android:paddingRight="15dp"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<!-- Title -->
|
||||
<TextView
|
||||
android:id="@id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="7dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:scrollHorizontally="true"
|
||||
android:textColor="@color/text_color_primary"
|
||||
android:textSize="17sp"/>
|
||||
|
||||
<!-- Subtitle (url) -->
|
||||
<TextView
|
||||
android:id="@+id/subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text_color_secondary"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Buttons -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/overlay_share_background_colour"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- TODO: Once API 11 is available, stick "showDividers=middle" into the parent and get rid
|
||||
of these evil separator views. -->
|
||||
|
||||
<!-- "Send to Firefox Sync" -->
|
||||
<org.mozilla.gecko.overlays.ui.SendTabList
|
||||
style="@style/ShareOverlayButton"
|
||||
android:id="@+id/overlay_send_tab_btn"
|
||||
android:background="@color/overlay_share_background_colour"
|
||||
android:padding="0dp"/>
|
||||
|
||||
<!-- Evil separator -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/background_light"/>
|
||||
|
||||
<!-- "Add to reading list" -->
|
||||
<TextView
|
||||
style="@style/ShareOverlayButton.Text"
|
||||
android:id="@+id/overlay_share_reading_list_btn"
|
||||
android:text="@string/overlay_share_reading_list_btn_label"
|
||||
android:drawableLeft="@drawable/overlay_readinglist_icon"/>
|
||||
|
||||
<!-- Evil separator -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/background_light"/>
|
||||
|
||||
<!-- "Add bookmark" -->
|
||||
<TextView
|
||||
style="@style/ShareOverlayButton.Text"
|
||||
android:id="@+id/overlay_share_bookmark_btn"
|
||||
android:text="@string/overlay_share_bookmark_btn_label"
|
||||
android:drawableLeft="@drawable/overlay_bookmark_icon"/>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Firefox logo (has to appear higher in the z-order than the content. -->
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/share_overlay_content"
|
||||
android:scaleType="center"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:src="@drawable/icon"
|
||||
android:layout_marginBottom="-6dp"/>
|
||||
</RelativeLayout>
|
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- 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/. -->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ListView
|
||||
style="@style/ShareOverlayButton"
|
||||
android:id="@+id/device_list"
|
||||
android:padding="0dp" >
|
||||
</ListView>
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="@style/ShareOverlayButton.Text"/>
|
58
mobile/android/base/resources/layout/overlay_share_toast.xml
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- 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/. -->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/overlay_share_toast"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/share_overlay_background"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:layout_gravity="bottom|center">
|
||||
|
||||
<!-- Header -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="0dp"
|
||||
android:background="@color/background_light"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="5dp"
|
||||
android:paddingRight="10dp"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp">
|
||||
|
||||
<!-- Large attractive green tick with label to the right -->
|
||||
<TextView
|
||||
style="@style/ShareOverlayButton.Text"
|
||||
android:id="@+id/overlay_toast_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/text_color_primary"
|
||||
android:textSize="14sp"
|
||||
android:drawableLeft="@drawable/overlay_check"/>
|
||||
|
||||
<!-- Evil separator -->
|
||||
<View
|
||||
android:id="@+id/overlay_toast_separator"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="1dp"
|
||||
android:background="@color/background_light"/>
|
||||
|
||||
<!-- Retry button -->
|
||||
<Button
|
||||
android:id="@+id/overlay_toast_retry_btn"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="@string/overlay_share_retry"
|
||||
android:onClick="selfDestruct" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
@ -48,6 +48,12 @@
|
||||
<color name="text_color_secondary_inverse">#DDDDDD</color>
|
||||
<color name="text_color_tertiary_inverse">#A4A7A9</color>
|
||||
|
||||
<!-- Colour used for share overlay button labels -->
|
||||
<color name="text_color_overlaybtn">#666666</color>
|
||||
|
||||
<!-- Colour used for share overlay button background -->
|
||||
<color name="overlay_share_background_colour">#FFD0CECB</color>
|
||||
|
||||
<!-- Disabled colors -->
|
||||
<color name="text_color_primary_disable_only">#999999</color>
|
||||
|
||||
|
@ -763,6 +763,29 @@
|
||||
<item name="android:gravity">right</item>
|
||||
</style>
|
||||
|
||||
<style name="ShareOverlayButton">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:minHeight">60dp</item>
|
||||
<item name="android:gravity">center_vertical</item>
|
||||
<item name="android:paddingLeft">15dp</item>
|
||||
<item name="android:paddingRight">15dp</item>
|
||||
<item name="android:paddingTop">17dp</item>
|
||||
<item name="android:paddingBottom">17dp</item>
|
||||
<item name="android:focusableInTouchMode">false</item>
|
||||
<item name="android:clickable">true</item>
|
||||
<item name="android:background">@android:drawable/list_selector_background</item>
|
||||
<item name="android:layout_margin">0dp</item>
|
||||
</style>
|
||||
|
||||
<style name="ShareOverlayButton.Text">
|
||||
<item name="android:drawablePadding">15dp</item>
|
||||
<item name="android:maxLines">1</item>
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="android:textColor">@color/text_color_overlaybtn</item>
|
||||
<item name="android:ellipsize">marquee</item>
|
||||
</style>
|
||||
|
||||
<style name="TabInput"></style>
|
||||
|
||||
<style name="TabInput.TabWidget">
|
||||
@ -780,6 +803,13 @@
|
||||
<item name="android:paddingTop">0dp</item>
|
||||
</style>
|
||||
|
||||
<!-- Make the share overlay activity appear like an overlay. -->
|
||||
<style name="ShareOverlayActivity">
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:backgroundDimEnabled">true</item>
|
||||
</style>
|
||||
<style name="OnboardStartLayout">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">match_parent</item>
|
||||
|
@ -110,6 +110,15 @@
|
||||
<string name="media_pause">&media_pause;</string>
|
||||
<string name="media_stop">&media_stop;</string>
|
||||
|
||||
<string name="overlay_share_send_other">&overlay_share_send_other;</string>
|
||||
<string name="overlay_share_header">&overlay_share_header;</string>
|
||||
<string name="overlay_share_bookmark_btn_label">&overlay_share_bookmark_btn_label;</string>
|
||||
<string name="overlay_share_reading_list_btn_label">&overlay_share_reading_list_btn_label;</string>
|
||||
<string name="overlay_share_send_tab_btn_label">&overlay_share_send_tab_btn_label;</string>
|
||||
<string name="overlay_share_no_url">&overlay_share_no_url;</string>
|
||||
<string name="overlay_share_retry">&overlay_share_retry;</string>
|
||||
<string name="overlay_share_select_device">&overlay_share_select_device;</string>
|
||||
|
||||
<string name="settings">&settings;</string>
|
||||
<string name="settings_title">&settings_title;</string>
|
||||
<string name="pref_category_advanced">&pref_category_advanced;</string>
|
||||
|