我迷失了 Android Apk 扩展程序

I'm lost with the Android Apk Exapnsion Procedure

我的应用最近达到了 "maximum" 100 MB 的大小,因此我必须了解 apk 扩展程序。我阅读了 google 的文档,但我发现它有点糟糕,所以我环顾四周并遵循了这个指南:https://kitefaster.com/2017/02/15/expansion-apk-files-android-studio/。现在,在添加 SampleDownloaderActivity、SampleDownloaderService 和 SampleAlarmReceiver 类 之后,我迷路了。我在 phone 的内部存储中的这个位置 Android/obb/com.mypackage.example 中创建了一个目录,就像 Google 文档提到的那样,所以我可以把我的扩展文件放在那里。但这是我的问题:

1) 我已经在应用程序的 manifest.xml 文件中添加了所需的权限,但我是否需要先通过代码请求它们才能让接收器、下载器和 downloaderActivity 正常工作?

2) 我想包含在我的 主扩展文件 中的文件是我的 xhdpi drawable 文件夹中的一些图像,但我不确定我需要做什么才能创建包含这些图像的 .obb 文件。我需要用它们创建一个 .zip 文件吗?我是否只是将它们放在上面提到的我创建的目录中?我需要做什么?

3) 假设前面的问题已经回答,如果文件已经存在于目录中或已经下载,我必须通过代码获取该目录并读取文件才能在我的应用程序中使用它们,对吗?

4) 如果文件不存在,我如何从 Google-Play 启动下载?根据我对文档的理解,我需要将我的应用程序的主要 activity 设为 "SampleDownloaderActivity",对吧?

5) 文件下载完成后,我是否必须在 SampleDownloaderActivity 的 onCreate 方法中创建一个意图,以便应用程序转到我想要的使用这些文件的 activity?

下面,我发布了 apk 扩展相关的代码文件,我在其中更改了我认为需要的内容。我需要改变其他东西吗?预先感谢您的帮助!

ApkExpDownloaderService.java

public class ApkExpDownloaderService extends DownloaderService {
// stuff for LVL -- MODIFY FOR YOUR APPLICATION!
private static final String BASE64_PUBLIC_KEY = "MY_KEY";
// used by the preference obfuscater
private static final byte[] SALT = new byte[] {
        // my array of bytes
};

/**
 * This public key comes from your Android Market publisher account, and it
 * used by the LVL to validate responses from Market on your behalf.
 */
@Override
public String getPublicKey() {
    return BASE64_PUBLIC_KEY;
}

/**
 * This is used by the preference obfuscater to make sure that your
 * obfuscated preferences are different than the ones used by other
 * applications.
 */
@Override
public byte[] getSALT() {
    return SALT;
}

/**
 * Fill this in with the class name for your alarm receiver. We do this
 * because receivers must be unique across all of Android (it's a good idea
 * to make sure that your receiver is in your unique package)
 */
@Override
public String getAlarmReceiverClassName() {
    return ApkExpAlarmReceiver.class.getName();
}

}

ApkExpDownloaderActivity.java

public class ApkExpDownloaderActivity extends Activity implements IDownloaderClient {
private static final String LOG_TAG = "LVLDownloader";
private ProgressBar mPB;

private TextView mStatusText;
private TextView mProgressFraction;
private TextView mProgressPercent;
private TextView mAverageSpeed;
private TextView mTimeRemaining;

private View mDashboard;
private View mCellMessage;

private Button mPauseButton;
private Button mWiFiSettingsButton;

private boolean mStatePaused;
private int mState;

private IDownloaderService mRemoteService;

private IStub mDownloaderClientStub;

private void setState(int newState) {
    if (mState != newState) {
        mState = newState;
        mStatusText.setText(Helpers.getDownloaderStringResourceIDFromState(newState));
    }
}

private void setButtonPausedState(boolean paused) {
    mStatePaused = paused;
    int stringResourceID = paused ? R.string.text_button_resume :
            R.string.text_button_pause;
    mPauseButton.setText(stringResourceID);
}

/**
 * This is a little helper class that demonstrates simple testing of an
 * Expansion APK file delivered by Market. You may not wish to hard-code
 * things such as file lengths into your executable... and you may wish to
 * turn this code off during application development.
 */
private static class XAPKFile {
    public final boolean mIsMain;
    public final int mFileVersion;
    public final long mFileSize;

