Android One AppPredictionService final version. Used to provide simple prediction as top row of app drawer.

Test: tested on crosshatch
BUG: b/130048609
Change-Id: I5d8d905afe3819f2e6eca0ae5e1914cb3f0ac9a1
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..da9b552
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,50 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_defaults {
+    name: "apppredictionservice_defaults",
+
+    privileged: true,
+
+    certificate: "platform",
+
+    optimize: {
+    },
+
+    sdk_version: "system_current",
+}
+
+filegroup {
+    name: "AppPredictionService-srcs",
+    srcs: [
+        "src/**/*.java",
+    ],
+}
+
+android_app {
+    name: "AppPredictionService",
+
+    defaults: ["apppredictionservice_defaults"],
+
+    manifest: "AndroidManifest.xml",
+
+    srcs: [
+        ":AppPredictionService-srcs",
+    ],
+
+    resource_dirs: [
+    ],
+
+    required: ["privapp_whitelist_com.android.apppredictionservice"],
+}
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..c85ddb6
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.apppredictionservice">
+
+    <application>
+        <service
+            android:name=".PredictionService"
+            android:exported="true">
+            <intent-filter>
+                <!-- This constant must match AppPredictionService.SERVICE_INTERFACE -->
+                <action android:name="android.service.appprediction.AppPredictionService" />
+            </intent-filter>
+        </service>
+
+    </application>
+</manifest>
diff --git a/src/com/android/apppredictionservice/PredictionService.java b/src/com/android/apppredictionservice/PredictionService.java
new file mode 100644
index 0000000..b25edeb
--- /dev/null
+++ b/src/com/android/apppredictionservice/PredictionService.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.apppredictionservice;
+
+import static android.os.Process.myUserHandle;
+import static android.text.TextUtils.isEmpty;
+
+import static java.util.Collections.emptyList;
+
+import android.app.prediction.AppPredictionContext;
+import android.app.prediction.AppPredictionSessionId;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.app.prediction.AppTargetId;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.CancellationSignal;
+import android.service.appprediction.AppPredictionService;
+import android.util.Log;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+
+/*
+ * New plugin that replaces prediction driven Aiai APK in P
+ * PredictionService simply populates the top row of the app
+ * drawer with the 5 most recently used apps. Each time a new
+ * app is launched, it is added to the left of the top row.
+ * Duplicates are not added.
+ */
+public class PredictionService extends AppPredictionService {
+
+    private static final String TAG = PredictionService.class.getSimpleName();
+
+    private final Set<AppPredictionSessionId> activeLauncherSessions = new HashSet<>();
+
+    private boolean mAppSuggestionsEnabled = true;
+
+    public static final String MY_PREF = "mypref";
+
+    private final List<AppTarget> predictionList = new ArrayList<>(5);
+
+    private final List<String> appNames = new ArrayList<>(5);
+    private final String[] appNameKeys = new String[] {
+            "first", "second", "third", "fourth", "fifth" };
+
+    SharedPreferences sharedPreferences;
+    SharedPreferences.Editor editor;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        Intent calendarIntent = new Intent(Intent.ACTION_MAIN);
+        calendarIntent.addCategory(Intent.CATEGORY_APP_CALENDAR);
+
+        Intent galleryIntent = new Intent(Intent.ACTION_MAIN);
+        galleryIntent.addCategory(Intent.CATEGORY_APP_GALLERY);
+
+        Intent mapsIntent = new Intent(Intent.ACTION_MAIN);
+        mapsIntent.addCategory(Intent.CATEGORY_APP_MAPS);
+
+        Intent emailIntent = new Intent(Intent.ACTION_MAIN);
+        emailIntent.addCategory(Intent.CATEGORY_APP_EMAIL);
+
+        Intent browserIntent = new Intent(Intent.ACTION_MAIN);
+        browserIntent.addCategory(Intent.CATEGORY_APP_BROWSER);
+
+        String[] DEFAULT_PACKAGES = new String[] {
+              getDefaultSystemHandlerActivityPackageName(calendarIntent),
+              getDefaultSystemHandlerActivityPackageName(galleryIntent),
+              getDefaultSystemHandlerActivityPackageName(mapsIntent),
+              getDefaultSystemHandlerActivityPackageName(emailIntent),
+              getDefaultSystemHandlerActivityPackageName(browserIntent),
+        };
+
+        Log.d(TAG, "AppPredictionService onCreate");
+        this.sharedPreferences = getSharedPreferences(MY_PREF, Context.MODE_PRIVATE);
+        this.editor = sharedPreferences.edit();
+
+        if (sharedPreferences.getString(appNameKeys[0], "").isEmpty()) {
+            // fill the list with defaults if first one is null when devices powers up for the first time
+            for (int i = 0; i < appNameKeys.length; i++) {
+                editor.putString(appNameKeys[i],
+                        getLauncherComponent(DEFAULT_PACKAGES[i]).flattenToShortString());
+            }
+            this.editor.apply();
+        }
+
+        for (int i = 0; i < appNameKeys.length; i++) {
+            String appName = sharedPreferences.getString(appNameKeys[i], "");
+            ComponentName cn = ComponentName.unflattenFromString(appName);
+            AppTarget target = new AppTarget.Builder(
+                    new AppTargetId(Integer.toString(i + 1)), cn.getPackageName(), myUserHandle())
+                    .setClassName(cn.getClassName())
+                    .build();
+            appNames.add(appName);
+            predictionList.add(target);
+        }
+        postPredictionUpdateToAllClients();
+    }
+
+    private ComponentName getLauncherComponent(String packageName) {
+        List<LauncherActivityInfo> infos = getSystemService(LauncherApps.class)
+                .getActivityList(packageName, myUserHandle());
+        if (infos.isEmpty()) {
+            return new ComponentName(packageName, "#");
+        } else {
+            return infos.get(0).getComponentName();
+        }
+    }
+
+    private void postPredictionUpdate(AppPredictionSessionId sessionId) {
+        updatePredictions(sessionId, mAppSuggestionsEnabled ? predictionList : emptyList());
+    }
+
+    private void postPredictionUpdateToAllClients() {
+        for (AppPredictionSessionId session : activeLauncherSessions) {
+            postPredictionUpdate(session);
+        }
+    }
+
+    @Override
+    public void onCreatePredictionSession(
+            AppPredictionContext context, AppPredictionSessionId sessionId) {
+        Log.d(TAG, "onCreatePredictionSession");
+
+        if (context.getUiSurface().equals("home") || context.getUiSurface().equals("overview")) {
+            activeLauncherSessions.add(sessionId);
+            postPredictionUpdate(sessionId);
+        }
+    }
+
+    @Override
+    public void onAppTargetEvent(AppPredictionSessionId sessionId, AppTargetEvent event) {
+
+        if (!activeLauncherSessions.contains(sessionId)) {
+            return;
+        }
+
+        boolean found = false;
+        Log.d(TAG, "onAppTargetEvent");
+
+        AppTarget target = event.getTarget();
+        if (target == null || isEmpty(target.getPackageName()) || isEmpty(target.getClassName())) {
+            return;
+        }
+        String mostRecentComponent = new ComponentName(
+                target.getPackageName(), target.getClassName()).flattenToString();
+
+        // Check if packageName already exists in existing list of appNames
+        for (String packageName:appNames) {
+            if (packageName.contains(target.getPackageName())) {
+                found = true;
+                break;
+            }
+        }
+
+        if (!found) {
+            appNames.remove(appNames.size() - 1);
+            appNames.add(0, mostRecentComponent);
+
+            for (int i = 0; i < appNameKeys.length; i++) {
+                editor.putString(appNameKeys[i], appNames.get(i));
+            }
+            editor.apply();
+
+            predictionList.remove(predictionList.size() - 1);
+            predictionList.add(0, event.getTarget());
+
+            Log.d(TAG, "onAppTargetEvent:: update predictions");
+            postPredictionUpdateToAllClients();
+        }
+    }
+
+    @Override
+    public void onLaunchLocationShown(
+            AppPredictionSessionId sessionId, String launchLocation, List<AppTargetId> targetIds) {
+        Log.d(TAG, "onLaunchLocationShown");
+    }
+
+    @Override
+    public void onSortAppTargets(
+            AppPredictionSessionId sessionId,
+            List<AppTarget> targets,
+            CancellationSignal cancellationSignal,
+            Consumer<List<AppTarget>> callback) {
+
+        Log.d(TAG, "onSortAppTargets");
+        if (!activeLauncherSessions.contains(sessionId)) {
+            callback.accept(emptyList());
+        } else {
+            // No-op
+            callback.accept(targets);
+        }
+    }
+
+    @Override
+    public void onRequestPredictionUpdate(AppPredictionSessionId sessionId) {
+        Log.d(TAG, "onRequestPredictionUpdate");
+
+        if (!activeLauncherSessions.contains(sessionId)) {
+            updatePredictions(sessionId, emptyList());
+        } else {
+            postPredictionUpdate(sessionId);
+            Log.d(TAG, "update predictions");
+        }
+    }
+
+    @Override
+    public void onDestroyPredictionSession(AppPredictionSessionId sessionId) {
+        Log.d(TAG, "onDestroyPredictionSession");
+        activeLauncherSessions.remove(sessionId);
+    }
+
+    @Override
+    public void onStartPredictionUpdates() {
+        Log.d(TAG, "onStartPredictionUpdates");
+    }
+
+    @Override
+    public void onStopPredictionUpdates() {
+        Log.d(TAG, "onStopPredictionUpdates");
+    }
+
+    public void setAppSuggestionsEnabled(boolean enabled) {
+        mAppSuggestionsEnabled = enabled;
+        postPredictionUpdateToAllClients();
+    }
+
+    private String getDefaultSystemHandlerActivityPackageName(Intent intent) {
+        return getDefaultSystemHandlerActivityPackageName(intent, 0);
+    }
+
+    private String getDefaultSystemHandlerActivityPackageName(Intent intent, int flags) {
+        ResolveInfo handler = getPackageManager().resolveActivity(intent, flags | PackageManager.MATCH_SYSTEM_ONLY);
+        if (handler == null) {
+            return null;
+        }
+        if ((handler.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+            return handler.activityInfo.packageName;
+        }
+        return null;
+    }
+}