Bug 1148431 - Create UI to inform users of Tab Queue and allow them to turn on or ignore (r=mcomella)

--HG--
rename : mobile/android/base/resources/drawable/firstrun_button_background.xml => mobile/android/base/resources/drawable/button_background_action_orange_round.xml
rename : mobile/android/base/resources/drawable/firstrun_button_enabled.xml => mobile/android/base/resources/drawable/button_enabled_action_orange_round.xml
rename : mobile/android/base/resources/drawable/firstrun_button_pressed.xml => mobile/android/base/resources/drawable/button_pressed_action_orange_round.xml
rename : mobile/android/base/resources/drawable/firstrun_button_background.xml => mobile/android/base/resources/drawable/tab_queue_dismiss_button_foreground.xml
This commit is contained in:
Martyn Haigh 2015-04-09 10:55:13 +01:00
Родитель 291cb18bf4
Коммит 7e66307d6e
22 изменённых файлов: 436 добавлений и 8 удалений

Просмотреть файл

@ -256,6 +256,10 @@
<service android:name="org.mozilla.gecko.tabqueue.TabQueueService" />
<activity android:name="org.mozilla.gecko.tabqueue.TabQueuePrompt"
android:launchMode="singleTop"
android:theme="@style/OverlayActivity" />
<activity android:name="org.mozilla.gecko.tabqueue.TabQueueDispatcher"
android:label="@MOZ_APP_DISPLAYNAME@"
android:launchMode="singleTask"
@ -428,7 +432,7 @@
reused. Ideally we create a new instance but Android L breaks this (bug 1137928). -->
<activity android:name="org.mozilla.gecko.overlays.ui.ShareDialog"
android:label="@string/overlay_share_label"
android:theme="@style/ShareOverlayActivity"
android:theme="@style/OverlayActivity"
android:configChanges="keyboard|keyboardHidden|mcc|mnc|locale|layoutDirection"
android:launchMode="singleTop"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize">

Просмотреть файл

