Android使用volley 下载文件DownloadManager类下载一个文件后,如何用代码删除下载内容里的下载记录

温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!&&|&&
LOFTER精选
网易考拉推荐
用微信&&“扫一扫”
将文章分享到朋友圈。
用易信&&“扫一扫”
将文章分享到朋友圈。
import java.io.UnsupportedEncodingE
import java.net.URLE
import android.app.A
import android.app.DownloadM
import android.app.DownloadManager.R
import android.content.BroadcastR
import android.content.C
import android.content.I
import android.content.IntentF
import android.content.SharedP
import android.database.C
import android.net.U
import android.os.B
import android.preference.PreferenceM
import android.util.L
import android.webkit.MimeTypeM
public class DownloadTestActivity extends Activity {
private DownloadManager downloadM
private SharedP
private static final String DL_ID = "downloadId";
/** Called when the activity is first created. */
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
downloadManager = (DownloadManager)getSystemService(DOWNLOAD_SERVICE);
prefs = PreferenceManager.getDefaultSharedPreferences(this);
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
unregisterReceiver(receiver);
protected void onResume() {
// TODO Auto-generated method stub
super.onResume();
if(!prefs.contains(DL_ID)) {
String url = "http://10.0.2.2/android/film/G3.mp4";
//开始下载
Uri resource = Uri.parse(encodeGB(url));
DownloadManager.Request request = new DownloadManager.Request(resource);
request.setAllowedNetworkTypes(Request.NETWORK_MOBILE | Request.NETWORK_WIFI);
request.setAllowedOverRoaming(false);
//设置文件类型
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
String mimeString = mimeTypeMap.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(url));
request.setMimeType(mimeString);
//在通知栏中显示
request.setShowRunningNotification(true);
request.setVisibleInDownloadsUi(true);
//sdcard的目录下的download文件夹
request.setDestinationInExternalPublicDir("/download/", "G3.mp4");
request.setTitle("移动G3广告");
long id = downloadManager.enqueue(request);
prefs.edit().putLong(DL_ID, id).commit();
//下载已经开始,检查状态
queryDownloadStatus();
registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
* 如果服务器不支持中文路径的情况下需要转换url的编码。
* @param string
public String encodeGB(String string)
//转换中文编码
String split[] = string.split("/");
for (int i = 1; i & split. i++) {
split[i] = URLEncoder.encode(split[i], "GB2312");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
split[0] = split[0]+"/"+split[i];
split[0] = split[0].replaceAll("\\+", "%20");//处理空格
return split[0];
private BroadcastReceiver receiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
//这里可以取得下载的id,这样就可以知道哪个文件下载完成了。适用与多个下载任务的监听
Log.v("intent", ""+intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0));
queryDownloadStatus();
private void queryDownloadStatus() {
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(prefs.getLong(DL_ID, 0));
Cursor c = downloadManager.query(query);
if(c.moveToFirst()) {
int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
switch(status) {
case DownloadManager.STATUS_PAUSED:
Log.v("down", "STATUS_PAUSED");
case DownloadManager.STATUS_PENDING:
Log.v("down", "STATUS_PENDING");
case DownloadManager.STATUS_RUNNING:
//正在下载,不做任何事情
Log.v("down", "STATUS_RUNNING");
case DownloadManager.STATUS_SUCCESSFUL:
Log.v("down", "下载完成");
case DownloadManager.STATUS_FAILED:
//清除已下载的内容,重新下载
Log.v("down", "STATUS_FAILED");
downloadManager.remove(prefs.getLong(DL_ID, 0));
prefs.edit().clear().commit();
最后需要的权限是:&uses-permission android:name="android.permission.INTERNET"/&&uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/&如果需要隐藏下载工具的提示和显示,修改代码:request.setShowRunningNotification(false);request.setVisibleInDownloadsUi(false);加入下面的权限:&uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION"/&
用微信&&“扫一扫”
将文章分享到朋友圈。
用易信&&“扫一扫”
将文章分享到朋友圈。
历史上的今天
在LOFTER的更多文章
loftPermalink:'',
id:'fks_',
blogTitle:'使用Android自带DownloadManager下载文件',
blogAbstract:'转
{if x.moveFrom=='wap'}
{elseif x.moveFrom=='iphone'}
{elseif x.moveFrom=='android'}
{elseif x.moveFrom=='mobile'}
${a.selfIntro|escape}{if great260}${suplement}{/if}
{list a as x}
推荐过这篇日志的人:
{list a as x}
{if !!b&&b.length>0}
他们还推荐了:
{list b as y}
转载记录:
{list d as x}
{list a as x}
{list a as x}
{list a as x}
{list a as x}
{if x_index>4}{break}{/if}
${fn2(x.publishTime,'yyyy-MM-dd HH:mm:ss')}
{list a as x}
{if !!(blogDetail.preBlogPermalink)}
{if !!(blogDetail.nextBlogPermalink)}
{list a as x}
{if defined('newslist')&&newslist.length>0}
{list newslist as x}
{if x_index>7}{break}{/if}
{list a as x}
{var first_option =}
{list x.voteDetailList as voteToOption}
{if voteToOption==1}
{if first_option==false},{/if}&&“${b[voteToOption_index]}”&&
{if (x.role!="-1") },“我是${c[x.role]}”&&{/if}
&&&&&&&&${fn1(x.voteTime)}
{if x.userName==''}{/if}
网易公司版权所有&&
{list x.l as y}
{if defined('wl')}
{list wl as x}{/list}Android开发笔记(六十一)文件下载管理DownloadManager
下载管理DownloadManager
文件下载其实是网络数据访问的一种特殊形式,使用普通的http请求也能完成,就是实现起来会繁琐一些。因为下载功能比较常用,而且业务功能相对统一,所以从Android 2.3(API level 9)开始,Android提供了DownloadManager用于统一管理下载功能。
要想使用下载功能,首先得构建一个下载请求,说明从哪里下载、下载参数为何、下载的文件保存到哪里等等。这个下载请求便是DownloadManager的子类Request,下面是该类的常用方法
Request构造函数 : 指定从哪个网络地址下载文件。
Request.setAllowedNetworkTypes : 指定允许进行下载的网络类型。Request.NETWORK_WIFI表示wifi环境(推荐),Request.NETWORK_MOBILE表示数据连接环境(不推荐),Request.NETWORK_BLUETOOTH表示蓝牙环境。
Request.setDestinationInExternalFilesDir : 设置下载文件在本地的保存路径。
Request.addRequestHeader : 给HTTP请求添加头部参数。
Request.setMimeType : 设置下载文件的媒体类型。
Request.setVisibleInDownloadsUi : 设置下载页面是否可见。
Request.setNotificationVisibility : 设置通知栏上的下载任务的可见类型。Request.VISIBILITY_HIDDEN表示隐藏,Request.VISIBILITY_VISIBLE表示下载时可见(下载完成后消失),Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED表示下载进行时与完成后都可见,Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION表示只有下载完成后可见。注意可见类型设置为VISIBILITY_HIDDEN时,需要在AndroidManifest.xml中加入对应权限,即android.permission.DOWNLOAD_WITHOUT_NOTIFICATION
Request.setTitle : 设置通知栏上的消息标题。不建议自行设置标题,因为默认标题是下载的文件名。
Request.setDescription : 设置通知栏上的消息描述。不建议自行设置描述,因为默认描述是系统估算的下载剩余时间。
构建下载请求完毕,然后才能进行下载的相关操作。下面是DownloadManager常用的下载方法:
enqueue : 将下载请求加入到任务队列中,即开始下载任务。该方法返回本次下载任务的编号。
remove : 取消指定编号的下载任务。
restartDownload : 重新下载指定编号的任务。
openDownloadedFile : 打开下载完成的文件。
getMimeTypeForDownloadedFile : 获取下载完成的文件的媒体类型。
查询下载进度
虽然下载进度可在通知栏上查看,但是有时APP自身也想了解当前的下载进度,那就要调用DownloadManager的query方法。该方法的输入参数是一个Query对象,返回结果集的Cursor游标,有关Cursor的用法参见《》。下面是Query类的常用方法:
query : 查询指定编号任务的当前下载信息。
Query.setFilterById : 根据编号来过滤下载任务。
Query.setFilterByStatus : 根据状态来过滤下载任务。
与文件下载有关的事件不是由监听器实现,而是由广播来实现。主要的下载事件有下面三个:
1、下载完成事件:在下载完成时,系统会发出一个action为DownloadManager.ACTION_DOWNLOAD_COMPLETE(android.intent.action.DOWNLOAD_COMPLETE)的广播,因此可注册一个该广播的接收器,用来判断当前下载任务是否已下载完毕,以及后续的处理。
2、下载进行时的通知栏点击事件:在下载过程中,用户点击通知栏上的下载任务,系统便发出action为DownloadManager.ACTION_NOTIFICATION_CLICKED(android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED)的广播,所以可注册该广播的接收器进行相关处理,比如说跳转到该任务的下载进度页面;
3、下载完成后的通知栏点击事件:在不同时刻点击下载任务,会触发不同的事件。下载未完成时点击,触发的是系统广播DownloadManager.ACTION_NOTIFICATION_CLICKED;下载完成后点击,触发的是系统的ACTION_VIEW即浏览页,该动作由系统根据媒体类型去寻找对应的程序来打开,并没有发出广播消息。如果我们要控制此时的点击行为,可以在Request中通过setMimeType方法设置媒体类型,这样Android就会按照这个类型做对应的浏览处理。
断点续传及其他
博主一开始学习DownloadManager时,就觉得好奇怪,该工具竟然没有提供暂停方法和恢复方法,这岂不意味着,文件下载没法断点续传了么?后来在实际开发中测试发现,DownloadManager其实比较智能,当网络一直是允许类型时,任务会一直下载;当网络断开或者不在允许范围内时,任务会自动暂停下载;只要网络连上或者切换到允许范围内,那么任务会自动恢复下载(这里会断点续传)。所以呢,开发者不用关心异常中断,也不用关心网络切换时的额外处理了,原来DownloadManager都已经帮我们实现了。
另外,同一个文件被重复下载时,已经下载完的文件并不会被覆盖,后来下载的文件会自动重命名。所以有时会发现下载下来的文件名与源文件名不一致,这很可能是重复下载造成了文件重命名。
自定义进度条
文件下载和上传都经常用到进度条,可是Android自带的ProgressBar无法显示进度百分比的文本。既然如此,我们还是基于ProgressBar自定义一个附带百分比文本的进度条,顺便复习一下自定义视图的用法。
首先在自定义类CustomProgressBar中声明一个画笔与百分比文本,然后提供百分比文本的设置和获取方法,最后重写onDraw方法,在控件中央使用drawText函数画上百分比文本。百分比文本的颜色可通过画笔的setColor来设置,文本大小可通过画笔的setTextSize来设置。
下面是CustomProgressBar的代码示例:
import android.content.C
import android.graphics.C
import android.graphics.C
import android.graphics.P
import android.graphics.R
import android.util.AttributeS
import android.widget.ProgressB
public class CustomProgressBar extends ProgressBar {
private String mProgressT
private Paint mP
public CustomProgressBar(Context context) {
super(context);
initPaint();
public CustomProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
public CustomProgressBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initPaint();
private void initPaint() {
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setTextSize(30);
public void setProgressText(String text) {
mProgressText =
public String getProgressText() {
return mProgressT
protected synchronized void onDraw(Canvas canvas) {
super.onDraw(canvas);
Rect rect = new Rect();
mPaint.getTextBounds(mProgressText, 0, mProgressText.length(), rect);
int x = (getWidth() / 2) - rect.centerX();
int y = (getHeight() / 2) - rect.centerY();
canvas.drawText(mProgressText, x, y, this.mPaint);
系统服务清单
到本文为止,Android的系统服务基本都用过了一遍,下面统计一下这些系统服务及其对应的章节说明:
CONNECTIVITY_SERVICE : 网络连接服务(android.net.ConnectivityManager),参见《》与《》。
TELEPHONY_SERVICE : 电话设备服务(android.telephony.TelephonyManager),参见《》和《》。
WIFI_SERVICE : wifi与热点服务(android.net.wifi.WifiManager),参见《》与《》。
LAYOUT_INFLATER_SERVICE : 布局填充服务(android.view.LayoutInflater),参见《》。
SEARCH_SERVICE : 搜索管理服务(android.app.SearchManager),参见《》。
WINDOW_SERVICE : 视窗管理服务(android.view.WindowManager),参见《》。
LOCATION_SERVICE : 定位服务(android.location.LocationManager),参见《》和《》。
ALARM_SERVICE : 闹钟/定时器服务(android.app.AlarmManager),参见《》。
NOTIFICATION_SERVICE : 通知推送服务(android.app.NotificationManager),参见《》。
AUDIO_SERVICE : 铃声服务(android.media.AudioManager),参见《》。
VIBRATOR_SERVICE : 震动服务(android.os.Vibrator),参见《》。
SENSOR_SERVICE : 传感器服务(android.hardware.SensorManager),参见《》。
ACTIVITY_SERVICE : 活动管理服务(android.app.ActivityManager),参见《》和《》。
DOWNLOAD_SERVICE : 下载管理服务(android.app.DownloadManager),参见《》。
INPUT_METHOD_SERVICE : 输入法服务(android.view.inputmethod.InputMethodManager),参见《》。
KEYGUARD_SERVICE : 键盘锁服务(android.app.KeyguardManager)
MEDIA_ROUTER_SERVICE : 媒体路由服务(android.media.MediaRouter)
POWER_SERVICE : 电源管理服务(android.os.PowerManager),参见《》
STORAGE_SERVICE : 存储管理服务(android.os.storage.StorageManager),参见《》。
UI_MODE_SERVICE : 界面模式服务(android.app.UiModeManager)
CLIPBOARD_SERVICE : 剪贴板服务(android.content.ClipboardManager),参见《》。
下面是文件下载查询进度的效果截图:
下面是文件下载的代码例子:
import java.util.HashM
import com.example.exmload.R;
import com.example.exmload.util.U
import android.annotation.SuppressL
import android.app.A
import android.app.DownloadM
import android.app.DownloadManager.Q
import android.app.DownloadManager.R
import android.content.BroadcastR
import android.content.C
import android.content.I
import android.database.C
import android.net.ConnectivityM
import android.net.NetworkI
import android.net.U
import android.os.B
import android.os.H
import android.util.L
import android.view.V
import android.view.View.OnClickL
import android.widget.B
import android.widget.ProgressB
import android.widget.TextV
import android.widget.T
@SuppressLint({ &DefaultLocale&, &SimpleDateFormat& })
public class DownloadActivity extends Activity implements OnClickListener {
private static final String TAG = &DownloadActivity&;
private ConnectivityManager mConnectM
private DownloadManager mDownloadM
private Button btn_download_notify_
private Button btn_download_notify_
private Button btn_download_without_
private Button btn_
private static TextView tv_
private static TextView tv_
private ProgressBar pb_
private static long mDownloadId = 0;
private Uri mApkUri = Uri.parse(&/appdown/&);
private Uri mJpgUri = Uri.parse(&http://c./news/q%3D100/sign=b8e532c9ee58c16fdfaaf51f3deeef01f3a2979a0.jpg&);
private String mApkDir = &Download/alipay.apk&;
private String mJpgDir = &Download/news.jpg&;
private HashMap&Integer,String& mStatusMap = new HashMap&Integer,String&();
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_download);
mConnectMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
mDownloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
btn_download_notify_apk = (Button) findViewById(R.id.btn_download_notify_apk);
btn_download_notify_jpg = (Button) findViewById(R.id.btn_download_notify_jpg);
btn_download_without_notify = (Button) findViewById(R.id.btn_download_without_notify);
btn_cancel = (Button) findViewById(R.id.btn_cancel);
tv_download = (TextView) findViewById(R.id.tv_download);
tv_notify = (TextView) findViewById(R.id.tv_notify);
pb_download = (ProgressBar) findViewById(R.id.pb_download);
pb_download = (ProgressBar) findViewById(R.id.pb_download);
btn_download_notify_apk.setOnClickListener(this);
btn_download_notify_jpg.setOnClickListener(this);
btn_download_without_notify.setOnClickListener(this);
btn_cancel.setOnClickListener(this);
mStatusMap.put(DownloadManager.STATUS_PENDING, &挂起&);
mStatusMap.put(DownloadManager.STATUS_RUNNING, &运行中&);
mStatusMap.put(DownloadManager.STATUS_PAUSED, &暂停&);
mStatusMap.put(DownloadManager.STATUS_SUCCESSFUL, &成功&);
mStatusMap.put(DownloadManager.STATUS_FAILED, &失败&);
//接收下载完成后的intent
public static class DownloadCompleteReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
long downId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
Log.d(TAG,& download complete! id : &+downId+&, mDownloadId=&+mDownloadId);
tv_download.setVisibility(View.VISIBLE);
tv_download.setText(Utils.getNowDateTime()+& 编号&+downId+&的下载任务已完成&);
//接收通知栏点击的intent
public static class NotificationClickReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
Log.d(TAG,& NotificationClickReceiver onReceive&);
if (intent.getAction().equals(DownloadManager.ACTION_NOTIFICATION_CLICKED)) {
long[] downIds = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
for (long downId : downIds) {
Log.d(TAG,& notify click! id : &+downId+&, mDownloadId=&+mDownloadId);
if (downId == mDownloadId) {
tv_notify.setVisibility(View.VISIBLE);
tv_notify.setText(Utils.getNowDateTime()+& 编号&+downId+&的下载进度条被点击了一下&);
public void onClick(View v) {
pb_download.setVisibility(View.GONE);
tv_download.setVisibility(View.GONE);
tv_notify.setVisibility(View.GONE);
NetworkInfo info = mConnectMgr.getActiveNetworkInfo();
if (info == null || info.getState() != NetworkInfo.State.CONNECTED) {
Toast.makeText(this, &当前无可用的上网连接&, Toast.LENGTH_LONG).show();
} else if (info.getType() != ConnectivityManager.TYPE_WIFI) {
Toast.makeText(this, &当前非wifi环境,请连接wifi后下载&, Toast.LENGTH_LONG).show();
if (v.getId() == R.id.btn_download_notify_apk) {
Request down = new Request(mApkUri);
down.setAllowedNetworkTypes(Request.NETWORK_MOBILE|Request.NETWORK_WIFI);
down.setTitle(&APK下载信息&);
down.setDescription(&这是一个APK下载任务&);
down.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
down.setVisibleInDownloadsUi(true);
down.setDestinationInExternalFilesDir(this, null, mApkDir);
mDownloadId = mDownloadManager.enqueue(down);
} else if (v.getId() == R.id.btn_download_notify_jpg) {
Request down_request = new Request(mJpgUri);
down_request.setAllowedNetworkTypes(Request.NETWORK_MOBILE|Request.NETWORK_WIFI);
down_request.setTitle(&JPG下载信息&);
down_request.setDescription(&这是一个JPG下载任务&);
down_request.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
down_request.setVisibleInDownloadsUi(true);
down_request.setDestinationInExternalFilesDir(this, null, mJpgDir);
mDownloadId = mDownloadManager.enqueue(down_request);
} else if (v.getId() == R.id.btn_download_without_notify) {
Request down_request = new Request(mApkUri);
down_request.setAllowedNetworkTypes(Request.NETWORK_MOBILE|Request.NETWORK_WIFI);
down_request.setNotificationVisibility(Request.VISIBILITY_HIDDEN);
down_request.setVisibleInDownloadsUi(false);
down_request.setDestinationInExternalFilesDir(this, null, mApkDir);
mDownloadId = mDownloadManager.enqueue(down_request);
mHandler.postDelayed(mRefresh, 1000);
} else if (v.getId() == R.id.btn_cancel) {
mDownloadManager.remove(mDownloadId);
tv_download.setVisibility(View.VISIBLE);
tv_download.setText(Utils.getNowDateTime()+& 编号&+mDownloadId+&的下载任务已取消&);
mHandler.removeCallbacks(mRefresh);
private Handler mHandler = new Handler();
private Runnable mRefresh = new Runnable() {
public void run() {
boolean bFinish =
Query down_query = new Query();
down_query.setFilterById(mDownloadId);
Cursor cursor = mDownloadManager.query(down_query);
if (cursor.moveToFirst()) {
for (;; cursor.moveToNext()) {
int nameIdx = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);
int mediaTypeIdx = cursor.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE);
int totalSizeIdx = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
int nowSizeIdx = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
int statusIdx = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
int progress = (int)(pb_download.getMax()*
cursor.getLong(nowSizeIdx)/cursor.getLong(totalSizeIdx));
pb_download.setVisibility(View.VISIBLE);
pb_download.setProgress(progress);
if (cursor.getString(nameIdx) == null) {
String desc = &&;
desc = String.format(&%s文件路径:%s\n&, desc, cursor.getString(nameIdx));
desc = String.format(&%s媒体类型:%s\n&, desc, cursor.getString(mediaTypeIdx));
desc = String.format(&%s文件总大小:%d\n&, desc, cursor.getLong(totalSizeIdx));
desc = String.format(&%s已下载大小:%d\n&, desc, cursor.getLong(nowSizeIdx));
desc = String.format(&%s下载进度:%d%%\n&, desc, progress);
desc = String.format(&%s下载状态:%s\n&, desc, mStatusMap.get(cursor.getInt(statusIdx)));
tv_notify.setVisibility(View.VISIBLE);
tv_notify.setText(desc);
if (progress == pb_download.getMax()) {
if (cursor.isLast() == true) {
cursor.close();
if (bFinish != true) {
mHandler.postDelayed(this, 1000);
看过本文的人也看了:
我要留言技术领域:
取消收藏确定要取消收藏吗?
删除图谱提示你保存在该图谱下的知识内容也会被删除,建议你先将内容移到其他图谱中。你确定要删除知识图谱及其内容吗?
删除节点提示无法删除该知识节点,因该节点下仍保存有相关知识内容!
删除节点提示你确定要删除该知识节点吗?

我要回帖

更多关于 文件夹正在使用 的文章

 

随机推荐