1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3; 18 19 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_APP_SHARE_TAP; 20 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageInfo; 25 import android.content.pm.PackageManager; 26 import android.net.Uri; 27 import android.os.Process; 28 import android.os.UserHandle; 29 import android.os.UserManager; 30 import android.provider.Settings; 31 import android.text.TextUtils; 32 import android.util.Log; 33 import android.view.View; 34 import android.widget.ImageView; 35 import android.widget.TextView; 36 import android.widget.Toast; 37 38 import androidx.core.content.FileProvider; 39 40 import com.android.internal.annotations.VisibleForTesting; 41 import com.android.launcher3.logging.StatsLogManager; 42 import com.android.launcher3.model.AppShareabilityChecker; 43 import com.android.launcher3.model.AppShareabilityJobService; 44 import com.android.launcher3.model.AppShareabilityManager; 45 import com.android.launcher3.model.AppShareabilityManager.ShareabilityStatus; 46 import com.android.launcher3.model.data.ItemInfo; 47 import com.android.launcher3.popup.SystemShortcut; 48 import com.android.launcher3.views.ActivityContext; 49 50 import java.io.File; 51 import java.util.Collections; 52 import java.util.Set; 53 import java.util.WeakHashMap; 54 55 /** 56 * Defines the Share system shortcut and its factory. 57 * This shortcut can be added to the app long-press menu on the home screen. 58 * Clicking the button will initiate peer-to-peer sharing of the app. 59 */ 60 public final class AppSharing { 61 /** 62 * This flag enables this feature. It is defined here rather than in launcher3's FeatureFlags 63 * because it is unique to Go and not toggleable at runtime. 64 */ 65 public static final boolean ENABLE_APP_SHARING = true; 66 /** 67 * With this flag enabled, the Share App button will be dynamically enabled/disabled based 68 * on each app's shareability status. 69 */ 70 public static final boolean ENABLE_SHAREABILITY_CHECK = true; 71 72 private static final String TAG = "AppSharing"; 73 private static final String FILE_PROVIDER_SUFFIX = ".overview.fileprovider"; 74 private static final String APP_EXTENSION = ".apk"; 75 private static final String APP_MIME_TYPE = "application/application"; 76 77 private final String mSharingComponent; 78 private AppShareabilityManager mShareabilityMgr; 79 AppSharing(Launcher launcher)80 private AppSharing(Launcher launcher) { 81 String sharingComponent = Settings.Secure.getString(launcher.getContentResolver(), 82 Settings.Secure.NEARBY_SHARING_COMPONENT); 83 mSharingComponent = TextUtils.isEmpty(sharingComponent) ? launcher.getText( 84 R.string.app_sharing_component).toString() : sharingComponent; 85 } 86 getShareableUri(Context context, String path, String displayName)87 private Uri getShareableUri(Context context, String path, String displayName) { 88 String authority = BuildConfig.APPLICATION_ID + FILE_PROVIDER_SUFFIX; 89 File pathFile = new File(path); 90 return FileProvider.getUriForFile(context, authority, pathFile, displayName); 91 } 92 getShortcut(Launcher launcher, ItemInfo info, View originalView)93 private SystemShortcut<Launcher> getShortcut(Launcher launcher, ItemInfo info, 94 View originalView) { 95 if (TextUtils.isEmpty(mSharingComponent)) { 96 return null; 97 } 98 return new Share(launcher, info, originalView); 99 } 100 101 /** 102 * Instantiates AppShareabilityManager, which then reads app shareability data from disk 103 * Also schedules a job to update those data 104 * @param context The application context 105 * @param checker An implementation of AppShareabilityChecker to perform the actual checks 106 * when updating the data 107 */ setUpShareabilityCache(Context context, AppShareabilityChecker checker)108 public static void setUpShareabilityCache(Context context, AppShareabilityChecker checker) { 109 AppShareabilityManager shareMgr = AppShareabilityManager.INSTANCE.get(context); 110 shareMgr.setShareabilityChecker(checker); 111 AppShareabilityJobService.schedule(context); 112 } 113 114 /** 115 * The Share App system shortcut, used to initiate p2p sharing of a given app 116 */ 117 public final class Share extends SystemShortcut<Launcher> { 118 private final boolean mSharingEnabledForUser; 119 120 private final Set<View> mBoundViews = Collections.newSetFromMap(new WeakHashMap<>()); 121 private boolean mIsEnabled = true; 122 private StatsLogManager mStatsLogManager; 123 Share(Launcher target, ItemInfo itemInfo, View originalView)124 public Share(Launcher target, ItemInfo itemInfo, View originalView) { 125 super(R.drawable.ic_share, R.string.app_share_drop_target_label, target, itemInfo, 126 originalView); 127 mStatsLogManager = ActivityContext.lookupContext(originalView.getContext()) 128 .getStatsLogManager(); 129 mSharingEnabledForUser = bluetoothSharingEnabled(target); 130 if (!mSharingEnabledForUser) { 131 setEnabled(false); 132 } else if (ENABLE_SHAREABILITY_CHECK) { 133 mShareabilityMgr = 134 AppShareabilityManager.INSTANCE.get(target.getApplicationContext()); 135 checkShareability(/* requestUpdateIfUnknown */ true); 136 } 137 } 138 139 @Override setIconAndLabelFor(View iconView, TextView labelView)140 public void setIconAndLabelFor(View iconView, TextView labelView) { 141 super.setIconAndLabelFor(iconView, labelView); 142 mBoundViews.add(iconView); 143 mBoundViews.add(labelView); 144 } 145 146 @Override setIconAndContentDescriptionFor(ImageView view)147 public void setIconAndContentDescriptionFor(ImageView view) { 148 super.setIconAndContentDescriptionFor(view); 149 mBoundViews.add(view); 150 } 151 152 @Override onClick(View view)153 public void onClick(View view) { 154 mStatsLogManager.logger().log(LAUNCHER_SYSTEM_SHORTCUT_APP_SHARE_TAP); 155 if (!mIsEnabled) { 156 showCannotShareToast(view.getContext()); 157 return; 158 } 159 160 Intent sendIntent = new Intent(); 161 sendIntent.setAction(Intent.ACTION_SEND); 162 163 ComponentName targetComponent = mItemInfo.getTargetComponent(); 164 if (targetComponent == null) { 165 Log.e(TAG, "Item missing target component"); 166 return; 167 } 168 String packageName = targetComponent.getPackageName(); 169 PackageManager packageManager = view.getContext().getPackageManager(); 170 String sourceDir, appLabel; 171 try { 172 PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); 173 sourceDir = packageInfo.applicationInfo.sourceDir; 174 appLabel = packageManager.getApplicationLabel(packageInfo.applicationInfo) 175 + APP_EXTENSION; 176 } catch (Exception e) { 177 Log.e(TAG, "Could not find info for package \"" + packageName + "\""); 178 return; 179 } 180 Uri uri = getShareableUri(view.getContext(), sourceDir, appLabel); 181 sendIntent.putExtra(Intent.EXTRA_STREAM, uri); 182 sendIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); 183 184 sendIntent.setType(APP_MIME_TYPE); 185 sendIntent.setComponent(ComponentName.unflattenFromString(mSharingComponent)); 186 187 UserHandle user = mItemInfo.user; 188 if (user != null && !user.equals(Process.myUserHandle())) { 189 mTarget.startActivityAsUser(sendIntent, user); 190 } else { 191 mTarget.startActivitySafely(view, sendIntent, mItemInfo); 192 } 193 194 AbstractFloatingView.closeAllOpenViews(mTarget); 195 } 196 onStatusUpdated(boolean success)197 private void onStatusUpdated(boolean success) { 198 if (!success) { 199 // Something went wrong. Specific error logged in AppShareabilityManager. 200 return; 201 } 202 checkShareability(/* requestUpdateIfUnknown */ false); 203 } 204 checkShareability(boolean requestUpdateIfUnknown)205 private void checkShareability(boolean requestUpdateIfUnknown) { 206 String packageName = mItemInfo.getTargetComponent().getPackageName(); 207 @ShareabilityStatus int status = mShareabilityMgr.getStatus(packageName); 208 setEnabled(status == ShareabilityStatus.SHAREABLE); 209 210 if (requestUpdateIfUnknown && status == ShareabilityStatus.UNKNOWN) { 211 mShareabilityMgr.requestAppStatusUpdate(packageName, this::onStatusUpdated); 212 } 213 } 214 bluetoothSharingEnabled(Context context)215 private boolean bluetoothSharingEnabled(Context context) { 216 return !context.getSystemService(UserManager.class) 217 .hasUserRestriction(UserManager.DISALLOW_BLUETOOTH_SHARING, mItemInfo.user); 218 } 219 showCannotShareToast(Context context)220 private void showCannotShareToast(Context context) { 221 ActivityContext activityContext = ActivityContext.lookupContext(context); 222 String blockedByMessage = activityContext.getStringCache() != null 223 ? activityContext.getStringCache().disabledByAdminMessage 224 : context.getString(R.string.blocked_by_policy); 225 226 CharSequence text = (mSharingEnabledForUser) 227 ? context.getText(R.string.toast_p2p_app_not_shareable) 228 : blockedByMessage; 229 int duration = Toast.LENGTH_SHORT; 230 Toast.makeText(context, text, duration).show(); 231 } 232 setEnabled(boolean isEnabled)233 public void setEnabled(boolean isEnabled) { 234 if (mIsEnabled != isEnabled) { 235 mIsEnabled = isEnabled; 236 mBoundViews.forEach(v -> v.setEnabled(isEnabled)); 237 } 238 } 239 isEnabled()240 public boolean isEnabled() { 241 return mIsEnabled; 242 } 243 244 @VisibleForTesting setStatsLogManager(StatsLogManager statsLogManager)245 void setStatsLogManager(StatsLogManager statsLogManager) { 246 mStatsLogManager = statsLogManager; 247 } 248 } 249 250 /** 251 * Shortcut factory for generating the Share App button 252 */ 253 public static final SystemShortcut.Factory<Launcher> SHORTCUT_FACTORY = 254 (launcher, itemInfo, originalView) -> 255 (new AppSharing(launcher)).getShortcut(launcher, itemInfo, originalView); 256 } 257