    XAPKFile(boolean isMain, int fileVersion, long fileSize) {
        mIsMain = isMain;
        mFileVersion = fileVersion;
        mFileSize = fileSize;
    }
}

/**
 * Here is where you place the data that the validator will use to determine
 * if the file was delivered correctly. This is encoded in the source code
 * so the application can easily determine whether the file has been
 * properly delivered without having to talk to the server. If the
 * application is using LVL for licensing, it may make sense to eliminate
 * these checks and to just rely on the server.
 */
private static final XAPKFile[] xAPKS = {
        new XAPKFile(
                true, // true signifies a main file
                21, // the version of the APK that the file was uploaded
                   // against
                687801613L // the length of the file in bytes
        )
};

/**
 * Go through each of the APK Expansion files defined in the structure above
 * and determine if the files are present and match the required size. Free
 * applications should definitely consider doing this, as this allows the
 * application to be launched for the first time without having a network
 * connection present. Paid applications that use LVL should probably do at
 * least one LVL check that requires the network to be present, so this is
 * not as necessary.
 * 
 * @return true if they are present.
 */
boolean expansionFilesDelivered() {
    for (XAPKFile xf : xAPKS) {
        String fileName = Helpers.getExpansionAPKFileName(this, xf.mIsMain, xf.mFileVersion);
        if (!Helpers.doesFileExist(this, fileName, xf.mFileSize, false))
            return false;
    }
    return true;
}

/**
 * Calculating a moving average for the validation speed so we don't get
 * jumpy calculations for time etc.
 */
static private final float SMOOTHING_FACTOR = 0.005f;

/**
 * Used by the async task
 */
private boolean mCancelValidation;

/**
 * Go through each of the Expansion APK files and open each as a zip file.
 * Calculate the CRC for each file and return false if any fail to match.
 * 
 * @return true if XAPKZipFile is successful
 */
void validateXAPKZipFiles() {
    AsyncTask<Object, DownloadProgressInfo, Boolean> validationTask = new AsyncTask<Object, DownloadProgressInfo, Boolean>() {

        @Override
        protected void onPreExecute() {
            mDashboard.setVisibility(View.VISIBLE);
            mCellMessage.setVisibility(View.GONE);
            mStatusText.setText(R.string.text_verifying_download);
            mPauseButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    mCancelValidation = true;
                }
            });
            mPauseButton.setText(R.string.text_button_cancel_verify);
            super.onPreExecute();
        }

        @Override
        protected Boolean doInBackground(Object... params) {
            for (XAPKFile xf : xAPKS) {
                String fileName = Helpers.getExpansionAPKFileName(
                        ApkExpDownloaderActivity.this,
                        xf.mIsMain, xf.mFileVersion);
                if (!Helpers.doesFileExist(ApkExpDownloaderActivity.this, fileName,
                        xf.mFileSize, false))
                    return false;
                fileName = Helpers
                        .generateSaveFileName(ApkExpDownloaderActivity.this, fileName);
                ZipResourceFile zrf;
                byte[] buf = new byte[1024 * 256];
                try {
                    zrf = new ZipResourceFile(fileName);
                    ZipEntryRO[] entries = zrf.getAllEntries();
                    /**
                     * First calculate the total compressed length
                     */
                    long totalCompressedLength = 0;
                    for (ZipEntryRO entry : entries) {
                        totalCompressedLength += entry.mCompressedLength;
                    }
                    float averageVerifySpeed = 0;
                    long totalBytesRemaining = totalCompressedLength;
                    long timeRemaining;
                    /**
                     * Then calculate a CRC for every file in the Zip file,
                     * comparing it to what is stored in the Zip directory.
                     * Note that for compressed Zip files we must extract
                     * the contents to do this comparison.
                     */
                    for (ZipEntryRO entry : entries) {
                        if (-1 != entry.mCRC32) {
                            long length = entry.mUncompressedLength;
                            CRC32 crc = new CRC32();
                            DataInputStream dis = null;
                            try {
                                dis = new DataInputStream(
                                        zrf.getInputStream(entry.mFileName));

                                long startTime = SystemClock.uptimeMillis();
                                while (length > 0) {
                                    int seek = (int) (length > buf.length ? buf.length
                                            : length);
                                    dis.readFully(buf, 0, seek);
                                    crc.update(buf, 0, seek);
                                    length -= seek;
                                    long currentTime = SystemClock.uptimeMillis();
                                    long timePassed = currentTime - startTime;
                                    if (timePassed > 0) {
                                        float currentSpeedSample = (float) seek
                                                / (float) timePassed;
                                        if (0 != averageVerifySpeed) {
                                            averageVerifySpeed = SMOOTHING_FACTOR
                                                    * currentSpeedSample
                                                    + (1 - SMOOTHING_FACTOR)
                                                    * averageVerifySpeed;
                                        } else {
                                            averageVerifySpeed = currentSpeedSample;
                                        }
                                        totalBytesRemaining -= seek;
                                        timeRemaining = (long) (totalBytesRemaining / averageVerifySpeed);
                                        this.publishProgress(
                                                new DownloadProgressInfo(
                                                        totalCompressedLength,
                                                        totalCompressedLength
                                                                - totalBytesRemaining,
                                                        timeRemaining,
                                                        averageVerifySpeed)
                                                );
                                    }
                                    startTime = currentTime;
                                    if (mCancelValidation)
                                        return true;
                                }
                                if (crc.getValue() != entry.mCRC32) {
                                    Log.e(Constants.TAG,
                                            "CRC does not match for entry: "
                                                    + entry.mFileName);
                                    Log.e(Constants.TAG,
                                            "In file: " + entry.getZipFileName());
                                    return false;
                                }
                            } finally {
                                if (null != dis) {
                                    dis.close();
                                }
                            }
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    return false;
                }
            }
            return true;
        }

        @Override
        protected void onProgressUpdate(DownloadProgressInfo... values) {
            onDownloadProgress(values[0]);
            super.onProgressUpdate(values);
        }

        @Override
        protected void onPostExecute(Boolean result) {
            if (result) {
                mDashboard.setVisibility(View.VISIBLE);
                mCellMessage.setVisibility(View.GONE);
                mStatusText.setText(R.string.text_validation_complete);
                mPauseButton.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        finish();
                    }
                });
                mPauseButton.setText(android.R.string.ok);
            } else {
                mDashboard.setVisibility(View.VISIBLE);
                mCellMessage.setVisibility(View.GONE);
                mStatusText.setText(R.string.text_validation_failed);
                mPauseButton.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        finish();
                    }
                });
                mPauseButton.setText(android.R.string.cancel);
            }
            super.onPostExecute(result);
        }

    };
    validationTask.execute(new Object());
}

