enable save image/video to Photos

This commit is contained in:
Danmei Chen 2021-02-25 09:40:23 +01:00
parent f5b683e9d7
commit 317665a90b
3 changed files with 133 additions and 13 deletions

View file

@ -1,11 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -30,6 +28,7 @@
<outlet property="messageImageView" destination="yMW-cT-bpU" id="MNr-F2-abQ"/>
<outlet property="messageText" destination="cx9-0K-P9L" id="kPh-s4-Ioy"/>
<outlet property="playButton" destination="cvc-tl-Pcf" id="eKJ-2T-LUl"/>
<outlet property="plusLongGestureRecognizer" destination="daf-cW-dRj" id="O5u-t0-uMe"/>
<outlet property="resendRecognizer" destination="5ZI-Ip-lGl" id="G2r-On-6mV"/>
<outlet property="totalView" destination="8I3-n2-0kS" id="aa8-j9-saW"/>
</connections>
@ -93,7 +92,7 @@
<rect key="frame" x="10" y="51" width="277" height="4"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
</progressView>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="N75-gL-R6t" userLabel="downloadButton" customClass="UIRoundBorderedButton">
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="N75-gL-R6t" userLabel="downloadButton" customClass="UIRoundBorderedButton">
<rect key="frame" x="87" y="58" width="113" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<accessibility key="accessibilityConfiguration" label="Download"/>
@ -105,7 +104,7 @@
<action selector="onDownloadClick:" destination="-1" eventType="touchUpInside" id="8BO-9E-iOX"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6dl-Nz-rdv" userLabel="cancelButton" customClass="UIRoundBorderedButton">
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6dl-Nz-rdv" userLabel="cancelButton" customClass="UIRoundBorderedButton">
<rect key="frame" x="87" y="58" width="113" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<accessibility key="accessibilityConfiguration" label="Cancel"/>
@ -120,8 +119,8 @@
</button>
</subviews>
</view>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="cvc-tl-Pcf" userLabel="playButton" customClass="UIRoundBorderedButton">
<rect key="frame" x="158" y="106" width="50" height="25"/>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="cvc-tl-Pcf" userLabel="playButton" customClass="UIRoundBorderedButton">
<rect key="frame" x="157" y="106" width="50" height="25"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<accessibility key="accessibilityConfiguration" label="Cancel"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
@ -157,7 +156,7 @@
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IGl-nl-xIE" userLabel="fileButton">
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IGl-nl-xIE" userLabel="fileButton">
<rect key="frame" x="180" y="0.0" width="50" height="50"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" red="1" green="0.39905477280000001" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -172,6 +171,7 @@
<connections>
<outletCollection property="gestureRecognizers" destination="5ZI-Ip-lGl" appends="YES" id="1iY-46-rRR"/>
<outletCollection property="gestureRecognizers" destination="aDF-hC-ddO" appends="YES" id="FIv-pl-I8J"/>
<outletCollection property="gestureRecognizers" destination="daf-cW-dRj" appends="YES" id="qgk-YT-Grl"/>
</connections>
</view>
</subviews>
@ -189,14 +189,19 @@
<action selector="onImageClick:" destination="-1" id="feN-LT-89b"/>
</connections>
</tapGestureRecognizer>
<pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="daf-cW-dRj" userLabel="plusClick">
<connections>
<action selector="onPlusClick:" destination="-1" id="oFW-FN-6xV"/>
</connections>
</pongPressGestureRecognizer>
</objects>
<resources>
<image name="avatar.png" width="259" height="259"/>
<image name="chat_read.png" width="25" height="25"/>
<image name="avatar.png" width="414.39999389648438" height="414.39999389648438"/>
<image name="chat_read.png" width="20" height="20"/>
<image name="color_A.png" width="2" height="2"/>
<image name="color_G.png" width="2" height="2"/>
<image name="color_I.png" width="2" height="2"/>
<image name="color_M.png" width="2" height="2"/>
<image name="linphone_logo.png" width="26" height="26"/>
<image name="linphone_logo.png" width="41.599998474121094" height="42.400001525878906"/>
</resources>
</document>

View file

