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