Overview
Menu

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;

/** 1. Preliminary verification of the request. Any request that doesn't require handling, will be directly fallback */
if (!this.checkRequest(request)) {
return;
}

/** 2. Utilize fetch to retrieve the source file */
let response = null;
try {
response = await fetch(request);
} catch (err) {
const error = {
code: 'FETCH_ORIGIN_ERROR',
message: err?.message,
};
response = new Response(JSON.stringify(error), {
status: 590,
});
}

/** 3. Verification of the response. A response that doesn't require handling, will be directly returned to the client */
if (!this.checkResponse(response)) {
return event.respondWith(response);
}

/** 4. Manage the APK file and respond to the client */
const { readable, writable } = new TransformStream();
this.handleStream(response, writable);

response.headers.set('Cache-Control', 'max-age=0');
const streamResponse = new Response(readable, response);

event.respondWith(streamResponse);
}

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

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

/** ATTENTION: By default, the 'comment' parameter is taken, should there be a need to modify the parameter name, please alter it here */
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;
}

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];
}

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

const responseBody = response.body;
const encoder = new TextEncoder();

const section = encoder.encode(comment);
const writer = writable.getWriter();
const reader = responseBody.getReader();

try {
let handledBytes = 0;
while (true) {
const result = await reader.read();

if (result.done) {
console.log('WRITE_COMMENT_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);
}

await writer.ready;
await writer.write(buffer);
}
} catch (err) {
console.error('WRITE_COMMENT_ERROR: ', err);
}

try {
await writer.ready;
await writer.close();
} catch (err) {
console.error('CLOSE_WRITER_ERROR: ', err);
} finally {
writer.releaseLock();
}
}
}

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.