Creates People ContentProvider

Change-Id: I00a901a8fb713b4ce1989bb76f054a47f60892f1
Test: PeopleProviderTest
Bug: 179988318
This commit is contained in:
Flavio Fiszman
2021-02-07 15:26:51 +00:00
parent 946dfb5e81
commit 6f24a63ba1
7 changed files with 463 additions and 6 deletions

View File

@@ -5477,6 +5477,9 @@
<permission android:name="android.permission.SIGNAL_REBOOT_READINESS"
android:protectionLevel="signature|privileged" />
<!-- @hide Allows an application to get a People Tile preview for a given shortcut. -->
<permission android:name="android.permission.GET_PEOPLE_TILE_PREVIEW"
android:protectionLevel="signature|recents" />
<!-- Attribution for Geofencing service. -->
<attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>

View File

@@ -600,6 +600,14 @@
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="false" />
<!-- ContentProvider that returns a People Tile preview for a given shortcut -->
<provider
android:name="com.android.systemui.people.PeopleProvider"
android:authorities="com.android.systemui.people.PeopleProvider"
android:exported="true"
android:permission="android.permission.GET_PEOPLE_TILE_PREVIEW">
</provider>
<!-- a gallery of delicious treats -->
<service
android:name=".DessertCaseDream"

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2021 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.system;
/**
* These strings are part of the {@link com.android.systemui.people.PeopleProvider} API
* contract. The API returns a People Tile preview that can be displayed by calling packages.
* The provider is part of the SystemUI service, and the strings live here for shared access with
* Launcher (caller).
*/
public class PeopleProviderUtils {
/**
* ContentProvider URI scheme.
* @hide
*/
public static final String PEOPLE_PROVIDER_SCHEME = "content://";
/**
* ContentProvider URI authority.
* @hide
*/
public static final String PEOPLE_PROVIDER_AUTHORITY =
"com.android.systemui.people.PeopleProvider";
/**
* Method name for getting People Tile preview.
* @hide
*/
public static final String GET_PEOPLE_TILE_PREVIEW_METHOD = "get_people_tile_preview";
/**
* Extras bundle key specifying shortcut Id of the People Tile preview requested.
* @hide
*/
public static final String EXTRAS_KEY_SHORTCUT_ID = "shortcut_id";
/**
* Extras bundle key specifying package name of the People Tile preview requested.
* @hide
*/
public static final String EXTRAS_KEY_PACKAGE_NAME = "package_name";
/**
* Extras bundle key specifying {@code UserHandle} of the People Tile preview requested.
* @hide
*/
public static final String EXTRAS_KEY_USER_HANDLE = "user_handle";
/**
* Response bundle key to access the returned People Tile preview.
* @hide
*/
public static final String RESPONSE_KEY_REMOTE_VIEWS = "remote_views";
/**
* Name of the permission needed to get a People Tile preview for a given conversation shortcut.
* @hide
*/
public static final String GET_PEOPLE_TILE_PREVIEW_PERMISSION =
"android.permission.GET_PEOPLE_TILE_PREVIEW";
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright (C) 2021 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.people;
import android.app.people.ConversationChannel;
import android.app.people.IPeopleManager;
import android.app.people.PeopleSpaceTile;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.util.Log;
import android.widget.RemoteViews;
import com.android.systemui.shared.system.PeopleProviderUtils;
/** API that returns a People Tile preview. */
public class PeopleProvider extends ContentProvider {
LauncherApps mLauncherApps;
IPeopleManager mPeopleManager;
private static final String TAG = "PeopleProvider";
private static final boolean DEBUG = PeopleSpaceUtils.DEBUG;
private static final String EMPTY_STRING = "";
@Override
public Bundle call(String method, String arg, Bundle extras) {
if (!doesCallerHavePermission()) {
String callingPackage = getCallingPackage();
Log.w(TAG, "API not accessible to calling package: " + callingPackage);
throw new SecurityException("API not accessible to calling package: " + callingPackage);
}
if (!PeopleProviderUtils.GET_PEOPLE_TILE_PREVIEW_METHOD.equals(method)) {
Log.w(TAG, "Invalid method");
throw new IllegalArgumentException("Invalid method");
}
// If services are not set as mocks in tests, fetch them now.
mPeopleManager = mPeopleManager != null ? mPeopleManager
: IPeopleManager.Stub.asInterface(
ServiceManager.getService(Context.PEOPLE_SERVICE));
mLauncherApps = mLauncherApps != null ? mLauncherApps
: getContext().getSystemService(LauncherApps.class);
if (mPeopleManager == null || mLauncherApps == null) {
Log.w(TAG, "Null system managers");
return null;
}
if (extras == null) {
Log.w(TAG, "Extras can't be null");
throw new IllegalArgumentException("Extras can't be null");
}
String shortcutId = extras.getString(
PeopleProviderUtils.EXTRAS_KEY_SHORTCUT_ID, EMPTY_STRING);
String packageName = extras.getString(
PeopleProviderUtils.EXTRAS_KEY_PACKAGE_NAME, EMPTY_STRING);
UserHandle userHandle = extras.getParcelable(
PeopleProviderUtils.EXTRAS_KEY_USER_HANDLE);
if (shortcutId.isEmpty()) {
Log.w(TAG, "Invalid shortcut id");
throw new IllegalArgumentException("Invalid shortcut id");
}
if (packageName.isEmpty()) {
Log.w(TAG, "Invalid package name");
throw new IllegalArgumentException("Invalid package name");
}
if (userHandle == null) {
Log.w(TAG, "Null user handle");
throw new IllegalArgumentException("Null user handle");
}
ConversationChannel channel;
try {
channel = mPeopleManager.getConversation(
packageName, userHandle.getIdentifier(), shortcutId);
} catch (Exception e) {
Log.w(TAG, "Exception getting tiles: " + e);
return null;
}
PeopleSpaceTile tile = PeopleSpaceUtils.getTile(channel, mLauncherApps);
if (tile == null) {
if (DEBUG) Log.i(TAG, "No tile was returned");
return null;
}
if (DEBUG) Log.i(TAG, "Returning tile preview for shortcutId: " + shortcutId);
RemoteViews view = PeopleSpaceUtils.createRemoteViews(getContext(), tile, 0);
final Bundle bundle = new Bundle();
bundle.putParcelable(PeopleProviderUtils.RESPONSE_KEY_REMOTE_VIEWS, view);
return bundle;
}
private boolean doesCallerHavePermission() {
return getContext().checkPermission(
PeopleProviderUtils.GET_PEOPLE_TILE_PREVIEW_PERMISSION,
Binder.getCallingPid(), Binder.getCallingUid())
== PackageManager.PERMISSION_GRANTED;
}
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
throw new IllegalArgumentException("Invalid method");
}
@Override
public String getType(Uri uri) {
throw new IllegalArgumentException("Invalid method");
}
@Override
public Uri insert(Uri uri, ContentValues initialValues) {
throw new IllegalArgumentException("Invalid method");
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new IllegalArgumentException("Invalid method");
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new IllegalArgumentException("Invalid method");
}
}