@ -40,6 +40,7 @@
@property(strong, nonatomic) IBOutlet UITapGestureRecognizer *imageGestureRecognizer;
@property (weak, nonatomic) IBOutlet UIButton *fileButton;
@property (weak, nonatomic) IBOutlet UIView *fileView;
@property (strong, nonatomic) IBOutlet UILongPressGestureRecognizer *plusLongGestureRecognizer;
- (void)setEvent:(LinphoneEventLog *)event;
- (void)setChatMessage:(LinphoneChatMessage *)message;
@ -50,6 +51,7 @@
- (IBAction)onResendClick:(id)event;
- (IBAction)onPlayClick:(id)sender;
- (IBAction)onFileClick:(id)sender;
- (IBAction)onPlusClick:(id)sender;
@end

View file

@ -72,6 +72,7 @@
- (void)setChatMessage:(LinphoneChatMessage *)amessage {
_imageGestureRecognizer.enabled = NO;
_plusLongGestureRecognizer.enabled = NO;
_messageImageView.image = nil;
_finalImage.image = nil;
_finalImage.hidden = TRUE;
@ -133,6 +134,7 @@ static const CGFloat CELL_IMAGE_X_MARGIN = 100;
dispatch_async(dispatch_get_main_queue(), ^{
_fileName.hidden = _fileView.hidden = _fileButton.hidden = NO;
_imageGestureRecognizer.enabled = NO;
_plusLongGestureRecognizer.enabled = NO;
});
}
@ -144,6 +146,7 @@ static const CGFloat CELL_IMAGE_X_MARGIN = 100;
[_messageImageView stopLoading];
_messageImageView.hidden = YES;
_imageGestureRecognizer.enabled = YES;
_plusLongGestureRecognizer.enabled = YES;
_finalImage.hidden = NO;
[self layoutSubviews];
});
@ -187,10 +190,12 @@ static const CGFloat CELL_IMAGE_X_MARGIN = 100;
UIImage *image = [[UIImage alloc] initWithData:data];
[self loadImageAsset:nil image:image];
_imageGestureRecognizer.enabled = YES;
_plusLongGestureRecognizer.enabled = YES;
} else {
// support previous versions
[self loadFirstImage:localImage type:PHAssetMediaTypeImage];
_imageGestureRecognizer.enabled = YES;
_plusLongGestureRecognizer.enabled = YES;
dispatch_async(dispatch_get_main_queue(), ^ {
UIImage *image = [chatTableView.imagesInChatroom objectForKey:localImage];
@ -208,10 +213,12 @@ static const CGFloat CELL_IMAGE_X_MARGIN = 100;
UIImage* image = [UIChatBubbleTextCell getImageFromVideoUrl:[ChatConversationView getCacheFileUrl:localVideo]];
[self loadImageAsset:nil image:image];
_imageGestureRecognizer.enabled = NO;
_plusLongGestureRecognizer.enabled = YES;
} else {
// support previous versions
[self loadFirstImage:localVideo type:PHAssetMediaTypeVideo];
_imageGestureRecognizer.enabled = NO;
_plusLongGestureRecognizer.enabled = YES;
dispatch_async(dispatch_get_main_queue(), ^ {
PHFetchResult<PHAsset *> *assets = [PHAsset fetchAssetsWithLocalIdentifiers:[NSArray arrayWithObject:localVideo] options:nil];
@ -251,11 +258,13 @@ static const CGFloat CELL_IMAGE_X_MARGIN = 100;
UIImage* image = [UIChatBubbleTextCell getImageFromVideoUrl:newVersion? [ChatConversationView getCacheFileUrl:localFile] : [VIEW(ChatConversationView) getICloudFileUrl:localFile]];
[self loadImageAsset:nil image:image];
_imageGestureRecognizer.enabled = NO;
_plusLongGestureRecognizer.enabled = YES;
} else if ([localFile hasSuffix:@"JPG"] || [localFile hasSuffix:@"PNG"] || [localFile hasSuffix:@"jpg"] || [localFile hasSuffix:@"png"]) {
NSData *data = newVersion? [ChatConversationView getCacheFileData:localFile] : [NSData dataWithContentsOfURL:[VIEW(ChatConversationView) getICloudFileUrl:localFile]];
UIImage *image = [[UIImage alloc] initWithData:data];
[self loadImageAsset:nil image:image];
_imageGestureRecognizer.enabled = YES;
_plusLongGestureRecognizer.enabled = YES;
} else {
NSString *text = [NSString stringWithFormat:@"📎 %@",localFile];
_fileName.text = text;
@ -361,6 +370,110 @@ static const CGFloat CELL_IMAGE_X_MARGIN = 100;
}];
}
- (IBAction)onPlusClick:(id)sender {
UILongPressGestureRecognizer *gesture = (UILongPressGestureRecognizer *)sender;
if (gesture.state != UIGestureRecognizerStateBegan) {
// allow only one click once time
return;
}
DTActionSheet *sheet = [[DTActionSheet alloc] initWithTitle:@""];
dispatch_async(dispatch_get_main_queue(), ^{
[sheet addButtonWithTitle:NSLocalizedString(@"Save to Photos", nil)
block:^() {
ChatConversationView *view = VIEW(ChatConversationView);
LinphoneContent *content = linphone_chat_message_get_file_transfer_information(self.message);
NSString *name = [NSString stringWithUTF8String:linphone_content_get_name(content)];
// get download path
NSString *filePath = [[LinphoneManager cacheDirectory] stringByAppendingPathComponent:name];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:filePath]) {
NSData* data = [NSData dataWithContentsOfFile:filePath];
NSString *fileType = [NSString stringWithUTF8String:linphone_content_get_type(content)];
// define a block , not called immediately. To avoid crash when saving photo before PHAuthorizationStatusNotDetermined.
void (^block)(void)= ^ {
if ([fileType isEqualToString:@"image"] ) {
// we're finished, save the image and update the message
UIImage *image = [UIImage imageWithData:data];
if (!image) {
[view showFileDownloadError];
return;
}
__block PHObjectPlaceholder *placeHolder;
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
PHAssetCreationRequest *request = [PHAssetCreationRequest creationRequestForAssetFromImage:image];
placeHolder = [request placeholderForCreatedAsset];
} completionHandler:^(BOOL success, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
LOGE(@"Cannot save image data downloaded [%@]", [error localizedDescription]);
UIAlertController *errView = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Transfer error", nil)
message:NSLocalizedString(@"Cannot write image to photo library",nil)
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {}];
[errView addAction:defaultAction];
[PhoneMainView.instance presentViewController:errView animated:YES completion:nil];
} else {
LOGI(@"Image saved to [%@]", [placeHolder localIdentifier]);
}
});
}];
} else if([fileType isEqualToString:@"video"]) {
// until image is properly saved, keep a reminder on it so that the
// chat bubble is aware of the fact that image is being saved to device
__block PHObjectPlaceholder *placeHolder;
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
PHAssetCreationRequest *request = [PHAssetCreationRequest creationRequestForAssetFromVideoAtFileURL:[NSURL fileURLWithPath:filePath]];
placeHolder = [request placeholderForCreatedAsset];
} completionHandler:^(BOOL success, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
LOGE(@"Cannot save video data downloaded [%@]", [error localizedDescription]);
UIAlertController *errView = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Transfer error", nil)
message:NSLocalizedString(@"Cannot write video to photo library", nil)
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {}];
[errView addAction:defaultAction];
[PhoneMainView.instance presentViewController:errView animated:YES completion:nil];
} else {
LOGI(@"video saved to [%@]", [placeHolder localIdentifier]);
}
});
}];
}
};
// When you save an image or video to a photo library, make sure that it is allowed. Otherwise, there will be a backup error.
if ([PHPhotoLibrary authorizationStatus] == PHAuthorizationStatusAuthorized) {
block();
} else {
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
dispatch_async(dispatch_get_main_queue(), ^{
if ([PHPhotoLibrary authorizationStatus] == PHAuthorizationStatusAuthorized) {
block();
} else {
[[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Photo's permission", nil) message:NSLocalizedString(@"Photo not authorized", nil) delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Continue", nil] show];
}
});
}];
}
}
}];
[sheet addCancelButtonWithTitle:NSLocalizedString(@"Cancel", nil) block:nil];
[sheet showInView:PhoneMainView.instance.view];
});
}
- (IBAction)onFileClick:(id)sender {
ChatConversationView *view = VIEW(ChatConversationView);
NSString *name = [LinphoneManager getMessageAppDataForKey:@"localfile" inMessage:self.message];