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.launcher3.model.AppShareabilityChecker; 41 import com.android.launcher3.model.AppShareabilityJobService; 42 import com.android.launcher3.model.AppShareabilityManager; 43 import com.android.launcher3.model.AppShareabilityManager.ShareabilityStatus; 44 import com.android.launcher3.model.data.ItemInfo; 45 import com.android.launcher3.popup.PopupDataProvider; 46 import com.android.launcher3.popup.SystemShortcut; 47 import com.android.launcher3.views.ActivityContext; 48 49 import java.io.File; 50 import java.util.Collections; 51 import java.util.Set; 52 import java.util.WeakHashMap; 53 54 /** 55 * Defines the Share system shortcut and its factory. 56 * This shortcut can be added to the app long-press menu on the home screen. 57 * Clicking the button will initiate peer-to-peer sharing of the app. 58 */ 59 public final class AppSharing { 60 /** 61 * This flag enables this feature. It is defined here rather than in launcher3's FeatureFlags 62 * because it is unique to Go and not toggleable at runtime. 63 */ 64 public static final boolean ENABLE_APP_SHARING = true; 65 /** 66 * With this flag enabled, the Share App button will be dynamically enabled/disabled based 67 * on each app's shareability status. 68 */ 69 public static final boolean ENABLE_SHAREABILITY_CHECK = true; 70 71 private static final String TAG = "AppSharing"; 72 private static final String FILE_PROVIDER_SUFFIX = ".overview.fileprovider"; 73 private static final String APP_EXTENSION = ".apk"; 74 private static final String APP_MIME_TYPE = "application/application"; 75 76 private final String mSharingComponent; 77 private AppShareabilityManager mShareabilityMgr; 78 AppSharing(Launcher launcher)79 private AppSharing(Launcher launcher) { 80 String sharingComponent = Settings.Secure.getString(launcher.getContentResolver(), 81 Settings.Secure.NEARBY_SHARING_COMPONENT); 82 mSharingComponent = TextUtils.isEmpty(sharingComponent) ? launcher.getText( 83 R.string.app_sharing_component).toString() : sharingComponent; 84 } 85 getShareableUri(Context context, String path, String displayName)86 private Uri getShareableUri(Context context, String path, String displayName) { 87 String authority = BuildConfig.APPLICATION_ID + FILE_PROVIDER_SUFFIX; 88 File pathFile = new File(path); 89 return FileProvider.getUriForFile(context, authority, pathFile, displayName); 90 } 91 getShortcut(Launcher launcher, ItemInfo info, View originalView)92 private SystemShortcut<Launcher> getShortcut(Launcher launcher, ItemInfo info, 93 View originalView) { 94 if (TextUtils.isEmpty(mSharingComponent)) { 95 return null; 96 } 97 return new Share(launcher, info, originalView); 98 } 99 100 /** 101 * Instantiates AppShareabilityManager, which then reads app shareability data from disk 102 * Also schedules a job to update those data 103 * @param context The application context 104 * @param checker An implementation of AppShareabilityChecker to perform the actual checks 105 * when updating the data 106 */ setUpShareabilityCache(Context context, AppShareabilityChecker checker)107 public static void setUpShareabilityCache(Context context, AppShareabilityChecker checker) { 108 AppShareabilityManager shareMgr = AppShareabilityManager.INSTANCE.get(context); 109 shareMgr.setShareabilityChecker(checker); 110 AppShareabilityJobService.schedule(context); 111 } 112 113 /** 114 * The Share App system shortcut, used to initiate p2p sharing of a given app 115 */ 116 public final class Share extends SystemShortcut<Launcher> { 117 private final PopupDataProvider mPopupDataProvider; 118 private final boolean mSharingEnabledForUser; 119 120 private final Set<View> mBoundViews = Collections.newSetFromMap(new WeakHashMap<>()); 121 private boolean mIsEnabled = true; 122 Share(Launcher target, ItemInfo itemInfo, View originalView)123 public Share(Launcher target, ItemInfo itemInfo, View originalView) { 124 super(R.drawable.ic_share, R.string.app_share_drop_target_label, target, itemInfo, 125 originalView); 126 mPopupDataProvider = target.getPopupDataProvider(); 127 128 mSharingEnabledForUser = bluetoothSharingEnabled(target); 129 if (!mSharingEnabledForUser) { 130 setEnabled(false); 131 } else if (ENABLE_SHAREABILITY_CHECK) { 132 mShareabilityMgr = 133 AppShareabilityManager.INSTANCE.get(target.getApplicationContext()); 134 checkShareability(/* requestUpdateIfUnknown */ true); 135 } 136 } 137 138 @Override setIconAndLabelFor(View iconView, TextView labelView)139 public void setIconAndLabelFor(View iconView, TextView labelView) { 140 super.setIconAndLabelFor(iconView, labelView); 141 mBoundViews.add(iconView); 142 mBoundViews.add(labelView); 143 } 144 145 @Override setIconAndContentDescriptionFor(ImageView view)146 public void setIconAndContentDescriptionFor(ImageView view) { 147 super.setIconAndContentDescriptionFor(view); 148 mBoundViews.add(view); 149 } 150 151 @Override onClick(View view)152 public void onClick(View view) { 153 ActivityContext.lookupContext(view.getContext()) 154 .getStatsLogManager().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 245 /** 246 * Shortcut factory for generating the Share App button 247 */ 248 public static final SystemShortcut.Factory<Launcher> SHORTCUT_FACTORY = 249 (launcher, itemInfo, originalView) -> 250 (new AppSharing(launcher)).getShortcut(launcher, itemInfo, originalView); 251 } 252