1 /* 2 * Copyright (C) 2013 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 androidx.appcompat.widget; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.content.pm.PackageManager; 22 import android.content.pm.ResolveInfo; 23 import android.graphics.drawable.Drawable; 24 import android.os.Build; 25 import android.util.TypedValue; 26 import android.view.Menu; 27 import android.view.MenuItem; 28 import android.view.MenuItem.OnMenuItemClickListener; 29 import android.view.SubMenu; 30 import android.view.View; 31 32 import androidx.appcompat.R; 33 import androidx.appcompat.content.res.AppCompatResources; 34 import androidx.appcompat.widget.ActivityChooserModel.OnChooseActivityListener; 35 import androidx.core.view.ActionProvider; 36 37 /** 38 * Provides a share action, which is suitable for an activity's app bar. Creates 39 * views that enable data sharing. If the provider appears in the 40 * overflow menu, it creates a submenu with the appropriate sharing 41 * actions. 42 * 43 * <h3 id="add-share-action">Adding a share action</h3> 44 * 45 * <p>To add a "share" action to your activity, put a 46 * <code>ShareActionProvider</code> in the app bar's menu resource. For 47 * example:</p> 48 * 49 * <pre> 50 * <item android:id="@+id/action_share" 51 * android:title="@string/share" 52 * app:showAsAction="ifRoom" 53 * app:actionProviderClass="androidx.appcompat.widget.ShareActionProvider"/> 54 * </pre> 55 * 56 * <p>You do not need to specify an icon, since the 57 * <code>ShareActionProvider</code> widget takes care of its own appearance and 58 * behavior. However, you do need to specify a title with 59 * <code>android:title</code>, in case the action ends up in the overflow 60 * menu.</p> 61 * 62 * <p>Next, set up the intent that contains the content your activity is 63 * able to share. You should create this intent in your handler for 64 * {@link android.app.Activity#onCreateOptionsMenu onCreateOptionsMenu()}, 65 * and update it every time the shareable content changes. To set up the 66 * intent:</p> 67 * 68 * <ol> 69 * <li>Get a reference to the ShareActionProvider by calling {@link 70 * android.view.MenuItem#getActionProvider getActionProvider()} and 71 * passing the share action's {@link android.view.MenuItem}. For 72 * example: 73 * 74 * <pre> 75 * MenuItem shareItem = menu.findItem(R.id.action_share); 76 * ShareActionProvider myShareActionProvider = 77 * (ShareActionProvider) MenuItemCompat.getActionProvider(shareItem);</pre></li> 78 * 79 * <li>Create an intent with the {@link android.content.Intent#ACTION_SEND} 80 * action, and attach the content shared by the activity. For example, the 81 * following intent shares an image: 82 * 83 * <pre> 84 * Intent myShareIntent = new Intent(Intent.ACTION_SEND); 85 * myShareIntent.setType("image/*"); 86 * myShareIntent.putExtra(Intent.EXTRA_STREAM, myImageUri);</pre></li> 87 * 88 * <li>Call {@link #setShareIntent setShareIntent()} to attach this intent to 89 * the action provider: 90 * 91 * <pre> 92 * myShareActionProvider.setShareIntent(myShareIntent); 93 * </pre></li> 94 * 95 * <li>When the content changes, modify the intent or create a new one, 96 * and call {@link #setShareIntent setShareIntent()} again. For example: 97 * 98 * <pre> 99 * // Image has changed! Update the intent: 100 * myShareIntent.putExtra(Intent.EXTRA_STREAM, myNewImageUri); 101 * myShareActionProvider.setShareIntent(myShareIntent);</pre></li> 102 * </ol> 103 * 104 * <h3 id="rankings">Share target rankings</h3> 105 * 106 * <p>The share action provider retains a ranking for each share target, 107 * based on how often the user chooses each one. The more often a user 108 * chooses a target, the higher its rank; the 109 * most-commonly used target appears in the app bar as the default target.</p> 110 * 111 * <p>By default, the target ranking information is stored in a private 112 * file with the name specified by {@link 113 * #DEFAULT_SHARE_HISTORY_FILE_NAME}. Ordinarily, the share action provider stores 114 * all the history in this single file. However, using a single set of 115 * rankings may not make sense if the 116 * share action provider is used for different kinds of content. For 117 * example, if the activity sometimes shares images and sometimes shares 118 * contacts, you would want to maintain two different sets of rankings.</p> 119 * 120 * <p>To set the history file, call {@link #setShareHistoryFileName 121 * setShareHistoryFileName()} and pass the name of an XML file. The file 122 * you specify is used until the next time you call {@link 123 * #setShareHistoryFileName setShareHistoryFileName()}.</p> 124 * 125 * @see ActionProvider 126 */ 127 public class ShareActionProvider extends ActionProvider { 128 129 /** 130 * Listener for the event of selecting a share target. 131 */ 132 public interface OnShareTargetSelectedListener { 133 134 /** 135 * Called when a share target has been selected. The client can 136 * decide whether to perform some action before the sharing is 137 * actually performed. 138 * <p> 139 * <strong>Note:</strong> Modifying the intent is not permitted and 140 * any changes to the latter will be ignored. 141 * </p> 142 * <p> 143 * <strong>Note:</strong> You should <strong>not</strong> handle the 144 * intent here. This callback aims to notify the client that a 145 * sharing is being performed, so the client can update the UI 146 * if necessary. 147 * </p> 148 * 149 * @param source The source of the notification. 150 * @param intent The intent for launching the chosen share target. 151 * @return The return result is ignored. Always return false for consistency. 152 */ onShareTargetSelected(ShareActionProvider source, Intent intent)153 public boolean onShareTargetSelected(ShareActionProvider source, Intent intent); 154 } 155 156 /** 157 * The default for the maximal number of activities shown in the sub-menu. 158 */ 159 private static final int DEFAULT_INITIAL_ACTIVITY_COUNT = 4; 160 161 /** 162 * The the maximum number activities shown in the sub-menu. 163 */ 164 private int mMaxShownActivityCount = DEFAULT_INITIAL_ACTIVITY_COUNT; 165 166 /** 167 * Listener for handling menu item clicks. 168 */ 169 private final ShareMenuItemOnMenuItemClickListener mOnMenuItemClickListener = 170 new ShareMenuItemOnMenuItemClickListener(); 171 172 /** 173 * The default name for storing share history. 174 */ 175 public static final String DEFAULT_SHARE_HISTORY_FILE_NAME = "share_history.xml"; 176 177 /** 178 * Context for accessing resources. 179 */ 180 final Context mContext; 181 182 /** 183 * The name of the file with share history data. 184 */ 185 String mShareHistoryFileName = DEFAULT_SHARE_HISTORY_FILE_NAME; 186 187 OnShareTargetSelectedListener mOnShareTargetSelectedListener; 188 189 private OnChooseActivityListener mOnChooseActivityListener; 190 191 /** 192 * Creates a new instance. 193 * 194 * @param context Context for accessing resources. 195 */ ShareActionProvider(Context context)196 public ShareActionProvider(Context context) { 197 super(context); 198 mContext = context; 199 } 200 201 /** 202 * Sets a listener to be notified when a share target has been selected. 203 * The listener can optionally decide to handle the selection and 204 * not rely on the default behavior which is to launch the activity. 205 * <p> 206 * <strong>Note:</strong> If you choose the backing share history file 207 * you will still be notified in this callback. 208 * </p> 209 * @param listener The listener. 210 */ setOnShareTargetSelectedListener(OnShareTargetSelectedListener listener)211 public void setOnShareTargetSelectedListener(OnShareTargetSelectedListener listener) { 212 mOnShareTargetSelectedListener = listener; 213 setActivityChooserPolicyIfNeeded(); 214 } 215 216 /** 217 * {@inheritDoc} 218 */ 219 @Override onCreateActionView()220 public View onCreateActionView() { 221 // Create the view and set its data model. 222 ActivityChooserView activityChooserView = new ActivityChooserView(mContext); 223 if (!activityChooserView.isInEditMode()) { 224 ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName); 225 activityChooserView.setActivityChooserModel(dataModel); 226 } 227 228 // Lookup and set the expand action icon. 229 TypedValue outTypedValue = new TypedValue(); 230 mContext.getTheme().resolveAttribute(R.attr.actionModeShareDrawable, outTypedValue, true); 231 Drawable drawable = AppCompatResources.getDrawable(mContext, outTypedValue.resourceId); 232 activityChooserView.setExpandActivityOverflowButtonDrawable(drawable); 233 activityChooserView.setProvider(this); 234 235 // Set content description. 236 activityChooserView.setDefaultActionButtonContentDescription( 237 R.string.abc_shareactionprovider_share_with_application); 238 activityChooserView.setExpandActivityOverflowButtonContentDescription( 239 R.string.abc_shareactionprovider_share_with); 240 241 return activityChooserView; 242 } 243 244 /** 245 * {@inheritDoc} 246 */ 247 @Override hasSubMenu()248 public boolean hasSubMenu() { 249 return true; 250 } 251 252 /** 253 * {@inheritDoc} 254 */ 255 @Override onPrepareSubMenu(SubMenu subMenu)256 public void onPrepareSubMenu(SubMenu subMenu) { 257 // Clear since the order of items may change. 258 subMenu.clear(); 259 260 ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName); 261 PackageManager packageManager = mContext.getPackageManager(); 262 263 final int expandedActivityCount = dataModel.getActivityCount(); 264 final int collapsedActivityCount = Math.min(expandedActivityCount, mMaxShownActivityCount); 265 266 // Populate the sub-menu with a sub set of the activities. 267 for (int i = 0; i < collapsedActivityCount; i++) { 268 ResolveInfo activity = dataModel.getActivity(i); 269 subMenu.add(0, i, i, activity.loadLabel(packageManager)) 270 .setIcon(activity.loadIcon(packageManager)) 271 .setOnMenuItemClickListener(mOnMenuItemClickListener); 272 } 273 274 if (collapsedActivityCount < expandedActivityCount) { 275 // Add a sub-menu for showing all activities as a list item. 276 SubMenu expandedSubMenu = subMenu.addSubMenu(Menu.NONE, collapsedActivityCount, 277 collapsedActivityCount, 278 mContext.getString(R.string.abc_activity_chooser_view_see_all)); 279 for (int i = 0; i < expandedActivityCount; i++) { 280 ResolveInfo activity = dataModel.getActivity(i); 281 expandedSubMenu.add(0, i, i, activity.loadLabel(packageManager)) 282 .setIcon(activity.loadIcon(packageManager)) 283 .setOnMenuItemClickListener(mOnMenuItemClickListener); 284 } 285 } 286 } 287 288 /** 289 * Sets the file name of a file for persisting the share history which 290 * history will be used for ordering share targets. This file will be used 291 * for all view created by {@link #onCreateActionView()}. Defaults to 292 * {@link #DEFAULT_SHARE_HISTORY_FILE_NAME}. Set to <code>null</code> 293 * if share history should not be persisted between sessions. 294 * 295 * <p class="note"> 296 * <strong>Note:</strong> The history file name can be set any time, however 297 * only the action views created by {@link #onCreateActionView()} after setting 298 * the file name will be backed by the provided file. Therefore, if you want to 299 * use different history files for sharing specific types of content, every time 300 * you change the history file with {@link #setShareHistoryFileName(String)} you must 301 * call {@link androidx.appcompat.app.AppCompatActivity#supportInvalidateOptionsMenu()} 302 * to recreate the action view. You should <strong>not</strong> call 303 * {@link androidx.appcompat.app.AppCompatActivity#supportInvalidateOptionsMenu()} from 304 * {@link androidx.appcompat.app.AppCompatActivity#onCreateOptionsMenu(Menu)}. 305 * 306 * <pre> 307 * private void doShare(Intent intent) { 308 * if (IMAGE.equals(intent.getMimeType())) { 309 * mShareActionProvider.setHistoryFileName(SHARE_IMAGE_HISTORY_FILE_NAME); 310 * } else if (TEXT.equals(intent.getMimeType())) { 311 * mShareActionProvider.setHistoryFileName(SHARE_TEXT_HISTORY_FILE_NAME); 312 * } 313 * mShareActionProvider.setIntent(intent); 314 * supportInvalidateOptionsMenu(); 315 * } 316 * </pre> 317 * 318 * @param shareHistoryFile The share history file name. 319 */ setShareHistoryFileName(String shareHistoryFile)320 public void setShareHistoryFileName(String shareHistoryFile) { 321 mShareHistoryFileName = shareHistoryFile; 322 setActivityChooserPolicyIfNeeded(); 323 } 324 325 /** 326 * Sets an intent with information about the share action. Here is a 327 * sample for constructing a share intent: 328 * 329 * <pre> 330 * Intent shareIntent = new Intent(Intent.ACTION_SEND); 331 * shareIntent.setType("image/*"); 332 * Uri uri = Uri.fromFile(new File(getFilesDir(), "foo.jpg")); 333 * shareIntent.putExtra(Intent.EXTRA_STREAM, uri.toString()); 334 * </pre> 335 * 336 * @param shareIntent The share intent. 337 * 338 * @see Intent#ACTION_SEND 339 * @see Intent#ACTION_SEND_MULTIPLE 340 */ setShareIntent(Intent shareIntent)341 public void setShareIntent(Intent shareIntent) { 342 if (shareIntent != null) { 343 final String action = shareIntent.getAction(); 344 if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) { 345 updateIntent(shareIntent); 346 } 347 } 348 ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, 349 mShareHistoryFileName); 350 dataModel.setIntent(shareIntent); 351 } 352 353 /** 354 * Reusable listener for handling share item clicks. 355 */ 356 private class ShareMenuItemOnMenuItemClickListener implements OnMenuItemClickListener { ShareMenuItemOnMenuItemClickListener()357 ShareMenuItemOnMenuItemClickListener() { 358 } 359 360 @Override onMenuItemClick(MenuItem item)361 public boolean onMenuItemClick(MenuItem item) { 362 ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, 363 mShareHistoryFileName); 364 final int itemId = item.getItemId(); 365 Intent launchIntent = dataModel.chooseActivity(itemId); 366 if (launchIntent != null) { 367 final String action = launchIntent.getAction(); 368 if (Intent.ACTION_SEND.equals(action) || 369 Intent.ACTION_SEND_MULTIPLE.equals(action)) { 370 updateIntent(launchIntent); 371 } 372 mContext.startActivity(launchIntent); 373 } 374 return true; 375 } 376 } 377 378 /** 379 * Set the activity chooser policy of the model backed by the current 380 * share history file if needed which is if there is a registered callback. 381 */ setActivityChooserPolicyIfNeeded()382 private void setActivityChooserPolicyIfNeeded() { 383 if (mOnShareTargetSelectedListener == null) { 384 return; 385 } 386 if (mOnChooseActivityListener == null) { 387 mOnChooseActivityListener = new ShareActivityChooserModelPolicy(); 388 } 389 ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName); 390 dataModel.setOnChooseActivityListener(mOnChooseActivityListener); 391 } 392 393 /** 394 * Policy that delegates to the {@link OnShareTargetSelectedListener}, if such. 395 */ 396 private class ShareActivityChooserModelPolicy implements OnChooseActivityListener { ShareActivityChooserModelPolicy()397 ShareActivityChooserModelPolicy() { 398 } 399 400 @Override onChooseActivity(ActivityChooserModel host, Intent intent)401 public boolean onChooseActivity(ActivityChooserModel host, Intent intent) { 402 if (mOnShareTargetSelectedListener != null) { 403 mOnShareTargetSelectedListener.onShareTargetSelected( 404 ShareActionProvider.this, intent); 405 } 406 return false; 407 } 408 } 409 updateIntent(Intent intent)410 void updateIntent(Intent intent) { 411 if (Build.VERSION.SDK_INT >= 21) { 412 // If we're on Lollipop, we can open the intent as a document 413 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | 414 Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 415 } else { 416 // Else, we will use the old CLEAR_WHEN_TASK_RESET flag 417 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 418 } 419 } 420 } 421