Merge "Content provider for quick affordances." into tm-qpr-dev
This commit is contained in:
@@ -195,6 +195,9 @@
|
|||||||
<permission android:name="com.android.systemui.permission.FLAGS"
|
<permission android:name="com.android.systemui.permission.FLAGS"
|
||||||
android:protectionLevel="signature" />
|
android:protectionLevel="signature" />
|
||||||
|
|
||||||
|
<permission android:name="android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
|
||||||
|
android:protectionLevel="signature|privileged" />
|
||||||
|
|
||||||
<!-- Adding Quick Settings tiles -->
|
<!-- Adding Quick Settings tiles -->
|
||||||
<uses-permission android:name="android.permission.BIND_QUICK_SETTINGS_TILE" />
|
<uses-permission android:name="android.permission.BIND_QUICK_SETTINGS_TILE" />
|
||||||
|
|
||||||
@@ -976,5 +979,12 @@
|
|||||||
<action android:name="com.android.systemui.action.DISMISS_VOLUME_PANEL_DIALOG" />
|
<action android:name="com.android.systemui.action.DISMISS_VOLUME_PANEL_DIALOG" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:authorities="com.android.systemui.keyguard.quickaffordance"
|
||||||
|
android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
|
||||||
|
/>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -34,6 +34,11 @@
|
|||||||
android:enabled="false"
|
android:enabled="false"
|
||||||
tools:replace="android:authorities"
|
tools:replace="android:authorities"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
<provider android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider"
|
||||||
|
android:authorities="com.android.systemui.test.keyguard.quickaffordance.disabled"
|
||||||
|
android:enabled="false"
|
||||||
|
tools:replace="android:authorities"
|
||||||
|
tools:node="remove" />
|
||||||
<provider android:name="com.android.keyguard.clock.ClockOptionsProvider"
|
<provider android:name="com.android.keyguard.clock.ClockOptionsProvider"
|
||||||
android:authorities="com.android.systemui.test.keyguard.clock.disabled"
|
android:authorities="com.android.systemui.test.keyguard.clock.disabled"
|
||||||
android:enabled="false"
|
android:enabled="false"
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.systemui.shared.keyguard.data.content
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
/** Contract definitions for querying content about keyguard quick affordances. */
|
||||||
|
object KeyguardQuickAffordanceProviderContract {
|
||||||
|
|
||||||
|
const val AUTHORITY = "com.android.systemui.keyguard.quickaffordance"
|
||||||
|
const val PERMISSION = "android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
|
||||||
|
|
||||||
|
private val BASE_URI: Uri =
|
||||||
|
Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table for slots.
|
||||||
|
*
|
||||||
|
* Slots are positions where affordances can be placed on the lock screen. Affordances that are
|
||||||
|
* placed on slots are said to be "selected". The system supports the idea of multiple
|
||||||
|
* affordances per slot, though the implementation may limit the number of affordances on each
|
||||||
|
* slot.
|
||||||
|
*
|
||||||
|
* Supported operations:
|
||||||
|
* - Query - to know which slots are available, query the [SlotTable.URI] [Uri]. The result set
|
||||||
|
* will contain rows with the [SlotTable.Columns] columns.
|
||||||
|
*/
|
||||||
|
object SlotTable {
|
||||||
|
const val TABLE_NAME = "slots"
|
||||||
|
val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
|
||||||
|
|
||||||
|
object Columns {
|
||||||
|
/** String. Unique ID for this slot. */
|
||||||
|
const val ID = "id"
|
||||||
|
/** Integer. The maximum number of affordances that can be placed in the slot. */
|
||||||
|
const val CAPACITY = "capacity"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table for affordances.
|
||||||
|
*
|
||||||
|
* Affordances are actions/buttons that the user can execute. They are placed on slots on the
|
||||||
|
* lock screen.
|
||||||
|
*
|
||||||
|
* Supported operations:
|
||||||
|
* - Query - to know about all the affordances that are available on the device, regardless of
|
||||||
|
* which ones are currently selected, query the [AffordanceTable.URI] [Uri]. The result set will
|
||||||
|
* contain rows, each with the columns specified in [AffordanceTable.Columns].
|
||||||
|
*/
|
||||||
|
object AffordanceTable {
|
||||||
|
const val TABLE_NAME = "affordances"
|
||||||
|
val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
|
||||||
|
|
||||||
|
object Columns {
|
||||||
|
/** String. Unique ID for this affordance. */
|
||||||
|
const val ID = "id"
|
||||||
|
/** String. User-visible name for this affordance. */
|
||||||
|
const val NAME = "name"
|
||||||
|
/**
|
||||||
|
* Integer. Resource ID for the drawable to load for this affordance. This is a resource
|
||||||
|
* ID from the system UI package.
|
||||||
|
*/
|
||||||
|
const val ICON = "icon"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table for selections.
|
||||||
|
*
|
||||||
|
* Selections are pairs of slot and affordance IDs.
|
||||||
|
*
|
||||||
|
* Supported operations:
|
||||||
|
* - Insert - to insert an affordance and place it in a slot, insert values for the columns into
|
||||||
|
* the [SelectionTable.URI] [Uri]. The maximum capacity rule is enforced by the system.
|
||||||
|
* Selecting a new affordance for a slot that is already full will automatically remove the
|
||||||
|
* oldest affordance from the slot.
|
||||||
|
* - Query - to know which affordances are set on which slots, query the [SelectionTable.URI]
|
||||||
|
* [Uri]. The result set will contain rows, each of which with the columns from
|
||||||
|
* [SelectionTable.Columns].
|
||||||
|
* - Delete - to unselect an affordance, removing it from a slot, delete from the
|
||||||
|
* [SelectionTable.URI] [Uri], passing in values for each column.
|
||||||
|
*/
|
||||||
|
object SelectionTable {
|
||||||
|
const val TABLE_NAME = "selections"
|
||||||
|
val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
|
||||||
|
|
||||||
|
object Columns {
|
||||||
|
/** String. Unique ID for the slot. */
|
||||||
|
const val SLOT_ID = "slot_id"
|
||||||
|
/** String. Unique ID for the selected affordance. */
|
||||||
|
const val AFFORDANCE_ID = "affordance_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package com.android.systemui.dagger;
|
package com.android.systemui.dagger;
|
||||||
|
|
||||||
|
import com.android.systemui.keyguard.KeyguardQuickAffordanceProvider;
|
||||||
import com.android.systemui.statusbar.QsFrameTranslateModule;
|
import com.android.systemui.statusbar.QsFrameTranslateModule;
|
||||||
|
|
||||||
import dagger.Subcomponent;
|
import dagger.Subcomponent;
|
||||||
@@ -42,4 +43,9 @@ public interface ReferenceSysUIComponent extends SysUIComponent {
|
|||||||
interface Builder extends SysUIComponent.Builder {
|
interface Builder extends SysUIComponent.Builder {
|
||||||
ReferenceSysUIComponent build();
|
ReferenceSysUIComponent build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Member injection into the supplied argument.
|
||||||
|
*/
|
||||||
|
void inject(KeyguardQuickAffordanceProvider keyguardQuickAffordanceProvider);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.systemui.keyguard
|
||||||
|
|
||||||
|
import android.content.ContentProvider
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.UriMatcher
|
||||||
|
import android.content.pm.ProviderInfo
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.database.MatrixCursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import com.android.systemui.SystemUIAppComponentFactoryBase
|
||||||
|
import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback
|
||||||
|
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
|
||||||
|
import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
class KeyguardQuickAffordanceProvider :
|
||||||
|
ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer {
|
||||||
|
|
||||||
|
@Inject lateinit var interactor: KeyguardQuickAffordanceInteractor
|
||||||
|
|
||||||
|
private lateinit var contextAvailableCallback: ContextAvailableCallback
|
||||||
|
|
||||||
|
private val uriMatcher =
|
||||||
|
UriMatcher(UriMatcher.NO_MATCH).apply {
|
||||||
|
addURI(
|
||||||
|
Contract.AUTHORITY,
|
||||||
|
Contract.SlotTable.TABLE_NAME,
|
||||||
|
MATCH_CODE_ALL_SLOTS,
|
||||||
|
)
|
||||||
|
addURI(
|
||||||
|
Contract.AUTHORITY,
|
||||||
|
Contract.AffordanceTable.TABLE_NAME,
|
||||||
|
MATCH_CODE_ALL_AFFORDANCES,
|
||||||
|
)
|
||||||
|
addURI(
|
||||||
|
Contract.AUTHORITY,
|
||||||
|
Contract.SelectionTable.TABLE_NAME,
|
||||||
|
MATCH_CODE_ALL_SELECTIONS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachInfo(context: Context?, info: ProviderInfo?) {
|
||||||
|
contextAvailableCallback.onContextAvailable(checkNotNull(context))
|
||||||
|
super.attachInfo(context, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setContextAvailableCallback(callback: ContextAvailableCallback) {
|
||||||
|
contextAvailableCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getType(uri: Uri): String? {
|
||||||
|
val prefix =
|
||||||
|
when (uriMatcher.match(uri)) {
|
||||||
|
MATCH_CODE_ALL_SLOTS,
|
||||||
|
MATCH_CODE_ALL_AFFORDANCES,
|
||||||
|
MATCH_CODE_ALL_SELECTIONS -> "vnd.android.cursor.dir/vnd."
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val tableName =
|
||||||
|
when (uriMatcher.match(uri)) {
|
||||||
|
MATCH_CODE_ALL_SLOTS -> Contract.SlotTable.TABLE_NAME
|
||||||
|
MATCH_CODE_ALL_AFFORDANCES -> Contract.AffordanceTable.TABLE_NAME
|
||||||
|
MATCH_CODE_ALL_SELECTIONS -> Contract.SelectionTable.TABLE_NAME
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefix == null || tableName == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return "$prefix${Contract.AUTHORITY}.$tableName"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun insert(uri: Uri, values: ContentValues?): Uri? {
|
||||||
|
if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
return insertSelection(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun query(
|
||||||
|
uri: Uri,
|
||||||
|
projection: Array<out String>?,
|
||||||
|
selection: String?,
|
||||||
|
selectionArgs: Array<out String>?,
|
||||||
|
sortOrder: String?,
|
||||||
|
): Cursor? {
|
||||||
|
return when (uriMatcher.match(uri)) {
|
||||||
|
MATCH_CODE_ALL_AFFORDANCES -> queryAffordances()
|
||||||
|
MATCH_CODE_ALL_SLOTS -> querySlots()
|
||||||
|
MATCH_CODE_ALL_SELECTIONS -> querySelections()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(
|
||||||
|
uri: Uri,
|
||||||
|
values: ContentValues?,
|
||||||
|
selection: String?,
|
||||||
|
selectionArgs: Array<out String>?,
|
||||||
|
): Int {
|
||||||
|
Log.e(TAG, "Update is not supported!")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(
|
||||||
|
uri: Uri,
|
||||||
|
selection: String?,
|
||||||
|
selectionArgs: Array<out String>?,
|
||||||
|
): Int {
|
||||||
|
if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleteSelection(uri, selectionArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertSelection(values: ContentValues?): Uri? {
|
||||||
|
if (values == null) {
|
||||||
|
throw IllegalArgumentException("Cannot insert selection, no values passed in!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.containsKey(Contract.SelectionTable.Columns.SLOT_ID)) {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Cannot insert selection, " +
|
||||||
|
"\"${Contract.SelectionTable.Columns.SLOT_ID}\" not specified!"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.containsKey(Contract.SelectionTable.Columns.AFFORDANCE_ID)) {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Cannot insert selection, " +
|
||||||
|
"\"${Contract.SelectionTable.Columns.AFFORDANCE_ID}\" not specified!"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val slotId = values.getAsString(Contract.SelectionTable.Columns.SLOT_ID)
|
||||||
|
val affordanceId = values.getAsString(Contract.SelectionTable.Columns.AFFORDANCE_ID)
|
||||||
|
|
||||||
|
if (slotId.isNullOrEmpty()) {
|
||||||
|
throw IllegalArgumentException("Cannot insert selection, slot ID was empty!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (affordanceId.isNullOrEmpty()) {
|
||||||
|
throw IllegalArgumentException("Cannot insert selection, affordance ID was empty!")
|
||||||
|
}
|
||||||
|
|
||||||
|
val success = runBlocking {
|
||||||
|
interactor.select(
|
||||||
|
slotId = slotId,
|
||||||
|
affordanceId = affordanceId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (success) {
|
||||||
|
Log.d(TAG, "Successfully selected $affordanceId for slot $slotId")
|
||||||
|
context?.contentResolver?.notifyChange(Contract.SelectionTable.URI, null)
|
||||||
|
Contract.SelectionTable.URI
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Failed to select $affordanceId for slot $slotId")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun querySelections(): Cursor {
|
||||||
|
return MatrixCursor(
|
||||||
|
arrayOf(
|
||||||
|
Contract.SelectionTable.Columns.SLOT_ID,
|
||||||
|
Contract.SelectionTable.Columns.AFFORDANCE_ID,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.apply {
|
||||||
|
val affordanceIdsBySlotId = runBlocking { interactor.getSelections() }
|
||||||
|
affordanceIdsBySlotId.entries.forEach { (slotId, affordanceIds) ->
|
||||||
|
affordanceIds.forEach { affordanceId ->
|
||||||
|
addRow(
|
||||||
|
arrayOf(
|
||||||
|
slotId,
|
||||||
|
affordanceId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun queryAffordances(): Cursor {
|
||||||
|
return MatrixCursor(
|
||||||
|
arrayOf(
|
||||||
|
Contract.AffordanceTable.Columns.ID,
|
||||||
|
Contract.AffordanceTable.Columns.NAME,
|
||||||
|
Contract.AffordanceTable.Columns.ICON,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.apply {
|
||||||
|
interactor.getAffordancePickerRepresentations().forEach { representation ->
|
||||||
|
addRow(
|
||||||
|
arrayOf(
|
||||||
|
representation.id,
|
||||||
|
representation.name,
|
||||||
|
representation.iconResourceId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun querySlots(): Cursor {
|
||||||
|
return MatrixCursor(
|
||||||
|
arrayOf(
|
||||||
|
Contract.SlotTable.Columns.ID,
|
||||||
|
Contract.SlotTable.Columns.CAPACITY,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.apply {
|
||||||
|
interactor.getSlotPickerRepresentations().forEach { representation ->
|
||||||
|
addRow(
|
||||||
|
arrayOf(
|
||||||
|
representation.id,
|
||||||
|
representation.maxSelectedAffordances,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteSelection(
|
||||||
|
uri: Uri,
|
||||||
|
selectionArgs: Array<out String>?,
|
||||||
|
): Int {
|
||||||
|
if (selectionArgs == null) {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Cannot delete selection, selection arguments not included!"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val (slotId, affordanceId) =
|
||||||
|
when (selectionArgs.size) {
|
||||||
|
1 -> Pair(selectionArgs[0], null)
|
||||||
|
2 -> Pair(selectionArgs[0], selectionArgs[1])
|
||||||
|
else ->
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Cannot delete selection, selection arguments has wrong size, expected to" +
|
||||||
|
" have 1 or 2 arguments, had ${selectionArgs.size} instead!"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val deleted = runBlocking {
|
||||||
|
interactor.unselect(
|
||||||
|
slotId = slotId,
|
||||||
|
affordanceId = affordanceId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (deleted) {
|
||||||
|
Log.d(TAG, "Successfully unselected $affordanceId for slot $slotId")
|
||||||
|
context?.contentResolver?.notifyChange(uri, null)
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Failed to unselect $affordanceId for slot $slotId")
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "KeyguardQuickAffordanceProvider"
|
||||||
|
private const val MATCH_CODE_ALL_SLOTS = 1
|
||||||
|
private const val MATCH_CODE_ALL_AFFORDANCES = 2
|
||||||
|
private const val MATCH_CODE_ALL_SELECTIONS = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -140,6 +140,12 @@
|
|||||||
tools:replace="android:authorities"
|
tools:replace="android:authorities"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|
||||||
|
<provider android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider"
|
||||||
|
android:authorities="com.android.systemui.test.keyguard.quickaffordance.disabled"
|
||||||
|
android:enabled="false"
|
||||||
|
tools:replace="android:authorities"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="com.android.systemui.test.fileprovider"
|
android:authorities="com.android.systemui.test.fileprovider"
|
||||||
|
|||||||
Reference in New Issue
Block a user