Skip to content

Commit dddc07a

Browse files
authored
Feature/fire button visited sites fix (#332)
* Update AGP to latest beta * Update to support library 28 to match target API version Requires a few tweaks along the way, mainly around nullability of variables * Potential interim solution for clearing process on 🔥 button press * Add competing process-restart implementations * Remove FireActivity approach to restarting app process * Move fire process name to strings resources * Add Lottie to project * Add flame animation * Remove fire splash activity layer list * Fix BrowserActivity menu xml reference * Show toast upon re-entering app after fire animation * Configure FireSplashActivity to show Lottie fire animation * Rename FireSplashActivity to FireActivity * Subclass FireActivity from AppCompatActivity to avoid dagger binding * Re-center animation vertically * Update bitrise.yml with latest config using VDT * Update to latest AGP plugin * Ensure FireActivity doesn't re-spawn Browser if user navigates away * Code tidy * Tidy up manifest and strings file to remove redundant label * Code formatting * Code formatting * Remove unused styles and colours * Clear data pixel fired when app restarted; avoid being killed mid-flight * Rename method to `triggerRestart` * Code format * Removed redundant adapter class as one is already provided
1 parent 02270bd commit dddc07a

21 files changed

+441
-48
lines changed

app/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ android {
7777
}
7878

7979
ext {
80-
supportLibrary = "27.1.1"
80+
supportLibrary = "28.0.0-rc01"
8181
architectureComponents = "1.1.1"
8282
architectureComponentsExtensions = "1.1.1"
8383
androidKtx = "0.3"
@@ -91,6 +91,7 @@ ext {
9191

9292

9393
dependencies {
94+
implementation "com.android.support:support-v4:$supportLibrary"
9495
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
9596
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
9697

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (c) 2018 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.fire
18+
19+
import android.arch.core.executor.testing.InstantTaskExecutorRule
20+
import android.support.test.annotation.UiThreadTest
21+
import com.duckduckgo.app.InstantSchedulersRule
22+
import org.junit.Assert.assertFalse
23+
import org.junit.Assert.assertTrue
24+
import org.junit.Before
25+
import org.junit.Rule
26+
import org.junit.Test
27+
28+
29+
class FireViewModelTest {
30+
31+
@Rule
32+
@JvmField
33+
val schedulers = InstantSchedulersRule()
34+
35+
@Rule
36+
@JvmField
37+
var instantTaskExecutorRule = InstantTaskExecutorRule()
38+
39+
private lateinit var viewState: FireViewModel.ViewState
40+
private lateinit var testee: FireViewModel
41+
42+
@UiThreadTest
43+
@Before
44+
fun setup() {
45+
testee = FireViewModel()
46+
testee.viewState.observeForever { viewState = it!! }
47+
}
48+
49+
@Test
50+
fun whenViewModelInitialisedThenAutoStartEnabled() {
51+
assertTrue(viewState.autoStart)
52+
}
53+
54+
@Test
55+
fun whenViewModelInitialisedThenAnimationEnabled() {
56+
assertTrue(viewState.animate)
57+
}
58+
59+
@Test
60+
fun whenTimerReachedThenAnimationDisabled() {
61+
testee.startDeathClock()
62+
assertFalse(viewState.animate)
63+
}
64+
65+
@Test
66+
fun whenUserLeftActivityThenAutoStartDisabled() {
67+
testee.onViewStopped()
68+
assertFalse(viewState.autoStart)
69+
}
70+
71+
@Test
72+
fun whenUserLeftActivityThenReturnedThenAutoStartEnabled() {
73+
testee.onViewStopped()
74+
assertFalse(viewState.autoStart)
75+
76+
testee.onViewRestarted()
77+
assertTrue(viewState.autoStart)
78+
}
79+
}

app/src/main/AndroidManifest.xml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
android:supportsRtl="true"
1818
android:theme="@style/AppTheme"
1919
tools:ignore="GoogleAppIndexingWarning">
20-
2120
<meta-data
2221
android:name="android.webkit.WebView.MetricsOptOut"
2322
android:value="true" />
@@ -38,12 +37,11 @@
3837

3938
<category android:name="android.intent.category.LAUNCHER" />
4039
</intent-filter>
41-
4240
</activity>
4341
<activity
4442
android:name="com.duckduckgo.app.onboarding.ui.OnboardingActivity"
45-
android:theme="@style/AppTheme.TransparentTheme"
46-
android:label="@string/appName" />
43+
android:label="@string/appName"
44+
android:theme="@style/AppTheme.TransparentTheme" />
4745
<activity
4846
android:name=".BrowserActivity"
4947
android:configChanges="keyboardHidden|orientation|screenSize"
@@ -130,6 +128,11 @@
130128
<activity
131129
android:name=".defaultBrowsing.DefaultBrowserInfoActivity"
132130
android:theme="@style/ModalCardTheme" />
131+
132+
<activity
133+
android:name="com.duckduckgo.app.fire.FireActivity"
134+
android:process="@string/fireProcessName"
135+
android:theme="@style/SplashTheme" />
133136
</application>
134137

135138
</manifest>

app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ class BookmarksActivity : DuckDuckGoActivity() {
172172
}
173173
}
174174

175-
class BookmarksViewHolder(itemView: View?, private val viewModel: BookmarksViewModel) :
175+
class BookmarksViewHolder(itemView: View, private val viewModel: BookmarksViewModel) :
176176
ViewHolder(itemView) {
177177

178178
lateinit var bookmark: BookmarkEntity

app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import com.duckduckgo.app.global.view.FireDialog
3636
import com.duckduckgo.app.privacy.ui.PrivacyDashboardActivity
3737
import com.duckduckgo.app.settings.SettingsActivity
3838
import com.duckduckgo.app.statistics.pixels.Pixel
39+
import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.FORGET_ALL_EXECUTED
3940
import com.duckduckgo.app.tabs.model.TabEntity
4041
import com.duckduckgo.app.tabs.ui.TabSwitcherActivity
4142
import org.jetbrains.anko.longToast
@@ -79,10 +80,11 @@ class BrowserActivity : DuckDuckGoActivity() {
7980
private fun openNewTab(tabId: String, url: String? = null) {
8081
val fragment = BrowserTabFragment.newInstance(tabId, url)
8182
val transaction = supportFragmentManager.beginTransaction()
82-
if (currentTab == null) {
83+
val tab = currentTab
84+
if (tab == null) {
8385
transaction.replace(R.id.fragmentContainer, fragment, tabId)
8486
} else {
85-
transaction.hide(currentTab)
87+
transaction.hide(tab)
8688
transaction.add(R.id.fragmentContainer, fragment, tabId)
8789
}
8890
transaction.commit()
@@ -120,14 +122,20 @@ class BrowserActivity : DuckDuckGoActivity() {
120122
return
121123
}
122124

123-
if (intent.getBooleanExtra(PERFORM_FIRE_ON_ENTRY_EXTRA, false) ) {
125+
if (intent.getBooleanExtra(PERFORM_FIRE_ON_ENTRY_EXTRA, false)) {
124126
viewModel.onClearRequested()
125-
clearPersonalDataAction.clear { viewModel.onClearComplete() }
127+
clearPersonalDataAction.clear()
126128
Toast.makeText(applicationContext, R.string.fireDataCleared, Toast.LENGTH_LONG).show()
127129
finish()
128130
return
129131
}
130132

133+
if (intent.getBooleanExtra(LAUNCHED_FROM_FIRE_EXTRA, false)) {
134+
Timber.i("Launched from fire")
135+
Toast.makeText(applicationContext, R.string.fireDataCleared, Toast.LENGTH_LONG).show()
136+
pixel.fire(FORGET_ALL_EXECUTED)
137+
}
138+
131139
if (launchNewSearch(intent)) {
132140
viewModel.onNewTabRequested()
133141
return
@@ -232,15 +240,17 @@ class BrowserActivity : DuckDuckGoActivity() {
232240

233241
companion object {
234242

235-
fun intent(context: Context, queryExtra: String? = null, newSearch: Boolean = false): Intent {
243+
fun intent(context: Context, queryExtra: String? = null, newSearch: Boolean = false, launchedFromFireAction: Boolean = false): Intent {
236244
val intent = Intent(context, BrowserActivity::class.java)
237245
intent.putExtra(EXTRA_TEXT, queryExtra)
238246
intent.putExtra(NEW_SEARCH_EXTRA, newSearch)
247+
intent.putExtra(LAUNCHED_FROM_FIRE_EXTRA, launchedFromFireAction)
239248
return intent
240249
}
241250

242251
const val NEW_SEARCH_EXTRA = "NEW_SEARCH_EXTRA"
243252
const val PERFORM_FIRE_ON_ENTRY_EXTRA = "PERFORM_FIRE_ON_ENTRY_EXTRA"
253+
const val LAUNCHED_FROM_FIRE_EXTRA = "LAUNCHED_FROM_FIRE_EXTRA"
244254
private const val DASHBOARD_REQUEST_CODE = 100
245255
}
246256

app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -469,8 +469,8 @@ class BrowserTabFragment : Fragment(), FindListener {
469469
private fun configureToolbar() {
470470
toolbar.inflateMenu(R.menu.menu_browser_activity)
471471

472-
toolbar.setOnMenuItemClickListener {
473-
when (it.itemId) {
472+
toolbar.setOnMenuItemClickListener { menuItem ->
473+
when (menuItem.itemId) {
474474
R.id.fire -> {
475475
browserActivity?.launchFire()
476476
return@setOnMenuItemClickListener true
@@ -714,7 +714,7 @@ class BrowserTabFragment : Fragment(), FindListener {
714714
}
715715

716716
private fun resetTabState() {
717-
omnibarTextInput.text.clear()
717+
omnibarTextInput.text?.clear()
718718
viewModel.resetView()
719719
destroyWebView()
720720
configureWebView()

app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.duckduckgo.app.browser.BrowserActivity
2121
import com.duckduckgo.app.browser.BrowserTabFragment
2222
import com.duckduckgo.app.browser.defaultBrowsing.DefaultBrowserInfoActivity
2323
import com.duckduckgo.app.feedback.ui.FeedbackActivity
24+
import com.duckduckgo.app.fire.FireActivity
2425
import com.duckduckgo.app.job.AppConfigurationJobService
2526
import com.duckduckgo.app.launch.LaunchActivity
2627
import com.duckduckgo.app.onboarding.ui.OnboardingActivity
@@ -87,6 +88,10 @@ abstract class AndroidBindingModule {
8788
@ContributesAndroidInjector
8889
abstract fun defaultBrowserInfoActivity(): DefaultBrowserInfoActivity
8990

91+
@ActivityScoped
92+
@ContributesAndroidInjector
93+
abstract fun fireActivity(): FireActivity
94+
9095
/* Fragments */
9196

9297
@ContributesAndroidInjector
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright (c) 2018 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.fire
18+
19+
import android.animation.Animator
20+
import android.animation.AnimatorListenerAdapter
21+
import android.app.Activity
22+
import android.app.ActivityManager
23+
import android.arch.lifecycle.Observer
24+
import android.arch.lifecycle.ViewModelProviders
25+
import android.content.Context
26+
import android.content.Intent
27+
import android.os.Bundle
28+
import android.os.Process
29+
import android.support.v4.app.ActivityOptionsCompat
30+
import com.duckduckgo.app.browser.BrowserActivity
31+
import com.duckduckgo.app.browser.R
32+
import com.duckduckgo.app.global.DuckDuckGoActivity
33+
import com.duckduckgo.app.global.ViewModelFactory
34+
import kotlinx.android.synthetic.main.activity_fire.*
35+
import javax.inject.Inject
36+
37+
/**
38+
* Activity which is responsible for killing the main process and restarting it. This Activity will automatically finish itself after a brief time.
39+
*
40+
* This Activity runs in a separate process so that it can seamlessly restart the main app process without much in the way of a jarring UX.
41+
*
42+
* The correct way to invoke this Activity is through its `triggerRestart(context)` method.
43+
*
44+
* This Activity was largely inspired by https://github.com/JakeWharton/ProcessPhoenix
45+
*
46+
* We need to detect the user leaving this activity and possibly returning to it:
47+
* if the user left our app to do something else, restarting our browser activity would feel wrong
48+
* if the user left our app but came back, we should restart the browser activity
49+
*/
50+
class FireActivity : DuckDuckGoActivity() {
51+
52+
@Inject
53+
lateinit var viewModelFactory: ViewModelFactory
54+
55+
private val viewModel: FireViewModel by lazy {
56+
ViewModelProviders.of(this, viewModelFactory).get(FireViewModel::class.java)
57+
}
58+
59+
override fun onCreate(savedInstanceState: Bundle?) {
60+
super.onCreate(savedInstanceState)
61+
setContentView(R.layout.activity_fire)
62+
if (savedInstanceState == null) {
63+
64+
fireAnimationView.addAnimatorListener(object : AnimatorListenerAdapter() {
65+
override fun onAnimationStart(animator: Animator?) {
66+
viewModel.startDeathClock()
67+
}
68+
})
69+
}
70+
71+
viewModel.viewState.observe(this, Observer<FireViewModel.ViewState> {
72+
it?.let { viewState ->
73+
if (!viewState.animate) {
74+
75+
// restart the app only if the user hasn't navigated away during the fire animation
76+
if (viewState.autoStart) {
77+
val intent = intent.getParcelableExtra<Intent>(KEY_RESTART_INTENTS)
78+
startActivity(intent, activityFadeOptions(this))
79+
}
80+
81+
viewModel.viewState.removeObservers(this)
82+
finish()
83+
killProcess()
84+
}
85+
}
86+
})
87+
}
88+
89+
override fun onStop() {
90+
super.onStop()
91+
92+
if (!isChangingConfigurations) {
93+
viewModel.onViewStopped()
94+
}
95+
}
96+
97+
override fun onRestart() {
98+
super.onRestart()
99+
viewModel.onViewRestarted()
100+
}
101+
102+
override fun onBackPressed() {
103+
// do nothing - the activity will kill itself soon enough
104+
}
105+
106+
companion object {
107+
private const val KEY_RESTART_INTENTS = "KEY_RESTART_INTENTS"
108+
109+
fun triggerRestart(context: Context) {
110+
triggerRestart(context, getRestartIntent(context))
111+
}
112+
113+
private fun triggerRestart(context: Context, nextIntent: Intent) {
114+
val intent = Intent(context, FireActivity::class.java)
115+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
116+
intent.putExtra(KEY_RESTART_INTENTS, nextIntent)
117+
118+
context.startActivity(intent, activityFadeOptions(context))
119+
if (context is Activity) {
120+
context.finish()
121+
}
122+
killProcess()
123+
}
124+
125+
private fun getRestartIntent(context: Context): Intent {
126+
val intent = BrowserActivity.intent(context, launchedFromFireAction = true)
127+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
128+
return intent
129+
}
130+
131+
private fun killProcess() {
132+
Runtime.getRuntime().exit(0)
133+
}
134+
135+
fun appRestarting(context: Context): Boolean {
136+
val currentProcessId = Process.myPid()
137+
val activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
138+
activityManager.runningAppProcesses?.forEach {
139+
if (it.pid == currentProcessId && it.processName.endsWith(context.getString(R.string.fireProcessName))) {
140+
return true
141+
}
142+
}
143+
return false
144+
}
145+
146+
private fun activityFadeOptions(context: Context): Bundle? {
147+
val config = ActivityOptionsCompat.makeCustomAnimation(context, android.R.anim.fade_in, android.R.anim.fade_out)
148+
return config.toBundle()
149+
}
150+
}
151+
}

0 commit comments

Comments
 (0)