/* * Created on Jan 28, 2009 * Created by Paul Gardner * * Copyright 2009 Vuze, Inc. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License only. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ package com.aelitis.azureus.core.devices.impl; import java.io.IOException; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.util.*; import org.gudy.azureus2.core3.disk.DiskManager; import org.gudy.azureus2.core3.disk.DiskManagerFileInfo; import org.gudy.azureus2.core3.disk.DiskManagerPiece; import org.gudy.azureus2.core3.download.DownloadManager; import org.gudy.azureus2.core3.global.GlobalManager; import org.gudy.azureus2.core3.global.GlobalManagerAdapter; import org.gudy.azureus2.core3.internat.MessageText; import org.gudy.azureus2.core3.peer.PEPeer; import org.gudy.azureus2.core3.peer.PEPeerManager; import org.gudy.azureus2.core3.torrent.TOTorrent; import org.gudy.azureus2.core3.torrent.TOTorrentAnnounceURLSet; import org.gudy.azureus2.core3.torrent.TOTorrentFactory; import org.gudy.azureus2.core3.util.*; import org.gudy.azureus2.plugins.download.Download; import org.gudy.azureus2.plugins.peers.Peer; import org.gudy.azureus2.pluginsimpl.local.PluginCoreUtils; import com.aelitis.azureus.core.AzureusCore; import com.aelitis.azureus.core.devices.*; import com.aelitis.azureus.core.security.CryptoManagerFactory; import com.aelitis.azureus.core.torrent.PlatformTorrentUtils; import com.aelitis.azureus.core.util.CopyOnWriteList; import com.aelitis.azureus.util.DownloadUtils; import com.aelitis.net.upnp.UPnPDevice; import com.aelitis.net.upnp.UPnPException; import com.aelitis.net.upnp.UPnPRootDevice; import com.aelitis.net.upnp.services.UPnPOfflineDownloader; public class DeviceOfflineDownloaderImpl extends DeviceUPnPImpl implements DeviceOfflineDownloader { public static final int UPDATE_MILLIS = 30*1000; public static final int UPDATE_TICKS = UPDATE_MILLIS/DeviceManagerImpl.DEVICE_UPDATE_PERIOD; public static final int UPDATE_SPACE_MILLIS = 3*60*1000; public static final int UPDATE_SPACE_TICKS = UPDATE_SPACE_MILLIS/DeviceManagerImpl.DEVICE_UPDATE_PERIOD; public static final String client_id = ByteFormatter.encodeString( CryptoManagerFactory.getSingleton().getSecureID()); private static final Object ERROR_KEY_OD = new Object(); private volatile UPnPOfflineDownloader service; private volatile String service_ip; private volatile String manufacturer; private long start_time = SystemTime.getMonotonousTime(); private volatile boolean update_space_outstanding = true; private volatile long space_on_device = -1; private volatile boolean closing; private AsyncDispatcher dispatcher = new AsyncDispatcher(); final FrequencyLimitedDispatcher freq_lim_updater = new FrequencyLimitedDispatcher( new AERunnable() { public void runSupport() { updateDownloads(); } }, 5*1000 ); private boolean start_of_day = true; private int consec_errors = 0; private int consec_success = 0; private Map<String,OfflineDownload> offline_downloads = new HashMap<String, OfflineDownload>(); private Map<String,TransferableDownload> transferable = new LinkedHashMap<String,TransferableDownload>(); private TransferableDownload current_transfer; private boolean is_transferring; private CopyOnWriteList<DeviceOfflineDownloaderListener> listeners = new CopyOnWriteList<DeviceOfflineDownloaderListener>(); protected DeviceOfflineDownloaderImpl( DeviceManagerImpl _manager, UPnPDevice _device, UPnPOfflineDownloader _service ) { super( _manager, _device, Device.DT_OFFLINE_DOWNLOADER ); setService( _service ); } protected DeviceOfflineDownloaderImpl( DeviceManagerImpl _manager, Map _map ) throws IOException { super(_manager, _map ); manufacturer = getPersistentStringProperty( PP_OD_MANUFACTURER, "?" ); } protected boolean updateFrom( DeviceImpl _other, boolean _is_alive ) { if ( !super.updateFrom( _other, _is_alive )){ return( false ); } if ( !( _other instanceof DeviceOfflineDownloaderImpl )){ Debug.out( "Inconsistent" ); return( false ); } DeviceOfflineDownloaderImpl other = (DeviceOfflineDownloaderImpl)_other; if ( service == null && other.service != null ){ setService( other.service ); updateDownloads(); } return( true ); } protected void setService( UPnPOfflineDownloader _service ) { service = _service; UPnPRootDevice root = service.getGenericService().getDevice().getRootDevice(); service_ip = root.getLocation().getHost(); try{ service_ip = InetAddress.getByName( service_ip ).getHostAddress(); }catch( Throwable e ){ Debug.out( e ); } Map cache = root.getDiscoveryCache(); if ( cache != null ){ setPersistentMapProperty( PP_OD_UPNP_DISC_CACHE, cache ); } manufacturer = root.getDevice().getManufacturer(); setPersistentStringProperty( PP_OD_MANUFACTURER, manufacturer ); updateDownloads(); } protected void UPnPInitialised() { super.UPnPInitialised(); if ( service == null ){ Map cache = getPersistentMapProperty( PP_OD_UPNP_DISC_CACHE, null ); if ( cache != null ){ getUPnPDeviceManager().injectDiscoveryCache( cache ); } } } protected void updateStatus( int tick_count ) { super.updateStatus( tick_count ); update_space_outstanding |= tick_count % UPDATE_SPACE_TICKS == 0; if ( tick_count % UPDATE_TICKS == 0 ){ updateDownloads(); } } protected void checkConfig() { freq_lim_updater.dispatch(); } protected void updateDownloads() { dispatcher.dispatch( new AERunnable() { public void runSupport() { if ( dispatcher.getQueueSize() == 0 ){ updateDownloadsSupport(); } } }); } protected void updateDownloadsSupport() { AzureusCore core = getManager().getAzureusCore(); if ( core == null || closing ){ // not yet initialised or closing return; } boolean warn_if_dead = SystemTime.getMonotonousTime() - start_time > 3*60*1000; if ( !isAlive() || service == null ){ // no usable service if ( warn_if_dead ){ setError( ERROR_KEY_OD, MessageText.getString( "device.od.error.notfound" )); } return; } String error_status = null; boolean force_status = false; Map<String,DownloadManager> new_offline_downloads = new HashMap<String,DownloadManager>(); Map<String,TransferableDownload> new_transferables = new HashMap<String,TransferableDownload>(); try{ if ( update_space_outstanding ){ try{ space_on_device = service.getFreeSpace( client_id ); update_space_outstanding = false; }catch( Throwable e ){ error_status = MessageText.getString( "device.od.error.opfailexcep", new String[]{ "GetFreeSpace", Debug.getNestedExceptionMessage( e )}); log( "Failed to get free space", e ); } } if ( space_on_device == 0 ){ error_status = MessageText.getString( "device.od.error.nospace" ); force_status = true; } Map<String,byte[]> old_cache = (Map<String,byte[]>)getPersistentMapProperty( PP_OD_STATE_CACHE, new HashMap<String,byte[]>()); Map<String,byte[]> new_cache = new HashMap<String, byte[]>(); GlobalManager gm = core.getGlobalManager(); if ( start_of_day ){ start_of_day = false; Map<String,Map> xfer_cache = getPersistentMapProperty( PP_OD_XFER_CACHE, new HashMap<String,Map>()); if ( xfer_cache.size() > 0 ){ List<DownloadManager> initial_downloads = gm.getDownloadManagers(); for ( DownloadManager download: initial_downloads ){ if ( download.isForceStart()){ TOTorrent torrent = download.getTorrent(); if ( torrent == null ){ continue; } try{ byte[] hash = torrent.getHash(); String hash_str = ByteFormatter.encodeString( hash ); Map m = xfer_cache.get( hash_str ); if ( m != null ){ if ( m.containsKey( "f" )){ log( download, "Resetting force-start" ); download.setForceStart( false ); } } }catch( Throwable e ){ Debug.printStackTrace(e); } } } } gm.addListener( new GlobalManagerAdapter() { public void downloadManagerAdded( DownloadManager dm ) { freq_lim_updater.dispatch(); } public void downloadManagerRemoved( DownloadManager dm ) { freq_lim_updater.dispatch(); } }, false ); } DeviceManager manager = getManager(); DeviceOfflineDownloaderManager dodm = manager.getOfflineDownlaoderManager(); List<DownloadManager> downloads; if ( dodm.isOfflineDownloadingEnabled() && isEnabled()){ List<DownloadManager> initial_downloads = gm.getDownloadManagers(); List<DownloadManager> relevant_downloads = new ArrayList<DownloadManager>( initial_downloads.size()); // remove uninteresting ones for ( DownloadManager download: initial_downloads ){ int state = download.getState(); if ( state == DownloadManager.STATE_SEEDING ){ // state == DownloadManager.STATE_ERROR ){ removed - might be out of disk space and fixable continue; } // don't include 'stopping' here as we go through stopping on way to queued if ( state == DownloadManager.STATE_STOPPED ){ // don't remove from downloader if simply paused if ( !download.isPaused()){ continue; } } // if it is complete then of no interest if ( download.isDownloadComplete( false )){ continue; } relevant_downloads.add( download ); } downloads = new ArrayList<DownloadManager>( relevant_downloads.size()); if ( dodm.getOfflineDownloadingIsAuto()){ boolean include_private = dodm.getOfflineDownloadingIncludePrivate(); if ( include_private ){ downloads.addAll( relevant_downloads ); }else{ for ( DownloadManager download: relevant_downloads ){ TOTorrent torrent = download.getTorrent(); if ( !TorrentUtils.isReallyPrivate( torrent )){ downloads.add( download ); } } } }else{ // manual, just use the tagged downloads for ( DownloadManager download: relevant_downloads ){ if ( dodm.isManualDownload( PluginCoreUtils.wrap( download ))){ downloads.add( download ); } } } }else{ downloads = new ArrayList<DownloadManager>(); } Map<DownloadManager,byte[]> download_map = new HashMap<DownloadManager, byte[]>(); for ( DownloadManager download: downloads ){ TOTorrent torrent = download.getTorrent(); if ( torrent == null ){ continue; } try{ byte[] hash = torrent.getHash(); String hash_str = ByteFormatter.encodeString( hash ); DiskManager disk = download.getDiskManager(); if ( disk == null ){ byte[] existing = old_cache.get( hash_str ); if ( existing != null ){ new_cache.put( hash_str, existing ); download_map.put( download, existing ); }else{ // assume not yet started and just use the non-skipped files DiskManagerFileInfo[] files = download.getDiskManagerFileInfo(); byte[] needed = new byte[( torrent.getNumberOfPieces() + 7 ) / 8]; int hits = 0; for ( DiskManagerFileInfo file: files ){ if ( file.isSkipped()){ continue; } int first_piece = file.getFirstPieceNumber(); int last_piece = first_piece + file.getNbPieces() - 1; int needed_pos = first_piece/8; int current_byte = 0; for ( int pos=first_piece;pos<=last_piece;pos++ ){ current_byte = current_byte << 1; current_byte += 1; hits++; if (( pos %8 ) == 7 ){ needed[needed_pos++] |= (byte)current_byte; current_byte = 0; } } if ( current_byte != 0 ){ needed[needed_pos++] |= (byte)(current_byte << (8 - (last_piece % 8))); } } if ( hits > 0 ){ new_cache.put( hash_str, needed ); download_map.put( download, needed ); } } }else{ DiskManagerPiece[] pieces = disk.getPieces(); byte[] needed = new byte[( pieces.length + 7 ) / 8]; int needed_pos = 0; int current_byte = 0; int pos = 0; int hits = 0; for ( DiskManagerPiece piece: pieces ){ current_byte = current_byte << 1; if ( piece.isNeeded() && !piece.isDone()){ current_byte += 1; hits++; } if (( pos %8 ) == 7 ){ needed[needed_pos++] = (byte)current_byte; current_byte = 0; } pos++; } if (( pos % 8 ) != 0 ){ needed[needed_pos++] = (byte)(current_byte << (8 - (pos % 8))); } if ( hits > 0 ){ new_cache.put( hash_str, needed ); download_map.put( download, needed ); } } }catch( Throwable e ){ Debug.out( e ); } } // store this so we have consistent record for downloads that queue/pause etc and therefore lose accessible piece details setPersistentMapProperty( PP_OD_STATE_CACHE, new_cache ); // sort by download priority List<Map.Entry<DownloadManager, byte[]>> entries = new ArrayList<Map.Entry<DownloadManager,byte[]>>( download_map.entrySet()); Collections.sort( entries, new Comparator<Map.Entry<DownloadManager, byte[]>>() { public int compare( Map.Entry<DownloadManager, byte[]> o1, Map.Entry<DownloadManager, byte[]> o2) { return( o1.getKey().getPosition() - o2.getKey().getPosition()); } }); String download_hashes = ""; Iterator<Map.Entry<DownloadManager, byte[]>> it = entries.iterator(); while( it.hasNext()){ Map.Entry<DownloadManager, byte[]> entry = it.next(); DownloadManager download = entry.getKey(); try{ String hash = ByteFormatter.encodeString( download.getTorrent().getHash()); download_hashes += ( download_hashes.length()==0?"":"," ) + hash; new_offline_downloads.put( hash, download ); }catch( Throwable e ){ log( download, "Failed to get download hash", e ); it.remove(); } } try{ String[] set_dl_results = service.setDownloads( client_id, download_hashes ); String set_dl_result = set_dl_results[0].trim(); String set_dl_status = set_dl_results[1]; if ( !set_dl_status.equals( "OK" )){ error_status = MessageText.getString( "device.od.error.opfailstatus", new String[]{ "SetDownloads", set_dl_status }); throw( new Exception( "Failing result returned: " + set_dl_status )); } String[] bits = Constants.PAT_SPLIT_COMMA.split(set_dl_result); int num_bits = set_dl_result.length()==0?0:bits.length; if ( num_bits != entries.size()){ log( "SetDownloads returned an invalid number of results (hashes=" + new_offline_downloads.size() + ",result=" + set_dl_result + ")"); }else{ it = entries.iterator(); int pos = 0; while( it.hasNext()){ Map.Entry<DownloadManager, byte[]> entry = it.next(); DownloadManager download = entry.getKey(); try{ TOTorrent torrent = download.getTorrent(); String hash_str = ByteFormatter.encodeString( torrent.getHash()); int status = Integer.parseInt( bits[ pos++ ]); boolean do_update = false; if ( status == 0 ){ do_update = true; }else if ( status == 1 ){ // need to add the torrent try{ // for vuze content add in the azid if ( PlatformTorrentUtils.isContent( torrent, true )){ String ext = DownloadUtils.getTrackerExtensions( PluginCoreUtils.wrap( download )); if ( ext != null && ext.length() > 0 ){ try{ if ( ext.startsWith( "&" )){ ext = ext.substring(1); } torrent = TOTorrentFactory.deserialiseFromMap( torrent.serialiseToMap()); torrent.setAnnounceURL( appendToURL( torrent.getAnnounceURL(), ext )); TOTorrentAnnounceURLSet[] sets = torrent.getAnnounceURLGroup().getAnnounceURLSets(); for ( TOTorrentAnnounceURLSet set: sets ){ URL[] urls = set.getAnnounceURLs(); for (int i=0;i<urls.length;i++){ urls[i] = appendToURL( urls[i], ext ); } } torrent.getAnnounceURLGroup().setAnnounceURLSets( sets ); }catch( Throwable e ){ log( "Torrent modification failed", e ); } } } String add_result = addTorrent( hash_str, ByteFormatter.encodeStringFully( BEncoder.encode( torrent.serialiseToMap()))); log( download, "AddDownload succeeded" ); if ( add_result.equals( "OK" )){ do_update = true; }else{ error_status = MessageText.getString( "device.od.error.opfailstatus", new String[]{ "AddDownload", add_result }); throw( new Exception( "Failed to add download: " + add_result )); } }catch( Throwable e ){ // TODO: prevent continual attempts to add same torrent? error_status = MessageText.getString( "device.od.error.opfailexcep", new String[]{ "AddDownload", Debug.getNestedExceptionMessage( e )}); log( download, "Failed to add download", e ); } }else{ error_status = MessageText.getString( "device.od.error.opfailstatus", new String[]{ "SetDownloads", String.valueOf( status )}); log( download, "SetDownloads: error status returned - " + status ); } if ( do_update ){ try{ byte[] required_map = entry.getValue(); String required_bitfield = ByteFormatter.encodeStringFully( required_map ); String[] update_results = service.updateDownload( client_id, hash_str, required_bitfield ); String have_bitfield = update_results[0]; String update_status = update_results[1]; if ( !update_status.equals( "OK" )){ error_status = MessageText.getString( "device.od.error.opfailstatus", new String[]{ "UpdateDownload", update_status }); throw( new Exception( "UpdateDownload: Failing result returned: " + update_status )); } int useful_piece_count = 0; if ( have_bitfield.length() > 0 ){ byte[] have_map = ByteFormatter.decodeString( have_bitfield ); if ( have_map.length != required_map.length ){ throw( new Exception( "UpdateDownload: Returned bitmap length invalid" )); } for ( int i=0;i<required_map.length;i++){ int x = ( required_map[i] & have_map[i] )&0xff; if ( x != 0 ){ for (int j=0;j<8;j++){ if ((x&0x01) != 0 ){ useful_piece_count++; } x >>= 1; } } } if ( useful_piece_count > 0 ) { long piece_size = torrent.getPieceLength(); new_transferables.put( hash_str, new TransferableDownload( download, hash_str, have_map, useful_piece_count * piece_size )); } } if ( useful_piece_count > 0 ){ log( download, "They have " + useful_piece_count + " pieces that we don't" ); } }catch( Throwable e ){ error_status = MessageText.getString( "device.od.error.opfailexcep", new String[]{ "UpdateDownload", Debug.getNestedExceptionMessage( e )}); log( download, "UpdateDownload failed", e ); } } }catch( Throwable e ){ log( download, "Processing failed", e ); } } } }catch( Throwable e ){ error_status = MessageText.getString( "device.od.error.opfailexcep", new String[]{ "SetDownloads", Debug.getNestedExceptionMessage( e )}); log( "SetDownloads failed", e ); } }finally{ updateTransferable( new_transferables ); List<OfflineDownload> new_ods = new ArrayList<OfflineDownload>(); List<OfflineDownload> del_ods = new ArrayList<OfflineDownload>(); List<OfflineDownload> cha_ods = new ArrayList<OfflineDownload>(); synchronized( offline_downloads ){ for (Map.Entry<String,DownloadManager> entry: new_offline_downloads.entrySet()){ String key = entry.getKey(); if ( !offline_downloads.containsKey( key )){ OfflineDownload new_od = new OfflineDownload( entry.getValue()); offline_downloads.put( key, new_od ); new_ods.add( new_od ); } } Iterator<Map.Entry<String,OfflineDownload>> it = offline_downloads.entrySet().iterator(); while( it.hasNext()){ Map.Entry<String,OfflineDownload> entry = it.next(); String key = entry.getKey(); OfflineDownload od = entry.getValue(); if ( new_offline_downloads.containsKey( key )){ TransferableDownload new_td = transferable.get( key ); TransferableDownload existing_td = od.getTransferable(); if ( new_td != existing_td ){ if ( !new_ods.contains( od )){ cha_ods.add( od ); } od.setTransferable( new_td ); } }else{ it.remove(); del_ods.add( od ); } } } for ( OfflineDownload od: new_ods ){ for ( DeviceOfflineDownloaderListener listener: listeners ){ try{ listener.downloadAdded( od ); }catch( Throwable e ){ Debug.out( e ); } } } for ( OfflineDownload od: cha_ods ){ for ( DeviceOfflineDownloaderListener listener: listeners ){ try{ listener.downloadChanged( od ); }catch( Throwable e ){ Debug.out( e ); } } } for ( OfflineDownload od: del_ods ){ for ( DeviceOfflineDownloaderListener listener: listeners ){ try{ listener.downloadRemoved( od ); }catch( Throwable e ){ Debug.out( e ); } } } updateError( error_status, force_status ); } } private String addTorrent( String hash_str, String torrent_data ) throws UPnPException { int chunk_size = 40*1024; int length = torrent_data.length(); if ( length < chunk_size ){ return( service.addDownload( client_id, hash_str, torrent_data )); }else{ String status = ""; int rem = length; for( int i=0; i<length; i+=chunk_size ){ int size = Math.min( rem, chunk_size ); status = service.addDownloadChunked( client_id, hash_str, torrent_data.substring( i, i+size ), i, length ); rem -= size; } return( status ); } } protected void updateError( String str, boolean force ) { if ( str == null ){ setError( ERROR_KEY_OD, null ); consec_errors = 0; consec_success++; }else{ // if device isn't connectable then replace the error with something more // user-friendly try{ if ( !service.getGenericService().isConnectable()){ str = MessageText.getString( "device.od.error.notfound" ); } }catch( Throwable e ){ Debug.out( e ); } consec_errors++; consec_success = 0; if ( consec_errors > 2 || force ){ setError( ERROR_KEY_OD, str ); } } } protected URL appendToURL( URL url, String ext ) throws MalformedURLException { String url_str = url.toExternalForm(); if ( url_str.indexOf( '?' ) == -1 ){ url_str += "?" + ext; }else{ url_str += "&" + ext; } return( new URL( url_str )); } protected void updateTransferable( Map<String,TransferableDownload> map ) { // remove non-transferable entries Iterator<Map.Entry<String,TransferableDownload>> it = transferable.entrySet().iterator(); while( it.hasNext()){ Map.Entry<String,TransferableDownload> entry = it.next(); if ( !map.containsKey( entry.getKey())){ TransferableDownload existing = entry.getValue(); if ( existing == current_transfer ){ current_transfer.deactivate(); current_transfer = null; } it.remove(); } } // add in new ones for ( TransferableDownload td: map.values()){ String hash = td.getHash(); if ( !transferable.containsKey( hash )){ transferable.put( hash, td ); } } if ( transferable.size() == 0 ){ if ( is_transferring ){ is_transferring = false; setBusy( false ); } return; } if ( !is_transferring ){ is_transferring = true; setBusy( true ); } // check current if ( current_transfer != null && transferable.size() > 0 ){ // rotate through them in case something's stuck for whatever reason long now = SystemTime.getMonotonousTime(); long runtime = now - current_transfer.getStartTime(); if ( runtime >= 30*1000 ){ boolean rotate = false; PEPeerManager pm = current_transfer.getDownload().getPeerManager(); if ( pm == null ){ rotate = true; }else{ if ( runtime > 3*60*1000 ){ List<PEPeer> peers = pm.getPeers( service_ip ); if ( peers.size() == 0 ){ rotate = true; }else{ PEPeer peer = peers.get(0); if ( peer.getStats().getDataReceiveRate() < 1024 ){ rotate = true; } } } } if ( rotate ){ current_transfer.deactivate(); current_transfer = null; } } } if ( current_transfer == null ){ Iterator<TransferableDownload> it2 = transferable.values().iterator(); current_transfer = it2.next(); it2.remove(); transferable.put( current_transfer.getHash(), current_transfer ); } if ( !current_transfer.isActive()){ current_transfer.activate(); } if ( current_transfer.isForced()){ Map<String,Map> xfer_cache = new HashMap<String,Map>(); Map m = new HashMap(); m.put( "f", new Long(1)); xfer_cache.put( current_transfer.getHash(), m ); setPersistentMapProperty( PP_OD_XFER_CACHE, xfer_cache ); } DownloadManager download = current_transfer.getDownload(); int data_port = current_transfer.getDataPort(); if ( data_port <= 0 ){ try{ String[] start_results = service.startDownload( client_id, current_transfer.getHash()); String start_status = start_results[1]; if ( !start_status.equals( "OK" )){ throw( new Exception( "Failing result returned: " + start_status )); } data_port = Integer.parseInt( start_results[0] ); log( download, "StartDownload succeeded - data port=" + data_port ); }catch( Throwable e ){ log( download, "StartDownload failed", e ); } } if ( data_port > 0 ){ current_transfer.setDataPort( data_port ); } final TransferableDownload transfer = current_transfer; dispatcher.dispatch( new AERunnable() { private final int[] count = { 0 }; public void runSupport() { count[0]++; if ( current_transfer != transfer || !transfer.isActive()){ return; } PEPeerManager pm = transfer.getDownload().getPeerManager(); if ( pm == null ){ return; } List<PEPeer> peers = pm.getPeers( service_ip ); if ( peers.size() > 0 ){ return; } Map user_data = new LightHashMap(); user_data.put( Peer.PR_PRIORITY_CONNECTION, new Boolean( true )); pm.addPeer( service_ip, transfer.getDataPort(), 0, false, user_data ); if ( count[0] < 3 ){ final AERunnable target = this; SimpleTimer.addEvent( "OD:retry", SystemTime.getCurrentTime()+5*1000, new TimerEventPerformer() { public void perform( org.gudy.azureus2.core3.util.TimerEvent event ) { dispatcher.dispatch( target ); }; }); } } }); } protected void close() { super.close(); final AESemaphore sem = new AESemaphore( "DOD:closer" ); dispatcher.dispatch( new AERunnable() { public void runSupport() { try{ closing = true; if ( service != null ){ try{ service.activate( client_id ); }catch( Throwable e ){ } } }finally{ sem.release(); } } }); sem.reserve(250); } public boolean isEnabled() { return( getPersistentBooleanProperty( PP_OD_ENABLED, false )); } public void setEnabled( boolean b ) { setPersistentBooleanProperty( PP_OD_ENABLED, b ); if ( b ){ freq_lim_updater.dispatch(); } } @Override public boolean isAlive() { if ( super.isAlive()){ // more restrictive test here to sync alive state with 'appears to be offline' // error messages return( service.getGenericService().isConnectable()); } return( false ); } public boolean hasShownFTUX() { return( getPersistentBooleanProperty( PP_OD_SHOWN_FTUX, false )); } public void setShownFTUX() { setPersistentBooleanProperty( PP_OD_SHOWN_FTUX, true ); } public String getManufacturer() { return( manufacturer ); } public long getSpaceAvailable( boolean force ) throws DeviceManagerException { if ( space_on_device >= 0 && !force ){ return( space_on_device ); } if ( service == null ){ throw( new DeviceManagerException( "Device is not online" )); } try{ space_on_device = service.getFreeSpace( client_id ); update_space_outstanding = false; return( space_on_device ); }catch( Throwable e ){ throw( new DeviceManagerException( "Failed to read available space", e )); } } public int getTransferingCount() { return( transferable.size()); } public DeviceOfflineDownload[] getDownloads() { synchronized( offline_downloads ){ return( offline_downloads.values().toArray( new DeviceOfflineDownload[ offline_downloads.size()])); } } public void addListener( DeviceOfflineDownloaderListener listener ) { listeners.add( listener ); } public void removeListener( DeviceOfflineDownloaderListener listener ) { listeners.remove( listener ); } protected void getDisplayProperties( List<String[]> dp ) { super.getDisplayProperties( dp ); String space_str = ""; if ( space_on_device >= 0 ){ space_str = DisplayFormatters.formatByteCountToKiBEtc( space_on_device ); } addDP( dp, "azbuddy.enabled", isEnabled()); addDP( dp, "device.od.space", space_str ); } protected void log( DownloadManager download, String str ) { log( download.getDisplayName() + ": " + str ); } protected void log( DownloadManager download, String str, Throwable e ) { log( download.getDisplayName() + ": " + str, e ); } protected void log( String str ) { super.log( "OfflineDownloader: " + str ); } protected void log( String str, Throwable e ) { super.log( "OfflineDownloader: " + str, e ); } protected class OfflineDownload implements DeviceOfflineDownload { private DownloadManager core_download; private Download download; private TransferableDownload transferable; protected OfflineDownload( DownloadManager _core_download ) { core_download = _core_download; download = PluginCoreUtils.wrap( core_download ); } public Download getDownload() { return( download ); } public boolean isTransfering() { return( transferable != null ); } public long getCurrentTransferSize() { TransferableDownload t = transferable; if ( t == null ){ return( 0 ); } return( t.getCurrentTransferSize()); } public long getRemaining() { TransferableDownload t = transferable; if ( t == null ){ return( 0 ); } return( t.getRemaining()); } protected void setTransferable( TransferableDownload td ) { transferable = td; } protected TransferableDownload getTransferable() { return( transferable ); } } protected class TransferableDownload { private DownloadManager download; private String hash_str; private byte[] have_map; private boolean active; private long start_time; private boolean forced; private int data_port; private long transfer_size; private volatile long last_calc; private volatile long last_calc_time; protected TransferableDownload( DownloadManager _download, String _hash_str, byte[] _have_map, long _transfer_size_estimate ) { download = _download; hash_str = _hash_str; have_map = _have_map; // not totally accurate, in general will be > required as based purely on piece // size as opposed to blocks. however, we need an initial estimate as the download // may not yet be running and therefore we can't get accurate size now transfer_size = _transfer_size_estimate; last_calc = transfer_size; } protected long calcDiff() { long now = SystemTime.getMonotonousTime(); if ( now - last_calc_time < 2*1000 ){ return( last_calc ); } DiskManager disk = download.getDiskManager(); if ( disk == null ){ return( last_calc ); } DiskManagerPiece[] pieces = disk.getPieces(); int pos = 0; int current = 0; long remaining = 0; for ( int i=0; i<pieces.length; i++ ){ if ( i % 8 == 0 ){ current = have_map[pos++]&0xff; } if (( current & 0x80 ) != 0 ){ DiskManagerPiece piece = pieces[i]; boolean[] written = piece.getWritten(); if ( written == null ){ if ( !piece.isDone()){ remaining += piece.getLength(); } }else{ for (int j=0;j<written.length;j++){ if ( !written[j] ){ remaining += piece.getBlockSize( j ); } } } } current <<= 1; } last_calc = remaining; last_calc_time = now; return( last_calc ); } protected long getCurrentTransferSize() { return( transfer_size ); } protected long getRemaining() { return( calcDiff()); } protected long getStartTime() { return( start_time ); } protected boolean isForced() { return( forced ); } protected boolean isActive() { return( active ); } protected int getDataPort() { return( data_port ); } protected void setDataPort( int dp ) { data_port = dp; } protected void activate() { active = true; start_time = SystemTime.getMonotonousTime(); if ( download.isForceStart()){ log( download, "Activating for transfer" ); }else{ log( download, "Activating for transfer; setting force-start" ); forced = true; download.setForceStart( true ); } } protected void deactivate() { active = false; if ( forced ){ log( download, "Deactivating for transfer; resetting force-start" ); download.setForceStart( false ); }else{ log( download, "Deactivating for transfer" ); } data_port = 0; } protected DownloadManager getDownload() { return( download ); } protected String getHash() { return( hash_str ); } protected byte[] getHaveMap() { return( have_map ); } } }