diff options
| -rw-r--r-- | Android.mk | 3 | ||||
| -rw-r--r-- | api/system-current.txt | 31 | ||||
| -rw-r--r-- | core/java/android/service/resolver/IResolverRankerResult.aidl | 27 | ||||
| -rw-r--r-- | core/java/android/service/resolver/IResolverRankerService.aidl | 29 | ||||
| -rw-r--r-- | core/java/android/service/resolver/ResolverRankerService.java | 187 | ||||
| -rw-r--r-- | core/java/android/service/resolver/ResolverTarget.aidl | 22 | ||||
| -rw-r--r-- | core/java/android/service/resolver/ResolverTarget.java | 216 | ||||
| -rw-r--r-- | core/java/com/android/internal/app/LRResolverRankerService.java | 199 | ||||
| -rw-r--r-- | core/java/com/android/internal/app/ResolverActivity.java | 3 | ||||
| -rw-r--r-- | core/java/com/android/internal/app/ResolverComparator.java | 516 | ||||
| -rw-r--r-- | core/java/com/android/internal/app/ResolverListController.java | 44 | ||||
| -rw-r--r-- | core/res/AndroidManifest.xml | 16 | ||||
| -rw-r--r-- | packages/SystemUI/AndroidManifest.xml | 3 |
13 files changed, 1099 insertions, 197 deletions
diff --git a/Android.mk b/Android.mk index 01fb73e0b5d1..915f103fca0c 100644 --- a/Android.mk +++ b/Android.mk @@ -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 \ diff --git a/api/system-current.txt b/api/system-current.txt index 5fff1166f052..e166eadc788a 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -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 { diff --git a/core/java/android/service/resolver/IResolverRankerResult.aidl b/core/java/android/service/resolver/IResolverRankerResult.aidl new file mode 100644 index 000000000000..bda315420b7b --- /dev/null +++ b/core/java/android/service/resolver/IResolverRankerResult.aidl @@ -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); +}
\ No newline at end of file diff --git a/core/java/android/service/resolver/IResolverRankerService.aidl b/core/java/android/service/resolver/IResolverRankerService.aidl new file mode 100644 index 000000000000..f0d747d974a7 --- /dev/null +++ b/core/java/android/service/resolver/IResolverRankerService.aidl @@ -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); +}
\ No newline at end of file diff --git a/core/java/android/service/resolver/ResolverRankerService.java b/core/java/android/service/resolver/ResolverRankerService.java new file mode 100644 index 000000000000..05067479bf45 --- /dev/null +++ b/core/java/android/service/resolver/ResolverRankerService.java @@ -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); + } + } + } +} diff --git a/core/java/android/service/resolver/ResolverTarget.aidl b/core/java/android/service/resolver/ResolverTarget.aidl new file mode 100644 index 000000000000..6cab2d4df908 --- /dev/null +++ b/core/java/android/service/resolver/ResolverTarget.aidl @@ -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; diff --git a/core/java/android/service/resolver/ResolverTarget.java b/core/java/android/service/resolver/ResolverTarget.java new file mode 100644 index 000000000000..fb3e2d738469 --- /dev/null +++ b/core/java/android/service/resolver/ResolverTarget.java @@ -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]; + } + }; +} diff --git a/core/java/com/android/internal/app/LRResolverRankerService.java b/core/java/com/android/internal/app/LRResolverRankerService.java new file mode 100644 index 000000000000..1cad7c770b7c --- /dev/null +++ b/core/java/com/android/internal/app/LRResolverRankerService.java @@ -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); + } +}
\ No newline at end of file diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 3f1c9adb1b68..622b70843cc2 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -530,6 +530,9 @@ public class ResolverActivity extends Activity { getMainThreadHandler().removeCallbacks(mPostListReadyRunnable); mPostListReadyRunnable = null; } + if (mAdapter != null && mAdapter.mResolverListController != null) { + mAdapter.mResolverListController.destroy(); + } } @Override diff --git a/core/java/com/android/internal/app/ResolverComparator.java b/core/java/com/android/internal/app/ResolverComparator.java index 096fcb83e755..73b62a5fe60d 100644 --- a/core/java/com/android/internal/app/ResolverComparator.java +++ b/core/java/com/android/internal/app/ResolverComparator.java @@ -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; + + 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; - public ResolverComparator(Context context, Intent intent, String referrerPackage) { + 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); + public void updateChooserCounts(String packageName, int userId, String action) { + if (mUsm != null) { + mUsm.reportChooserSelection(packageName, userId, mContentType, mAnnotations, action); } + } - @Override - public String toString() { - return "ScoredTarget{" + componentInfo - + " lastTimeUsed: " + lastTimeUsed - + " timeSpent: " + timeSpent - + " launchCount: " + launchCount - + " chooserCount: " + chooserCount - + " selectProb: " + selectProb - + "}"; + // update ranking model when the connection to it is valid. + public void updateModel(ComponentName componentName) { + 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."); + } + } } + } - 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); + // 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 ArrayMap<String, Float> getFeatures() { - return features; + if (DEBUG) { + Log.d(TAG, "Unbinded Resolver Ranker."); } } - public void updateChooserCounts(String packageName, int userId, String action) { - if (mUsm != null) { - mUsm.reportChooserSelection(packageName, userId, mContentType, mAnnotations, action); + // 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; + } } - } - - public void updateModel(ComponentName componentName) { - if (mScoredTargets == null || componentName == null || - !mScoredTargets.containsKey(componentName)) { + Intent intent = resolveRankerService(); + if (intent == null) { return; } - ScoredTarget selected = mScoredTargets.get(componentName); - for (ComponentName targetComponent : mScoredTargets.keySet()) { - if (targetComponent.equals(componentName)) { + 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; } - 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); + 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, "Succeeded to retrieve a ranker: " + componentName); } + intent.setComponent(componentName); + return intent; } - mRanker.commitUpdate(); + return null; } - 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; + // 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 SharedPreferences mParamSharedPref; - private ArrayMap<String, Float> mFeatureWeights; - private float mBias; + private class ResolverRankerServiceConnection implements ServiceConnection { + private final CountDownLatch mConnectSignal; - public LogisticRegressionAppRanker(Context context) { - mParamSharedPref = getParamSharedPref(context); - initModel(); + public ResolverRankerServiceConnection(CountDownLatch connectSignal) { + mConnectSignal = connectSignal; } - 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); + 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); + } } - return (float) (1.0 / (1.0 + Math.exp(-mBias - sum))); - } + }; - public void update(ArrayMap<String, Float> target, float predict, boolean isSelected) { - if (target == null) { - return; + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (DEBUG) { + Log.d(TAG, "onServiceConnected: " + name); } - 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); + synchronized (mLock) { + mRanker = IResolverRankerService.Stub.asInterface(service); + mConnectSignal.countDown(); } + } + + @Override + public void onServiceDisconnected(ComponentName name) { if (DEBUG) { - Log.d(TAG, "Weights: " + mFeatureWeights + " Bias: " + mBias); + Log.d(TAG, "onServiceDisconnected: " + name); + } + synchronized (mLock) { + destroy(); } } - 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 void destroy() { + synchronized (mLock) { + mRanker = null; } - 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. + 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, "Context Package Name: " + context.getPackageName()); + 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); } - 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); } + mAfterCompute.afterCompute(); + } - 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)); - } + // 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; } } diff --git a/core/java/com/android/internal/app/ResolverListController.java b/core/java/com/android/internal/app/ResolverListController.java index 4071ff4ebd5a..e8bebb74bdd0 100644 --- a/core/java/com/android/internal/app/ResolverListController.java +++ b/core/java/com/android/internal/app/ResolverListController.java @@ -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(); + } + } } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 313041e8e0c2..cf6f37fc7972 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -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> diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 1147f16f5522..bf39bc4b96ea 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -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" |