Skip to content

Commit b67ad76

Browse files
RankoRthestinger
authored andcommitted
Improve low storage warning UX
1 parent 19b18a1 commit b67ad76

13 files changed

Lines changed: 409 additions & 403 deletions

AndroidManifest.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,6 @@
401401
android:exported="false"
402402
android:theme="@style/Theme.Compose.Dialog" />
403403

404-
<activity android:name=".ui.SmsStorageLowWarningActivity"
405-
android:theme="@style/Translucent"
406-
android:configChanges="orientation|screenSize|keyboardHidden" />
407-
408404
<receiver android:name=".receiver.StorageStatusReceiver"
409405
android:exported="true">
410406
<intent-filter>
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package com.android.messaging.datamodel
2+
3+
import android.content.ContentValues
4+
import android.content.Context
5+
import android.content.ContextWrapper
6+
import android.content.res.Resources
7+
import android.database.sqlite.SQLiteDatabase
8+
import android.database.sqlite.SQLiteFullException
9+
import android.database.sqlite.SQLiteStatement
10+
import com.android.messaging.FactoryTestAccess
11+
import com.android.messaging.R
12+
import com.android.messaging.sms.SmsStorageStatusManager
13+
import com.android.messaging.testutil.installTestFactory
14+
import com.android.messaging.util.DebugUtils
15+
import com.android.messaging.util.UiUtils
16+
import io.mockk.Runs
17+
import io.mockk.every
18+
import io.mockk.just
19+
import io.mockk.mockk
20+
import io.mockk.mockkStatic
21+
import io.mockk.unmockkStatic
22+
import io.mockk.verify
23+
import org.junit.After
24+
import org.junit.Assert.assertEquals
25+
import org.junit.Before
26+
import org.junit.Test
27+
import org.junit.runner.RunWith
28+
import org.robolectric.RobolectricTestRunner
29+
import org.robolectric.RuntimeEnvironment
30+
import org.robolectric.annotation.Config
31+
32+
@RunWith(RobolectricTestRunner::class)
33+
@Config(sdk = [36])
34+
internal class DatabaseWrapperStorageFullTest {
35+
36+
private lateinit var context: Context
37+
private lateinit var applicationContext: Context
38+
private lateinit var database: SQLiteDatabase
39+
private lateinit var databaseWrapper: DatabaseWrapper
40+
private lateinit var resources: Resources
41+
42+
@Before
43+
fun setUp() {
44+
context = RuntimeEnvironment.getApplication().applicationContext
45+
resources = mockk(relaxed = true)
46+
every { resources.getInteger(any()) } returns 0
47+
every { resources.getString(R.string.db_full) } returns "Database full"
48+
applicationContext = createResourceContext()
49+
installTestFactory(context = applicationContext)
50+
UiUtils.DEFAULT_INTERPOLATOR.hashCode()
51+
database = mockk(relaxed = true)
52+
databaseWrapper = DatabaseWrapper(context, database)
53+
mockkStatic(DebugUtils::class)
54+
mockkStatic(SmsStorageStatusManager::class)
55+
mockkStatic(UiUtils::class)
56+
every { DebugUtils.maybePlayDebugNoise(any(), any()) } just Runs
57+
every { SmsStorageStatusManager.handleStorageFull() } just Runs
58+
every { UiUtils.showToastAtBottom(R.string.db_full) } just Runs
59+
}
60+
61+
@After
62+
fun tearDown() {
63+
unmockkStatic(DebugUtils::class)
64+
unmockkStatic(SmsStorageStatusManager::class)
65+
unmockkStatic(UiUtils::class)
66+
FactoryTestAccess.reset()
67+
}
68+
69+
@Test
70+
fun endTransaction_whenDatabaseIsFull_requestsStorageWarningOnce() {
71+
every { database.endTransaction() } throws SQLiteFullException()
72+
databaseWrapper.beginTransaction()
73+
74+
databaseWrapper.endTransaction()
75+
76+
verifyStorageWarningRequested()
77+
}
78+
79+
@Test
80+
fun insertWithOnConflict_whenDatabaseIsFull_requestsStorageWarningOnce() {
81+
every {
82+
database.insertWithOnConflict(any(), any(), any(), any())
83+
} throws SQLiteFullException()
84+
85+
databaseWrapper.insertWithOnConflict(
86+
"table",
87+
null,
88+
ContentValues(),
89+
SQLiteDatabase.CONFLICT_NONE,
90+
)
91+
92+
verifyStorageWarningRequested()
93+
}
94+
95+
@Test
96+
fun update_whenDatabaseIsFull_returnsZeroAndRequestsStorageWarningOnce() {
97+
every { database.update(any(), any(), any(), any()) } throws SQLiteFullException()
98+
99+
val count = databaseWrapper.update("table", ContentValues(), null, null)
100+
101+
assertEquals(0, count)
102+
verifyStorageWarningRequested()
103+
}
104+
105+
@Test
106+
fun delete_whenDatabaseIsFull_returnsZeroAndRequestsStorageWarningOnce() {
107+
every { database.delete(any(), any(), any()) } throws SQLiteFullException()
108+
109+
val count = databaseWrapper.delete("table", null, null)
110+
111+
assertEquals(0, count)
112+
verifyStorageWarningRequested()
113+
}
114+
115+
@Test
116+
fun insert_whenDatabaseIsFull_returnsMinusOneAndRequestsStorageWarningOnce() {
117+
every { database.insert(any(), any(), any()) } throws SQLiteFullException()
118+
119+
val rowId = databaseWrapper.insert("table", null, ContentValues())
120+
121+
assertEquals(-1L, rowId)
122+
verifyStorageWarningRequested()
123+
}
124+
125+
@Test
126+
fun replace_whenDatabaseIsFull_returnsMinusOneAndRequestsStorageWarningOnce() {
127+
every { database.replace(any(), any(), any()) } throws SQLiteFullException()
128+
129+
val rowId = databaseWrapper.replace("table", null, ContentValues())
130+
131+
assertEquals(-1L, rowId)
132+
verifyStorageWarningRequested()
133+
}
134+
135+
@Test
136+
fun execSqlWithBindArgs_whenDatabaseIsFull_requestsStorageWarningOnce() {
137+
every { database.execSQL(any(), any()) } throws SQLiteFullException()
138+
139+
databaseWrapper.execSQL("DELETE FROM table", emptyArray())
140+
141+
verifyStorageWarningRequested()
142+
}
143+
144+
@Test
145+
fun execSql_whenDatabaseIsFull_requestsStorageWarningOnce() {
146+
every { database.execSQL(any<String>()) } throws SQLiteFullException()
147+
148+
databaseWrapper.execSQL("DELETE FROM table")
149+
150+
verifyStorageWarningRequested()
151+
}
152+
153+
@Test
154+
fun execSqlUpdateDelete_whenDatabaseIsFull_returnsZeroAndRequestsStorageWarningOnce() {
155+
val statement = mockk<SQLiteStatement>()
156+
every { database.compileStatement(any()) } returns statement
157+
every { statement.executeUpdateDelete() } throws SQLiteFullException()
158+
159+
val rowsUpdated = databaseWrapper.execSQLUpdateDelete("DELETE FROM table")
160+
161+
assertEquals(0, rowsUpdated)
162+
verifyStorageWarningRequested()
163+
}
164+
165+
private fun verifyStorageWarningRequested() {
166+
verify(exactly = 1) { SmsStorageStatusManager.handleStorageFull() }
167+
}
168+
169+
private fun createResourceContext(): Context {
170+
return object : ContextWrapper(context) {
171+
override fun getResources(): Resources {
172+
return this@DatabaseWrapperStorageFullTest.resources
173+
}
174+
}
175+
}
176+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package com.android.messaging.sms
2+
3+
import android.app.Notification
4+
import android.app.NotificationManager
5+
import android.content.Context
6+
import android.content.Intent
7+
import android.content.pm.ActivityInfo
8+
import android.content.pm.ApplicationInfo
9+
import android.content.pm.ResolveInfo
10+
import android.content.res.Resources
11+
import android.provider.Settings
12+
import com.android.messaging.Factory
13+
import com.android.messaging.FactoryTestAccess
14+
import com.android.messaging.R
15+
import com.android.messaging.testutil.installTestFactory
16+
import com.android.messaging.ui.UIIntentsImpl
17+
import com.android.messaging.util.PendingIntentConstants
18+
import com.android.messaging.util.PhoneUtils
19+
import io.mockk.every
20+
import io.mockk.mockk
21+
import org.junit.After
22+
import org.junit.Assert.assertEquals
23+
import org.junit.Assert.assertFalse
24+
import org.junit.Assert.assertNull
25+
import org.junit.Assert.assertTrue
26+
import org.junit.Before
27+
import org.junit.Test
28+
import org.junit.runner.RunWith
29+
import org.robolectric.RobolectricTestRunner
30+
import org.robolectric.RuntimeEnvironment
31+
import org.robolectric.Shadows.shadowOf
32+
import org.robolectric.annotation.Config
33+
import org.robolectric.annotation.Implementation
34+
import org.robolectric.annotation.Implements
35+
import org.robolectric.annotation.RealObject
36+
import org.robolectric.shadow.api.Shadow.directlyOn
37+
import org.robolectric.shadows.ShadowNotificationManager
38+
import org.robolectric.util.ReflectionHelpers.ClassParameter
39+
40+
private const val STORAGE_WARNING_TITLE = "Storage space running out"
41+
private const val STORAGE_WARNING_TEXT =
42+
"Messaging might not send or receive messages until more space is available on your device."
43+
private const val STORAGE_WARNING_TICKER = "Low SMS storage. You may need to delete messages."
44+
45+
@RunWith(RobolectricTestRunner::class)
46+
@Config(sdk = [36], shadows = [StorageWarningResourcesShadow::class])
47+
internal class SmsStorageStatusManagerTest {
48+
49+
private lateinit var context: Context
50+
private lateinit var phoneUtils: PhoneUtils
51+
52+
@Before
53+
fun setUp() {
54+
ShadowNotificationManager.reset()
55+
context = RuntimeEnvironment.getApplication().applicationContext
56+
phoneUtils = mockk()
57+
every { phoneUtils.isSmsEnabled() } returns true
58+
installTestFactory(
59+
context = context,
60+
phoneUtils = phoneUtils,
61+
)
62+
every { requireNotNull(Factory.get()).getUIIntents() } returns UIIntentsImpl()
63+
shadowOf(context.packageManager).setResolveInfosForIntent(
64+
Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS),
65+
listOf(
66+
ResolveInfo().apply {
67+
activityInfo = ActivityInfo().apply {
68+
applicationInfo = ApplicationInfo().apply {
69+
packageName = "com.android.settings"
70+
}
71+
packageName = "com.android.settings"
72+
name = "StorageSettingsActivity"
73+
}
74+
},
75+
),
76+
)
77+
}
78+
79+
@After
80+
fun tearDown() {
81+
ShadowNotificationManager.reset()
82+
FactoryTestAccess.reset()
83+
}
84+
85+
@Test
86+
fun handleStorageLow_postsDismissibleStorageSettingsNotification() {
87+
SmsStorageStatusManager.handleStorageLow()
88+
89+
assertStorageWarningNotification()
90+
}
91+
92+
@Test
93+
fun handleStorageFull_postsDismissibleStorageSettingsNotification() {
94+
SmsStorageStatusManager.handleStorageFull()
95+
96+
assertStorageWarningNotification()
97+
}
98+
99+
@Test
100+
fun handleStorageOk_cancelsStorageWarningNotification() {
101+
SmsStorageStatusManager.handleStorageLow()
102+
103+
SmsStorageStatusManager.handleStorageOk()
104+
105+
assertNull(storageNotification())
106+
}
107+
108+
@Test
109+
fun storageWarnings_doNotPostWhenMessagingIsNotTheDefaultSmsApp() {
110+
every { phoneUtils.isSmsEnabled() } returns false
111+
112+
SmsStorageStatusManager.handleStorageLow()
113+
SmsStorageStatusManager.handleStorageFull()
114+
115+
assertEquals(0, shadowOf(notificationManager()).size())
116+
}
117+
118+
@Test
119+
fun lowStorageNotification_usesGenericSettingsWhenStorageSettingsCannotBeResolved() {
120+
shadowOf(context.packageManager).setResolveInfosForIntent(
121+
Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS),
122+
emptyList(),
123+
)
124+
125+
val pendingIntent = UIIntentsImpl().getPendingIntentForLowStorageNotifications(context)
126+
127+
assertEquals(Settings.ACTION_SETTINGS, shadowOf(pendingIntent).savedIntent.action)
128+
}
129+
130+
private fun assertStorageWarningNotification() {
131+
val notification = requireNotNull(storageNotification())
132+
133+
assertEquals(
134+
STORAGE_WARNING_TITLE,
135+
notification.extras.getCharSequence(Notification.EXTRA_TITLE).toString(),
136+
)
137+
assertEquals(
138+
STORAGE_WARNING_TICKER,
139+
notification.tickerText.toString(),
140+
)
141+
assertEquals(
142+
STORAGE_WARNING_TEXT,
143+
notification.extras.getCharSequence(Notification.EXTRA_TEXT).toString(),
144+
)
145+
assertEquals(
146+
STORAGE_WARNING_TEXT,
147+
notification.extras.getCharSequence(Notification.EXTRA_BIG_TEXT).toString(),
148+
)
149+
assertEquals(R.drawable.ic_sms_light, notification.smallIcon.resId)
150+
assertFalse(notification.flags and Notification.FLAG_ONGOING_EVENT != 0)
151+
assertFalse(notification.flags and Notification.FLAG_AUTO_CANCEL != 0)
152+
153+
val contentIntent = requireNotNull(notification.contentIntent)
154+
val shadowPendingIntent = shadowOf(contentIntent)
155+
assertTrue(shadowPendingIntent.isActivity)
156+
assertTrue(shadowPendingIntent.isImmutable)
157+
assertEquals(
158+
Settings.ACTION_INTERNAL_STORAGE_SETTINGS,
159+
shadowPendingIntent.savedIntent.action,
160+
)
161+
}
162+
163+
private fun storageNotification(): Notification? {
164+
return shadowOf(notificationManager()).getNotification(
165+
"${context.packageName}:smsstoragelow",
166+
PendingIntentConstants.SMS_STORAGE_LOW_NOTIFICATION_ID,
167+
)
168+
}
169+
170+
private fun notificationManager(): NotificationManager {
171+
return requireNotNull(context.getSystemService(NotificationManager::class.java))
172+
}
173+
}
174+
175+
@Implements(Resources::class)
176+
internal class StorageWarningResourcesShadow {
177+
178+
@RealObject
179+
private lateinit var realResources: Resources
180+
181+
@Implementation
182+
fun getString(resourceId: Int): String {
183+
return when (resourceId) {
184+
R.string.sms_storage_low_title -> STORAGE_WARNING_TITLE
185+
R.string.sms_storage_low_text -> STORAGE_WARNING_TEXT
186+
R.string.sms_storage_low_notification_ticker -> STORAGE_WARNING_TICKER
187+
else -> directlyOn(
188+
realResources,
189+
Resources::class.java,
190+
"getString",
191+
ClassParameter.from(Int::class.javaPrimitiveType!!, resourceId),
192+
)
193+
}
194+
}
195+
}

0 commit comments

Comments
 (0)