이 페이지는 현재 영어로만 제공되며 한국어 버전은 곧 제공될 예정입니다. 기다려 주셔서 감사드립니다.

Step 2: Write the Channel Information into the APK Package with EdgeOne Edge Functions

Through EdgeOne edge function, we can dynamically write channel information into the APK package. Users only need to access the domain bound to the edge function and trigger the appropriate configuration to enable the edge function, achieving dynamic packaging and accelerated distribution of the APK.

Step 1: Add an Acceleration Domain Name for Enhanced Distribution Speeds

Please follow the instructions in Adding A Domain Name for Acceleration to add an acceleration domain, for example: www.example.com, and configure the origin server to the COS where the Android APK parent package is located, as shown below:
Note:
This domain will be used to access and download the APK installation package.


Step 2: Create an Edge Function for Triggering Channel Information Writing

1. Follow the instructions in Function Management to create an edge function and copy the following code into the function code.
const CUSTOM_BLOCK_VALUE_LENGTH = 10240;
const APK_SIGNING_BLOCK_MAGIC_LENGTH = 16;
const APK_SIGNING_BLOCK_OFFSET_LENGTH = 8;

const APK_COMMENT_LENGTH = 512;

class EdgePack {
totalSize;
signVersion;
centralDirectoryOffset;
customBlockValueStart;
customBlockValueEnd;
rangeRelativeOffset;
customInfo;

constructor() {
this.totalSize = null;
this.signVersion = null;
this.centralDirectoryOffset = null;
this.customBlockValueStart = null;
this.customBlockValueEnd = null;
this.rangeRelativeOffset = null;
this.customInfo = null;
}

async handle(event) {
const { request } = event;

const headers = new Headers(request.headers);

const modifiedRequest = new Request(request, { headers });

if (!this.checkRequest(modifiedRequest)) {
return;
}

let response = null;
try {
const headRequest = new Request(modifiedRequest.url, {
method: 'HEAD',
headers: modifiedRequest.headers,
});
response = await fetch(headRequest);
} catch (err) {
const error = {
code: 'FETCH_ORIGIN_ERROR',
message: err?.message,
};
response = new Response(JSON.stringify(error), {
status: 590,
});
}

if (!this.checkResponse(response)) {
return event.respondWith(response);
}

response.headers.set('Cache-Control', 'max-age=0');

const streamResponse = new Response(
await this.combineStreams(modifiedRequest),
response
);

event.respondWith(streamResponse);
}

getRelativeOffset(response) {
const start = this.customBlockValueStart;
const end = this.customBlockValueEnd;

const range = response.headers.get('Content-Range');

if (!range) return start;

const match = range.match(/bytes\s*(\d*)-(\d*)/i);
if (!match || match?.length < 2) {
return start;
}

if (+match[2] < start || +match[1] > end) {
return null;
}

return start - +match[1];
}

checkRequest(request) {
if (request.method !== 'GET') {
return false;
}

if (request.headers.has('Range')) {
return false;
}

const { pathname, searchParams } = new URL(request.url);

const comment = searchParams?.get('comment');

if (!pathname.endsWith('.apk') || !comment) {
return false;
}

this.customInfo = comment;
return true;
}

checkResponse(response) {
if (response.status !== 200 && response.status !== 206) {
return false;
}

const contentLength = response.headers.get('Content-Length');

if (response.body === null || contentLength === null) {
return false;
}

this.totalSize = Number(contentLength);

const cosOffsetHeader = response.headers.get('x-cos-meta-edgepack-offset');
const cosTypeHeader = response.headers.get('x-cos-meta-edgepack-type');

if (!cosOffsetHeader || !cosTypeHeader) {
return false;
}

this.signVersion = cosTypeHeader;
this.centralDirectoryOffset = Number(cosOffsetHeader);

if (this.signVersion === 'v1') {
this.customBlockValueStart = this.totalSize - APK_COMMENT_LENGTH;
this.customBlockValueEnd = this.totalSize;
} else {
this.customBlockValueStart =
this.centralDirectoryOffset -
CUSTOM_BLOCK_VALUE_LENGTH -
APK_SIGNING_BLOCK_MAGIC_LENGTH -
APK_SIGNING_BLOCK_OFFSET_LENGTH;
this.customBlockValueEnd = this.centralDirectoryOffset;
}

this.rangeRelativeOffset = this.getRelativeOffset(response);

if (this.rangeRelativeOffset === null) {
return false;
}

return true;
}

async combineStreams(request) {
const { readable, writable } = new TransformStream();
this.handleStream(request, writable);
return readable;
}

async handleStream(request, writable) {
const comment = this.customInfo;
const relativeOffset = this.rangeRelativeOffset;

const encoder = new TextEncoder();
const section = encoder.encode(comment);

try {
const apkHeader = await this.apkHeaderStream(request);

// Return to Blob data
const apkBody = await this.apkBodyStream(
request,
section,
relativeOffset
);
const apkBodyStream = apkBody.stream();

const apkTail = await this.apkTailStream(request);

const sources = [apkHeader, apkBodyStream, apkTail];

for (const stream of sources) {
try {
await stream.pipeTo(writable, {
preventClose: true,
});
} catch (e) {
console.error('STREAM_ERROR: ', e);
}
}
} catch (err) {
console.error('HANDLE_STREAM_ERROR: ', err);
} finally {
let writer = writable.getWriter();
writer.close();
writer.releaseLock();
}
}

async apkHeaderStream(request) {
const headers = new Headers(request.headers);
headers.set('Range', `bytes=0-${this.customBlockValueStart - 1}`);

//Obtaining the part before the signature block.
const headResponse = await fetch(request, {
headers: headers,
});

return headResponse.body;
}

async apkBodyStream(request, section = null, relativeOffset = 0) {
const headers = new Headers(request.headers);
headers.set(
'Range',
`bytes=${this.customBlockValueStart}-${this.customBlockValueEnd - 1}`
);

const middleResponse = await fetch(request, {
headers: headers,
});

const reader = middleResponse.body.getReader();

let outputBuffers = [];
try {
let handledBytes = this.customBlockValueStart;
while (true) {
const result = await reader.read();

if (result.done) {
console.log('APK_BODY_STREAM_DONE');
break;
}

const startByteOffset = handledBytes;
const buffer = result.value;
handledBytes += buffer.byteLength;

const min = Math.max(startByteOffset, relativeOffset);
const max = Math.min(relativeOffset + section.byteLength, handledBytes);

if (min < max) {
const bufferStart = min - startByteOffset;
const sectionStart = min - relativeOffset;
const sectionEnd = max - relativeOffset;

const replacement = section.subarray(sectionStart, sectionEnd);

new Uint8Array(buffer).set(replacement, bufferStart);
}

outputBuffers.push(buffer);
}
} catch (err) {
console.error('APK_BODY_STREAM_ERROR: ', err);
}
return new Blob(outputBuffers);
}

async apkTailStream(request) {
const headers = new Headers(request.headers);
headers.set(
'Range',
`bytes=${this.customBlockValueEnd}-${this.totalSize - 1}`
);

const tailResponse = await fetch(request, {
headers: headers,
});

return tailResponse.body;
}
}

async function handleEvent(event) {
const edgepack = new EdgePack();
await edgepack.handle(event);
}

addEventListener('fetch', handleEvent);
2. After deploying the function, configure the trigger rule under Function Management as directed, where the HOST value is the acceleration domain name created in Step 1, as shown below:

3. Click OK to complete the creation of the trigger rule. When users access the domain www.example.com with a file suffix of .apk, it will trigger the edge function for dynamic packaging.