/**
 * If the download isn't present, we initialize the download UI. This ties
 * all of the controls into the remote service calls.
 */
private void initializeDownloadUI() {
    mDownloaderClientStub = DownloaderClientMarshaller.CreateStub
            (this, ApkExpDownloaderService.class);
    setContentView(R.layout.main);

    mPB = (ProgressBar) findViewById(R.id.progressBar);
    mStatusText = (TextView) findViewById(R.id.statusText);
    mProgressFraction = (TextView) findViewById(R.id.progressAsFraction);
    mProgressPercent = (TextView) findViewById(R.id.progressAsPercentage);
    mAverageSpeed = (TextView) findViewById(R.id.progressAverageSpeed);
    mTimeRemaining = (TextView) findViewById(R.id.progressTimeRemaining);
    mDashboard = findViewById(R.id.downloaderDashboard);
    mCellMessage = findViewById(R.id.approveCellular);
    mPauseButton = (Button) findViewById(R.id.pauseButton);
    mWiFiSettingsButton = (Button) findViewById(R.id.wifiSettingsButton);

    mPauseButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (mStatePaused) {
                mRemoteService.requestContinueDownload();
            } else {
                mRemoteService.requestPauseDownload();
            }
            setButtonPausedState(!mStatePaused);
        }
    });

    mWiFiSettingsButton.setOnClickListener(new View.OnClickListener() {

        @Override
        public void onClick(View v) {
            startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS));
        }
    });

    Button resumeOnCell = (Button) findViewById(R.id.resumeOverCellular);
    resumeOnCell.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            mRemoteService.setDownloadFlags(IDownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR);
            mRemoteService.requestContinueDownload();
            mCellMessage.setVisibility(View.GONE);
        }
    });

}