@ -51,6 +51,7 @@ import org.mozilla.gecko.prompts.Prompt;
import org.mozilla.gecko.prompts.PromptListItem;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.tabqueue.TabQueueHelper;
import org.mozilla.gecko.tabqueue.TabQueuePrompt;
import org.mozilla.gecko.tabs.TabHistoryController;
import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
import org.mozilla.gecko.tabs.TabHistoryFragment;
@ -167,6 +168,7 @@ public class BrowserApp extends GeckoApp
// Request ID for startActivityForResult.
private static final int ACTIVITY_REQUEST_PREFERENCES = 1001;
private static final int ACTIVITY_REQUEST_TAB_QUEUE = 2001;
@RobocopTarget
public static final String EXTRA_SKIP_STARTPANE = "skipstartpane";
@ -714,6 +716,8 @@ public class BrowserApp extends GeckoApp
// Show the target URL immediately in the toolbar.
mBrowserToolbar.setTitle(intent.getDataString());
showTabQueuePromptIfApplicable(intent);
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT);
} else if (GuestSession.NOTIFICATION_INTENT.equals(action)) {
GuestSession.handleIntent(this, intent);
@ -2427,6 +2431,11 @@ public class BrowserApp extends GeckoApp
}
});
break;
case ACTIVITY_REQUEST_TAB_QUEUE:
TabQueueHelper.processTabQueuePromptResponse(resultCode, this);
break;
default:
super.onActivityResult(requestCode, resultCode, data);
}
@ -3418,6 +3427,8 @@ public class BrowserApp extends GeckoApp
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, method);
}
showTabQueuePromptIfApplicable(intent);
super.onNewIntent(intent);
if (AppConstants.MOZ_ANDROID_BEAM && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
@ -3468,6 +3479,22 @@ public class BrowserApp extends GeckoApp
}
}
private void showTabQueuePromptIfApplicable(final Intent intent) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
// We only want to show the prompt if the browser has been opened from an external url
if (AppConstants.NIGHTLY_BUILD && AppConstants.MOZ_ANDROID_TAB_QUEUE
&& mInitialized
&& Intent.ACTION_VIEW.equals(intent.getAction())
&& TabQueueHelper.shouldShowTabQueuePrompt(BrowserApp.this)) {
Intent promptIntent = new Intent(BrowserApp.this, TabQueuePrompt.class);
startActivityForResult(promptIntent, ACTIVITY_REQUEST_TAB_QUEUE);
}
}
});
}
@Override
protected NotificationClient makeNotificationClient() {
// The service is local to Fennec, so we can use it to keep

Просмотреть файл

@ -194,6 +194,11 @@
<!ENTITY tab_queue_toast_message "Open later">
<!ENTITY tab_queue_toast_action "Open now">
<!ENTITY tab_queue_prompt_title "Opening multiple links?">
<!ENTITY tab_queue_prompt_text "Open them without switching to Firefox each time.">
<!ENTITY tab_queue_prompt_tip_text "you can change this later in Settings">
<!ENTITY tab_queue_prompt_positive_action_button "Enable">
<!ENTITY tab_queue_prompt_negative_action_button "Not now">
<!-- Localization note (tab_queue_notification_text_plural) : The
formatD is replaced with the number of tabs queued. The
number of tabs queued is always more than one. We can't use

Просмотреть файл

@ -423,6 +423,7 @@ gbjar.sources += [
'Tab.java',
'tabqueue/TabQueueDispatcher.java',
'tabqueue/TabQueueHelper.java',
'tabqueue/TabQueuePrompt.java',
'tabqueue/TabQueueService.java',
'Tabs.java',
'tabs/PrivateTabsPanel.java',

Двоичные данные
mobile/android/base/resources/drawable-hdpi/img_check.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 954 B

Двоичные данные
mobile/android/base/resources/drawable-mdpi/img_check.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 675 B

Двоичные данные
mobile/android/base/resources/drawable-xhdpi/img_check.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.3 KiB

Двоичные данные
mobile/android/base/resources/drawable-xxhdpi/img_check.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.8 KiB

Просмотреть файл

@ -5,7 +5,7 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@drawable/firstrun_button_pressed" />
android:drawable="@drawable/button_pressed_action_orange_round" />
<item android:state_enabled="true"
android:drawable="@drawable/firstrun_button_enabled" />
android:drawable="@drawable/button_enabled_action_orange_round" />
</selector>

Просмотреть файл

@ -0,0 +1,14 @@
<?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/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:color="@color/tab_queue_dismiss_button_foreground_pressed" />
<item android:color="@color/tab_queue_dismiss_button_foreground"/>
</selector>

Просмотреть файл

@ -52,7 +52,7 @@
<Button android:id="@+id/welcome_account"
style="@style/Widget.Firstrun.Button"
android:background="@drawable/firstrun_button_background"
android:background="@drawable/button_background_action_orange_round"
android:layout_gravity="center"
android:text="@string/firstrun_welcome_button_account"/>

Просмотреть файл

@ -52,7 +52,7 @@
<Button android:id="@+id/welcome_account"
style="@style/Widget.Firstrun.Button"
android:background="@drawable/firstrun_button_background"
android:background="@drawable/button_background_action_orange_round"
android:layout_gravity="center"
android:text="@string/firstrun_welcome_button_account"/>

Просмотреть файл

@ -0,0 +1,120 @@
<?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/. -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tab_queue_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false">
<LinearLayout
android:layout_width="@dimen/tab_queue_container_width"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:background="@android:color/white"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="@dimen/tab_queue_content_width"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-light"
android:gravity="center_horizontal"
android:paddingTop="40dp"
android:text="@string/tab_queue_prompt_title"
android:textColor="@color/text_and_tabs_tray_grey"
android:textSize="20sp"
tools:text="Opening multiple links?" />
<TextView
android:id="@+id/text"
android:layout_width="@dimen/tab_queue_content_width"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:lineSpacingMultiplier="1.25"
android:paddingTop="20dp"
android:text="@string/tab_queue_prompt_text"
android:textColor="@color/placeholder_grey"
android:textSize="16sp"
tools:text="Open them without switching to Firefox each time." />
<TextView
android:id="@+id/tip_text"
android:layout_width="@dimen/tab_queue_content_width"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:paddingBottom="30dp"
android:paddingTop="20dp"
android:text="@string/tab_queue_prompt_tip_text"
android:textColor="@color/action_orange"
android:textSize="14sp"
android:textStyle="italic"
tools:text="you can change this later in Settings" />
<FrameLayout
android:id="@+id/bottom_container"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_gravity="center"
android:layout_marginBottom="40dp">
<ImageView
android:id="@+id/enabled_confirmation"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:src="@drawable/img_check"
android:visibility="gone" />
<LinearLayout
android:id="@+id/button_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="horizontal">
<TextView
android:id="@+id/cancel_button"
style="@style/Widget.BaseButton"
android:layout_width="@dimen/tab_queue_button_width"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@color/android:white"
android:text="@string/tab_queue_prompt_negative_action_button"
android:textColor="@drawable/tab_queue_dismiss_button_foreground"
android:textSize="16sp"
tools:text="Not now" />
<Button
android:id="@+id/ok_button"
style="@style/Widget.BaseButton"
android:layout_width="@dimen/tab_queue_button_width"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@drawable/button_background_action_orange_round"
android:text="@string/tab_queue_prompt_positive_action_button"
android:textColor="@android:color/white"
android:textSize="16sp"
tools:text="Enable" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
</FrameLayout>

Просмотреть файл

@ -18,4 +18,6 @@
<dimen name="reading_list_row_height">96dp</dimen>
<dimen name="reading_list_row_padding_right">15dp</dimen>
<dimen name="tab_queue_container_width">360dp</dimen>
</resources>

Просмотреть файл

@ -70,6 +70,11 @@
<color name="firstrun_tabstrip">#1193CB</color>
<color name="firstrun_pager_background">#16A3DF</color>
<!-- Tab Queue -->
<color name="tab_queue_dismiss_button_foreground">#16A3DF</color>
<color name="tab_queue_dismiss_button_foreground_pressed">#1193CB</color>
<color name="tab_queue_background">#16A3DF</color>
<!--
Application theme colors
-->

Просмотреть файл

@ -65,6 +65,10 @@
<dimen name="firstrun_content_width">300dp</dimen>
<dimen name="firstrun_min_height">180dp</dimen>
<dimen name="tab_queue_content_width">260dp</dimen>
<dimen name="tab_queue_button_width">148dp</dimen>
<dimen name="tab_queue_container_width">@dimen/match_parent</dimen>
<!-- Site security icon -->
<!-- If one of these values changes, they all should. -->
<dimen name="site_security_bottom_margin">.5dp</dimen>
@ -207,4 +211,8 @@
<dimen name="find_in_page_matchcase_padding">10dip</dimen>
<dimen name="find_in_page_control_margin_top">2dip</dimen>
<!-- http://blog.danlew.net/2015/01/06/handling-android-resources-with-non-standard-formats/ -->
<item name="match_parent" type="dimen">-1</item>
<item name="wrap_content" type="dimen">-2</item>
</resources>

Просмотреть файл

@ -106,8 +106,8 @@
<style name="GeckoStartPane" parent="GeckoBase"/>
<!-- Make the share overlay activity appear like an overlay. -->
<style name="ShareOverlayActivity">
<!-- Make an activity appear like an overlay. -->
<style name="OverlayActivity">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsTranslucent">true</item>

Просмотреть файл

@ -241,7 +241,11 @@
<string name="pref_tab_queue_title">&pref_tab_queue_title;</string>
<string name="pref_tab_queue_summary">&pref_tab_queue_summary;</string>
<string name="tab_queue_prompt_title">&tab_queue_prompt_title;</string>
<string name="tab_queue_prompt_text">&tab_queue_prompt_text;</string>
<string name="tab_queue_prompt_tip_text">&tab_queue_prompt_tip_text;</string>
<string name="tab_queue_prompt_positive_action_button">&tab_queue_prompt_positive_action_button;</string>
<string name="tab_queue_prompt_negative_action_button">&tab_queue_prompt_negative_action_button;</string>
<string name="tab_queue_toast_message">&tab_queue_toast_message;</string>
<string name="tab_queue_toast_action">&tab_queue_toast_action;</string>
<string name="tab_queue_notification_text_singular">&tab_queue_notification_text_singular;</string>

Просмотреть файл

@ -34,6 +34,54 @@ public class TabQueueHelper {
public static final int TAB_QUEUE_NOTIFICATION_ID = R.id.tabQueueNotification;
public static final String PREF_TAB_QUEUE_COUNT = "tab_queue_count";
public static final String PREF_TAB_QUEUE_LAUNCHES = "tab_queue_launches";
public static final String PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN = "tab_queue_times_prompt_shown";
public static final int MAX_TIMES_TO_SHOW_PROMPT = 3;
public static final int EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT = 3;
// result codes for returning from the prompt
public static final int TAB_QUEUE_YES = 201;
public static final int TAB_QUEUE_NO = 202;
/**
* Check if we should show the tab queue prompt
*
* @param context
* @return true if we should display the prompt, false if not.
*/
public static boolean shouldShowTabQueuePrompt(Context context) {
final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
boolean isTabQueueEnabled = prefs.getBoolean(GeckoPreferences.PREFS_TAB_QUEUE, false);
int numberOfTimesTabQueuePromptSeen = prefs.getInt(PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN, 0);
// Exit early if the feature is already enabled or the user has seen the
// prompt more than MAX_TIMES_TO_SHOW_PROMPT times.
if (isTabQueueEnabled || numberOfTimesTabQueuePromptSeen >= MAX_TIMES_TO_SHOW_PROMPT) {
return false;
}
final int viewActionIntentLaunches = prefs.getInt(PREF_TAB_QUEUE_LAUNCHES, 0) + 1;
if (viewActionIntentLaunches < EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT) {
// Allow a few external links to open before we prompt the user.
prefs.edit().putInt(PREF_TAB_QUEUE_LAUNCHES, viewActionIntentLaunches).apply();
} else if (viewActionIntentLaunches == EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT) {
// Reset to avoid repeatedly showing the prompt if the user doesn't interact with it and
// we get more external VIEW action intents in.
final SharedPreferences.Editor editor = prefs.edit();
editor.remove(TabQueueHelper.PREF_TAB_QUEUE_LAUNCHES);
int timesPromptShown = prefs.getInt(TabQueueHelper.PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN, 0) + 1;
editor.putInt(TabQueueHelper.PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN, timesPromptShown);
editor.apply();
// Show the prompt
return true;
}
return false;
}
/**
* Reads file and converts any content to JSON, adds passed in URL to the data and writes back to the file,
@ -147,4 +195,37 @@ public class TabQueueHelper {
final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
prefs.edit().remove(PREF_TAB_QUEUE_COUNT).apply();
}
public static void processTabQueuePromptResponse(int resultCode, Context context) {
final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
final SharedPreferences.Editor editor = prefs.edit();
switch (resultCode) {
case TAB_QUEUE_YES:
editor.putBoolean(GeckoPreferences.PREFS_TAB_QUEUE, true);
// By making this one more than EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT we ensure the prompt
// will never show again without having to keep track of an extra pref.
editor.putInt(TabQueueHelper.PREF_TAB_QUEUE_LAUNCHES,
TabQueueHelper.EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT + 1);
break;
case TAB_QUEUE_NO:
// The user clicked the 'no' button, so let's make sure the user never sees the prompt again by
// maxing out the pref used to count the VIEW action intents received and times they've seen the prompt.
editor.putInt(TabQueueHelper.PREF_TAB_QUEUE_LAUNCHES,
TabQueueHelper.EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT + 1);
editor.putInt(TabQueueHelper.PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN,
TabQueueHelper.MAX_TIMES_TO_SHOW_PROMPT + 1);
break;
default:
// We shouldn't ever get here.
Log.w(LOGTAG, "Unrecognized result code received from the tab queue prompt: " + resultCode);
}
editor.apply();
}
}

Просмотреть файл

@ -0,0 +1,157 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.tabqueue;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.R;
import org.mozilla.gecko.animation.TransitionsTracker;
import android.os.Bundle;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorListenerAdapter;
import com.nineoldandroids.animation.AnimatorSet;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.view.ViewHelper;
public class TabQueuePrompt extends Locales.LocaleAwareActivity {
public static final String LOGTAG = "Gecko" + TabQueuePrompt.class.getSimpleName();
// Flag set during animation to prevent animation multiple-start.
private boolean isAnimating;
private View containerView;
private View buttonContainer;
private View enabledConfirmation;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
showTabQueueEnablePrompt();
}
private void showTabQueueEnablePrompt() {
setContentView(R.layout.tab_queue_prompt);
findViewById(R.id.ok_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onConfirmButtonPressed();
}
});
findViewById(R.id.cancel_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setResult(TabQueueHelper.TAB_QUEUE_NO);
finish();
}
});
containerView = findViewById(R.id.tab_queue_container);
buttonContainer = findViewById(R.id.button_container);
enabledConfirmation = findViewById(R.id.enabled_confirmation);
ViewHelper.setTranslationY(containerView, 500);
ViewHelper.setAlpha(containerView, 0);
final Animator translateAnimator = ObjectAnimator.ofFloat(containerView, "translationY", 0);
translateAnimator.setDuration(400);
final Animator alphaAnimator = ObjectAnimator.ofFloat(containerView, "alpha", 1);
alphaAnimator.setStartDelay(200);
alphaAnimator.setDuration(600);
final AnimatorSet set = new AnimatorSet();
set.playTogether(alphaAnimator, translateAnimator);
set.setStartDelay(400);
TransitionsTracker.track(set);
set.start();
}
@Override
public void finish() {
super.finish();
// Don't perform an activity-dismiss animation.
overridePendingTransition(0, 0);
}
private void onConfirmButtonPressed() {
enabledConfirmation.setVisibility(View.VISIBLE);
ViewHelper.setAlpha(enabledConfirmation, 0);
final Animator buttonsAlphaAnimator = ObjectAnimator.ofFloat(buttonContainer, "alpha", 0);
buttonsAlphaAnimator.setDuration(300);
final Animator messagesAlphaAnimator = ObjectAnimator.ofFloat(enabledConfirmation, "alpha", 1);
messagesAlphaAnimator.setDuration(300);
messagesAlphaAnimator.setStartDelay(200);
final AnimatorSet set = new AnimatorSet();
set.playTogether(buttonsAlphaAnimator, messagesAlphaAnimator);
TransitionsTracker.track(set);
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
slideOut();
setResult(TabQueueHelper.TAB_QUEUE_YES);
}
}, 1000);
}
});
set.start();
}
/**
* Slide the overlay down off the screen and destroy it.
*/
private void slideOut() {
if (isAnimating) {
return;
}
isAnimating = true;
ObjectAnimator animator = ObjectAnimator.ofFloat(containerView, "translationY", containerView.getHeight());
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
finish();
}
});
animator.start();
}
/**
* 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;
}
}