Merge "Slice background worker with Wi-Fi Slice"
This commit is contained in:
@@ -89,6 +89,16 @@ public interface CustomSliceable {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings Slices which can represent component lists that are updatable by the
|
||||||
|
* {@link SliceBackgroundWorker} returned here.
|
||||||
|
*
|
||||||
|
* @return a {@link SliceBackgroundWorker} for fetching the list of results in the background.
|
||||||
|
*/
|
||||||
|
default SliceBackgroundWorker getBackgroundWorker() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standardize the intents returned to indicate actions by the Slice.
|
* Standardize the intents returned to indicate actions by the Slice.
|
||||||
* <p>
|
* <p>
|
||||||
|
@@ -28,6 +28,7 @@ import android.os.StrictMode;
|
|||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.provider.SettingsSlicesContract;
|
import android.provider.SettingsSlicesContract;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.ArrayMap;
|
||||||
import android.util.ArraySet;
|
import android.util.ArraySet;
|
||||||
import android.util.KeyValueListParser;
|
import android.util.KeyValueListParser;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -132,6 +133,8 @@ public class SettingsSliceProvider extends SliceProvider {
|
|||||||
|
|
||||||
final Set<Uri> mRegisteredUris = new ArraySet<>();
|
final Set<Uri> mRegisteredUris = new ArraySet<>();
|
||||||
|
|
||||||
|
final Map<Uri, SliceBackgroundWorker> mWorkerMap = new ArrayMap<>();
|
||||||
|
|
||||||
public SettingsSliceProvider() {
|
public SettingsSliceProvider() {
|
||||||
super(READ_SEARCH_INDEXABLES);
|
super(READ_SEARCH_INDEXABLES);
|
||||||
}
|
}
|
||||||
@@ -166,6 +169,7 @@ public class SettingsSliceProvider extends SliceProvider {
|
|||||||
if (filter != null) {
|
if (filter != null) {
|
||||||
registerIntentToUri(filter, sliceUri);
|
registerIntentToUri(filter, sliceUri);
|
||||||
}
|
}
|
||||||
|
startBackgroundWorker(sliceable);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +198,7 @@ public class SettingsSliceProvider extends SliceProvider {
|
|||||||
SliceBroadcastRelay.unregisterReceivers(getContext(), sliceUri);
|
SliceBroadcastRelay.unregisterReceivers(getContext(), sliceUri);
|
||||||
mRegisteredUris.remove(sliceUri);
|
mRegisteredUris.remove(sliceUri);
|
||||||
}
|
}
|
||||||
|
stopBackgroundWorker(sliceUri);
|
||||||
mSliceDataCache.remove(sliceUri);
|
mSliceDataCache.remove(sliceUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,6 +358,31 @@ public class SettingsSliceProvider extends SliceProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void startBackgroundWorker(CustomSliceable sliceable) {
|
||||||
|
final SliceBackgroundWorker worker = sliceable.getBackgroundWorker();
|
||||||
|
if (worker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Uri uri = sliceable.getUri();
|
||||||
|
Log.d(TAG, "Starting background worker for: " + uri);
|
||||||
|
if (mWorkerMap.containsKey(uri)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mWorkerMap.put(uri, worker);
|
||||||
|
worker.onSlicePinned();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopBackgroundWorker(Uri uri) {
|
||||||
|
final SliceBackgroundWorker worker = mWorkerMap.get(uri);
|
||||||
|
if (worker != null) {
|
||||||
|
Log.d(TAG, "Stopping background worker for: " + uri);
|
||||||
|
worker.onSliceUnpinned();
|
||||||
|
mWorkerMap.remove(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<Uri> buildUrisFromKeys(List<String> keys, String authority) {
|
private List<Uri> buildUrisFromKeys(List<String> keys, String authority) {
|
||||||
final List<Uri> descendants = new ArrayList<>();
|
final List<Uri> descendants = new ArrayList<>();
|
||||||
|
|
||||||
|
85
src/com/android/settings/slices/SliceBackgroundWorker.java
Normal file
85
src/com/android/settings/slices/SliceBackgroundWorker.java
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.settings.slices;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Slice background worker is used to make Settings Slices be able to work with data that is
|
||||||
|
* changing continuously, e.g. available Wi-Fi networks.
|
||||||
|
*
|
||||||
|
* The background worker will be started at {@link SettingsSliceProvider#onSlicePinned(Uri)}, and be
|
||||||
|
* stopped at {@link SettingsSliceProvider#onSliceUnpinned(Uri)}.
|
||||||
|
*
|
||||||
|
* {@link SliceBackgroundWorker} caches the results, uses the cache to compare if there is any data
|
||||||
|
* changed, and then notifies the Slice {@link Uri} to update.
|
||||||
|
*/
|
||||||
|
public abstract class SliceBackgroundWorker<E> {
|
||||||
|
|
||||||
|
private final ContentResolver mContentResolver;
|
||||||
|
private final Uri mUri;
|
||||||
|
|
||||||
|
private List<E> mCachedResults;
|
||||||
|
|
||||||
|
protected SliceBackgroundWorker(ContentResolver cr, Uri uri) {
|
||||||
|
mContentResolver = cr;
|
||||||
|
mUri = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the Slice is pinned. This is the place to register callbacks or initialize scan
|
||||||
|
* tasks.
|
||||||
|
*/
|
||||||
|
protected abstract void onSlicePinned();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the Slice is unpinned. This is the place to unregister callbacks or perform any
|
||||||
|
* final cleanup.
|
||||||
|
*/
|
||||||
|
protected abstract void onSliceUnpinned();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return a {@link List} of cached results
|
||||||
|
*/
|
||||||
|
public final List<E> getResults() {
|
||||||
|
return mCachedResults == null ? null : new ArrayList<>(mCachedResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the results when data changes
|
||||||
|
*/
|
||||||
|
protected final void updateResults(List<E> results) {
|
||||||
|
boolean needNotify = false;
|
||||||
|
|
||||||
|
if (results == null) {
|
||||||
|
if (mCachedResults != null) {
|
||||||
|
needNotify = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
needNotify = !results.equals(mCachedResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needNotify) {
|
||||||
|
mCachedResults = results;
|
||||||
|
mContentResolver.notifyChange(mUri, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -37,12 +37,6 @@ public class WifiDialogActivity extends Activity implements WifiDialog.WifiDialo
|
|||||||
|
|
||||||
private static final String TAG = "WifiDialogActivity";
|
private static final String TAG = "WifiDialogActivity";
|
||||||
|
|
||||||
private static final int RESULT_CONNECTED = RESULT_FIRST_USER;
|
|
||||||
private static final int RESULT_FORGET = RESULT_FIRST_USER + 1;
|
|
||||||
|
|
||||||
private static final String KEY_ACCESS_POINT_STATE = "access_point_state";
|
|
||||||
private static final String KEY_WIFI_CONFIGURATION = "wifi_configuration";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boolean extra indicating whether this activity should connect to an access point on the
|
* Boolean extra indicating whether this activity should connect to an access point on the
|
||||||
* caller's behalf. If this is set to false, the caller should check
|
* caller's behalf. If this is set to false, the caller should check
|
||||||
@@ -51,6 +45,11 @@ public class WifiDialogActivity extends Activity implements WifiDialog.WifiDialo
|
|||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final String KEY_CONNECT_FOR_CALLER = "connect_for_caller";
|
static final String KEY_CONNECT_FOR_CALLER = "connect_for_caller";
|
||||||
|
static final String KEY_ACCESS_POINT_STATE = "access_point_state";
|
||||||
|
private static final String KEY_WIFI_CONFIGURATION = "wifi_configuration";
|
||||||
|
|
||||||
|
private static final int RESULT_CONNECTED = RESULT_FIRST_USER;
|
||||||
|
private static final int RESULT_FORGET = RESULT_FIRST_USER + 1;
|
||||||
|
|
||||||
private WifiDialog mDialog;
|
private WifiDialog mDialog;
|
||||||
|
|
||||||
|
@@ -29,6 +29,9 @@ import android.net.Uri;
|
|||||||
import android.net.wifi.WifiInfo;
|
import android.net.wifi.WifiInfo;
|
||||||
import android.net.wifi.WifiManager;
|
import android.net.wifi.WifiManager;
|
||||||
import android.net.wifi.WifiSsid;
|
import android.net.wifi.WifiSsid;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.provider.SettingsSlicesContract;
|
import android.provider.SettingsSlicesContract;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
@@ -42,8 +45,16 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
|||||||
import com.android.settings.R;
|
import com.android.settings.R;
|
||||||
import com.android.settings.SubSettings;
|
import com.android.settings.SubSettings;
|
||||||
import com.android.settings.Utils;
|
import com.android.settings.Utils;
|
||||||
|
import com.android.settings.core.SubSettingLauncher;
|
||||||
import com.android.settings.slices.CustomSliceable;
|
import com.android.settings.slices.CustomSliceable;
|
||||||
|
import com.android.settings.slices.SliceBackgroundWorker;
|
||||||
import com.android.settings.slices.SliceBuilderUtils;
|
import com.android.settings.slices.SliceBuilderUtils;
|
||||||
|
import com.android.settings.wifi.details.WifiNetworkDetailsFragment;
|
||||||
|
import com.android.settingslib.wifi.AccessPoint;
|
||||||
|
import com.android.settingslib.wifi.WifiTracker;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class to build a Wifi Slice, and handle all associated actions.
|
* Utility class to build a Wifi Slice, and handle all associated actions.
|
||||||
@@ -96,14 +107,73 @@ public class WifiSlice implements CustomSliceable {
|
|||||||
final SliceAction toggleSliceAction = new SliceAction(toggleAction, null /* actionTitle */,
|
final SliceAction toggleSliceAction = new SliceAction(toggleAction, null /* actionTitle */,
|
||||||
isWifiEnabled);
|
isWifiEnabled);
|
||||||
|
|
||||||
return new ListBuilder(mContext, WIFI_URI, ListBuilder.INFINITY)
|
final ListBuilder listBuilder = new ListBuilder(mContext, WIFI_URI, ListBuilder.INFINITY)
|
||||||
.setAccentColor(color)
|
.setAccentColor(color)
|
||||||
.addRow(new RowBuilder()
|
.addRow(new RowBuilder()
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setSubtitle(summary)
|
.setSubtitle(summary)
|
||||||
.addEndItem(toggleSliceAction)
|
.addEndItem(toggleSliceAction)
|
||||||
.setPrimaryAction(primarySliceAction))
|
.setPrimaryAction(primarySliceAction));
|
||||||
.build();
|
|
||||||
|
if (isWifiEnabled) {
|
||||||
|
final List<AccessPoint> result = getBackgroundWorker().getResults();
|
||||||
|
if (result != null && !result.isEmpty()) {
|
||||||
|
for (AccessPoint ap : result) {
|
||||||
|
listBuilder.addRow(getAccessPointRow(ap));
|
||||||
|
}
|
||||||
|
listBuilder.setSeeMoreAction(primaryAction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private RowBuilder getAccessPointRow(AccessPoint accessPoint) {
|
||||||
|
final String title = accessPoint.getConfigName();
|
||||||
|
final IconCompat levelIcon = IconCompat.createWithResource(mContext,
|
||||||
|
com.android.settingslib.Utils.getWifiIconResource(accessPoint.getLevel()));
|
||||||
|
final RowBuilder rowBuilder = new RowBuilder()
|
||||||
|
.setTitleItem(levelIcon, ListBuilder.ICON_IMAGE)
|
||||||
|
.setTitle(title)
|
||||||
|
.setSubtitle(accessPoint.getSettingsSummary())
|
||||||
|
.setPrimaryAction(new SliceAction(
|
||||||
|
getAccessPointAction(accessPoint), levelIcon, title));
|
||||||
|
|
||||||
|
final IconCompat endIcon = getEndIcon(accessPoint);
|
||||||
|
if (endIcon != null) {
|
||||||
|
rowBuilder.addEndItem(endIcon, ListBuilder.ICON_IMAGE);
|
||||||
|
}
|
||||||
|
return rowBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IconCompat getEndIcon(AccessPoint accessPoint) {
|
||||||
|
if (accessPoint.isActive()) {
|
||||||
|
return IconCompat.createWithResource(mContext, R.drawable.ic_settings);
|
||||||
|
} else if (accessPoint.getSecurity() != AccessPoint.SECURITY_NONE) {
|
||||||
|
return IconCompat.createWithResource(mContext, R.drawable.ic_friction_lock_closed);
|
||||||
|
} else if (accessPoint.isMetered()) {
|
||||||
|
return IconCompat.createWithResource(mContext, R.drawable.ic_friction_money);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PendingIntent getAccessPointAction(AccessPoint accessPoint) {
|
||||||
|
final Bundle extras = new Bundle();
|
||||||
|
accessPoint.saveWifiState(extras);
|
||||||
|
|
||||||
|
Intent intent;
|
||||||
|
if (accessPoint.isActive()) {
|
||||||
|
intent = new SubSettingLauncher(mContext)
|
||||||
|
.setTitleRes(R.string.pref_title_network_details)
|
||||||
|
.setDestination(WifiNetworkDetailsFragment.class.getName())
|
||||||
|
.setArguments(extras)
|
||||||
|
.setSourceMetricsCategory(MetricsEvent.WIFI)
|
||||||
|
.toIntent();
|
||||||
|
} else {
|
||||||
|
intent = new Intent(mContext, WifiDialogActivity.class);
|
||||||
|
intent.putExtra(WifiDialogActivity.KEY_ACCESS_POINT_STATE, extras);
|
||||||
|
}
|
||||||
|
return PendingIntent.getActivity(mContext, accessPoint.hashCode() /* requestCode */,
|
||||||
|
intent, 0 /* flags */);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,4 +246,75 @@ public class WifiSlice implements CustomSliceable {
|
|||||||
return PendingIntent.getActivity(mContext, 0 /* requestCode */,
|
return PendingIntent.getActivity(mContext, 0 /* requestCode */,
|
||||||
intent, 0 /* flags */);
|
intent, 0 /* flags */);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SliceBackgroundWorker getBackgroundWorker() {
|
||||||
|
return WifiScanWorker.getInstance(mContext, WIFI_URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class WifiScanWorker extends SliceBackgroundWorker<AccessPoint>
|
||||||
|
implements WifiTracker.WifiListener {
|
||||||
|
|
||||||
|
private static WifiScanWorker mWifiScanWorker;
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
|
private WifiTracker mWifiTracker;
|
||||||
|
private WifiManager mWifiManager;
|
||||||
|
|
||||||
|
private WifiScanWorker(Context context, Uri uri) {
|
||||||
|
super(context.getContentResolver(), uri);
|
||||||
|
mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WifiScanWorker getInstance(Context context, Uri uri) {
|
||||||
|
if (mWifiScanWorker == null) {
|
||||||
|
mWifiScanWorker = new WifiScanWorker(context, uri);
|
||||||
|
}
|
||||||
|
return mWifiScanWorker;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSlicePinned() {
|
||||||
|
new Handler(Looper.getMainLooper()).post(() -> {
|
||||||
|
mWifiTracker = new WifiTracker(mContext, this, true, true);
|
||||||
|
mWifiManager = mWifiTracker.getManager();
|
||||||
|
mWifiTracker.onStart();
|
||||||
|
onAccessPointsChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSliceUnpinned() {
|
||||||
|
mWifiTracker.onStop();
|
||||||
|
mWifiTracker.onDestroy();
|
||||||
|
mWifiScanWorker = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWifiStateChanged(int state) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnectedChanged() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAccessPointsChanged() {
|
||||||
|
// in case state has changed
|
||||||
|
if (!mWifiManager.isWifiEnabled()) {
|
||||||
|
updateResults(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// AccessPoints are sorted by the WifiTracker
|
||||||
|
final List<AccessPoint> accessPoints = mWifiTracker.getAccessPoints();
|
||||||
|
final List<AccessPoint> resultList = new ArrayList<>();
|
||||||
|
for (AccessPoint ap : accessPoints) {
|
||||||
|
if (ap.isReachable()) {
|
||||||
|
resultList.add(ap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateResults(resultList);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -118,7 +118,7 @@ public class SettingsSliceProviderTest {
|
|||||||
mProvider.mSliceWeakDataCache = new HashMap<>();
|
mProvider.mSliceWeakDataCache = new HashMap<>();
|
||||||
mProvider.mSliceDataCache = new HashMap<>();
|
mProvider.mSliceDataCache = new HashMap<>();
|
||||||
mProvider.mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(mContext);
|
mProvider.mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(mContext);
|
||||||
mProvider.mCustomSliceManager = new CustomSliceManager(mContext);
|
mProvider.mCustomSliceManager = spy(new CustomSliceManager(mContext));
|
||||||
when(mProvider.getContext()).thenReturn(mContext);
|
when(mProvider.getContext()).thenReturn(mContext);
|
||||||
|
|
||||||
mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase();
|
mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase();
|
||||||
@@ -481,6 +481,44 @@ public class SettingsSliceProviderTest {
|
|||||||
mProvider.onSlicePinned(uri);
|
mProvider.onSlicePinned(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SliceBackgroundWorker initBackgroundWorker(Uri uri) {
|
||||||
|
final SliceBackgroundWorker worker = spy(new SliceBackgroundWorker(
|
||||||
|
mContext.getContentResolver(), uri) {
|
||||||
|
@Override
|
||||||
|
public void onSlicePinned() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSliceUnpinned() {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final WifiSlice wifiSlice = spy(new WifiSlice(mContext));
|
||||||
|
when(wifiSlice.getBackgroundWorker()).thenReturn(worker);
|
||||||
|
when(mProvider.mCustomSliceManager.getSliceableFromUri(uri)).thenReturn(wifiSlice);
|
||||||
|
return worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onSlicePinned_backgroundWorker_started() {
|
||||||
|
final Uri uri = WifiSlice.WIFI_URI;
|
||||||
|
final SliceBackgroundWorker worker = initBackgroundWorker(uri);
|
||||||
|
|
||||||
|
mProvider.onSlicePinned(uri);
|
||||||
|
|
||||||
|
verify(worker).onSlicePinned();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onSlicePinned_backgroundWorker_stopped() {
|
||||||
|
final Uri uri = WifiSlice.WIFI_URI;
|
||||||
|
final SliceBackgroundWorker worker = initBackgroundWorker(uri);
|
||||||
|
|
||||||
|
mProvider.onSlicePinned(uri);
|
||||||
|
mProvider.onSliceUnpinned(uri);
|
||||||
|
|
||||||
|
verify(worker).onSliceUnpinned();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void grantWhitelistedPackagePermissions_noWhitelist_shouldNotGrant() {
|
public void grantWhitelistedPackagePermissions_noWhitelist_shouldNotGrant() {
|
||||||
final List<Uri> uris = new ArrayList<>();
|
final List<Uri> uris = new ArrayList<>();
|
||||||
|
Reference in New Issue
Block a user