/**
 * Called when the activity is first create; we wouldn't create a layout in
 * the case where we have the file and are moving to another activity
 * without downloading.
 */
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    /**
     * Both downloading and validation make use of the "download" UI
     */
    initializeDownloadUI();

    /**
     * Before we do anything, are the files we expect already here and
     * delivered (presumably by Market) For free titles, this is probably
     * worth doing. (so no Market request is necessary)
     */
    if (!expansionFilesDelivered()) {

        try {
            Intent launchIntent = ApkExpDownloaderActivity.this
                    .getIntent();
            Intent intentToLaunchThisActivityFromNotification = new Intent(
                    ApkExpDownloaderActivity
                    .this, ApkExpDownloaderActivity.this.getClass());
            intentToLaunchThisActivityFromNotification.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                    Intent.FLAG_ACTIVITY_CLEAR_TOP);
            intentToLaunchThisActivityFromNotification.setAction(launchIntent.getAction());

            if (launchIntent.getCategories() != null) {
                for (String category : launchIntent.getCategories()) {
                    intentToLaunchThisActivityFromNotification.addCategory(category);
                }
            }

            // Build PendingIntent used to open this activity from
            // Notification
            PendingIntent pendingIntent = PendingIntent.getActivity(
                    ApkExpDownloaderActivity.this,
                    0, intentToLaunchThisActivityFromNotification,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            // Request to start the download
            int startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(this,
                    pendingIntent, ApkExpDownloaderService.class);

            if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
                // The DownloaderService has started downloading the files,
                // show progress
                initializeDownloadUI();
                return;
            } // otherwise, download not needed so we fall through to
              // starting the movie
        } catch (NameNotFoundException e) {
            Log.e(LOG_TAG, "Cannot find own package! MAYDAY!");
            e.printStackTrace();
        }

    } else {
        validateXAPKZipFiles();
    }

}

/**
 * Connect the stub to our service on start.
 */
@Override
protected void onStart() {
    if (null != mDownloaderClientStub) {
        mDownloaderClientStub.connect(this);
    }
    super.onStart();
}

/**
 * Disconnect the stub from our service on stop
 */
@Override
protected void onStop() {
    if (null != mDownloaderClientStub) {
        mDownloaderClientStub.disconnect(this);
    }
    super.onStop();
}

//TODO:sp need more info on this
/**
 * Critical implementation detail. In onServiceConnected we create the
 * remote service and marshaler. This is how we pass the client information
 * back to the service so the client can be properly notified of changes. We
 * must do this every time we reconnect to the service.
 */
@Override
public void onServiceConnected(Messenger m) {
    mRemoteService = DownloaderServiceMarshaller.CreateProxy(m);
    mRemoteService.onClientUpdated(mDownloaderClientStub.getMessenger());
}

/**
 * The download state should trigger changes in the UI --- it may be useful
 * to show the state as being indeterminate at times. This sample can be
 * considered a guideline.
 */