View File

@@ -49,6 +49,7 @@ import android.provider.ContactsContract;
import android.provider.Settings;
import android.service.notification.ConversationChannelWrapper;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
@@ -438,7 +439,7 @@ public class PeopleSpaceUtils {
}
/** Creates a {@link RemoteViews} for {@code tile}. */
private static RemoteViews createRemoteViews(Context context,
public static RemoteViews createRemoteViews(Context context,
PeopleSpaceTile tile, int appWidgetId) {
RemoteViews views;
if (tile.getNotificationKey() != null) {
@@ -455,6 +456,9 @@ public class PeopleSpaceUtils {
PeopleSpaceTile tile, int appWidgetId) {
try {
views.setTextViewText(R.id.name, tile.getUserName().toString());
views.setImageViewIcon(R.id.person_icon, tile.getUserIcon());
views.setBoolean(R.id.content_background, "setClipToOutline", true);
views.setImageViewBitmap(
R.id.package_icon,
PeopleSpaceUtils.convertDrawableToBitmap(
@@ -462,8 +466,6 @@ public class PeopleSpaceUtils {
tile.getPackageName())
)
);
views.setImageViewIcon(R.id.person_icon, tile.getUserIcon());
views.setBoolean(R.id.content_background, "setClipToOutline", true);
Intent activityIntent = new Intent(context, LaunchConversationActivity.class);
activityIntent.addFlags(
@@ -612,6 +614,22 @@ public class PeopleSpaceUtils {
.collect(Collectors.toList());
}
/** Returns {@code PeopleSpaceTile} based on provided {@ConversationChannel}. */
public static PeopleSpaceTile getTile(ConversationChannel channel, LauncherApps launcherApps) {
if (channel == null) {
Log.i(TAG, "ConversationChannel is null");
return null;
}
PeopleSpaceTile tile = new PeopleSpaceTile.Builder(channel, launcherApps).build();
if (!PeopleSpaceUtils.shouldKeepConversation(tile)) {
Log.i(TAG, "PeopleSpaceTile is not valid");
return null;
}
return tile;
}
/** Returns the last interaction time with the user specified by {@code PeopleSpaceTile}. */
private static Long getLastInteraction(IPeopleManager peopleManager,
PeopleSpaceTile tile) {
@@ -701,7 +719,7 @@ public class PeopleSpaceUtils {
* </li>
*/
public static boolean shouldKeepConversation(PeopleSpaceTile tile) {
return tile != null && tile.getUserName().length() != 0;
return tile != null && !TextUtils.isEmpty(tile.getUserName());
}
private static boolean hasBirthdayStatus(PeopleSpaceTile tile, Context context) {
@@ -792,8 +810,7 @@ public class PeopleSpaceUtils {
private static void updateAppWidgetOptionsAndView(AppWidgetManager appWidgetManager,
Context context, int appWidgetId, PeopleSpaceTile tile) {
updateAppWidgetOptions(appWidgetManager, appWidgetId, tile);
RemoteViews views = createRemoteViews(context,
tile, appWidgetId);
RemoteViews views = createRemoteViews(context, tile, appWidgetId);
appWidgetManager.updateAppWidget(appWidgetId, views);
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright (C) 2021 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.people;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import android.app.people.ConversationChannel;
import android.app.people.IPeopleManager;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.testing.AndroidTestingRunner;
import android.widget.RemoteViews;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.shared.system.PeopleProviderUtils;
import junit.framework.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidTestingRunner.class)
@SmallTest
public class PeopleProviderTest extends SysuiTestCase {
private static final String TAG = "PeopleProviderTest";
private static final Uri URI = Uri.parse(PeopleProviderUtils.PEOPLE_PROVIDER_SCHEME
+ PeopleProviderUtils.PEOPLE_PROVIDER_AUTHORITY);
private static final String SHORTCUT_ID_A = "shortcut_id_a";
private static final String PACKAGE_NAME_A = "package_name_a";
private static final UserHandle USER_HANDLE_A = UserHandle.of(1);
private static final String USERNAME = "username";
private final ShortcutInfo mShortcutInfo =
new ShortcutInfo.Builder(mContext, SHORTCUT_ID_A).setLongLabel(USERNAME).build();
private final ConversationChannel mConversationChannel =
new ConversationChannel(mShortcutInfo, USER_HANDLE_A.getIdentifier(),
null, null, 0L, false);
private Bundle mExtras = new Bundle();
@Mock
private LauncherApps mLauncherApps;
@Mock
private PackageManager mPackageManager;
@Mock
private IPeopleManager mPeopleManager;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mContext.setMockPackageManager(mPackageManager);
PeopleProviderTestable provider = new PeopleProviderTestable();
provider.initializeForTesting(
mContext, PeopleProviderUtils.PEOPLE_PROVIDER_AUTHORITY);
provider.setLauncherApps(mLauncherApps);
provider.setPeopleManager(mPeopleManager);
mContext.getContentResolver().addProvider(
PeopleProviderUtils.PEOPLE_PROVIDER_AUTHORITY, provider);
mContext.getTestablePermissions().setPermission(
PeopleProviderUtils.GET_PEOPLE_TILE_PREVIEW_PERMISSION,
PackageManager.PERMISSION_GRANTED);
when(mPeopleManager.getConversation(
eq(PACKAGE_NAME_A), eq(USER_HANDLE_A.getIdentifier()), eq(SHORTCUT_ID_A)))
.thenReturn(mConversationChannel);
mExtras.putString(PeopleProviderUtils.EXTRAS_KEY_SHORTCUT_ID, SHORTCUT_ID_A);
mExtras.putString(PeopleProviderUtils.EXTRAS_KEY_PACKAGE_NAME, PACKAGE_NAME_A);
mExtras.putParcelable(PeopleProviderUtils.EXTRAS_KEY_USER_HANDLE, USER_HANDLE_A);
}
@Test
public void testPermissionDeniedThrowsSecurityException() throws RemoteException {
mContext.getTestablePermissions().setPermission(
PeopleProviderUtils.GET_PEOPLE_TILE_PREVIEW_PERMISSION,
PackageManager.PERMISSION_DENIED);
try {
Bundle result = mContext.getContentResolver()
.call(URI, PeopleProviderUtils.GET_PEOPLE_TILE_PREVIEW_METHOD, null, null);
Assert.fail("Call should have failed with SecurityException");
} catch (SecurityException e) {
} catch (Exception e) {
Assert.fail("Call should have failed with SecurityException");
}
}
@Test
public void testPermissionGrantedNoExtraReturnsNull() throws RemoteException {
try {
Bundle result = mContext.getContentResolver()
.call(URI, PeopleProviderUtils.GET_PEOPLE_TILE_PREVIEW_METHOD, null, null);
Assert.fail("Call should have failed with IllegalArgumentException");
} catch (IllegalArgumentException e) {
} catch (Exception e) {
Assert.fail("Call should have failed with IllegalArgumentException");
}
}
@Test
public void testPermissionGrantedExtrasReturnsRemoteViews() throws RemoteException {
try {
Bundle result = mContext.getContentResolver().call(
URI, PeopleProviderUtils.GET_PEOPLE_TILE_PREVIEW_METHOD, null, mExtras);
RemoteViews views = result.getParcelable(
PeopleProviderUtils.RESPONSE_KEY_REMOTE_VIEWS);
assertThat(views).isNotNull();
} catch (Exception e) {
Assert.fail("Fail " + e);
}
}
@Test
public void testPermissionGrantedNoConversationForShortcutReturnsNull() throws RemoteException {
when(mPeopleManager.getConversation(
eq(PACKAGE_NAME_A), eq(USER_HANDLE_A.getIdentifier()), eq(SHORTCUT_ID_A)))
.thenReturn(null);
try {
Bundle result = mContext.getContentResolver().call(
URI, PeopleProviderUtils.GET_PEOPLE_TILE_PREVIEW_METHOD, null, mExtras);
assertThat(result).isNull();
} catch (Exception e) {
Assert.fail("Fail " + e);
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2021 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.people;
import android.app.people.IPeopleManager;
import android.content.Context;
import android.content.pm.LauncherApps;
import android.content.pm.ProviderInfo;
public class PeopleProviderTestable extends PeopleProvider {
public void initializeForTesting(Context context, String authority) {
ProviderInfo info = new ProviderInfo();
info.authority = authority;
attachInfoForTesting(context, info);
}
void setLauncherApps(LauncherApps launcherApps) {
mLauncherApps = launcherApps;
}
void setPeopleManager(IPeopleManager peopleManager) {
mPeopleManager = peopleManager;
}
}