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 17package android.support.v7.widget; 18 19import android.content.Context; 20import android.content.Intent; 21import android.content.pm.PackageManager; 22import android.content.pm.ResolveInfo; 23import android.graphics.drawable.Drawable; 24import android.os.Build; 25import android.support.v4.view.ActionProvider; 26import android.support.v7.appcompat.R; 27import android.support.v7.widget.ActivityChooserModel.OnChooseActivityListener; 28import android.util.TypedValue; 29import android.view.Menu; 30import android.view.MenuItem; 31import android.view.MenuItem.OnMenuItemClickListener; 32import android.view.SubMenu; 33import android.view.View; 34 35/** 36 * Provides a share action, which is suitable for an activity's app bar. Creates 37 * views that enable data sharing. If the provider appears in the 38 * overflow menu, it creates a submenu with the appropriate sharing 39 * actions. 40 * 41 * <h3 id="add-share-action">Adding a share action</h3> 42 * 43 * <p>To add a "share" action to your activity, put a 44 * <code>ShareActionProvider</code> in the app bar's menu resource. For 45 * example:</p> 46 * 47 * <pre> 48 * <item android:id="@+id/action_share" 49 * android:title="@string/share" 50 * app:showAsAction="ifRoom" 51 * app:actionProviderClass="android.support.v7.widget.ShareActionProvider"/> 52 * </pre> 53 * 54 * <p>You do not need to specify an icon, since the 55 * <code>ShareActionProvider</code> widget takes care of its own appearance and 56 * behavior. However, you do need to specify a title with 57 * <code>android:title</code>, in case the action ends up in the overflow 58 * menu.</p> 59 * 60 * <p>Next, set up the intent that contains the content your activity is 61 * able to share. You should create this intent in your handler for 62 * {@link android.app.Activity#onCreateOptionsMenu onCreateOptionsMenu()}, 63 * and update it every time the shareable content changes. To set up the 64 * intent:</p> 65 * 66 * <ol> 67 * <li>Get a reference to the ShareActionProvider by calling {@link 68 * android.view.MenuItem#getActionProvider getActionProvider()} and 69 * passing the share action's {@link android.view.MenuItem}. For 70 * example: 71 * 72 * <pre> 73 * MenuItem shareItem = menu.findItem(R.id.action_share); 74 * ShareActionProvider myShareActionProvider = 75 * (ShareActionProvider) MenuItemCompat.getActionProvider(shareItem);</pre></li> 76 * 77 * <li>Create an intent with the {@link android.content.Intent#ACTION_SEND} 78 * action, and attach the content shared by the activity. For example, the 79 * following intent shares an image: 80 * 81 * <pre> 82 * Intent myShareIntent = new Intent(Intent.ACTION_SEND); 83 * myShareIntent.setType("image/*"); 84 * myShareIntent.putExtra(Intent.EXTRA_STREAM, myImageUri);</pre></li> 85 * 86 * <li>Call {@link #setShareIntent setShareIntent()} to attach this intent to 87 * the action provider: 88 * 89 * <pre> 90 * myShareActionProvider.setShareIntent(myShareIntent); 91 * </pre></li> 92 * 93 * <li>When the content changes, modify the intent or create a new one, 94 * and call {@link #setShareIntent setShareIntent()} again. For example: 95 * 96 * <pre> 97 * // Image has changed! Update the intent: 98 * myShareIntent.putExtra(Intent.EXTRA_STREAM, myNewImageUri); 99 * myShareActionProvider.setShareIntent(myShareIntent);</pre></li> 100 * </ol> 101 * 102 * <h3 id="rankings">Share target rankings</h3> 103 * 104 * <p>The share action provider retains a ranking for each share target, 105 * based on how often the user chooses each one. The more often a user 106 * chooses a target, the higher its rank; the 107 * most-commonly used target appears in the app bar as the default target.</p> 108 * 109 * <p>By default, the target ranking information is stored in a private 110 * file with the name specified by {@link 111 * #DEFAULT_SHARE_HISTORY_FILE_NAME}. Ordinarily, the share action provider stores 112 * all the history in this single file. However, using a single set of 113 * rankings may not make sense if the 114 * share action provider is used for different kinds of content. For 115 * example, if the activity sometimes shares images and sometimes shares 116 * contacts, you would want to maintain two different sets of rankings.</p> 117 * 118 * <p>To set the history file, call {@link #setShareHistoryFileName 119 * setShareHistoryFileName()} and pass the name of an XML file. The file 120 * you specify is used until the next time you call {@link 121 * #setShareHistoryFileName setShareHistoryFileName()}.</p> 122 * 123 * @see ActionProvider 124 */ 125public class ShareActionProvider extends ActionProvider { 126 127 /** 128 * Listener for the event of selecting a share target. 129 */ 130 public interface OnShareTargetSelectedListener { 131 132 /** 133 * Called when a share target has been selected. The client can 134 * decide whether to perform some action before the sharing is 135 * actually performed. 136 * <p> 137 * <strong>Note:</strong> Modifying the intent is not permitted and 138 * any changes to the latter will be ignored. 139 * </p> 140 * <p> 141 * <strong>Note:</strong> You should <strong>not</strong> handle the 142 * intent here. This callback aims to notify the client that a 143 * sharing is being performed, so the client can update the UI 144 * if necessary. 145 * </p> 146 * 147 * @param source The source of the notification. 148 * @param intent The intent for launching the chosen share target. 149 * @return The return result is ignored. Always return false for consistency. 150 */ 151 public boolean onShareTargetSelected(ShareActionProvider source, Intent intent); 152 } 153 154 /** 155 * The default for the maximal number of activities shown in the sub-menu. 156 */ 157 private static final int DEFAULT_INITIAL_ACTIVITY_COUNT = 4; 158 159 /** 160 * The the maximum number activities shown in the sub-menu. 161 */ 162 private int mMaxShownActivityCount = DEFAULT_INITIAL_ACTIVITY_COUNT; 163 164 /** 165 * Listener for handling menu item clicks. 166 */ 167 private final ShareMenuItemOnMenuItemClickListener mOnMenuItemClickListener = 168 new ShareMenuItemOnMenuItemClickListener(); 169 170 /** 171 * The default name for storing share history. 172 */ 173 public static final String DEFAULT_SHARE_HISTORY_FILE_NAME = "share_history.xml"; 174 175 /** 176 * Context for accessing resources. 177 */ 178 private final Context mContext; 179 180 /** 181 * The name of the file with share history data. 182 */ 183 private String mShareHistoryFileName = DEFAULT_SHARE_HISTORY_FILE_NAME; 184 185 private OnShareTargetSelectedListener mOnShareTargetSelectedListener; 186 187 private OnChooseActivityListener mOnChooseActivityListener; 188 189 /** 190 * Creates a new instance. 191 * 192 * @param context Context for accessing resources. 193 */ 194 public ShareActionProvider(Context context) { 195 super(context); 196 mContext = context; 197 } 198 199 /** 200 * Sets a listener to be notified when a share target has been selected. 201 * The listener can optionally decide to handle the selection and 202 * not rely on the default behavior which is to launch the activity. 203 * <p> 204 * <strong>Note:</strong> If you choose the backing share history file 205 * you will still be notified in this callback. 206 * </p> 207 * @param listener The listener. 208 */ 209 public void setOnShareTargetSelectedListener(OnShareTargetSelectedListener listener) { 210 mOnShareTargetSelectedListener = listener; 211 setActivityChooserPolicyIfNeeded(); 212 } 213 214 /** 215 * {@inheritDoc} 216 */ 217 @Override 218 public View onCreateActionView() { 219 // Create the view and set its data model. 220 ActivityChooserView activityChooserView = new ActivityChooserView(mContext); 221 if (!activityChooserView.isInEditMode()) { 222 ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName); 223 activityChooserView.setActivityChooserModel(dataModel); 224 } 225 226 // Lookup and set the expand action icon. 227 TypedValue outTypedValue = new TypedValue(); 228 mContext.getTheme().resolveAttribute(R.attr.actionModeShareDrawable, outTypedValue, true); 229 Drawable drawable = AppCompatDrawableManager.get() 230 .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 247 public boolean hasSubMenu() { 248 return true; 249 } 250 251 /** 252 * {@inheritDoc} 253 */ 254 @Override 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 */ 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 */ 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 { 356 @Override 357 public boolean onMenuItemClick(MenuItem item) { 358 ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, 359 mShareHistoryFileName); 360 final int itemId = item.getItemId(); 361 Intent launchIntent = dataModel.chooseActivity(itemId); 362 if (launchIntent != null) { 363 final String action = launchIntent.getAction(); 364 if (Intent.ACTION_SEND.equals(action) || 365 Intent.ACTION_SEND_MULTIPLE.equals(action)) { 366 updateIntent(launchIntent); 367 } 368 mContext.startActivity(launchIntent); 369 } 370 return true; 371 } 372 } 373 374 /** 375 * Set the activity chooser policy of the model backed by the current 376 * share history file if needed which is if there is a registered callback. 377 */ 378 private void setActivityChooserPolicyIfNeeded() { 379 if (mOnShareTargetSelectedListener == null) { 380 return; 381 } 382 if (mOnChooseActivityListener == null) { 383 mOnChooseActivityListener = new ShareActivityChooserModelPolicy(); 384 } 385 ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName); 386 dataModel.setOnChooseActivityListener(mOnChooseActivityListener); 387 } 388 389 /** 390 * Policy that delegates to the {@link OnShareTargetSelectedListener}, if such. 391 */ 392 private class ShareActivityChooserModelPolicy implements OnChooseActivityListener { 393 @Override 394 public boolean onChooseActivity(ActivityChooserModel host, Intent intent) { 395 if (mOnShareTargetSelectedListener != null) { 396 mOnShareTargetSelectedListener.onShareTargetSelected( 397 ShareActionProvider.this, intent); 398 } 399 return false; 400 } 401 } 402 403 private void updateIntent(Intent intent) { 404 if (Build.VERSION.SDK_INT >= 21) { 405 // If we're on Lollipop, we can open the intent as a document 406 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | 407 Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 408 } else { 409 // Else, we will use the old CLEAR_WHEN_TASK_RESET flag 410 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 411 } 412 } 413} 414