Merge "Add a service to rank apps for ResolverActivity." into oc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
5a0ee44ce5
@ -321,6 +321,8 @@ LOCAL_SRC_FILES += \
|
||||
core/java/android/service/wallpaper/IWallpaperService.aidl \
|
||||
core/java/android/service/chooser/IChooserTargetService.aidl \
|
||||
core/java/android/service/chooser/IChooserTargetResult.aidl \
|
||||
core/java/android/service/resolver/IResolverRankerService.aidl \
|
||||
core/java/android/service/resolver/IResolverRankerResult.aidl \
|
||||
core/java/android/text/ITextClassificationService.aidl \
|
||||
core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl\
|
||||
core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl\
|
||||
@ -729,6 +731,7 @@ aidl_files := \
|
||||
frameworks/base/core/java/android/service/notification/SnoozeCriterion.aidl \
|
||||
frameworks/base/core/java/android/service/notification/StatusBarNotification.aidl \
|
||||
frameworks/base/core/java/android/service/chooser/ChooserTarget.aidl \
|
||||
frameworks/base/core/java/android/service/resolver/ResolverTarget.aidl \
|
||||
frameworks/base/core/java/android/speech/tts/Voice.aidl \
|
||||
frameworks/base/core/java/android/app/usage/CacheQuotaHint.aidl \
|
||||
frameworks/base/core/java/android/app/usage/ExternalStorageStats.aidl \
|
||||
|
@ -53,6 +53,7 @@ package android {
|
||||
field public static final java.lang.String BIND_PRINT_SERVICE = "android.permission.BIND_PRINT_SERVICE";
|
||||
field public static final java.lang.String BIND_QUICK_SETTINGS_TILE = "android.permission.BIND_QUICK_SETTINGS_TILE";
|
||||
field public static final java.lang.String BIND_REMOTEVIEWS = "android.permission.BIND_REMOTEVIEWS";
|
||||
field public static final java.lang.String BIND_RESOLVER_RANKER_SERVICE = "android.permission.BIND_RESOLVER_RANKER_SERVICE";
|
||||
field public static final java.lang.String BIND_RUNTIME_PERMISSION_PRESENTER_SERVICE = "android.permission.BIND_RUNTIME_PERMISSION_PRESENTER_SERVICE";
|
||||
field public static final java.lang.String BIND_SCREENING_SERVICE = "android.permission.BIND_SCREENING_SERVICE";
|
||||
field public static final java.lang.String BIND_TELECOM_CONNECTION_SERVICE = "android.permission.BIND_TELECOM_CONNECTION_SERVICE";
|
||||
@ -40627,6 +40628,36 @@ package android.service.quicksettings {
|
||||
|
||||
}
|
||||
|
||||
package android.service.resolver {
|
||||
|
||||
public abstract class ResolverRankerService extends android.app.Service {
|
||||
ctor public ResolverRankerService();
|
||||
method public android.os.IBinder onBind(android.content.Intent);
|
||||
method public void onPredictSharingProbabilities(java.util.List<android.service.resolver.ResolverTarget>);
|
||||
method public void onTrainRankingModel(java.util.List<android.service.resolver.ResolverTarget>, int);
|
||||
field public static final java.lang.String BIND_PERMISSION = "android.permission.BIND_RESOLVER_RANKER_SERVICE";
|
||||
field public static final java.lang.String SERVICE_INTERFACE = "android.service.resolver.ResolverRankerService";
|
||||
}
|
||||
|
||||
public final class ResolverTarget implements android.os.Parcelable {
|
||||
ctor public ResolverTarget();
|
||||
method public int describeContents();
|
||||
method public float getChooserScore();
|
||||
method public float getLaunchScore();
|
||||
method public float getRecencyScore();
|
||||
method public float getSelectProbability();
|
||||
method public float getTimeSpentScore();
|
||||
method public void setChooserScore(float);
|
||||
method public void setLaunchScore(float);
|
||||
method public void setRecencyScore(float);
|
||||
method public void setSelectProbability(float);
|
||||
method public void setTimeSpentScore(float);
|
||||
method public void writeToParcel(android.os.Parcel, int);
|
||||
field public static final android.os.Parcelable.Creator<android.service.resolver.ResolverTarget> CREATOR;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
package android.service.restrictions {
|
||||
|
||||
public abstract class RestrictionsReceiver extends android.content.BroadcastReceiver {
|
||||
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.service.resolver;
|
||||
|
||||
import android.service.resolver.ResolverTarget;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
oneway interface IResolverRankerResult
|
||||
{
|
||||
void sendResult(in List<ResolverTarget> results);
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.service.resolver;
|
||||
|
||||
import android.service.resolver.IResolverRankerResult;
|
||||
import android.service.resolver.ResolverTarget;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
oneway interface IResolverRankerService
|
||||
{
|
||||
void predict(in List<ResolverTarget> targets, IResolverRankerResult result);
|
||||
void train(in List<ResolverTarget> targets, int selectedPosition);
|
||||
}
|
187
core/java/android/service/resolver/ResolverRankerService.java
Normal file
187
core/java/android/service/resolver/ResolverRankerService.java
Normal file
@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.service.resolver;
|
||||
|
||||
import android.annotation.SdkConstant;
|
||||
import android.annotation.SystemApi;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.RemoteException;
|
||||
import android.service.resolver.ResolverTarget;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A service to rank apps according to usage stats of apps, when the system is resolving targets for
|
||||
* an Intent.
|
||||
*
|
||||
* <p>To extend this class, you must declare the service in your manifest file with the
|
||||
* {@link android.Manifest.permission#BIND_RESOLVER_RANKER_SERVICE} permission, and include an
|
||||
* intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p>
|
||||
* <pre>
|
||||
* <service android:name=".MyResolverRankerService"
|
||||
* android:exported="true"
|
||||
* android:priority="100"
|
||||
* android:permission="android.permission.BIND_RESOLVER_RANKER_SERVICE">
|
||||
* <intent-filter>
|
||||
* <action android:name="android.service.resolver.ResolverRankerService" />
|
||||
* </intent-filter>
|
||||
* </service>
|
||||
* </pre>
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public abstract class ResolverRankerService extends Service {
|
||||
|
||||
private static final String TAG = "ResolverRankerService";
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
/**
|
||||
* The Intent action that a service must respond to. Add it to the intent filter of the service
|
||||
* in its manifest.
|
||||
*/
|
||||
@SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
|
||||
public static final String SERVICE_INTERFACE = "android.service.resolver.ResolverRankerService";
|
||||
|
||||
/**
|
||||
* The permission that a service must require to ensure that only Android system can bind to it.
|
||||
* If this permission is not enforced in the AndroidManifest of the service, the system will
|
||||
* skip that service.
|
||||
*/
|
||||
public static final String BIND_PERMISSION = "android.permission.BIND_RESOLVER_RANKER_SERVICE";
|
||||
|
||||
private ResolverRankerServiceWrapper mWrapper = null;
|
||||
|
||||
/**
|
||||
* Called by the system to retrieve a list of probabilities to rank apps/options. To implement
|
||||
* it, set selectProbability of each input {@link ResolverTarget}. The higher the
|
||||
* selectProbability is, the more likely the {@link ResolverTarget} will be selected by the
|
||||
* user. Override this function to provide prediction results.
|
||||
*
|
||||
* @param targets a list of {@link ResolverTarget}, for the list of apps to be ranked.
|
||||
*
|
||||
* @throws Exception when the prediction task fails.
|
||||
*/
|
||||
public void onPredictSharingProbabilities(final List<ResolverTarget> targets) {}
|
||||
|
||||
/**
|
||||
* Called by the system to train/update a ranking service, after the user makes a selection from
|
||||
* the ranked list of apps. Override this function to enable model updates.
|
||||
*
|
||||
* @param targets a list of {@link ResolverTarget}, for the list of apps to be ranked.
|
||||
* @param selectedPosition the position of the selected app in the list.
|
||||
*
|
||||
* @throws Exception when the training task fails.
|
||||
*/
|
||||
public void onTrainRankingModel(
|
||||
final List<ResolverTarget> targets, final int selectedPosition) {}
|
||||
|
||||
private static final String HANDLER_THREAD_NAME = "RESOLVER_RANKER_SERVICE";
|
||||
private volatile Handler mHandler;
|
||||
private HandlerThread mHandlerThread;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
if (DEBUG) Log.d(TAG, "onBind " + intent);
|
||||
if (!SERVICE_INTERFACE.equals(intent.getAction())) {
|
||||
if (DEBUG) Log.d(TAG, "bad intent action " + intent.getAction() + "; returning null");
|
||||
return null;
|
||||
}
|
||||
if (mHandlerThread == null) {
|
||||
mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME);
|
||||
mHandlerThread.start();
|
||||
mHandler = new Handler(mHandlerThread.getLooper());
|
||||
}
|
||||
if (mWrapper == null) {
|
||||
mWrapper = new ResolverRankerServiceWrapper();
|
||||
}
|
||||
return mWrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
mHandler = null;
|
||||
if (mHandlerThread != null) {
|
||||
mHandlerThread.quitSafely();
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private static void sendResult(List<ResolverTarget> targets, IResolverRankerResult result) {
|
||||
try {
|
||||
result.sendResult(targets);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "failed to send results: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private class ResolverRankerServiceWrapper extends IResolverRankerService.Stub {
|
||||
|
||||
@Override
|
||||
public void predict(final List<ResolverTarget> targets, final IResolverRankerResult result)
|
||||
throws RemoteException {
|
||||
Runnable predictRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "predict calls onPredictSharingProbabilities.");
|
||||
}
|
||||
onPredictSharingProbabilities(targets);
|
||||
sendResult(targets, result);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "onPredictSharingProbabilities failed; send null results: " + e);
|
||||
sendResult(null, result);
|
||||
}
|
||||
}
|
||||
};
|
||||
final Handler h = mHandler;
|
||||
if (h != null) {
|
||||
h.post(predictRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void train(final List<ResolverTarget> targets, final int selectedPosition)
|
||||
throws RemoteException {
|
||||
Runnable trainRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "train calls onTranRankingModel");
|
||||
}
|
||||
onTrainRankingModel(targets, selectedPosition);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "onTrainRankingModel failed; skip train: " + e);
|
||||
}
|
||||
}
|
||||
};
|
||||
final Handler h = mHandler;
|
||||
if (h != null) {
|
||||
h.post(trainRunnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
core/java/android/service/resolver/ResolverTarget.aidl
Normal file
22
core/java/android/service/resolver/ResolverTarget.aidl
Normal file
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.service.resolver;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
parcelable ResolverTarget;
|
216
core/java/android/service/resolver/ResolverTarget.java
Normal file
216
core/java/android/service/resolver/ResolverTarget.java
Normal file
@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.service.resolver;
|
||||
|
||||
import android.annotation.SystemApi;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.ArrayMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A ResolverTarget contains features by which an app or option will be ranked, in
|
||||
* {@link ResolverRankerService}.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public final class ResolverTarget implements Parcelable {
|
||||
private static final String TAG = "ResolverTarget";
|
||||
|
||||
/**
|
||||
* a float score for recency of last use.
|
||||
*/
|
||||
private float mRecencyScore;
|
||||
|
||||
/**
|
||||
* a float score for total time spent.
|
||||
*/
|
||||
private float mTimeSpentScore;
|
||||
|
||||
/**
|
||||
* a float score for number of launches.
|
||||
*/
|
||||
private float mLaunchScore;
|
||||
|
||||
/**
|
||||
* a float score for number of selected.
|
||||
*/
|
||||
private float mChooserScore;
|
||||
|
||||
/**
|
||||
* a float score for the probability to be selected.
|
||||
*/
|
||||
private float mSelectProbability;
|
||||
|
||||
// constructor for the class.
|
||||
public ResolverTarget() {}
|
||||
|
||||
ResolverTarget(Parcel in) {
|
||||
mRecencyScore = in.readFloat();
|
||||
mTimeSpentScore = in.readFloat();
|
||||
mLaunchScore = in.readFloat();
|
||||
mChooserScore = in.readFloat();
|
||||
mSelectProbability = in.readFloat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the score for how recently the target was used in the foreground.
|
||||
*
|
||||
* @return a float score whose range is [0, 1]. The higher the score is, the more recently the
|
||||
* target was used.
|
||||
*/
|
||||
public float getRecencyScore() {
|
||||
return mRecencyScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the score for how recently the target was used in the foreground.
|
||||
*
|
||||
* @param recencyScore a float score whose range is [0, 1]. The higher the score is, the more
|
||||
* recently the target was used.
|
||||
*/
|
||||
public void setRecencyScore(float recencyScore) {
|
||||
this.mRecencyScore = recencyScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the score for how long the target has been used in the foreground.
|
||||
*
|
||||
* @return a float score whose range is [0, 1]. The higher the score is, the longer the target
|
||||
* has been used for.
|
||||
*/
|
||||
public float getTimeSpentScore() {
|
||||
return mTimeSpentScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the score for how long the target has been used in the foreground.
|
||||
*
|
||||
* @param timeSpentScore a float score whose range is [0, 1]. The higher the score is, the
|
||||
* longer the target has been used for.
|
||||
*/
|
||||
public void setTimeSpentScore(float timeSpentScore) {
|
||||
this.mTimeSpentScore = timeSpentScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the score for how many times the target has been launched to the foreground.
|
||||
*
|
||||
* @return a float score whose range is [0, 1]. The higher the score is, the more times the
|
||||
* target has been launched.
|
||||
*/
|
||||
public float getLaunchScore() {
|
||||
return mLaunchScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the score for how many times the target has been launched to the foreground.
|
||||
*
|
||||
* @param launchScore a float score whose range is [0, 1]. The higher the score is, the more
|
||||
* times the target has been launched.
|
||||
*/
|
||||
public void setLaunchScore(float launchScore) {
|
||||
this.mLaunchScore = launchScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the score for how many times the target has been selected by the user to share the same
|
||||
* types of content.
|
||||
*
|
||||
* @return a float score whose range is [0, 1]. The higher the score is, the
|
||||
* more times the target has been selected by the user to share the same types of content for.
|
||||
*/
|
||||
public float getChooserScore() {
|
||||
return mChooserScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the score for how many times the target has been selected by the user to share the same
|
||||
* types of content.
|
||||
*
|
||||
* @param chooserScore a float score whose range is [0, 1]. The higher the score is, the more
|
||||
* times the target has been selected by the user to share the same types
|
||||
* of content for.
|
||||
*/
|
||||
public void setChooserScore(float chooserScore) {
|
||||
this.mChooserScore = chooserScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the probability of how likely this target will be selected by the user.
|
||||
*
|
||||
* @return a float score whose range is [0, 1]. The higher the score is, the more likely the
|
||||
* user is going to select this target.
|
||||
*/
|
||||
public float getSelectProbability() {
|
||||
return mSelectProbability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the probability for how like this target will be selected by the user.
|
||||
*
|
||||
* @param selectProbability a float score whose range is [0, 1]. The higher the score is, the
|
||||
* more likely tht user is going to select this target.
|
||||
*/
|
||||
public void setSelectProbability(float selectProbability) {
|
||||
this.mSelectProbability = selectProbability;
|
||||
}
|
||||
|
||||
// serialize the class to a string.
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ResolverTarget{"
|
||||
+ mRecencyScore + ", "
|
||||
+ mTimeSpentScore + ", "
|
||||
+ mLaunchScore + ", "
|
||||
+ mChooserScore + ", "
|
||||
+ mSelectProbability + "}";
|
||||
}
|
||||
|
||||
// describes the kinds of special objects contained in this Parcelable instance's marshaled
|
||||
// representation.
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// flattens this object in to a Parcel.
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeFloat(mRecencyScore);
|
||||
dest.writeFloat(mTimeSpentScore);
|
||||
dest.writeFloat(mLaunchScore);
|
||||
dest.writeFloat(mChooserScore);
|
||||
dest.writeFloat(mSelectProbability);
|
||||
}
|
||||
|
||||
// creator definition for the class.
|
||||
public static final Creator<ResolverTarget> CREATOR
|
||||
= new Creator<ResolverTarget>() {
|
||||
@Override
|
||||
public ResolverTarget createFromParcel(Parcel source) {
|
||||
return new ResolverTarget(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolverTarget[] newArray(int size) {
|
||||
return new ResolverTarget[size];
|
||||
}
|
||||
};
|
||||
}
|
199
core/java/com/android/internal/app/LRResolverRankerService.java
Normal file
199
core/java/com/android/internal/app/LRResolverRankerService.java
Normal file
@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.internal.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.os.storage.StorageManager;
|
||||
import android.service.resolver.ResolverRankerService;
|
||||
import android.service.resolver.ResolverTarget;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A Logistic Regression based {@link android.service.resolver.ResolverRankerService}, to be used
|
||||
* in {@link ResolverComparator}.
|
||||
*/
|
||||
public final class LRResolverRankerService extends ResolverRankerService {
|
||||
private static final String TAG = "LRResolverRankerService";
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final String PARAM_SHARED_PREF_NAME = "resolver_ranker_params";
|
||||
private static final String BIAS_PREF_KEY = "bias";
|
||||
private static final String VERSION_PREF_KEY = "version";
|
||||
|
||||
private static final String LAUNCH_SCORE = "launch";
|
||||
private static final String TIME_SPENT_SCORE = "timeSpent";
|
||||
private static final String RECENCY_SCORE = "recency";
|
||||
private static final String CHOOSER_SCORE = "chooser";
|
||||
|
||||
// parameters for a pre-trained model, to initialize the app ranker. When updating the
|
||||
// pre-trained model, please update these params, as well as initModel().
|
||||
private static final int CURRENT_VERSION = 1;
|
||||
private static final float LEARNING_RATE = 0.0001f;
|
||||
private static final float REGULARIZER_PARAM = 0.0001f;
|
||||
|
||||
private SharedPreferences mParamSharedPref;
|
||||
private ArrayMap<String, Float> mFeatureWeights;
|
||||
private float mBias;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
initModel();
|
||||
return super.onBind(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPredictSharingProbabilities(List<ResolverTarget> targets) {
|
||||
final int size = targets.size();
|
||||
for (int i = 0; i < size; ++i) {
|
||||
ResolverTarget target = targets.get(i);
|
||||
ArrayMap<String, Float> features = getFeatures(target);
|
||||
target.setSelectProbability(predict(features));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrainRankingModel(List<ResolverTarget> targets, int selectedPosition) {
|
||||
final int size = targets.size();
|
||||
if (selectedPosition < 0 || selectedPosition >= size) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Invalid Position of Selected App " + selectedPosition);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final ArrayMap<String, Float> positive = getFeatures(targets.get(selectedPosition));
|
||||
final float positiveProbability = targets.get(selectedPosition).getSelectProbability();
|
||||
final int targetSize = targets.size();
|
||||
for (int i = 0; i < targetSize; ++i) {
|
||||
if (i == selectedPosition) {
|
||||
continue;
|
||||
}
|
||||
final ArrayMap<String, Float> negative = getFeatures(targets.get(i));
|
||||
final float negativeProbability = targets.get(i).getSelectProbability();
|
||||
if (negativeProbability > positiveProbability) {
|
||||
update(negative, negativeProbability, false);
|
||||
update(positive, positiveProbability, true);
|
||||
}
|
||||
}
|
||||
commitUpdate();
|
||||
}
|
||||
|
||||
private void initModel() {
|
||||
mParamSharedPref = getParamSharedPref();
|
||||
mFeatureWeights = new ArrayMap<>(4);
|
||||
if (mParamSharedPref == null ||
|
||||
mParamSharedPref.getInt(VERSION_PREF_KEY, 0) < CURRENT_VERSION) {
|
||||
// Initializing the app ranker to a pre-trained model. When updating the pre-trained
|
||||
// model, please increment CURRENT_VERSION, and update LEARNING_RATE and
|
||||
// REGULARIZER_PARAM.
|
||||
mBias = -1.6568f;
|
||||
mFeatureWeights.put(LAUNCH_SCORE, 2.5543f);
|
||||
mFeatureWeights.put(TIME_SPENT_SCORE, 2.8412f);
|
||||
mFeatureWeights.put(RECENCY_SCORE, 0.269f);
|
||||
mFeatureWeights.put(CHOOSER_SCORE, 4.2222f);
|
||||
} else {
|
||||
mBias = mParamSharedPref.getFloat(BIAS_PREF_KEY, 0.0f);
|
||||
mFeatureWeights.put(LAUNCH_SCORE, mParamSharedPref.getFloat(LAUNCH_SCORE, 0.0f));
|
||||
mFeatureWeights.put(
|
||||
TIME_SPENT_SCORE, mParamSharedPref.getFloat(TIME_SPENT_SCORE, 0.0f));
|
||||
mFeatureWeights.put(RECENCY_SCORE, mParamSharedPref.getFloat(RECENCY_SCORE, 0.0f));
|
||||
mFeatureWeights.put(CHOOSER_SCORE, mParamSharedPref.getFloat(CHOOSER_SCORE, 0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayMap<String, Float> getFeatures(ResolverTarget target) {
|
||||
ArrayMap<String, Float> features = new ArrayMap<>(4);
|
||||
features.put(RECENCY_SCORE, target.getRecencyScore());
|
||||
features.put(TIME_SPENT_SCORE, target.getTimeSpentScore());
|
||||
features.put(LAUNCH_SCORE, target.getLaunchScore());
|
||||
features.put(CHOOSER_SCORE, target.getChooserScore());
|
||||
return features;
|
||||
}
|
||||
|
||||
private float predict(ArrayMap<String, Float> target) {
|
||||
if (target == null) {
|
||||
return 0.0f;
|
||||
}
|
||||
final int featureSize = target.size();
|
||||
float sum = 0.0f;
|
||||
for (int i = 0; i < featureSize; i++) {
|
||||
String featureName = target.keyAt(i);
|
||||
float weight = mFeatureWeights.getOrDefault(featureName, 0.0f);
|
||||
sum += weight * target.valueAt(i);
|
||||
}
|
||||
return (float) (1.0 / (1.0 + Math.exp(-mBias - sum)));
|
||||
}
|
||||
|
||||
private void update(ArrayMap<String, Float> target, float predict, boolean isSelected) {
|
||||
if (target == null) {
|
||||
return;
|
||||
}
|
||||
final int featureSize = target.size();
|
||||
float error = isSelected ? 1.0f - predict : -predict;
|
||||
for (int i = 0; i < featureSize; i++) {
|
||||
String featureName = target.keyAt(i);
|
||||
float currentWeight = mFeatureWeights.getOrDefault(featureName, 0.0f);
|
||||
mBias += LEARNING_RATE * error;
|
||||
currentWeight = currentWeight - LEARNING_RATE * REGULARIZER_PARAM * currentWeight +
|
||||
LEARNING_RATE * error * target.valueAt(i);
|
||||
mFeatureWeights.put(featureName, currentWeight);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Weights: " + mFeatureWeights + " Bias: " + mBias);
|
||||
}
|
||||
}
|
||||
|
||||
private void commitUpdate() {
|
||||
try {
|
||||
SharedPreferences.Editor editor = mParamSharedPref.edit();
|
||||
editor.putFloat(BIAS_PREF_KEY, mBias);
|
||||
final int size = mFeatureWeights.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
editor.putFloat(mFeatureWeights.keyAt(i), mFeatureWeights.valueAt(i));
|
||||
}
|
||||
editor.putInt(VERSION_PREF_KEY, CURRENT_VERSION);
|
||||
editor.apply();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to commit update" + e);
|
||||
}
|
||||
}
|
||||
|
||||
private SharedPreferences getParamSharedPref() {
|
||||
// The package info in the context isn't initialized in the way it is for normal apps,
|
||||
// so the standard, name-based context.getSharedPreferences doesn't work. Instead, we
|
||||
// build the path manually below using the same policy that appears in ContextImpl.
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Context Package Name: " + getPackageName());
|
||||
}
|
||||
final File prefsFile = new File(new File(
|
||||
Environment.getDataUserCePackageDirectory(
|
||||
StorageManager.UUID_PRIVATE_INTERNAL, getUserId(), getPackageName()),
|
||||
"shared_prefs"),
|
||||
PARAM_SHARED_PREF_NAME + ".xml");
|
||||
return getSharedPreferences(prefsFile, Context.MODE_PRIVATE);
|
||||
}
|
||||
}
|
@ -530,6 +530,9 @@ public class ResolverActivity extends Activity {
|
||||
getMainThreadHandler().removeCallbacks(mPostListReadyRunnable);
|
||||
mPostListReadyRunnable = null;
|
||||
}
|
||||
if (mAdapter != null && mAdapter.mResolverListController != null) {
|
||||
mAdapter.mResolverListController.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -26,20 +26,34 @@ import android.content.IntentFilter;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.ComponentInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.RemoteException;
|
||||
import android.os.storage.StorageManager;
|
||||
import android.os.UserHandle;
|
||||
import android.service.resolver.IResolverRankerService;
|
||||
import android.service.resolver.IResolverRankerResult;
|
||||
import android.service.resolver.ResolverRankerService;
|
||||
import android.service.resolver.ResolverTarget;
|
||||
import android.text.TextUtils;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.InterruptedException;
|
||||
import java.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -61,11 +75,15 @@ class ResolverComparator implements Comparator<ResolvedComponentInfo> {
|
||||
|
||||
private static final float RECENCY_MULTIPLIER = 2.f;
|
||||
|
||||
// feature names used in ranking.
|
||||
private static final String LAUNCH_SCORE = "launch";
|
||||
private static final String TIME_SPENT_SCORE = "timeSpent";
|
||||
private static final String RECENCY_SCORE = "recency";
|
||||
private static final String CHOOSER_SCORE = "chooser";
|
||||
// message types
|
||||
private static final int RESOLVER_RANKER_SERVICE_RESULT = 0;
|
||||
private static final int RESOLVER_RANKER_RESULT_TIMEOUT = 1;
|
||||
|
||||
// timeout for establishing connections with a ResolverRankerService.
|
||||
private static final int CONNECTION_COST_TIMEOUT_MILLIS = 200;
|
||||
// timeout for establishing connections with a ResolverRankerService, collecting features and
|
||||
// predicting ranking scores.
|
||||
private static final int WATCHDOG_TIMEOUT_MILLIS = 500;
|
||||
|
||||
private final Collator mCollator;
|
||||
private final boolean mHttp;
|
||||
@ -74,18 +92,74 @@ class ResolverComparator implements Comparator<ResolvedComponentInfo> {
|
||||
private final Map<String, UsageStats> mStats;
|
||||
private final long mCurrentTime;
|
||||
private final long mSinceTime;
|
||||
private final LinkedHashMap<ComponentName, ScoredTarget> mScoredTargets = new LinkedHashMap<>();
|
||||
private final LinkedHashMap<ComponentName, ResolverTarget> mTargetsDict = new LinkedHashMap<>();
|
||||
private final String mReferrerPackage;
|
||||
private final Object mLock = new Object();
|
||||
private ArrayList<ResolverTarget> mTargets;
|
||||
private String mContentType;
|
||||
private String[] mAnnotations;
|
||||
private String mAction;
|
||||
private LogisticRegressionAppRanker mRanker;
|
||||
private IResolverRankerService mRanker;
|
||||
private ResolverRankerServiceConnection mConnection;
|
||||
private AfterCompute mAfterCompute;
|
||||
private Context mContext;
|
||||
private CountDownLatch mConnectSignal;
|
||||
|
||||
public ResolverComparator(Context context, Intent intent, String referrerPackage) {
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper()) {
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case RESOLVER_RANKER_SERVICE_RESULT:
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "RESOLVER_RANKER_SERVICE_RESULT");
|
||||
}
|
||||
if (mHandler.hasMessages(RESOLVER_RANKER_RESULT_TIMEOUT)) {
|
||||
if (msg.obj != null) {
|
||||
final List<ResolverTarget> receivedTargets =
|
||||
(List<ResolverTarget>) msg.obj;
|
||||
if (receivedTargets != null && mTargets != null
|
||||
&& receivedTargets.size() == mTargets.size()) {
|
||||
final int size = mTargets.size();
|
||||
for (int i = 0; i < size; ++i) {
|
||||
mTargets.get(i).setSelectProbability(
|
||||
receivedTargets.get(i).getSelectProbability());
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Sizes of sent and received ResolverTargets diff.");
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Receiving null prediction results.");
|
||||
}
|
||||
mHandler.removeMessages(RESOLVER_RANKER_RESULT_TIMEOUT);
|
||||
mAfterCompute.afterCompute();
|
||||
}
|
||||
break;
|
||||
|
||||
case RESOLVER_RANKER_RESULT_TIMEOUT:
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "RESOLVER_RANKER_RESULT_TIMEOUT; unbinding services");
|
||||
}
|
||||
mHandler.removeMessages(RESOLVER_RANKER_SERVICE_RESULT);
|
||||
mAfterCompute.afterCompute();
|
||||
break;
|
||||
|
||||
default:
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public interface AfterCompute {
|
||||
public void afterCompute ();
|
||||
}
|
||||
|
||||
public ResolverComparator(Context context, Intent intent, String referrerPackage,
|
||||
AfterCompute afterCompute) {
|
||||
mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
|
||||
String scheme = intent.getScheme();
|
||||
mHttp = "http".equals(scheme) || "https".equals(scheme);
|
||||
mReferrerPackage = referrerPackage;
|
||||
mAfterCompute = afterCompute;
|
||||
mContext = context;
|
||||
|
||||
mPm = context.getPackageManager();
|
||||
mUsm = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
|
||||
@ -96,9 +170,9 @@ class ResolverComparator implements Comparator<ResolvedComponentInfo> {
|
||||
mContentType = intent.getType();
|
||||
getContentAnnotations(intent);
|
||||
mAction = intent.getAction();
|
||||
mRanker = new LogisticRegressionAppRanker(context);
|
||||
}
|
||||
|
||||
// get annotations of content from intent.
|
||||
public void getContentAnnotations(Intent intent) {
|
||||
ArrayList<String> annotations = intent.getStringArrayListExtra(
|
||||
Intent.EXTRA_CONTENT_ANNOTATIONS);
|
||||
@ -114,20 +188,24 @@ class ResolverComparator implements Comparator<ResolvedComponentInfo> {
|
||||
}
|
||||
}
|
||||
|
||||
public void setCallBack(AfterCompute afterCompute) {
|
||||
mAfterCompute = afterCompute;
|
||||
}
|
||||
|
||||
// compute features for each target according to usage stats of targets.
|
||||
public void compute(List<ResolvedComponentInfo> targets) {
|
||||
mScoredTargets.clear();
|
||||
reset();
|
||||
|
||||
final long recentSinceTime = mCurrentTime - RECENCY_TIME_PERIOD;
|
||||
|
||||
long mostRecentlyUsedTime = recentSinceTime + 1;
|
||||
long mostTimeSpent = 1;
|
||||
int mostLaunched = 1;
|
||||
int mostSelected = 1;
|
||||
float mostRecencyScore = 1.0f;
|
||||
float mostTimeSpentScore = 1.0f;
|
||||
float mostLaunchScore = 1.0f;
|
||||
float mostChooserScore = 1.0f;
|
||||
|
||||
for (ResolvedComponentInfo target : targets) {
|
||||
final ScoredTarget scoredTarget
|
||||
= new ScoredTarget(target.getResolveInfoAt(0).activityInfo);
|
||||
mScoredTargets.put(target.name, scoredTarget);
|
||||
final ResolverTarget resolverTarget = new ResolverTarget();
|
||||
mTargetsDict.put(target.name, resolverTarget);
|
||||
final UsageStats pkStats = mStats.get(target.name.getPackageName());
|
||||
if (pkStats != null) {
|
||||
// Only count recency for apps that weren't the caller
|
||||
@ -135,31 +213,33 @@ class ResolverComparator implements Comparator<ResolvedComponentInfo> {
|
||||
// Persistent processes muck this up, so omit them too.
|
||||
if (!target.name.getPackageName().equals(mReferrerPackage)
|
||||
&& !isPersistentProcess(target)) {
|
||||
final long lastTimeUsed = pkStats.getLastTimeUsed();
|
||||
scoredTarget.lastTimeUsed = lastTimeUsed;
|
||||
if (lastTimeUsed > mostRecentlyUsedTime) {
|
||||
mostRecentlyUsedTime = lastTimeUsed;
|
||||
final float recencyScore =
|
||||
(float) Math.max(pkStats.getLastTimeUsed() - recentSinceTime, 0);
|
||||
resolverTarget.setRecencyScore(recencyScore);
|
||||
if (recencyScore > mostRecencyScore) {
|
||||
mostRecencyScore = recencyScore;
|
||||
}
|
||||
}
|
||||
final long timeSpent = pkStats.getTotalTimeInForeground();
|
||||
scoredTarget.timeSpent = timeSpent;
|
||||
if (timeSpent > mostTimeSpent) {
|
||||
mostTimeSpent = timeSpent;
|
||||
final float timeSpentScore = (float) pkStats.getTotalTimeInForeground();
|
||||
resolverTarget.setTimeSpentScore(timeSpentScore);
|
||||
if (timeSpentScore > mostTimeSpentScore) {
|
||||
mostTimeSpentScore = timeSpentScore;
|
||||
}
|
||||
final int launched = pkStats.mLaunchCount;
|
||||
scoredTarget.launchCount = launched;
|
||||
if (launched > mostLaunched) {
|
||||
mostLaunched = launched;
|
||||
final float launchScore = (float) pkStats.mLaunchCount;
|
||||
resolverTarget.setLaunchScore(launchScore);
|
||||
if (launchScore > mostLaunchScore) {
|
||||
mostLaunchScore = launchScore;
|
||||
}
|
||||
|
||||
int selected = 0;
|
||||
float chooserScore = 0.0f;
|
||||
if (pkStats.mChooserCounts != null && mAction != null
|
||||
&& pkStats.mChooserCounts.get(mAction) != null) {
|
||||
selected = pkStats.mChooserCounts.get(mAction).getOrDefault(mContentType, 0);
|
||||
chooserScore = (float) pkStats.mChooserCounts.get(mAction)
|
||||
.getOrDefault(mContentType, 0);
|
||||
if (mAnnotations != null) {
|
||||
final int size = mAnnotations.length;
|
||||
for (int i = 0; i < size; i++) {
|
||||
selected += pkStats.mChooserCounts.get(mAction)
|
||||
chooserScore += (float) pkStats.mChooserCounts.get(mAction)
|
||||
.getOrDefault(mAnnotations[i], 0);
|
||||
}
|
||||
}
|
||||
@ -169,44 +249,37 @@ class ResolverComparator implements Comparator<ResolvedComponentInfo> {
|
||||
Log.d(TAG, "Action type is null");
|
||||
} else {
|
||||
Log.d(TAG, "Chooser Count of " + mAction + ":" +
|
||||
target.name.getPackageName() + " is " + Integer.toString(selected));
|
||||
target.name.getPackageName() + " is " +
|
||||
Float.toString(chooserScore));
|
||||
}
|
||||
}
|
||||
scoredTarget.chooserCount = selected;
|
||||
if (selected > mostSelected) {
|
||||
mostSelected = selected;
|
||||
resolverTarget.setChooserScore(chooserScore);
|
||||
if (chooserScore > mostChooserScore) {
|
||||
mostChooserScore = chooserScore;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "compute - mostRecentlyUsedTime: " + mostRecentlyUsedTime
|
||||
+ " mostTimeSpent: " + mostTimeSpent
|
||||
+ " recentSinceTime: " + recentSinceTime
|
||||
+ " mostLaunched: " + mostLaunched);
|
||||
Log.d(TAG, "compute - mostRecencyScore: " + mostRecencyScore
|
||||
+ " mostTimeSpentScore: " + mostTimeSpentScore
|
||||
+ " mostLaunchScore: " + mostLaunchScore
|
||||
+ " mostChooserScore: " + mostChooserScore);
|
||||
}
|
||||
|
||||
for (ScoredTarget target : mScoredTargets.values()) {
|
||||
final float recency = (float) Math.max(target.lastTimeUsed - recentSinceTime, 0)
|
||||
/ (mostRecentlyUsedTime - recentSinceTime);
|
||||
target.setFeatures((float) target.launchCount / mostLaunched,
|
||||
(float) target.timeSpent / mostTimeSpent,
|
||||
recency * recency * RECENCY_MULTIPLIER,
|
||||
(float) target.chooserCount / mostSelected);
|
||||
target.selectProb = mRanker.predict(target.getFeatures());
|
||||
mTargets = new ArrayList<>(mTargetsDict.values());
|
||||
for (ResolverTarget target : mTargets) {
|
||||
final float recency = target.getRecencyScore() / mostRecencyScore;
|
||||
setFeatures(target, recency * recency * RECENCY_MULTIPLIER,
|
||||
target.getLaunchScore() / mostLaunchScore,
|
||||
target.getTimeSpentScore() / mostTimeSpentScore,
|
||||
target.getChooserScore() / mostChooserScore);
|
||||
addDefaultSelectProbability(target);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Scores: " + target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isPersistentProcess(ResolvedComponentInfo rci) {
|
||||
if (rci != null && rci.getCount() > 0) {
|
||||
return (rci.getResolveInfoAt(0).activityInfo.applicationInfo.flags &
|
||||
ApplicationInfo.FLAG_PERSISTENT) != 0;
|
||||
}
|
||||
return false;
|
||||
predictSelectProbabilities(mTargets);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -245,16 +318,16 @@ class ResolverComparator implements Comparator<ResolvedComponentInfo> {
|
||||
// Pinned items stay stable within a normal lexical sort and ignore scoring.
|
||||
if (!lPinned && !rPinned) {
|
||||
if (mStats != null) {
|
||||
final ScoredTarget lhsTarget = mScoredTargets.get(new ComponentName(
|
||||
final ResolverTarget lhsTarget = mTargetsDict.get(new ComponentName(
|
||||
lhs.activityInfo.packageName, lhs.activityInfo.name));
|
||||
final ScoredTarget rhsTarget = mScoredTargets.get(new ComponentName(
|
||||
final ResolverTarget rhsTarget = mTargetsDict.get(new ComponentName(
|
||||
rhs.activityInfo.packageName, rhs.activityInfo.name));
|
||||
|
||||
final int selectProbDiff = Float.compare(
|
||||
rhsTarget.selectProb, lhsTarget.selectProb);
|
||||
final int selectProbabilityDiff = Float.compare(
|
||||
rhsTarget.getSelectProbability(), lhsTarget.getSelectProbability());
|
||||
|
||||
if (selectProbDiff != 0) {
|
||||
return selectProbDiff > 0 ? 1 : -1;
|
||||
if (selectProbabilityDiff != 0) {
|
||||
return selectProbabilityDiff > 0 ? 1 : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -268,177 +341,234 @@ class ResolverComparator implements Comparator<ResolvedComponentInfo> {
|
||||
}
|
||||
|
||||
public float getScore(ComponentName name) {
|
||||
final ScoredTarget target = mScoredTargets.get(name);
|
||||
final ResolverTarget target = mTargetsDict.get(name);
|
||||
if (target != null) {
|
||||
return target.selectProb;
|
||||
return target.getSelectProbability();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static class ScoredTarget {
|
||||
public final ComponentInfo componentInfo;
|
||||
public long lastTimeUsed;
|
||||
public long timeSpent;
|
||||
public long launchCount;
|
||||
public long chooserCount;
|
||||
public ArrayMap<String, Float> features;
|
||||
public float selectProb;
|
||||
|
||||
public ScoredTarget(ComponentInfo ci) {
|
||||
componentInfo = ci;
|
||||
features = new ArrayMap<>(5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ScoredTarget{" + componentInfo
|
||||
+ " lastTimeUsed: " + lastTimeUsed
|
||||
+ " timeSpent: " + timeSpent
|
||||
+ " launchCount: " + launchCount
|
||||
+ " chooserCount: " + chooserCount
|
||||
+ " selectProb: " + selectProb
|
||||
+ "}";
|
||||
}
|
||||
|
||||
public void setFeatures(float launchCountScore, float usageTimeScore, float recencyScore,
|
||||
float chooserCountScore) {
|
||||
features.put(LAUNCH_SCORE, launchCountScore);
|
||||
features.put(TIME_SPENT_SCORE, usageTimeScore);
|
||||
features.put(RECENCY_SCORE, recencyScore);
|
||||
features.put(CHOOSER_SCORE, chooserCountScore);
|
||||
}
|
||||
|
||||
public ArrayMap<String, Float> getFeatures() {
|
||||
return features;
|
||||
}
|
||||
}
|
||||
|
||||
public void updateChooserCounts(String packageName, int userId, String action) {
|
||||
if (mUsm != null) {
|
||||
mUsm.reportChooserSelection(packageName, userId, mContentType, mAnnotations, action);
|
||||
}
|
||||
}
|
||||
|
||||
// update ranking model when the connection to it is valid.
|
||||
public void updateModel(ComponentName componentName) {
|
||||
if (mScoredTargets == null || componentName == null ||
|
||||
!mScoredTargets.containsKey(componentName)) {
|
||||
return;
|
||||
}
|
||||
ScoredTarget selected = mScoredTargets.get(componentName);
|
||||
for (ComponentName targetComponent : mScoredTargets.keySet()) {
|
||||
if (targetComponent.equals(componentName)) {
|
||||
continue;
|
||||
}
|
||||
ScoredTarget target = mScoredTargets.get(targetComponent);
|
||||
// A potential point of optimization. Save updates or derive a closed form for the
|
||||
// positive case, to avoid calculating them repeatedly.
|
||||
if (target.selectProb >= selected.selectProb) {
|
||||
mRanker.update(target.getFeatures(), target.selectProb, false);
|
||||
mRanker.update(selected.getFeatures(), selected.selectProb, true);
|
||||
synchronized (mLock) {
|
||||
if (mRanker != null) {
|
||||
try {
|
||||
int selectedPos = new ArrayList<ComponentName>(mTargetsDict.keySet())
|
||||
.indexOf(componentName);
|
||||
if (selectedPos > 0) {
|
||||
mRanker.train(mTargets, selectedPos);
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Selected a unknown component: " + componentName);
|
||||
}
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Error in Train: " + e);
|
||||
}
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Ranker is null; skip updateModel.");
|
||||
}
|
||||
}
|
||||
}
|
||||
mRanker.commitUpdate();
|
||||
}
|
||||
|
||||
class LogisticRegressionAppRanker {
|
||||
private static final String PARAM_SHARED_PREF_NAME = "resolver_ranker_params";
|
||||
private static final String BIAS_PREF_KEY = "bias";
|
||||
private static final String VERSION_PREF_KEY = "version";
|
||||
|
||||
// parameters for a pre-trained model, to initialize the app ranker. When updating the
|
||||
// pre-trained model, please update these params, as well as initModel().
|
||||
private static final int CURRENT_VERSION = 1;
|
||||
private static final float LEARNING_RATE = 0.0001f;
|
||||
private static final float REGULARIZER_PARAM = 0.0001f;
|
||||
|
||||
private SharedPreferences mParamSharedPref;
|
||||
private ArrayMap<String, Float> mFeatureWeights;
|
||||
private float mBias;
|
||||
|
||||
public LogisticRegressionAppRanker(Context context) {
|
||||
mParamSharedPref = getParamSharedPref(context);
|
||||
initModel();
|
||||
// unbind the service and clear unhandled messges.
|
||||
public void destroy() {
|
||||
mHandler.removeMessages(RESOLVER_RANKER_SERVICE_RESULT);
|
||||
mHandler.removeMessages(RESOLVER_RANKER_RESULT_TIMEOUT);
|
||||
if (mConnection != null) {
|
||||
mContext.unbindService(mConnection);
|
||||
mConnection.destroy();
|
||||
}
|
||||
|
||||
public float predict(ArrayMap<String, Float> target) {
|
||||
if (target == null) {
|
||||
return 0.0f;
|
||||
}
|
||||
final int featureSize = target.size();
|
||||
float sum = 0.0f;
|
||||
for (int i = 0; i < featureSize; i++) {
|
||||
String featureName = target.keyAt(i);
|
||||
float weight = mFeatureWeights.getOrDefault(featureName, 0.0f);
|
||||
sum += weight * target.valueAt(i);
|
||||
}
|
||||
return (float) (1.0 / (1.0 + Math.exp(-mBias - sum)));
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Unbinded Resolver Ranker.");
|
||||
}
|
||||
}
|
||||
|
||||
public void update(ArrayMap<String, Float> target, float predict, boolean isSelected) {
|
||||
if (target == null) {
|
||||
// connect to a ranking service.
|
||||
private void initRanker(Context context) {
|
||||
synchronized (mLock) {
|
||||
if (mConnection != null && mRanker != null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Ranker still exists; reusing the existing one.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
final int featureSize = target.size();
|
||||
float error = isSelected ? 1.0f - predict : -predict;
|
||||
for (int i = 0; i < featureSize; i++) {
|
||||
String featureName = target.keyAt(i);
|
||||
float currentWeight = mFeatureWeights.getOrDefault(featureName, 0.0f);
|
||||
mBias += LEARNING_RATE * error;
|
||||
currentWeight = currentWeight - LEARNING_RATE * REGULARIZER_PARAM * currentWeight +
|
||||
LEARNING_RATE * error * target.valueAt(i);
|
||||
mFeatureWeights.put(featureName, currentWeight);
|
||||
}
|
||||
Intent intent = resolveRankerService();
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
mConnectSignal = new CountDownLatch(1);
|
||||
mConnection = new ResolverRankerServiceConnection(mConnectSignal);
|
||||
context.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE, UserHandle.SYSTEM);
|
||||
}
|
||||
|
||||
// resolve the service for ranking.
|
||||
private Intent resolveRankerService() {
|
||||
Intent intent = new Intent(ResolverRankerService.SERVICE_INTERFACE);
|
||||
final List<ResolveInfo> resolveInfos = mPm.queryIntentServices(intent, 0);
|
||||
for (ResolveInfo resolveInfo : resolveInfos) {
|
||||
if (resolveInfo == null || resolveInfo.serviceInfo == null
|
||||
|| resolveInfo.serviceInfo.applicationInfo == null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Failed to retrieve a ranker: " + resolveInfo);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
ComponentName componentName = new ComponentName(
|
||||
resolveInfo.serviceInfo.applicationInfo.packageName,
|
||||
resolveInfo.serviceInfo.name);
|
||||
try {
|
||||
final String perm = mPm.getServiceInfo(componentName, 0).permission;
|
||||
if (!ResolverRankerService.BIND_PERMISSION.equals(perm)) {
|
||||
Log.w(TAG, "ResolverRankerService " + componentName + " does not require"
|
||||
+ " permission " + ResolverRankerService.BIND_PERMISSION
|
||||
+ " - this service will not be queried for ResolverComparator."
|
||||
+ " add android:permission=\""
|
||||
+ ResolverRankerService.BIND_PERMISSION + "\""
|
||||
+ " to the <service> tag for " + componentName
|
||||
+ " in the manifest.");
|
||||
continue;
|
||||
}
|
||||
} catch (NameNotFoundException e) {
|
||||
Log.e(TAG, "Could not look up service " + componentName
|
||||
+ "; component name not found");
|
||||
continue;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Weights: " + mFeatureWeights + " Bias: " + mBias);
|
||||
Log.d(TAG, "Succeeded to retrieve a ranker: " + componentName);
|
||||
}
|
||||
intent.setComponent(componentName);
|
||||
return intent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// set a watchdog, to avoid waiting for ranking service for too long.
|
||||
private void startWatchDog(int timeOutLimit) {
|
||||
if (DEBUG) Log.d(TAG, "Setting watchdog timer for " + timeOutLimit + "ms");
|
||||
if (mHandler == null) {
|
||||
Log.d(TAG, "Error: Handler is Null; Needs to be initialized.");
|
||||
}
|
||||
mHandler.sendEmptyMessageDelayed(RESOLVER_RANKER_RESULT_TIMEOUT, timeOutLimit);
|
||||
}
|
||||
|
||||
private class ResolverRankerServiceConnection implements ServiceConnection {
|
||||
private final CountDownLatch mConnectSignal;
|
||||
|
||||
public ResolverRankerServiceConnection(CountDownLatch connectSignal) {
|
||||
mConnectSignal = connectSignal;
|
||||
}
|
||||
|
||||
public void commitUpdate() {
|
||||
SharedPreferences.Editor editor = mParamSharedPref.edit();
|
||||
editor.putFloat(BIAS_PREF_KEY, mBias);
|
||||
final int size = mFeatureWeights.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
editor.putFloat(mFeatureWeights.keyAt(i), mFeatureWeights.valueAt(i));
|
||||
public final IResolverRankerResult resolverRankerResult =
|
||||
new IResolverRankerResult.Stub() {
|
||||
@Override
|
||||
public void sendResult(List<ResolverTarget> targets) throws RemoteException {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Sending Result back to Resolver: " + targets);
|
||||
}
|
||||
synchronized (mLock) {
|
||||
final Message msg = Message.obtain();
|
||||
msg.what = RESOLVER_RANKER_SERVICE_RESULT;
|
||||
msg.obj = targets;
|
||||
mHandler.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
editor.putInt(VERSION_PREF_KEY, CURRENT_VERSION);
|
||||
editor.apply();
|
||||
}
|
||||
};
|
||||
|
||||
private SharedPreferences getParamSharedPref(Context context) {
|
||||
// The package info in the context isn't initialized in the way it is for normal apps,
|
||||
// so the standard, name-based context.getSharedPreferences doesn't work. Instead, we
|
||||
// build the path manually below using the same policy that appears in ContextImpl.
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Context Package Name: " + context.getPackageName());
|
||||
Log.d(TAG, "onServiceConnected: " + name);
|
||||
}
|
||||
synchronized (mLock) {
|
||||
mRanker = IResolverRankerService.Stub.asInterface(service);
|
||||
mConnectSignal.countDown();
|
||||
}
|
||||
final File prefsFile = new File(new File(
|
||||
Environment.getDataUserCePackageDirectory(StorageManager.UUID_PRIVATE_INTERNAL,
|
||||
context.getUserId(), context.getPackageName()),
|
||||
"shared_prefs"),
|
||||
PARAM_SHARED_PREF_NAME + ".xml");
|
||||
return context.getSharedPreferences(prefsFile, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private void initModel() {
|
||||
mFeatureWeights = new ArrayMap<>(4);
|
||||
if (mParamSharedPref == null ||
|
||||
mParamSharedPref.getInt(VERSION_PREF_KEY, 0) < CURRENT_VERSION) {
|
||||
// Initializing the app ranker to a pre-trained model. When updating the pre-trained
|
||||
// model, please increment CURRENT_VERSION, and update LEARNING_RATE and
|
||||
// REGULARIZER_PARAM.
|
||||
mBias = -1.6568f;
|
||||
mFeatureWeights.put(LAUNCH_SCORE, 2.5543f);
|
||||
mFeatureWeights.put(TIME_SPENT_SCORE, 2.8412f);
|
||||
mFeatureWeights.put(RECENCY_SCORE, 0.269f);
|
||||
mFeatureWeights.put(CHOOSER_SCORE, 4.2222f);
|
||||
} else {
|
||||
mBias = mParamSharedPref.getFloat(BIAS_PREF_KEY, 0.0f);
|
||||
mFeatureWeights.put(LAUNCH_SCORE, mParamSharedPref.getFloat(LAUNCH_SCORE, 0.0f));
|
||||
mFeatureWeights.put(
|
||||
TIME_SPENT_SCORE, mParamSharedPref.getFloat(TIME_SPENT_SCORE, 0.0f));
|
||||
mFeatureWeights.put(RECENCY_SCORE, mParamSharedPref.getFloat(RECENCY_SCORE, 0.0f));
|
||||
mFeatureWeights.put(CHOOSER_SCORE, mParamSharedPref.getFloat(CHOOSER_SCORE, 0.0f));
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onServiceDisconnected: " + name);
|
||||
}
|
||||
synchronized (mLock) {
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
synchronized (mLock) {
|
||||
mRanker = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
mTargetsDict.clear();
|
||||
mTargets = null;
|
||||
startWatchDog(WATCHDOG_TIMEOUT_MILLIS);
|
||||
initRanker(mContext);
|
||||
}
|
||||
|
||||
// predict select probabilities if ranking service is valid.
|
||||
private void predictSelectProbabilities(List<ResolverTarget> targets) {
|
||||
if (mConnection == null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Has not found valid ResolverRankerService; Skip Prediction");
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
try {
|
||||
mConnectSignal.await(CONNECTION_COST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
|
||||
synchronized (mLock) {
|
||||
if (mRanker != null) {
|
||||
mRanker.predict(targets, mConnection.resolverRankerResult);
|
||||
return;
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Ranker has not been initialized; skip predict.");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Error in Wait for Service Connection.");
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Error in Predict: " + e);
|
||||
}
|
||||
}
|
||||
mAfterCompute.afterCompute();
|
||||
}
|
||||
|
||||
// adds select prob as the default values, according to a pre-trained Logistic Regression model.
|
||||
private void addDefaultSelectProbability(ResolverTarget target) {
|
||||
float sum = 2.5543f * target.getLaunchScore() + 2.8412f * target.getTimeSpentScore() +
|
||||
0.269f * target.getRecencyScore() + 4.2222f * target.getChooserScore();
|
||||
target.setSelectProbability((float) (1.0 / (1.0 + Math.exp(1.6568f - sum))));
|
||||
}
|
||||
|
||||
// sets features for each target
|
||||
private void setFeatures(ResolverTarget target, float recencyScore, float launchScore,
|
||||
float timeSpentScore, float chooserScore) {
|
||||
target.setRecencyScore(recencyScore);
|
||||
target.setLaunchScore(launchScore);
|
||||
target.setTimeSpentScore(timeSpentScore);
|
||||
target.setChooserScore(chooserScore);
|
||||
}
|
||||
|
||||
static boolean isPersistentProcess(ResolvedComponentInfo rci) {
|
||||
if (rci != null && rci.getCount() > 0) {
|
||||
return (rci.getResolveInfoAt(0).activityInfo.applicationInfo.flags &
|
||||
ApplicationInfo.FLAG_PERSISTENT) != 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -32,8 +32,10 @@ import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.lang.InterruptedException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -205,14 +207,42 @@ public class ResolverListController {
|
||||
return listToReturn;
|
||||
}
|
||||
|
||||
private class ComputeCallback implements ResolverComparator.AfterCompute {
|
||||
|
||||
private CountDownLatch mFinishComputeSignal;
|
||||
|
||||
public ComputeCallback(CountDownLatch finishComputeSignal) {
|
||||
mFinishComputeSignal = finishComputeSignal;
|
||||
}
|
||||
|
||||
public void afterCompute () {
|
||||
mFinishComputeSignal.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@WorkerThread
|
||||
public void sort(List<ResolverActivity.ResolvedComponentInfo> inputList) {
|
||||
final CountDownLatch finishComputeSignal = new CountDownLatch(1);
|
||||
ComputeCallback callback = new ComputeCallback(finishComputeSignal);
|
||||
if (mResolverComparator == null) {
|
||||
mResolverComparator = new ResolverComparator(mContext, mTargetIntent, mReferrerPackage);
|
||||
mResolverComparator =
|
||||
new ResolverComparator(mContext, mTargetIntent, mReferrerPackage, callback);
|
||||
} else {
|
||||
mResolverComparator.setCallBack(callback);
|
||||
}
|
||||
try {
|
||||
long beforeRank = System.currentTimeMillis();
|
||||
mResolverComparator.compute(inputList);
|
||||
finishComputeSignal.await();
|
||||
Collections.sort(inputList, mResolverComparator);
|
||||
long afterRank = System.currentTimeMillis();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Time Cost: " + Long.toString(afterRank - beforeRank));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Compute & Sort was interrupted: " + e);
|
||||
}
|
||||
mResolverComparator.compute(inputList);
|
||||
Collections.sort(inputList, mResolverComparator);
|
||||
}
|
||||
|
||||
private static boolean isSameResolvedComponent(ResolveInfo a,
|
||||
@ -233,7 +263,7 @@ public class ResolverListController {
|
||||
@VisibleForTesting
|
||||
public float getScore(ResolverActivity.DisplayResolveInfo target) {
|
||||
if (mResolverComparator == null) {
|
||||
mResolverComparator = new ResolverComparator(mContext, mTargetIntent, mReferrerPackage);
|
||||
return 0.0f;
|
||||
}
|
||||
return mResolverComparator.getScore(target.getResolvedComponentName());
|
||||
}
|
||||
@ -249,4 +279,10 @@ public class ResolverListController {
|
||||
mResolverComparator.updateChooserCounts(packageName, userId, action);
|
||||
}
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
if (mResolverComparator != null) {
|
||||
mResolverComparator.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3130,6 +3130,15 @@
|
||||
<permission android:name="android.permission.BIND_CHOOSER_TARGET_SERVICE"
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
<!-- @SystemApi Must be required by services that extend
|
||||
{@link android.service.resolver.ResolverRankerService}, to ensure that only the system can
|
||||
bind to them.
|
||||
<p>Protection level: signature
|
||||
@hide
|
||||
-->
|
||||
<permission android:name="android.permission.BIND_RESOLVER_RANKER_SERVICE"
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
<!-- Must be required by a {@link
|
||||
android.service.notification.ConditionProviderService},
|
||||
to ensure that only the system can bind to it.
|
||||
@ -3641,6 +3650,13 @@
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" >
|
||||
</service>
|
||||
|
||||
<service android:name="com.android.internal.app.LRResolverRankerService"
|
||||
android:permission="android.permission.BIND_RESOLVER_RANKER_SERVICE"
|
||||
android:priority="-1" >
|
||||
<intent-filter>
|
||||
<action android:name="android.service.resolver.ResolverRankerService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -183,6 +183,9 @@
|
||||
<!-- to control accessibility volume -->
|
||||
<uses-permission android:name="android.permission.CHANGE_ACCESSIBILITY_VOLUME" />
|
||||
|
||||
<!-- to access ResolverRankerServices -->
|
||||
<uses-permission android:name="android.permission.BIND_RESOLVER_RANKER_SERVICE" />
|
||||
|
||||
<application
|
||||
android:name=".SystemUIApplication"
|
||||
android:persistent="true"
|
||||
|
Reference in New Issue
Block a user