diff options
Diffstat (limited to 'go')
8 files changed, 503 insertions, 13 deletions
diff --git a/go/AndroidManifest.xml b/go/AndroidManifest.xml index 2671604f94..728b3262f0 100644 --- a/go/AndroidManifest.xml +++ b/go/AndroidManifest.xml @@ -54,6 +54,10 @@ android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" android:enabled="false" tools:node="replace" /> + + <service + android:name="com.android.launcher3.model.AppShareabilityJobService" + android:permission="android.permission.BIND_JOB_SERVICE" /> </application> </manifest> diff --git a/go/quickstep/res/values/integers.xml b/go/quickstep/res/values/integers.xml new file mode 100644 index 0000000000..e6e8111eab --- /dev/null +++ b/go/quickstep/res/values/integers.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<resources> + <!-- Job IDs must be integers unique within their package --> + <integer name="app_shareability_job_id">200</integer> +</resources>
\ No newline at end of file diff --git a/go/quickstep/src/com/android/launcher3/AppSharing.java b/go/quickstep/src/com/android/launcher3/AppSharing.java index b72e71c2b5..fd5456c73b 100644 --- a/go/quickstep/src/com/android/launcher3/AppSharing.java +++ b/go/quickstep/src/com/android/launcher3/AppSharing.java @@ -28,7 +28,12 @@ import android.view.View; import androidx.core.content.FileProvider; +import com.android.launcher3.model.AppShareabilityChecker; +import com.android.launcher3.model.AppShareabilityJobService; +import com.android.launcher3.model.AppShareabilityManager; +import com.android.launcher3.model.AppShareabilityManager.ShareabilityStatus; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.popup.SystemShortcut; import java.io.File; @@ -44,6 +49,11 @@ public final class AppSharing { * because it is unique to Go and not toggleable at runtime. */ public static final boolean ENABLE_APP_SHARING = true; + /** + * With this flag enabled, the Share App button will be dynamically enabled/disabled based + * on each app's shareability status. + */ + public static final boolean ENABLE_SHAREABILITY_CHECK = false; private static final String TAG = "AppSharing"; private static final String FILE_PROVIDER_SUFFIX = ".overview.fileprovider"; @@ -51,22 +61,12 @@ public final class AppSharing { private static final String APP_MIME_TYPE = "application/application"; private final String mSharingComponent; + private AppShareabilityManager mShareabilityMgr; private AppSharing(Launcher launcher) { mSharingComponent = launcher.getText(R.string.app_sharing_component).toString(); } - private boolean canShare(ItemInfo info) { - /** - * TODO: Implement once b/168831749 has been resolved - * The implementation should check the validity of the app. - * It should also check whether the app is free or paid, returning false in the latter case. - * For now, all checks occur in the sharing app. - * So, we simply check whether the sharing app is defined. - */ - return !TextUtils.isEmpty(mSharingComponent); - } - private Uri getShareableUri(Context context, String path, String displayName) { String authority = BuildConfig.APPLICATION_ID + FILE_PROVIDER_SUFFIX; File pathFile = new File(path); @@ -74,19 +74,39 @@ public final class AppSharing { } private SystemShortcut<Launcher> getShortcut(Launcher launcher, ItemInfo info) { - if (!canShare(info)) { + if (TextUtils.isEmpty(mSharingComponent)) { return null; } - return new Share(launcher, info); } /** + * Instantiates AppShareabilityManager, which then reads app shareability data from disk + * Also schedules a job to update those data + * @param context The application context + * @param checker An implementation of AppShareabilityChecker to perform the actual checks + * when updating the data + */ + public static void setUpShareabilityCache(Context context, AppShareabilityChecker checker) { + AppShareabilityManager shareMgr = AppShareabilityManager.INSTANCE.get(context); + shareMgr.setShareabilityChecker(checker); + AppShareabilityJobService.schedule(context); + } + + /** * The Share App system shortcut, used to initiate p2p sharing of a given app */ public final class Share extends SystemShortcut<Launcher> { + private PopupDataProvider mPopupDataProvider; + public Share(Launcher target, ItemInfo itemInfo) { super(R.drawable.ic_share, R.string.app_share_drop_target_label, target, itemInfo); + mPopupDataProvider = target.getPopupDataProvider(); + + if (ENABLE_SHAREABILITY_CHECK) { + mShareabilityMgr = AppShareabilityManager.INSTANCE.get(target); + checkShareability(/* requestUpdateIfUnknown */ true); + } } @Override @@ -122,6 +142,27 @@ public final class AppSharing { AbstractFloatingView.closeAllOpenViews(mTarget); } + + private void onStatusUpdated(boolean success) { + if (!success) { + // Something went wrong. Specific error logged in AppShareabilityManager. + return; + } + checkShareability(/* requestUpdateIfUnknown */ false); + mTarget.runOnUiThread(() -> { + mPopupDataProvider.redrawSystemShortcuts(); + }); + } + + private void checkShareability(boolean requestUpdateIfUnknown) { + String packageName = mItemInfo.getTargetComponent().getPackageName(); + @ShareabilityStatus int status = mShareabilityMgr.getStatus(packageName); + setEnabled(status == ShareabilityStatus.SHAREABLE); + + if (requestUpdateIfUnknown && status == ShareabilityStatus.UNKNOWN) { + mShareabilityMgr.requestAppStatusUpdate(packageName, this::onStatusUpdated); + } + } } /** diff --git a/go/quickstep/src/com/android/launcher3/model/AppShareabilityChecker.java b/go/quickstep/src/com/android/launcher3/model/AppShareabilityChecker.java new file mode 100644 index 0000000000..0a82904670 --- /dev/null +++ b/go/quickstep/src/com/android/launcher3/model/AppShareabilityChecker.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.model; + +import androidx.annotation.Nullable; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Interface for checking apps' shareability. Implementations need to be able to determine whether + * apps are shareable given their package names. + */ +public interface AppShareabilityChecker { + /** + * Checks the shareability of the provided apps. Once the check is complete, updates the + * provided manager with the results and calls the (optionally) provided callback. + * @param packageNames The apps to check + * @param shareMgr The manager to receive the results + * @param callback Optional callback to be invoked when the check is finished + */ + void checkApps(List<String> packageNames, AppShareabilityManager shareMgr, + @Nullable Consumer<Boolean> callback); +} diff --git a/go/quickstep/src/com/android/launcher3/model/AppShareabilityDatabase.java b/go/quickstep/src/com/android/launcher3/model/AppShareabilityDatabase.java new file mode 100644 index 0000000000..03eed7eccc --- /dev/null +++ b/go/quickstep/src/com/android/launcher3/model/AppShareabilityDatabase.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.model; + +import androidx.room.Dao; +import androidx.room.Database; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; +import androidx.room.RoomDatabase; +import androidx.room.Update; + +import java.util.List; + +/** + * This database maintains a collection of AppShareabilityStatus items + * In its intended use case, there will be one entry for each app installed on the device + */ +@Database(entities = {AppShareabilityStatus.class}, exportSchema = false, version = 1) +public abstract class AppShareabilityDatabase extends RoomDatabase { + /** + * Data Access Object for this database + */ + @Dao + public interface ShareabilityDao { + /** Add an AppShareabilityStatus to the database */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insertAppStatus(AppShareabilityStatus status); + + /** Add a collection of AppShareabilityStatus objects to the database */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insertAppStatuses(AppShareabilityStatus... statuses); + + /** + * Update an AppShareabilityStatus in the database + * @return The number of entries successfully updated + */ + @Update + int updateAppStatus(AppShareabilityStatus status); + + /** Retrieve all entries from the database */ + @Query("SELECT * FROM AppShareabilityStatus") + List<AppShareabilityStatus> getAllEntries(); + } + + protected abstract ShareabilityDao shareabilityDao(); +} diff --git a/go/quickstep/src/com/android/launcher3/model/AppShareabilityJobService.java b/go/quickstep/src/com/android/launcher3/model/AppShareabilityJobService.java new file mode 100644 index 0000000000..60bea53c70 --- /dev/null +++ b/go/quickstep/src/com/android/launcher3/model/AppShareabilityJobService.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.model; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.util.Log; + +import com.android.launcher3.R; + +/** + * A job to request AppShareabilityManager to update its shareability data + * The shareability status of an app is not expected to change often, so this job is only + * run periodically. + */ +public final class AppShareabilityJobService extends JobService { + + private static final String TAG = "AppShareabilityJobService"; + // Run this job once a week + private static final int RECURRENCE_INTERVAL_MILLIS = 604800000; + + @Override + public boolean onStartJob(final JobParameters params) { + Context context = getApplicationContext(); + AppShareabilityManager.INSTANCE.get(context).requestFullUpdate(); + return false; // Job is finished + } + + @Override + public boolean onStopJob(final JobParameters params) { + Log.d(TAG, "App shareability data update job stopped; id=" + params.getJobId() + + ", reason=" + + JobParameters.getInternalReasonCodeDescription(params.getStopReason())); + return true; // Reschedule the job + } + + /** + * Creates and schedules the job. + * Does not schedule a duplicate job if one is already pending. + * @param context The application context + */ + public static void schedule(Context context) { + final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + + final JobInfo pendingJob = jobScheduler.getPendingJob(R.integer.app_shareability_job_id); + if (pendingJob != null) { + // Don't schedule duplicate jobs + return; + } + + final JobInfo newJob = new JobInfo.Builder(R.integer.app_shareability_job_id, + new ComponentName(context, AppShareabilityJobService.class)) + .setPeriodic(RECURRENCE_INTERVAL_MILLIS) + .setPersisted(true) + .setRequiresBatteryNotLow(true) + .build(); + jobScheduler.schedule(newJob); + } +} diff --git a/go/quickstep/src/com/android/launcher3/model/AppShareabilityManager.java b/go/quickstep/src/com/android/launcher3/model/AppShareabilityManager.java new file mode 100644 index 0000000000..cf80c35972 --- /dev/null +++ b/go/quickstep/src/com/android/launcher3/model/AppShareabilityManager.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.model; + +import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.content.Context; +import android.content.pm.LauncherActivityInfo; +import android.content.pm.LauncherApps; +import android.os.Process; +import android.util.ArrayMap; +import android.util.Log; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import androidx.room.Room; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.launcher3.model.AppShareabilityDatabase.ShareabilityDao; +import com.android.launcher3.util.MainThreadInitializedObject; + +import java.lang.annotation.Retention; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * This class maintains the shareability status of installed apps. + * Each app's status is retrieved from the Play Store's API. Statuses are cached in order + * to limit extraneous calls to that API (which can be time-consuming). + */ +public final class AppShareabilityManager { + @Retention(SOURCE) + @IntDef({ + ShareabilityStatus.UNKNOWN, + ShareabilityStatus.NOT_SHAREABLE, + ShareabilityStatus.SHAREABLE + }) + public @interface ShareabilityStatus { + int UNKNOWN = 0; + int NOT_SHAREABLE = 1; + int SHAREABLE = 2; + } + + private static final String TAG = "AppShareabilityManager"; + private static final String DB_NAME = "shareabilityDatabase"; + public static MainThreadInitializedObject<AppShareabilityManager> INSTANCE = + new MainThreadInitializedObject<>(AppShareabilityManager::new); + + private final Context mContext; + // Local map to store the data in memory for quick access + private final Map<String, Integer> mDataMap; + // Database to persist the data across reboots + private AppShareabilityDatabase mDatabase; + // Data Access Object for the database + private ShareabilityDao mDao; + // Class to perform shareability checks + private AppShareabilityChecker mShareChecker; + + private AppShareabilityManager(Context context) { + mContext = context; + mDataMap = new ArrayMap<>(); + mDatabase = Room.databaseBuilder(mContext, AppShareabilityDatabase.class, DB_NAME).build(); + mDao = mDatabase.shareabilityDao(); + MODEL_EXECUTOR.post(this::readFromDB); + } + + /** + * Set the shareability checker. The checker determines whether given apps are shareable. + * This must be set before the manager can update its data. + * @param checker Implementation of AppShareabilityChecker to perform the checks + */ + public void setShareabilityChecker(AppShareabilityChecker checker) { + mShareChecker = checker; + } + + /** + * Retrieve the ShareabilityStatus of an app from the local map + * This does not interact with the saved database + * @param packageName The app's package name + * @return The status as a ShareabilityStatus integer + */ + public synchronized @ShareabilityStatus int getStatus(String packageName) { + @ShareabilityStatus int status = ShareabilityStatus.UNKNOWN; + if (mDataMap.containsKey(packageName)) { + status = mDataMap.get(packageName); + } + return status; + } + + /** + * Set the status of a given app. This updates the local map as well as the saved database. + */ + public synchronized void setStatus(String packageName, @ShareabilityStatus int status) { + mDataMap.put(packageName, status); + + // Write to the database on a separate thread + MODEL_EXECUTOR.post(() -> + mDao.insertAppStatus(new AppShareabilityStatus(packageName, status))); + } + + /** + * Set the statuses of given apps. This updates the local map as well as the saved database. + */ + public synchronized void setStatuses(List<AppShareabilityStatus> statuses) { + for (int i = 0, size = statuses.size(); i < size; i++) { + AppShareabilityStatus entry = statuses.get(i); + mDataMap.put(entry.packageName, entry.status); + } + + // Write to the database on a separate thread + MODEL_EXECUTOR.post(() -> + mDao.insertAppStatuses(statuses.toArray(new AppShareabilityStatus[0]))); + } + + /** + * Request a status update for a specific app + * @param packageName The app's package name + * @param callback Optional callback to be called when the update is complete. The received + * Boolean denotes whether the update was successful. + */ + public void requestAppStatusUpdate(String packageName, @Nullable Consumer<Boolean> callback) { + MODEL_EXECUTOR.post(() -> updateCache(packageName, callback)); + } + + /** + * Request a status update for all apps + */ + public void requestFullUpdate() { + MODEL_EXECUTOR.post(this::updateCache); + } + + /** + * Update the cached shareability data for all installed apps + */ + @WorkerThread + private void updateCache() { + updateCache(/* packageName */ null, /* callback */ null); + } + + /** + * Update the cached shareability data + * @param packageName A specific package to update. If null, all installed apps will be updated. + * @param callback Optional callback to be called when the update is complete. The received + * Boolean denotes whether the update was successful. + */ + @WorkerThread + private void updateCache(@Nullable String packageName, @Nullable Consumer<Boolean> callback) { + if (mShareChecker == null) { + Log.e(TAG, "AppShareabilityChecker not set"); + return; + } + + List<String> packageNames = new ArrayList<>(); + if (packageName != null) { + packageNames.add(packageName); + } else { + LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class); + List<LauncherActivityInfo> installedApps = + launcherApps.getActivityList(/* packageName */ null, Process.myUserHandle()); + for (int i = 0, size = installedApps.size(); i < size; i++) { + packageNames.add(installedApps.get(i).getApplicationInfo().packageName); + } + } + + mShareChecker.checkApps(packageNames, this, callback); + } + + @WorkerThread + private synchronized void readFromDB() { + mDataMap.clear(); + List<AppShareabilityStatus> entries = mDao.getAllEntries(); + for (int i = 0, size = entries.size(); i < size; i++) { + AppShareabilityStatus entry = entries.get(i); + mDataMap.put(entry.packageName, entry.status); + } + } + + /** + * Provides a testable instance of this class + * This instance allows database queries on the main thread + * @hide */ + @VisibleForTesting + public static AppShareabilityManager getTestInstance(Context context) { + AppShareabilityManager manager = new AppShareabilityManager(context); + manager.mDatabase.close(); + manager.mDatabase = Room.inMemoryDatabaseBuilder(context, AppShareabilityDatabase.class) + .allowMainThreadQueries() + .build(); + manager.mDao = manager.mDatabase.shareabilityDao(); + return manager; + } +} diff --git a/go/quickstep/src/com/android/launcher3/model/AppShareabilityStatus.java b/go/quickstep/src/com/android/launcher3/model/AppShareabilityStatus.java new file mode 100644 index 0000000000..61018c6b25 --- /dev/null +++ b/go/quickstep/src/com/android/launcher3/model/AppShareabilityStatus.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.model; + +import androidx.annotation.NonNull; +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +import com.android.launcher3.model.AppShareabilityManager.ShareabilityStatus; + +/** + * Database entry to hold the shareability status of a single app + */ +@Entity +public class AppShareabilityStatus { + @PrimaryKey + @NonNull + public String packageName; + + public @ShareabilityStatus int status; + + public AppShareabilityStatus(@NonNull String packageName, @ShareabilityStatus int status) { + this.packageName = packageName; + this.status = status; + } +} |