Add logging for direct share target
To answer the question if users share mainly with 1 or 2 direct targets or with a multitude of contacts, we need to log the direct target + package name. For privacy, this gets hashed with a salt that expires by default every 7 days. The PH flag will allow us to change the expiration time if we obtain PWG permission for that. Bug: 126365511 Test: New test in ChooserActivityTest + manual testing of consistency and flag rollout using adb shell device_config put systemui hash_salt_max_days with multiple values Change-Id: Ib4255b3eb39ca91ccb5803dc036ffe0ea83a27c9
This commit is contained in:
205
core/java/android/util/HashedStringCache.java
Normal file
205
core/java/android/util/HashedStringCache.java
Normal file
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Environment;
|
||||
import android.os.storage.StorageManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
/**
|
||||
* HashedStringCache provides hashing functionality with an underlying LRUCache and expiring salt.
|
||||
* Salt and expiration time are being stored under the tag passed in by the calling package --
|
||||
* intended usage is the calling package name.
|
||||
* TODO: Add unit tests b/129870147
|
||||
* @hide
|
||||
*/
|
||||
public class HashedStringCache {
|
||||
private static HashedStringCache sHashedStringCache = null;
|
||||
private static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
private static final int HASH_CACHE_SIZE = 100;
|
||||
private static final int HASH_LENGTH = 8;
|
||||
private static final String HASH_SALT = "_hash_salt";
|
||||
private static final String HASH_SALT_DATE = "_hash_salt_date";
|
||||
private static final String HASH_SALT_GEN = "_hash_salt_gen";
|
||||
// For privacy we need to rotate the salt regularly
|
||||
private static final long DAYS_TO_MILLIS = 1000 * 60 * 60 * 24;
|
||||
private static final int MAX_SALT_DAYS = 100;
|
||||
private final LruCache<String, String> mHashes;
|
||||
private final SecureRandom mSecureRandom;
|
||||
private final Object mPreferenceLock = new Object();
|
||||
private final MessageDigest mDigester;
|
||||
private byte[] mSalt;
|
||||
private int mSaltGen;
|
||||
private SharedPreferences mSharedPreferences;
|
||||
|
||||
private static final String TAG = "HashedStringCache";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private HashedStringCache() {
|
||||
mHashes = new LruCache<>(HASH_CACHE_SIZE);
|
||||
mSecureRandom = new SecureRandom();
|
||||
try {
|
||||
mDigester = MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
// this can't happen - MD5 is always present
|
||||
throw new RuntimeException(impossible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return - instance of the HashedStringCache
|
||||
* @hide
|
||||
*/
|
||||
public static HashedStringCache getInstance() {
|
||||
if (sHashedStringCache == null) {
|
||||
sHashedStringCache = new HashedStringCache();
|
||||
}
|
||||
return sHashedStringCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take the string and context and create a hash of the string. Trigger refresh on salt if salt
|
||||
* is more than 7 days old
|
||||
* @param context - callers context to retrieve SharedPreferences
|
||||
* @param clearText - string that needs to be hashed
|
||||
* @param tag - class name to use for storing values in shared preferences
|
||||
* @param saltExpirationDays - number of days we may keep the same salt
|
||||
* special value -1 will short-circuit and always return null.
|
||||
* @return - HashResult containing the hashed string and the generation of the hash salt, null
|
||||
* if clearText string is empty
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public HashResult hashString(Context context, String tag, String clearText,
|
||||
int saltExpirationDays) {
|
||||
if (TextUtils.isEmpty(clearText) || saltExpirationDays == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
populateSaltValues(context, tag, saltExpirationDays);
|
||||
String hashText = mHashes.get(clearText);
|
||||
if (hashText != null) {
|
||||
return new HashResult(hashText, mSaltGen);
|
||||
}
|
||||
|
||||
mDigester.reset();
|
||||
mDigester.update(mSalt);
|
||||
mDigester.update(clearText.getBytes(UTF_8));
|
||||
byte[] bytes = mDigester.digest();
|
||||
int len = Math.min(HASH_LENGTH, bytes.length);
|
||||
hashText = Base64.encodeToString(bytes, 0, len, Base64.NO_PADDING | Base64.NO_WRAP);
|
||||
mHashes.put(clearText, hashText);
|
||||
|
||||
return new HashResult(hashText, mSaltGen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the mSharedPreferences and checks if there is a salt present and if it's older than
|
||||
* 7 days
|
||||
* @param tag - class name to use for storing values in shared preferences
|
||||
* @param saltExpirationDays - number of days we may keep the same salt
|
||||
* @param saltDate - the date retrieved from configuration
|
||||
* @return - true if no salt or salt is older than 7 days
|
||||
*/
|
||||
private boolean checkNeedsNewSalt(String tag, int saltExpirationDays, long saltDate) {
|
||||
if (saltDate == 0 || saltExpirationDays < -1) {
|
||||
return true;
|
||||
}
|
||||
if (saltExpirationDays > MAX_SALT_DAYS) {
|
||||
saltExpirationDays = MAX_SALT_DAYS;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
long delta = now - saltDate;
|
||||
// Check for delta < 0 to make sure we catch if someone puts their phone far in the
|
||||
// future and then goes back to normal time.
|
||||
return delta >= saltExpirationDays * DAYS_TO_MILLIS || delta < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the salt and saltGen member variables if they aren't already set / need refreshing.
|
||||
* @param context - to get sharedPreferences
|
||||
* @param tag - class name to use for storing values in shared preferences
|
||||
* @param saltExpirationDays - number of days we may keep the same salt
|
||||
*/
|
||||
private void populateSaltValues(Context context, String tag, int saltExpirationDays) {
|
||||
synchronized (mPreferenceLock) {
|
||||
// check if we need to refresh the salt
|
||||
mSharedPreferences = getHashSharedPreferences(context);
|
||||
long saltDate = mSharedPreferences.getLong(tag + HASH_SALT_DATE, 0);
|
||||
boolean needsNewSalt = checkNeedsNewSalt(tag, saltExpirationDays, saltDate);
|
||||
if (needsNewSalt) {
|
||||
mHashes.evictAll();
|
||||
}
|
||||
if (mSalt == null || needsNewSalt) {
|
||||
String saltString = mSharedPreferences.getString(tag + HASH_SALT, null);
|
||||
mSaltGen = mSharedPreferences.getInt(tag + HASH_SALT_GEN, 0);
|
||||
if (saltString == null || needsNewSalt) {
|
||||
mSaltGen++;
|
||||
byte[] saltBytes = new byte[16];
|
||||
mSecureRandom.nextBytes(saltBytes);
|
||||
saltString = Base64.encodeToString(saltBytes,
|
||||
Base64.NO_PADDING | Base64.NO_WRAP);
|
||||
mSharedPreferences.edit()
|
||||
.putString(tag + HASH_SALT, saltString)
|
||||
.putInt(tag + HASH_SALT_GEN, mSaltGen)
|
||||
.putLong(tag + HASH_SALT_DATE, System.currentTimeMillis()).apply();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "created a new salt: " + saltString);
|
||||
}
|
||||
}
|
||||
mSalt = saltString.getBytes(UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Android:ui doesn't have persistent preferences, so need to fall back on this hack originally
|
||||
* from ChooserActivity.java
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
private SharedPreferences getHashSharedPreferences(Context context) {
|
||||
final File prefsFile = new File(new File(
|
||||
Environment.getDataUserCePackageDirectory(
|
||||
StorageManager.UUID_PRIVATE_INTERNAL,
|
||||
context.getUserId(), context.getPackageName()),
|
||||
"shared_prefs"),
|
||||
"hashed_cache.xml");
|
||||
return context.getSharedPreferences(prefsFile, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to hold hashed string and salt generation.
|
||||
*/
|
||||
public class HashResult {
|
||||
public String hashedString;
|
||||
public int saltGeneration;
|
||||
|
||||
public HashResult(String hString, int saltGen) {
|
||||
hashedString = hString;
|
||||
saltGeneration = saltGen;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,7 @@ import android.os.RemoteException;
|
||||
import android.os.ResultReceiver;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.provider.DeviceConfig;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.Downloads;
|
||||
import android.provider.OpenableColumns;
|
||||
@@ -83,6 +84,7 @@ import android.service.chooser.IChooserTargetResult;
|
||||
import android.service.chooser.IChooserTargetService;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.HashedStringCache;
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
import android.util.Slog;
|
||||
@@ -106,6 +108,7 @@ import android.widget.Toast;
|
||||
|
||||
import com.android.internal.R;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
|
||||
import com.android.internal.logging.MetricsLogger;
|
||||
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
||||
import com.android.internal.util.ImageUtils;
|
||||
@@ -170,6 +173,11 @@ public class ChooserActivity extends ResolverActivity {
|
||||
private static final int QUERY_TARGET_SERVICE_LIMIT = 5;
|
||||
private static final int WATCHDOG_TIMEOUT_MILLIS = 3000;
|
||||
|
||||
private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7;
|
||||
private int mMaxHashSaltDays = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
|
||||
SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS,
|
||||
DEFAULT_SALT_EXPIRATION_DAYS);
|
||||
|
||||
private Bundle mReplacementExtras;
|
||||
private IntentSender mChosenComponentSender;
|
||||
private IntentSender mRefinementIntentSender;
|
||||
@@ -201,7 +209,8 @@ public class ChooserActivity extends ResolverActivity {
|
||||
private static final int SHORTCUT_MANAGER_SHARE_TARGET_RESULT_COMPLETED = 4;
|
||||
private static final int LIST_VIEW_UPDATE_MESSAGE = 5;
|
||||
|
||||
private static final int LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS = 250;
|
||||
@VisibleForTesting
|
||||
public static final int LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS = 250;
|
||||
|
||||
private boolean mListViewDataChanged = false;
|
||||
|
||||
@@ -991,6 +1000,7 @@ public class ChooserActivity extends ResolverActivity {
|
||||
// Lower values mean the ranking was better.
|
||||
int cat = 0;
|
||||
int value = which;
|
||||
HashedStringCache.HashResult directTargetHashed = null;
|
||||
switch (mChooserListAdapter.getPositionTargetType(which)) {
|
||||
case ChooserListAdapter.TARGET_CALLER:
|
||||
cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET;
|
||||
@@ -998,6 +1008,17 @@ public class ChooserActivity extends ResolverActivity {
|
||||
break;
|
||||
case ChooserListAdapter.TARGET_SERVICE:
|
||||
cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET;
|
||||
value -= mChooserListAdapter.getCallerTargetCount();
|
||||
// Log the package name + target name to answer the question if most users
|
||||
// share to mostly the same person or to a bunch of different people.
|
||||
ChooserTarget target =
|
||||
mChooserListAdapter.mServiceTargets.get(value).getChooserTarget();
|
||||
directTargetHashed = HashedStringCache.getInstance().hashString(
|
||||
this,
|
||||
TAG,
|
||||
target.getComponentName().getPackageName()
|
||||
+ target.getTitle().toString(),
|
||||
mMaxHashSaltDays);
|
||||
break;
|
||||
case ChooserListAdapter.TARGET_STANDARD:
|
||||
cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET;
|
||||
@@ -1007,6 +1028,15 @@ public class ChooserActivity extends ResolverActivity {
|
||||
}
|
||||
|
||||
if (cat != 0) {
|
||||
LogMaker targetLogMaker = new LogMaker(cat).setSubtype(value);
|
||||
if (directTargetHashed != null) {
|
||||
targetLogMaker.addTaggedData(
|
||||
MetricsEvent.FIELD_HASHED_TARGET_NAME, directTargetHashed.hashedString);
|
||||
targetLogMaker.addTaggedData(
|
||||
MetricsEvent.FIELD_HASHED_TARGET_SALT_GEN,
|
||||
directTargetHashed.saltGeneration);
|
||||
}
|
||||
getMetricsLogger().write(targetLogMaker);
|
||||
MetricsLogger.action(this, cat, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,5 +95,10 @@ public final class SystemUiDeviceConfigFlags {
|
||||
public static final String COMPACT_MEDIA_SEEKBAR_ENABLED =
|
||||
"compact_media_notification_seekbar_enabled";
|
||||
|
||||
/**
|
||||
* (int) Maximum number of days to retain the salt for hashing direct share targets in logging
|
||||
*/
|
||||
public static final String HASH_SALT_MAX_DAYS = "hash_salt_max_days";
|
||||
|
||||
private SystemUiDeviceConfigFlags() { }
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import android.app.usage.UsageStatsManager;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipDescription;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ResolveInfo;
|
||||
@@ -47,8 +48,10 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.metrics.LogMaker;
|
||||
import android.net.Uri;
|
||||
import android.service.chooser.ChooserTarget;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
@@ -735,6 +738,120 @@ public class ChooserActivityTest {
|
||||
onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed()));
|
||||
}
|
||||
|
||||
// This test is too long and too slow and should not be taken as an example for future tests.
|
||||
// This is necessary because it tests that multiple calls result in the same result but
|
||||
// normally a test this long should be broken into smaller tests testing individual components.
|
||||
@Test
|
||||
public void testDirectTargetSelectionLogging() throws InterruptedException {
|
||||
Intent sendIntent = createSendTextIntent();
|
||||
// We need app targets for direct targets to get displayed
|
||||
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
|
||||
when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
|
||||
Mockito.anyBoolean(),
|
||||
Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
|
||||
|
||||
// Set up resources
|
||||
MetricsLogger mockLogger = sOverrides.metricsLogger;
|
||||
ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
|
||||
// Create direct share target
|
||||
List<ChooserTarget> serviceTargets = createDirectShareTargets(1);
|
||||
ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
|
||||
|
||||
// Start activity
|
||||
final ChooserWrapperActivity activity = mActivityRule
|
||||
.launchActivity(Intent.createChooser(sendIntent, null));
|
||||
|
||||
// Insert the direct share target
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync(
|
||||
() -> activity.getAdapter().addServiceResults(
|
||||
activity.createTestDisplayResolveInfo(sendIntent,
|
||||
ri,
|
||||
"testLabel",
|
||||
"testInfo",
|
||||
sendIntent),
|
||||
serviceTargets,
|
||||
false)
|
||||
);
|
||||
// Thread.sleep shouldn't be a thing in an integration test but it's
|
||||
// necessary here because of the way the code is structured
|
||||
// TODO: restructure the tests b/129870719
|
||||
Thread.sleep(ChooserActivity.LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS);
|
||||
|
||||
assertThat("Chooser should have 3 targets (2apps, 1 direct)",
|
||||
activity.getAdapter().getCount(), is(3));
|
||||
assertThat("Chooser should have exactly one selectable direct target",
|
||||
activity.getAdapter().getSelectableServiceTargetCount(), is(1));
|
||||
assertThat("The resolver info must match the resolver info used to create the target",
|
||||
activity.getAdapter().getItem(0).getResolveInfo(), is(ri));
|
||||
|
||||
// Click on the direct target
|
||||
String name = serviceTargets.get(0).getTitle().toString();
|
||||
onView(withText(name))
|
||||
.perform(click());
|
||||
waitForIdle();
|
||||
|
||||
// Currently we're seeing 3 invocations
|
||||
// 1. ChooserActivity.onCreate()
|
||||
// 2. ChooserActivity$ChooserRowAdapter.createContentPreviewView()
|
||||
// 3. ChooserActivity.startSelected -- which is the one we're after
|
||||
verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture());
|
||||
assertThat(logMakerCaptor.getAllValues().get(2).getCategory(),
|
||||
is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET));
|
||||
String hashedName = (String) logMakerCaptor
|
||||
.getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_HASHED_TARGET_NAME);
|
||||
assertThat("Hash is not predictable but must be obfuscated",
|
||||
hashedName, is(not(name)));
|
||||
|
||||
// Running the same again to check if the hashed name is the same as before.
|
||||
|
||||
Intent sendIntent2 = createSendTextIntent();
|
||||
|
||||
// Start activity
|
||||
final ChooserWrapperActivity activity2 = mActivityRule
|
||||
.launchActivity(Intent.createChooser(sendIntent2, null));
|
||||
waitForIdle();
|
||||
|
||||
// Insert the direct share target
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync(
|
||||
() -> activity2.getAdapter().addServiceResults(
|
||||
activity2.createTestDisplayResolveInfo(sendIntent,
|
||||
ri,
|
||||
"testLabel",
|
||||
"testInfo",
|
||||
sendIntent),
|
||||
serviceTargets,
|
||||
false)
|
||||
);
|
||||
// Thread.sleep shouldn't be a thing in an integration test but it's
|
||||
// necessary here because of the way the code is structured
|
||||
// TODO: restructure the tests b/129870719
|
||||
Thread.sleep(ChooserActivity.LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS);
|
||||
|
||||
assertThat("Chooser should have 3 targets (2apps, 1 direct)",
|
||||
activity2.getAdapter().getCount(), is(3));
|
||||
assertThat("Chooser should have exactly one selectable direct target",
|
||||
activity2.getAdapter().getSelectableServiceTargetCount(), is(1));
|
||||
assertThat("The resolver info must match the resolver info used to create the target",
|
||||
activity2.getAdapter().getItem(0).getResolveInfo(), is(ri));
|
||||
|
||||
// Click on the direct target
|
||||
onView(withText(name))
|
||||
.perform(click());
|
||||
waitForIdle();
|
||||
|
||||
// Currently we're seeing 6 invocations (3 from above, doubled up)
|
||||
// 4. ChooserActivity.onCreate()
|
||||
// 5. ChooserActivity$ChooserRowAdapter.createContentPreviewView()
|
||||
// 6. ChooserActivity.startSelected -- which is the one we're after
|
||||
verify(mockLogger, Mockito.times(6)).write(logMakerCaptor.capture());
|
||||
assertThat(logMakerCaptor.getAllValues().get(5).getCategory(),
|
||||
is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET));
|
||||
String hashedName2 = (String) logMakerCaptor
|
||||
.getAllValues().get(5).getTaggedData(MetricsEvent.FIELD_HASHED_TARGET_NAME);
|
||||
assertThat("Hashing the same name should result in the same hashed value",
|
||||
hashedName2, is(hashedName));
|
||||
}
|
||||
|
||||
private Intent createSendTextIntent() {
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.setAction(Intent.ACTION_SEND);
|
||||
@@ -798,6 +915,23 @@ public class ChooserActivityTest {
|
||||
return infoList;
|
||||
}
|
||||
|
||||
private List<ChooserTarget> createDirectShareTargets(int numberOfResults) {
|
||||
Icon icon = Icon.createWithBitmap(createBitmap());
|
||||
String testTitle = "testTitle";
|
||||
List<ChooserTarget> targets = new ArrayList<>();
|
||||
for (int i = 0; i < numberOfResults; i++) {
|
||||
ComponentName componentName = ResolverDataProvider.createComponentName(i);
|
||||
ChooserTarget tempTarget = new ChooserTarget(
|
||||
testTitle + i,
|
||||
icon,
|
||||
(float) (1 - ((i + 1) / 10.0)),
|
||||
componentName,
|
||||
null);
|
||||
targets.add(tempTarget);
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
private void waitForIdle() {
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ import static org.mockito.Mockito.mock;
|
||||
import android.app.usage.UsageStatsManager;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
@@ -121,6 +123,11 @@ public class ChooserWrapperActivity extends ChooserActivity {
|
||||
return super.isWorkProfile();
|
||||
}
|
||||
|
||||
public DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri,
|
||||
CharSequence pLabel, CharSequence pInfo, Intent pOrigIntent) {
|
||||
return new DisplayResolveInfo(originalIntent, pri, pLabel, pInfo, pOrigIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
* We cannot directly mock the activity created since instrumentation creates it.
|
||||
* <p>
|
||||
|
||||
@@ -7167,6 +7167,14 @@ message MetricsEvent {
|
||||
// OS: Q
|
||||
ACTION_DISPLAY_WHITE_BALANCE_SETTING_CHANGED = 1703;
|
||||
|
||||
// Action: ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET
|
||||
// Direct share target hashed with rotating salt
|
||||
FIELD_HASHED_TARGET_NAME = 1704;
|
||||
|
||||
// Action: ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET
|
||||
// Salt generation for the above hashed direct share target
|
||||
FIELD_HASHED_TARGET_SALT_GEN = 1705;
|
||||
|
||||
// ---- End Q Constants, all Q constants go above this line ----
|
||||
// Add new aosp constants above this line.
|
||||
// END OF AOSP CONSTANTS
|
||||
|
||||
Reference in New Issue
Block a user