@Override
public void onDownloadStateChanged(int newState) {
    setState(newState);
    boolean showDashboard = true;
    boolean showCellMessage = false;
    boolean paused;
    boolean indeterminate;
    switch (newState) {
        case IDownloaderClient.STATE_IDLE:
            // STATE_IDLE means the service is listening, so it's
            // safe to start making calls via mRemoteService.
            paused = false;
            indeterminate = true;
            break;
        case IDownloaderClient.STATE_CONNECTING:
        case IDownloaderClient.STATE_FETCHING_URL:
            showDashboard = true;
            paused = false;
            indeterminate = true;
            break;
        case IDownloaderClient.STATE_DOWNLOADING:
            paused = false;
            showDashboard = true;
            indeterminate = false;
            break;

        case IDownloaderClient.STATE_FAILED_CANCELED:
        case IDownloaderClient.STATE_FAILED:
        case IDownloaderClient.STATE_FAILED_FETCHING_URL:
        case IDownloaderClient.STATE_FAILED_UNLICENSED:
            paused = true;
            showDashboard = false;
            indeterminate = false;
            break;
        case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
        case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
            showDashboard = false;
            paused = true;
            indeterminate = false;
            showCellMessage = true;
            break;

        case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
            paused = true;
            indeterminate = false;
            break;
        case IDownloaderClient.STATE_PAUSED_ROAMING:
        case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
            paused = true;
            indeterminate = false;
            break;
        case IDownloaderClient.STATE_COMPLETED:
            showDashboard = false;
            paused = false;
            indeterminate = false;
            validateXAPKZipFiles();
            return;
        default:
            paused = true;
            indeterminate = true;
            showDashboard = true;
    }
    int newDashboardVisibility = showDashboard ? View.VISIBLE : View.GONE;
    if (mDashboard.getVisibility() != newDashboardVisibility) {
        mDashboard.setVisibility(newDashboardVisibility);
    }
    int cellMessageVisibility = showCellMessage ? View.VISIBLE : View.GONE;
    if (mCellMessage.getVisibility() != cellMessageVisibility) {
        mCellMessage.setVisibility(cellMessageVisibility);
    }

    mPB.setIndeterminate(indeterminate);
    setButtonPausedState(paused);
}

/**
 * Sets the state of the various controls based on the progressinfo object
 * sent from the downloader service.
 */
@Override
public void onDownloadProgress(DownloadProgressInfo progress) {
    mAverageSpeed.setText(getString(R.string.kilobytes_per_second,
            Helpers.getSpeedString(progress.mCurrentSpeed)));
    mTimeRemaining.setText(getString(R.string.time_remaining,
            Helpers.getTimeRemaining(progress.mTimeRemaining)));

    progress.mOverallTotal = progress.mOverallTotal;
    mPB.setMax((int) (progress.mOverallTotal >> 8));
    mPB.setProgress((int) (progress.mOverallProgress >> 8));
    mProgressPercent.setText(Long.toString(progress.mOverallProgress
            * 100 /
            progress.mOverallTotal) + "%");
    mProgressFraction.setText(Helpers.getDownloadProgressString
            (progress.mOverallProgress,
                    progress.mOverallTotal));
}

@Override
protected void onDestroy() {
    this.mCancelValidation = true;
    super.onDestroy();
}

}

ApkExpReceiver.java

public class ApkExpAlarmReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
    try {
        DownloaderClientMarshaller.startDownloadServiceIfRequired(context, intent, ApkExpDownloaderService.class);
    } catch (NameNotFoundException e) {
        e.printStackTrace();
    }       
}

}
  1. 权限是一个更大的话题,独立于 OBB 文件。与其在这里解释,不如推荐the documentation。简而言之,对于 6.0 之前的 Android 设备,它们只需要在清单中。对于 6.0 之后的 Android 设备,您应该在运行时的适当时刻请求它们。
  2. OBB 顾名思义是一个 "Opaque" 二进制 blob。您可以选择任何您想要的文件格式,google 只是将其视为一团比特。许多应用程序使用 zip 文件,但您不必,您可以使用您喜欢的任何文件格式。
  3. 正确。
  4. 示例下载 activity 顾名思义 - 示例。您可以使用 activity,也可以重复使用部分代码在您自己的应用程序中进行下载。无论您做什么,下载都可能需要一些时间,需要权限,并且在离线时无法工作,因此您需要为所有这些情况向用户显示适当的用户界面。什么是合适的 UI 会因不同的应用程序而异。