1/*
2 * Copyright (c) 2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15import UIExtensionContentSession from '@ohos.app.ability.UIExtensionContentSession';
16import fs from '@ohos.file.fs';
17import image from '@ohos.multimedia.image';
18import media from '@ohos.multimedia.media';
19import ability from '@ohos.ability.ability';
20import { Constants, Log } from '@ohos/common';
21import common from '@ohos.app.ability.common';
22import photoAccessHelper from '@ohos.file.photoAccessHelper';
23import { CustomContentDialog } from '@ohos.arkui.advanced.Dialog';
24
25const TAG: string = 'SaveUIExtension';
26let storage = LocalStorage.getShared();
27
28@Entry(storage)
29@Component
30export struct SaveUIExtensionPage {
31  private context = getContext(this) as common.UIExtensionContext;
32  private uris: Array<string> = [];
33  private bundleName: string = '';
34  private appName: string = '';
35  private appId: string = '';
36  private photoTypeArray: photoAccessHelper.PhotoType[] = [];
37  private srcFileUris: Array<photoAccessHelper.PhotoCreationConfig> = [];
38  private title: string = 'app.string.third_save_dialog_picture_and_video';
39  private wantParam: Record<string, Object> = storage.get('wantParam') as Record<string, Object>;
40  private session: UIExtensionContentSession =
41    storage.get<UIExtensionContentSession>('session') as UIExtensionContentSession;
42  @State private imageMaps: (string | image.PixelMap)[] = ['', ''];
43
44  dialogController: CustomDialogController | null = new CustomDialogController({
45    builder: CustomContentDialog({
46      contentBuilder: () => {
47        this.buildContent();
48      },
49      contentAreaPadding: { right: 0 },
50      buttons: [
51        {
52          value: $r('app.string.dialog_ban'),
53          buttonStyle: ButtonStyleMode.TEXTUAL,
54          action: () => {
55            this.setSaveResult(false);
56          }
57        },
58        {
59          value: $r('app.string.dialog_allow'),
60          buttonStyle: ButtonStyleMode.TEXTUAL,
61          action: () => {
62            this.setSaveResult(true);
63          }
64        }
65      ],
66    }),
67    autoCancel: false,
68    cancel: () => {
69      this.context.terminateSelf();
70    }
71  });
72
73  @Builder
74  buildContent(): void {
75    Column() {
76      Shape() {
77        Circle({ width: Constants.NUMBER_32, height: Constants.NUMBER_32 })
78          .fill($r('sys.color.multi_color_03'))
79        Row() {
80          SymbolGlyph($r('sys.symbol.picture_fill'))
81            .fontSize(Constants.NUMBER_20)
82            .fontColor([$r('sys.color.ohos_id_color_primary_contrary')])
83            .draggable(false)
84        }
85        .offset({ x: $r('app.float.offset_6'), y: $r('app.float.offset_6') })
86      }
87
88      Column() {
89        Text($r(this.title, this.appName, this.uris.length))
90          .textAlign(TextAlign.Center)
91          .fontSize($r('sys.float.Title_S'))
92          .fontWeight(FontWeight.Bold)
93          .fontColor($r('sys.color.font_primary'))
94          .textOverflow({ overflow: TextOverflow.Ellipsis })
95          .maxLines(Constants.NUMBER_2)
96          .minFontSize($r('sys.float.Subtitle_M'))
97          .maxFontSize($r('sys.float.Title_S'))
98          .heightAdaptivePolicy(TextHeightAdaptivePolicy.MIN_FONT_SIZE_FIRST)
99      }
100      .height($r('app.float.dialog_title_height'))
101      .justifyContent(FlexAlign.Center)
102
103      Stack({ alignContent: Alignment.Bottom }) {
104        if (this.uris.length > 1) {
105          Column() {
106            Image(this.imageMaps[1])
107              .objectFit(ImageFit.Cover)
108              .border({
109                radius: $r('app.float.radius_24'),
110                color: $r('sys.color.ohos_id_color_click_effect'),
111                width: $r('app.float.save_image_border')
112              })
113              .height('100%')
114              .width('100%')
115              .opacity(0.4)
116          }
117          .padding({ left: $r('app.float.padding_16'), right: $r('app.float.padding_16') })
118        }
119
120        Image(this.imageMaps[0])
121          .objectFit(ImageFit.Cover)
122          .border({
123            radius: $r('app.float.radius_24'),
124            color: $r('sys.color.ohos_id_color_click_effect'),
125            width: $r('app.float.save_image_border')
126          })
127          .height(this.uris.length > 1 ? $r('app.float.third_delete_dialog_ico_height_multi') : '100%')
128          .width('100%')
129          .margin({ top: this.uris.length > 1 ? $r('app.float.margin_8') : 0 })
130      }
131      .width('100%')
132      .height($r('app.float.third_delete_dialog_ico_height'))
133      .alignContent(Alignment.Top)
134    }
135    .alignItems(HorizontalAlign.Center)
136    .width('100%')
137    .padding({
138      top: $r('app.float.padding_24'),
139      bottom: $r('app.float.padding_8'),
140      left: $r('app.float.dialog_content_padding_left'),
141      right: $r('app.float.dialog_content_padding_right')
142    })
143  }
144
145  build() {}
146
147  onPageShow() {
148    this.session.setWindowBackgroundColor('#00000000');
149  }
150
151  aboutToAppear() {
152    this.uris = this.wantParam?.['ability.params.stream'] as string[];
153    this.bundleName = this.wantParam?.bundleName as string;
154    this.appName = this.wantParam?.appName as string;
155    this.appId = this.wantParam?.appId as string;
156    let titleArray = this.wantParam?.titleArray as string[];
157    let extensionArray = this.wantParam?.extensionArray as string[];
158    let photoTypeArray = this.wantParam?.photoTypeArray as photoAccessHelper.PhotoType[];
159    let photoSubtypeArray = this.wantParam?.photoSubTypeArray as photoAccessHelper.PhotoSubtype[];
160    this.photoTypeArray = photoTypeArray;
161    this.getImageMaps(0);
162    this.getImageMaps(1);
163    try {
164      let imageArray = photoTypeArray.filter(type => type === photoAccessHelper.PhotoType.IMAGE);
165      if (imageArray.length === photoTypeArray.length) {
166        this.title = 'app.string.third_save_dialog_picture';
167      } else if (imageArray.length === 0) {
168        this.title = 'app.string.third_save_dialog_video';
169      }
170      titleArray.forEach((title, idx) => {
171        let photoCreateConfig = {
172          title,
173          fileNameExtension: extensionArray[idx],
174          photoType: photoTypeArray[idx],
175          subtype: photoSubtypeArray[idx]
176        } as photoAccessHelper.PhotoCreationConfig;
177        this.srcFileUris.push(photoCreateConfig);
178      })
179      Log.info(TAG, `srcFileUris: ${JSON.stringify(this.srcFileUris)}.`);
180      this.dialogController?.open();
181    } catch (err) {
182      Log.error(TAG, `aboutToAppear data collection failure. err: ${JSON.stringify(err)}`);
183      this.session.terminateSelf();
184    }
185  }
186
187  private async setSaveResult(isSave: boolean) {
188    if (isSave) {
189      let result = await this.saveBox();
190      let abilityResult: ability.AbilityResult = {
191        resultCode: 0,
192        want: {
193          parameters: {
194            'desFileUris': result
195          }
196        }
197      };
198      Log.info(TAG, 'terminateSelfWithResult start, abilityResult:' + JSON.stringify(abilityResult));
199      this.session.terminateSelfWithResult(abilityResult);
200    } else {
201      let abilityResult: ability.AbilityResult = {
202        resultCode: -1
203      };
204      Log.info(TAG, 'terminateSelfWithResult start, isSave:' + isSave);
205      this.session.terminateSelfWithResult(abilityResult);
206    }
207  }
208
209  async saveBox(): Promise<Array<string>> {
210    try {
211      let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.context);
212      let result: string[] =
213        await phAccessHelper.createAssetsForApp(this.bundleName, this.appName, this.appId, this.srcFileUris);
214      Log.info(TAG, `Photo agentCreateAssets: ${JSON.stringify(result)}`);
215      return result;
216    } catch (err) {
217      Log.error(TAG, `Photo agentCreateAssets failed with error: ${err.code}, ${err.message}.`);
218      return [];
219    }
220  }
221
222  private async getImageMaps(index: number) {
223    if (!this.uris) {
224      return;
225    }
226    if (index > this.uris.length - 1) {
227      return;
228    }
229    let uri: string = this.uris[index];
230    let type: photoAccessHelper.PhotoType = this.photoTypeArray[index];
231    let imgFile: fs.File | undefined;
232    let imageSource: image.ImageSource | undefined;
233    let avImageGenerator: media.AVImageGenerator | undefined;
234    try {
235      if (type === photoAccessHelper.PhotoType.IMAGE) {
236        imgFile = fs.openSync(uri);
237        imageSource = image.createImageSource(imgFile.fd);
238        this.imageMaps[index] = imageSource.createPixelMapSync();
239      } else {
240        avImageGenerator = await media.createAVImageGenerator();
241        avImageGenerator.fdSrc = fs.openSync(uri);
242        this.imageMaps[index] =
243          await avImageGenerator.fetchFrameByTime(0, media.AVImageQueryOptions.AV_IMAGE_QUERY_CLOSEST_SYNC, {});
244      }
245    } catch (err) {
246      Log.error(TAG, `get PixelMap failed with error: ${err.code}, ${err.message}.`);
247    } finally {
248      if (type === photoAccessHelper.PhotoType.IMAGE) {
249        imageSource?.release();
250        fs.closeSync(imgFile?.fd);
251      } else {
252        fs.closeSync(avImageGenerator?.fdSrc?.fd);
253        avImageGenerator?.release();
254      }
255    }
256  }
257}