1/******************************************************************************
2 * Copyright (c) 2010-2012 Transmission authors and contributors
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a
5 * copy of this software and associated documentation files (the "Software"),
6 * to deal in the Software without restriction, including without limitation
7 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 * and/or sell copies of the Software, and to permit persons to whom the
9 * Software is furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20 * DEALINGS IN THE SOFTWARE.
21 *****************************************************************************/
22
23#include <libtransmission/transmission.h>
24#include <libtransmission/utils.h>
25
26#import "InfoPeersViewController.h"
27#import "NSApplicationAdditions.h"
28#import "NSStringAdditions.h"
29#import "PeerProgressIndicatorCell.h"
30#import "Torrent.h"
31#import "WebSeedTableView.h"
32
33#define ANIMATION_ID_KEY @"animationId"
34#define WEB_SEED_ANIMATION_ID @"webSeed"
35
36@interface InfoPeersViewController (Private)
37
38- (void) setupInfo;
39
40- (void) setWebSeedTableHidden: (BOOL) hide animate: (BOOL) animate;
41- (NSArray *) peerSortDescriptors;
42
43@end
44
45@implementation InfoPeersViewController
46
47- (id) init
48{
49    if ((self = [super initWithNibName: @"InfoPeersView" bundle: nil]))
50    {
51        [self setTitle: NSLocalizedString(@"Peers", "Inspector view -> title")];
52    }
53
54    return self;
55}
56
57- (void) awakeFromNib
58{
59    const CGFloat height = [[NSUserDefaults standardUserDefaults] floatForKey: @"InspectorContentHeightPeers"];
60    if (height != 0.0)
61    {
62        NSRect viewRect = [[self view] frame];
63        viewRect.size.height = height;
64        [[self view] setFrame: viewRect];
65    }
66
67    //set table header text
68    [[[fPeerTable tableColumnWithIdentifier: @"IP"] headerCell] setStringValue: NSLocalizedString(@"IP Address",
69                                                                        "inspector -> peer table -> header")];
70    [[[fPeerTable tableColumnWithIdentifier: @"Client"] headerCell] setStringValue: NSLocalizedString(@"Client",
71                                                                        "inspector -> peer table -> header")];
72    [[[fPeerTable tableColumnWithIdentifier: @"DL From"] headerCell] setStringValue: NSLocalizedString(@"DL",
73                                                                        "inspector -> peer table -> header")];
74    [[[fPeerTable tableColumnWithIdentifier: @"UL To"] headerCell] setStringValue: NSLocalizedString(@"UL",
75                                                                        "inspector -> peer table -> header")];
76
77    [[[fWebSeedTable tableColumnWithIdentifier: @"Address"] headerCell] setStringValue: NSLocalizedString(@"Web Seeds",
78                                                                        "inspector -> web seed table -> header")];
79    [[[fWebSeedTable tableColumnWithIdentifier: @"DL From"] headerCell] setStringValue: NSLocalizedString(@"DL",
80                                                                        "inspector -> web seed table -> header")];
81
82    //set table header tool tips
83    [[fPeerTable tableColumnWithIdentifier: @"Encryption"] setHeaderToolTip: NSLocalizedString(@"Encrypted Connection",
84                                                                        "inspector -> peer table -> header tool tip")];
85    [[fPeerTable tableColumnWithIdentifier: @"Progress"] setHeaderToolTip: NSLocalizedString(@"Available",
86                                                                        "inspector -> peer table -> header tool tip")];
87    [[fPeerTable tableColumnWithIdentifier: @"DL From"] setHeaderToolTip: NSLocalizedString(@"Downloading From Peer",
88                                                                        "inspector -> peer table -> header tool tip")];
89    [[fPeerTable tableColumnWithIdentifier: @"UL To"] setHeaderToolTip: NSLocalizedString(@"Uploading To Peer",
90                                                                        "inspector -> peer table -> header tool tip")];
91
92    [[fWebSeedTable tableColumnWithIdentifier: @"DL From"] setHeaderToolTip: NSLocalizedString(@"Downloading From Web Seed",
93                                                                        "inspector -> web seed table -> header tool tip")];
94
95    //prepare for animating peer table and web seed table
96    fViewTopMargin = fWebSeedTableTopConstraint.constant;
97
98    CABasicAnimation * webSeedTableAnimation = [CABasicAnimation animation];
99    [webSeedTableAnimation setTimingFunction: [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionLinear]];
100    [webSeedTableAnimation setDuration: 0.125];
101    [webSeedTableAnimation setDelegate: self];
102    [webSeedTableAnimation setValue: WEB_SEED_ANIMATION_ID forKey: ANIMATION_ID_KEY];
103    [fWebSeedTableTopConstraint setAnimations: @{ @"constant": webSeedTableAnimation }];
104
105    [self setWebSeedTableHidden: YES animate: NO];
106}
107
108
109#warning subclass?
110- (void) setInfoForTorrents: (NSArray *) torrents
111{
112    //don't check if it's the same in case the metadata changed
113    fTorrents = torrents;
114
115    fSet = NO;
116}
117
118- (void) updateInfo
119{
120    if (!fSet)
121        [self setupInfo];
122
123    if ([fTorrents count] == 0)
124        return;
125
126    if (!fPeers)
127        fPeers = [[NSMutableArray alloc] init];
128    else
129        [fPeers removeAllObjects];
130
131    if (!fWebSeeds)
132        fWebSeeds = [[NSMutableArray alloc] init];
133    else
134        [fWebSeeds removeAllObjects];
135
136    NSUInteger connected = 0, tracker = 0, incoming = 0, cache = 0, lpd = 0, pex = 0, dht = 0, ltep = 0,
137                toUs = 0, fromUs = 0;
138    BOOL anyActive = false;
139    for (Torrent * torrent in fTorrents)
140    {
141        if ([torrent webSeedCount] > 0)
142            [fWebSeeds addObjectsFromArray: [torrent webSeeds]];
143
144        if ([torrent isActive])
145        {
146            anyActive = YES;
147            [fPeers addObjectsFromArray: [torrent peers]];
148
149            const NSUInteger connectedThis = [torrent totalPeersConnected];
150            if (connectedThis > 0)
151            {
152                connected += [torrent totalPeersConnected];
153                tracker += [torrent totalPeersTracker];
154                incoming += [torrent totalPeersIncoming];
155                cache += [torrent totalPeersCache];
156                lpd += [torrent totalPeersLocal];
157                pex += [torrent totalPeersPex];
158                dht += [torrent totalPeersDHT];
159                ltep += [torrent totalPeersLTEP];
160
161                toUs += [torrent peersSendingToUs];
162                fromUs += [torrent peersGettingFromUs];
163            }
164        }
165    }
166
167    [fPeers sortUsingDescriptors: [self peerSortDescriptors]];
168    [fPeerTable reloadData];
169
170    [fWebSeeds sortUsingDescriptors: [fWebSeedTable sortDescriptors]];
171    [fWebSeedTable reloadData];
172    [fWebSeedTable setWebSeeds: fWebSeeds];
173
174    if (anyActive)
175    {
176        NSString * connectedText = [NSString stringWithFormat: NSLocalizedString(@"%d Connected", "Inspector -> Peers tab -> peers"),
177                                    connected];
178
179        if (connected > 0)
180        {
181            NSMutableArray * upDownComponents = [NSMutableArray arrayWithCapacity: 2];
182            if (toUs > 0)
183                [upDownComponents addObject: [NSString stringWithFormat:
184                                        NSLocalizedString(@"DL from %d", "Inspector -> Peers tab -> peers"), toUs]];
185            if (fromUs > 0)
186                [upDownComponents addObject: [NSString stringWithFormat:
187                                        NSLocalizedString(@"UL to %d", "Inspector -> Peers tab -> peers"), fromUs]];
188            if ([upDownComponents count] > 0)
189                connectedText = [connectedText stringByAppendingFormat: @": %@", [upDownComponents componentsJoinedByString: @", "]];
190
191            NSMutableArray * fromComponents = [NSMutableArray arrayWithCapacity: 7];
192            if (tracker > 0)
193                [fromComponents addObject: [NSString stringWithFormat:
194                                        NSLocalizedString(@"%d tracker", "Inspector -> Peers tab -> peers"), tracker]];
195            if (incoming > 0)
196                [fromComponents addObject: [NSString stringWithFormat:
197                                        NSLocalizedString(@"%d incoming", "Inspector -> Peers tab -> peers"), incoming]];
198            if (cache > 0)
199                [fromComponents addObject: [NSString stringWithFormat:
200                                        NSLocalizedString(@"%d cache", "Inspector -> Peers tab -> peers"), cache]];
201            if (lpd > 0)
202                [fromComponents addObject: [NSString stringWithFormat:
203                                        NSLocalizedString(@"%d local discovery", "Inspector -> Peers tab -> peers"), lpd]];
204            if (pex > 0)
205                [fromComponents addObject: [NSString stringWithFormat:
206                                        NSLocalizedString(@"%d PEX", "Inspector -> Peers tab -> peers"), pex]];
207            if (dht > 0)
208                [fromComponents addObject: [NSString stringWithFormat:
209                                        NSLocalizedString(@"%d DHT", "Inspector -> Peers tab -> peers"), dht]];
210            if (ltep > 0)
211                [fromComponents addObject: [NSString stringWithFormat:
212                                        NSLocalizedString(@"%d LTEP", "Inspector -> Peers tab -> peers"), ltep]];
213
214            connectedText = [connectedText stringByAppendingFormat: @"\n%@", [fromComponents componentsJoinedByString: @", "]];
215        }
216
217        [fConnectedPeersField setStringValue: connectedText];
218    }
219    else
220    {
221        NSString * notActiveString;
222        if ([fTorrents count] == 1)
223            notActiveString = NSLocalizedString(@"Transfer Not Active", "Inspector -> Peers tab -> peers");
224        else
225            notActiveString = NSLocalizedString(@"Transfers Not Active", "Inspector -> Peers tab -> peers");
226
227        [fConnectedPeersField setStringValue: notActiveString];
228    }
229}
230
231- (void) saveViewSize
232{
233    [[NSUserDefaults standardUserDefaults] setFloat: NSHeight([[self view] frame]) forKey: @"InspectorContentHeightPeers"];
234}
235
236- (void) clearView
237{
238    fPeers = nil;
239    fWebSeeds = nil;
240}
241
242- (NSInteger) numberOfRowsInTableView: (NSTableView *) tableView
243{
244    if (tableView == fWebSeedTable)
245        return fWebSeeds ? [fWebSeeds count] : 0;
246    else
247        return fPeers ? [fPeers count] : 0;
248}
249
250- (id) tableView: (NSTableView *) tableView objectValueForTableColumn: (NSTableColumn *) column row: (NSInteger) row
251{
252    if (tableView == fWebSeedTable)
253    {
254        NSString * ident = [column identifier];
255        NSDictionary * webSeed = fWebSeeds[row];
256
257        if ([ident isEqualToString: @"DL From"])
258        {
259            NSNumber * rate;
260            return (rate = webSeed[@"DL From Rate"]) ? [NSString stringForSpeedAbbrev: [rate doubleValue]] : @"";
261        }
262        else
263            return webSeed[@"Address"];
264    }
265    else
266    {
267        NSString * ident = [column identifier];
268        NSDictionary * peer = fPeers[row];
269
270        if ([ident isEqualToString: @"Encryption"])
271            return [peer[@"Encryption"] boolValue] ? [NSImage imageNamed: @"Lock"] : nil;
272        else if ([ident isEqualToString: @"Client"])
273            return peer[@"Client"];
274        else if  ([ident isEqualToString: @"Progress"])
275            return peer[@"Progress"];
276        else if ([ident isEqualToString: @"UL To"])
277        {
278            NSNumber * rate;
279            return (rate = peer[@"UL To Rate"]) ? [NSString stringForSpeedAbbrev: [rate doubleValue]] : @"";
280        }
281        else if ([ident isEqualToString: @"DL From"])
282        {
283            NSNumber * rate;
284            return (rate = peer[@"DL From Rate"]) ? [NSString stringForSpeedAbbrev: [rate doubleValue]] : @"";
285        }
286        else
287            return peer[@"IP"];
288    }
289}
290
291- (void) tableView: (NSTableView *) tableView willDisplayCell: (id) cell forTableColumn: (NSTableColumn *) tableColumn
292    row: (NSInteger) row
293{
294    if (tableView == fPeerTable)
295    {
296        NSString * ident = [tableColumn identifier];
297
298        if  ([ident isEqualToString: @"Progress"])
299        {
300            NSDictionary * peer = fPeers[row];
301            [(PeerProgressIndicatorCell *)cell setSeed: [peer[@"Seed"] boolValue]];
302        }
303    }
304}
305
306- (void) tableView: (NSTableView *) tableView didClickTableColumn: (NSTableColumn *) tableColumn
307{
308    if (tableView == fWebSeedTable)
309    {
310        if (fWebSeeds)
311        {
312            [fWebSeeds sortUsingDescriptors: [fWebSeedTable sortDescriptors]];
313            [tableView reloadData];
314        }
315    }
316    else
317    {
318        if (fPeers)
319        {
320            [fPeers sortUsingDescriptors: [self peerSortDescriptors]];
321            [tableView reloadData];
322        }
323    }
324}
325
326- (BOOL) tableView: (NSTableView *) tableView shouldSelectRow: (NSInteger) row
327{
328    return tableView != fPeerTable;
329}
330
331- (NSString *) tableView: (NSTableView *) tableView toolTipForCell: (NSCell *) cell rect: (NSRectPointer) rect
332                tableColumn: (NSTableColumn *) column row: (NSInteger) row mouseLocation: (NSPoint) mouseLocation
333{
334    if (tableView == fPeerTable)
335    {
336        const BOOL multiple = [fTorrents count] > 1;
337
338        NSDictionary * peer = fPeers[row];
339        NSMutableArray * components = [NSMutableArray arrayWithCapacity: multiple ? 6 : 5];
340
341        if (multiple)
342            [components addObject: peer[@"Name"]];
343
344        const CGFloat progress = [peer[@"Progress"] floatValue];
345        NSString * progressString = [NSString stringWithFormat: NSLocalizedString(@"Progress: %@",
346                                        "Inspector -> Peers tab -> table row tooltip"),
347                                        [NSString percentString: progress longDecimals: NO]];
348        if (progress < 1.0 && [peer[@"Seed"] boolValue])
349            progressString = [progressString stringByAppendingFormat: @" (%@)", NSLocalizedString(@"Partial Seed",
350                                "Inspector -> Peers tab -> table row tooltip")];
351        [components addObject: progressString];
352
353        NSString * protocolString = [peer[@"uTP"] boolValue] ? @"\u00b5TP" : @"TCP";
354        if ([peer[@"Encryption"] boolValue])
355            protocolString = [protocolString stringByAppendingFormat: @" (%@)",
356                                NSLocalizedString(@"encrypted", "Inspector -> Peers tab -> table row tooltip")];
357        [components addObject: [NSString stringWithFormat:
358                                NSLocalizedString(@"Protocol: %@", "Inspector -> Peers tab -> table row tooltip"),
359                                protocolString]];
360
361        NSString * portString;
362        NSInteger port;
363        if ((port = [peer[@"Port"] intValue]) > 0)
364            portString = [NSString stringWithFormat: @"%ld", port];
365        else
366            portString = NSLocalizedString(@"N/A", "Inspector -> Peers tab -> table row tooltip");
367        [components addObject: [NSString stringWithFormat: @"%@: %@", NSLocalizedString(@"Port",
368            "Inspector -> Peers tab -> table row tooltip"), portString]];
369
370        const NSInteger peerFrom = [peer[@"From"] integerValue];
371        switch (peerFrom)
372        {
373            case TR_PEER_FROM_TRACKER:
374                [components addObject: NSLocalizedString(@"From: tracker", "Inspector -> Peers tab -> table row tooltip")];
375                break;
376            case TR_PEER_FROM_INCOMING:
377                [components addObject: NSLocalizedString(@"From: incoming connection", "Inspector -> Peers tab -> table row tooltip")];
378                break;
379            case TR_PEER_FROM_RESUME:
380                [components addObject: NSLocalizedString(@"From: cache", "Inspector -> Peers tab -> table row tooltip")];
381                break;
382            case TR_PEER_FROM_LPD:
383                [components addObject: NSLocalizedString(@"From: local peer discovery", "Inspector -> Peers tab -> table row tooltip")];
384                break;
385            case TR_PEER_FROM_PEX:
386                [components addObject: NSLocalizedString(@"From: peer exchange", "Inspector -> Peers tab -> table row tooltip")];
387                break;
388            case TR_PEER_FROM_DHT:
389                [components addObject: NSLocalizedString(@"From: distributed hash table", "Inspector -> Peers tab -> table row tooltip")];
390                break;
391            case TR_PEER_FROM_LTEP:
392                [components addObject: NSLocalizedString(@"From: libtorrent extension protocol handshake",
393                                        "Inspector -> Peers tab -> table row tooltip")];
394                break;
395            default:
396                NSAssert1(NO, @"Peer from unknown source: %ld", peerFrom);
397        }
398
399        //determing status strings from flags
400        NSMutableArray * statusArray = [NSMutableArray arrayWithCapacity: 6];
401        NSString * flags = peer[@"Flags"];
402
403        if ([flags rangeOfString: @"D"].location != NSNotFound)
404            [statusArray addObject: NSLocalizedString(@"Currently downloading (interested and not choked)",
405                "Inspector -> peer -> status")];
406        if ([flags rangeOfString: @"d"].location != NSNotFound)
407            [statusArray addObject: NSLocalizedString(@"You want to download, but peer does not want to send (interested and choked)",
408                "Inspector -> peer -> status")];
409        if ([flags rangeOfString: @"U"].location != NSNotFound)
410            [statusArray addObject: NSLocalizedString(@"Currently uploading (interested and not choked)",
411                "Inspector -> peer -> status")];
412        if ([flags rangeOfString: @"u"].location != NSNotFound)
413            [statusArray addObject: NSLocalizedString(@"Peer wants you to upload, but you do not want to (interested and choked)",
414                "Inspector -> peer -> status")];
415        if ([flags rangeOfString: @"K"].location != NSNotFound)
416            [statusArray addObject: NSLocalizedString(@"Peer is unchoking you, but you are not interested",
417                "Inspector -> peer -> status")];
418        if ([flags rangeOfString: @"?"].location != NSNotFound)
419            [statusArray addObject: NSLocalizedString(@"You unchoked the peer, but the peer is not interested",
420                "Inspector -> peer -> status")];
421
422        if ([statusArray count] > 0)
423        {
424            NSString * statusStrings = [statusArray componentsJoinedByString: @"\n\n"];
425            [components addObject: [@"\n" stringByAppendingString: statusStrings]];
426        }
427
428        return [components componentsJoinedByString: @"\n"];
429    }
430    else
431    {
432        if ([fTorrents count] > 1)
433            return fWebSeeds[row][@"Name"];
434    }
435
436    return nil;
437}
438
439- (void) animationDidStart: (CAAnimation *) animation
440{
441    if (![[animation valueForKey: ANIMATION_ID_KEY] isEqualToString: WEB_SEED_ANIMATION_ID])
442        return;
443
444    [[fWebSeedTable enclosingScrollView] setHidden: NO];
445}
446
447- (void) animationDidStop: (CAAnimation *) animation finished: (BOOL) finished
448{
449    if (![[animation valueForKey: ANIMATION_ID_KEY] isEqualToString: WEB_SEED_ANIMATION_ID])
450        return;
451
452    [[fWebSeedTable enclosingScrollView] setHidden: finished && fWebSeedTableTopConstraint.constant < 0];
453}
454
455@end
456
457@implementation InfoPeersViewController (Private)
458
459- (void) setupInfo
460{
461    __block BOOL hasWebSeeds = NO;
462
463    if ([fTorrents count] == 0)
464    {
465        fPeers = nil;
466        [fPeerTable reloadData];
467
468        [fConnectedPeersField setStringValue: @""];
469    }
470    else
471    {
472        [fTorrents enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(Torrent * torrent, NSUInteger idx, BOOL *stop) {
473            if ([torrent webSeedCount] > 0)
474            {
475                hasWebSeeds = YES;
476                *stop = YES;
477            }
478        }];
479    }
480
481    if (!hasWebSeeds)
482    {
483        fWebSeeds = nil;
484        [fWebSeedTable reloadData];
485    }
486    else
487        [fWebSeedTable deselectAll: self];
488    [self setWebSeedTableHidden: !hasWebSeeds animate: YES];
489
490    fSet = YES;
491}
492
493- (void) setWebSeedTableHidden: (BOOL) hide animate: (BOOL) animate
494{
495    if (animate && (![[self view] window] || ![[[self view] window] isVisible]))
496        animate = NO;
497
498    const CGFloat webSeedTableTopMargin = hide ? -NSHeight([[fWebSeedTable enclosingScrollView] frame]) : fViewTopMargin;
499
500    [(animate ? [fWebSeedTableTopConstraint animator] : fWebSeedTableTopConstraint) setConstant: webSeedTableTopMargin];
501}
502
503- (NSArray *) peerSortDescriptors
504{
505    NSMutableArray * descriptors = [NSMutableArray arrayWithCapacity: 2];
506
507    NSArray * oldDescriptors = [fPeerTable sortDescriptors];
508    BOOL useSecond = YES, asc = YES;
509    if ([oldDescriptors count] > 0)
510    {
511        NSSortDescriptor * descriptor = oldDescriptors[0];
512        [descriptors addObject: descriptor];
513
514        if ((useSecond = ![[descriptor key] isEqualToString: @"IP"]))
515            asc = [descriptor ascending];
516    }
517
518    //sort by IP after primary sort
519    if (useSecond)
520    {
521        NSSortDescriptor * secondDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"IP" ascending: asc selector: @selector(compareNumeric:)];
522        [descriptors addObject: secondDescriptor];
523    }
524
525    return descriptors;
526}
527
528@end
529