package com.steamcommunity.siplus.steamscreenshots; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.zip.CRC32; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import com.google.common.io.Files; import com.google.common.primitives.UnsignedLongs; import com.google.protobuf.ByteString; import android.app.Activity; import android.app.ProgressDialog; import android.content.DialogInterface; import android.content.res.Resources; import android.graphics.BitmapFactory; import android.os.Bundle; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; public final class UploadActivity extends Activity { static final String EXTRASTATE_ACCOUNT = Utility.PACKAGE + ".UploadActivity.EXTRASTATE_ACCOUNT"; static final String EXTRASTATE_GAME = Utility.PACKAGE + ".UploadActivity.EXTRASTATE_GAME"; static final String EXTRASTATE_SCREENSHOTS = Utility.PACKAGE + ".UploadActivity.EXTRASTATE_SCREENSHOTS"; static final String STATE_ERROR = Utility.PACKAGE + ".UploadActivity.STATE_ERROR"; static final String STATE_SIZE = Utility.PACKAGE + ".UploadActivity.STATE_SIZE"; static final String STATE_TASK = Utility.PACKAGE + ".UploadActivity.STATE_TASK"; static final String STATE_UPLOADED = Utility.PACKAGE + ".UploadActivity.STATE_UPLOADED"; SteamshotsAccount mAccount; int mError; boolean mFromApp; String mGame; String mGameName; ProgressDialog mProgress; Resources mResources; int[] mScreenshots; long mSize; UploadTask mTask; int mUploaded; EditText widgetCaption; TextView widgetError; LinearLayout widgetErrorFrame; EditText widgetLocation; TextView widgetSize; CheckBox widgetSpoiler; Button widgetUploadButton; Spinner widgetVisibility; void refreshHeader() { if (AsyncTaskWithRunID.isRunning(mTask)) { widgetSize.setText(R.string.upload_uploading); widgetUploadButton.setEnabled(false); return; } int i; String file; String path = ScreenshotName.folderPath(mAccount.mSteamID, mGame); int[] screenshots = mScreenshots; int length = screenshots.length; long size = 0; for (i = 0; i < length; ++i) { file = ScreenshotName.nameToString(screenshots[i]); size += (new File(path, file)).length() + (new File(path, file + ScreenshotName.THUMB_SUFFIX)).length(); } mSize = size; widgetSize.setText(mResources.getQuantityString(R.plurals.upload_header, length, length, formatSize(mSize), mGameName)); widgetUploadButton.setEnabled(true); } CharSequence formatSize(long size) { int resid; int value; if (size < 1024L) { resid = R.string.upload_size_bytes; value = (int)size; } else if (size < 1048576L) { resid = R.string.upload_size_kilobytes; value = (int)(size / 1024L); } else if (size < 1073741824L) { resid = R.string.upload_size_megabytes; value = (int)(size / 1048576L); } else { resid = R.string.upload_size_gigabytes; value = (int)(size / 1073741824L); } return mResources.getString(resid, value); } void hideUploadProgress() { if (mProgress == null) { return; } mProgress.dismiss(); mProgress = null; } public void listenerUploadOnClick(View view) { if ((mScreenshots.length == 0) || AsyncTaskWithRunID.isRunning(mTask)) { return; } if (!Utility.isConnected(this)) { setErrorText(R.string.error_unconnected); return; } setErrorText(0); mTask = new UploadTask(this); mTask.execute((Void)(null)); refreshHeader(); showUploadProgress(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle bundle; if (savedInstanceState != null) { bundle = savedInstanceState; mSize = savedInstanceState.getLong(STATE_SIZE); mUploaded = savedInstanceState.getInt(STATE_UPLOADED); } else { bundle = getIntent().getExtras(); if (bundle == null) { finish(); return; } } mAccount = bundle.getParcelable(EXTRASTATE_ACCOUNT); mGame = bundle.getString(EXTRASTATE_GAME); mScreenshots = bundle.getIntArray(EXTRASTATE_SCREENSHOTS); if ((mAccount == null) || (mGame == null) || (mScreenshots == null)) { finish(); return; } mResources = getResources(); mGameName = Utility.applicationLabel(getPackageManager(), mGame); Utility.enableActionBarBack(this); setContentView(R.layout.view_upload); widgetCaption = (EditText)(findViewById(R.id.view_upload_caption)); widgetError = (TextView)(findViewById(R.id.view_upload_error)); widgetErrorFrame = (LinearLayout)(findViewById(R.id.view_upload_error_frame)); widgetLocation = (EditText)(findViewById(R.id.view_upload_location)); widgetSize = (TextView)(findViewById(R.id.view_upload_size)); widgetSpoiler = (CheckBox)(findViewById(R.id.view_upload_spoiler)); widgetUploadButton = (Button)(findViewById(R.id.view_upload_button)); widgetVisibility = (Spinner)(findViewById(R.id.view_upload_visibility)); if (savedInstanceState != null) { setErrorText(savedInstanceState.getInt(STATE_ERROR)); mTask = (UploadTask)(AsyncTaskWithRunID.findRun(savedInstanceState.getInt(STATE_TASK))); if (mTask != null) { mTask.mActivity = this; if (!mTask.isCancelled()) { showUploadProgress(); } } } else { setErrorText(0); } refreshHeader(); } @Override protected void onDestroy() { super.onDestroy(); hideUploadProgress(); if (isChangingConfigurations()) { return; } if (AsyncTaskWithRunID.isRunning(mTask)) { mTask.mActivity = null; mTask.cancel(false); } } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() != android.R.id.home) { return super.onOptionsItemSelected(item); } finish(); return true; } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(EXTRASTATE_ACCOUNT, mAccount); outState.putInt(STATE_ERROR, mError); outState.putString(EXTRASTATE_GAME, mGame); outState.putIntArray(EXTRASTATE_SCREENSHOTS, mScreenshots); outState.putLong(STATE_SIZE, mSize); if (AsyncTaskWithRunID.isRunning(mTask)) { outState.putInt(STATE_TASK, mTask.mRun); } outState.putInt(STATE_UPLOADED, mUploaded); } void setErrorText(int resid) { mError = resid; if (resid == 0) { widgetErrorFrame.setVisibility(View.GONE); return; } widgetError.setText(resid); widgetErrorFrame.setVisibility(View.VISIBLE); } void showUploadProgress() { ProgressDialog dialog = new ProgressDialog(this); mProgress = dialog; dialog.setOnCancelListener(new UploadProgressOnCancel(this)); dialog.setMax(mScreenshots.length); dialog.setProgress(mUploaded); dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); dialog.setTitle(R.string.upload_progress); dialog.show(); } void updateProgress(int progress) { mUploaded = progress; if (mProgress != null) { mProgress.setProgress(progress); } } void uploadCancelled() { int uploaded = mUploaded; if (uploaded != 0) { int[] screenshots = mScreenshots; int length = screenshots.length - uploaded; mScreenshots = new int[length]; System.arraycopy(screenshots, uploaded, mScreenshots, uploaded, length); } refreshHeader(); } void uploadFailed() { if (mUploaded == mScreenshots.length) { uploadSucceeded(); return; } hideUploadProgress(); setErrorText(mTask.mError); uploadCancelled(); } void uploadSucceeded() { hideUploadProgress(); finish(); } } class UploadProgressOnCancel implements DialogInterface.OnCancelListener { UploadActivity mActivity; UploadProgressOnCancel(UploadActivity activity) { mActivity = activity; } @Override public void onCancel(DialogInterface dialog) { UploadActivity activity = mActivity; activity.mProgress = null; UploadTask task = activity.mTask; if (AsyncTaskWithRunID.isRunning(task)) { task.cancel(false); } activity.uploadCancelled(); } } class UploadTask extends AsyncTaskWithRunID { SteamshotsAccount mAccount; UploadActivity mActivity; String mCaption; Connection mCM; int mError; int mHeight; long mJob; String mLocation; boolean mLoggedOn; String mGame; String mGameID; String mGameName; int[] mScreenshots; boolean mSpoiler; Connection mUFS; int mUploaded; int mVisibility; int mWidth; UploadTask(UploadActivity activity) { mActivity = activity; ClientLogonOutgoing.initializeMachineID(activity); } boolean connect() { if (isCancelled()) {return false;} Connection cm = new Connection(); try { cm.connect(Connection.CM_ADDRESS, Connection.CM_PORT); } catch (ConnectionException e) { mError = R.string.error_connection; return false; } if (isCancelled()) {return false;} mCM = cm; SteamshotsAccount account = mAccount; cm.mSteamID = (account.mSteamID & 0x7ff00000ffffffffL) | 0x200000000L; try { cm.sendMessage(new ClientLogonOutgoing(account.mName, account.mLoginKey, account.mGuardHash)); } catch (OutgoingException e) { mError = R.string.error_connection; return false; } if (isCancelled()) {return false;} long sessionToken; String ufsAddress; int ufsPort; try { ClientLogonResponseIncoming logonResponse = new ClientLogonResponseIncoming( cm.waitForMessage(ClientLogonResponseIncoming.MESSAGE)); if (isCancelled()) {return false;} switch (logonResponse.mEResult) { case EResult.OK: break; case EResult.ALREADY_LOGGED_IN_ELSEWHERE: case EResult.LOGGED_IN_ELSEWHERE: case EResult.PASSWORD_REQUIRED_TO_KICK_SESSION: mError = R.string.error_elsewhere; return false; default: mError = R.string.upload_error_eresult; return false; } cm.mSessionID = logonResponse.mHeader.mSessionID; ClientSessionTokenIncoming sessionTokenIncoming = new ClientSessionTokenIncoming( cm.waitForMessage(ClientSessionTokenIncoming.MESSAGE)); if (isCancelled()) {return false;} sessionToken = sessionTokenIncoming.mToken; ClientServerListIncoming serverList = new ClientServerListIncoming( cm.waitForMessage(ClientServerListIncoming.MESSAGE)); if (isCancelled()) {return false;} if (serverList.mUFSAddress != null) { ufsAddress = serverList.mUFSAddress; ufsPort = serverList.mUFSPort; } else { serverList = new ClientServerListIncoming(cm.waitForMessage(ClientServerListIncoming.MESSAGE)); if (serverList.mUFSAddress == null) { mError = R.string.error_connection; return false; } ufsAddress = serverList.mUFSAddress; ufsPort = serverList.mUFSPort; } } catch (IncomingException e) { mError = R.string.error_connection; return false; } cm.sendHeartbeat(); Connection ufs = new Connection(); try { ufs.connect(ufsAddress, ufsPort); } catch (ConnectionException e) { mError = R.string.error_connection; return false; } if (isCancelled()) {return false;} mUFS = ufs; ufs.mSteamID = cm.mSteamID; try { ufs.sendMessage(new ClientUFSLoginRequestOutgoing(sessionToken)); } catch (OutgoingException e) { mError = R.string.error_connection; return false; } if (isCancelled()) {return false;} cm.sendHeartbeat(); try { ClientUFSLoginResponseIncoming ufsLoginResponse = new ClientUFSLoginResponseIncoming( ufs.waitForMessage(ClientUFSLoginResponseIncoming.MESSAGE)); if (isCancelled()) {return false;} ufs.mSessionID = ufsLoginResponse.mHeader.mSessionID; if (ufsLoginResponse.mEResult == EResult.OK) { return true; } } catch (IncomingException e) {} mError = R.string.error_connection; return false; } void disconnect() { if (mUFS != null) { mUFS.disconnect(); } Connection cm = mCM; if (cm != null) { if (mLoggedOn) { try { cm.sendMessage(new ClientLogOffOutgoing()); } catch (OutgoingException e) {} } cm.disconnect(); } } @Override protected Boolean doInBackground(Void... params) { if (isCancelled() || !connect()) { return false; } Connection cm = mCM; int i; Connection ufs = mUFS; ClientUCMAddScreenshotOutgoing outgoing = new ClientUCMAddScreenshotOutgoing( mGameName, mGameID, mCaption, mLocation, mVisibility, mSpoiler); int[] screenshots = mScreenshots; for (i = 0; i < screenshots.length; ++i) { if (isCancelled()) {return false;} if (!uploadScreenshot(screenshots[i], outgoing)) { mError = R.string.upload_error; return false; } ufs.sendHeartbeat(); cm.sendHeartbeat(); publishProgress(++mUploaded); } return true; } @Override void onCancelledWithRunID(boolean result) { disconnect(); if (mActivity != null) { mActivity.uploadFailed(); } } @Override void onPostExecuteWithRunID(boolean result) { disconnect(); UploadActivity activity = mActivity; if (activity == null) { return; } if (result) { activity.uploadSucceeded(); } else { activity.uploadFailed(); } } @Override void onPreExecuteWithRunID() { UploadActivity activity = mActivity; mAccount = activity.mAccount; String game = activity.mGame; mGame = game; int[] screenshots = activity.mScreenshots; mScreenshots = new int[screenshots.length]; System.arraycopy(screenshots, 0, mScreenshots, 0, screenshots.length); mCaption = activity.widgetCaption.getText().toString(); mLocation = activity.widgetLocation.getText().toString(); mSpoiler = activity.widgetSpoiler.isChecked(); int visibility = activity.widgetVisibility.getSelectedItemPosition(); if (visibility < 0) { mVisibility = 8; } else { mVisibility = 8 >> visibility; } mError = 0; mJob = 1; mLoggedOn = false; mUploaded = 0; CRC32 crc = new CRC32(); crc.update(game.getBytes()); mGameID = UnsignedLongs.toString((crc.getValue() << 32L) | -0x7ffffffffe000000L); mGameName = Utility.applicationLabel(activity.getPackageManager(), mGame); } @Override protected void onProgressUpdate(Integer... progress) { if (mActivity != null) { mActivity.updateProgress(progress[0]); } } boolean uploadFile(String local, String remote, long time, boolean querySize) { File localFile = new File(local); if (isCancelled() || !localFile.isFile()) { return false; } if (localFile.length() > 33554432L) { // 32 MiB. Just a random number. return false; } byte[] data; int dataLength; byte[] zip; try { data = Files.toByteArray(localFile); dataLength = data.length; ByteArrayOutputStream zipByteArrayStream = new ByteArrayOutputStream(dataLength); ZipOutputStream zipStream = new ZipOutputStream(zipByteArrayStream); zipStream.putNextEntry(new ZipEntry("z")); zipStream.write(data); zipStream.closeEntry(); zipStream.finish(); zip = zipByteArrayStream.toByteArray(); } catch (IOException e) { return false; } if (querySize) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(data, 0, data.length, options); mHeight = options.outHeight; mWidth = options.outWidth; } if (isCancelled()) {return false;} byte[] hashBytes = Utility.shaHash(data); if (hashBytes == null) { return false; } ByteString hash = ByteString.copyFrom(hashBytes); int zipLength = zip.length; try { Connection cm = mCM; Connection ufs = mUFS; if (isCancelled()) {return false;} ufs.sendMessage(new ClientUFSUploadFileRequestOutgoing(++mJob, remote, dataLength, zipLength, hash, time)); cm.sendHeartbeat(); if (isCancelled()) {return false;} ClientUFSUploadFileResponseIncoming response = new ClientUFSUploadFileResponseIncoming( ufs.waitForMessage(ClientUFSUploadFileResponseIncoming.MESSAGE)); int eresult = response.mEResult; if (eresult == EResult.DUPLICATE_REQUEST) { return true; // Reuploading after cancelling } if (response.mUnsupportedModes || (response.mEResult != EResult.OK)) { return false; } cm.sendHeartbeat(); ClientUFSFileChunkOutgoing chunk = new ClientUFSFileChunkOutgoing(response.mHeader.mJobSource, hash); int i; for (i = 0; i < zipLength; i += ClientUFSFileChunkOutgoing.CHUNK_SIZE) { if (isCancelled()) {return false;} chunk.setData(zip, i); ufs.sendMessage(chunk); cm.sendHeartbeat(); } if (isCancelled()) {return false;} ClientUFSUploadFileFinishedIncoming finished = new ClientUFSUploadFileFinishedIncoming( ufs.waitForMessage(ClientUFSUploadFileFinishedIncoming.MESSAGE)); cm.sendHeartbeat(); return finished.mEResult == EResult.OK; } catch (IncomingException e) { e.printStackTrace(); return false; } catch (OutgoingException e) { e.printStackTrace(); return false; } } boolean uploadScreenshot(int screenshot, ClientUCMAddScreenshotOutgoing outgoing) { String game = mGame; long steamID = mAccount.mSteamID; ArrayList<UploadedCaption> captions = UploadedCaption.fromFileSorted(steamID, game); if (captions != null) { Iterator<UploadedCaption> iterator; for (iterator = captions.iterator(); iterator.hasNext(); ) { if (iterator.next().mScreenshot == screenshot) { return true; // Late cancel } } } String name = ScreenshotName.nameToString(screenshot); String cloudFile = mGameID + "/screenshots/" + name; String cloudThumb = mGameID + "/screenshots/thumbnails/" + name; String path = ScreenshotName.folderPath(steamID, game) + name; long time = ScreenshotName.creationTime(screenshot, steamID, game) / 1000L; if (!uploadFile(path, cloudFile, time, true)) { return false; } if (isCancelled()) {return false;} if (!uploadFile(path + ScreenshotName.THUMB_SUFFIX, cloudThumb, time, false)) { return false; } // Must not cancel here, because the upload has succeeded outgoing.setFile(screenshot, mWidth, mHeight, time, ++mJob); Connection cm = mCM; try { cm.sendMessage(outgoing); mUFS.sendHeartbeat(); ClientUCMAddScreenshotResponseIncoming response = new ClientUCMAddScreenshotResponseIncoming( cm.waitForMessage(ClientUCMAddScreenshotResponseIncoming.MESSAGE)); if (response.mEResult != EResult.OK) { return false; } if (captions == null) { captions = new ArrayList<UploadedCaption>(1); } captions.add(new UploadedCaption(screenshot, response.mHandle, mCaption)); return UploadedCaption.toFile(captions, steamID, game); } catch (IncomingException e) { return false; } catch (OutgoingException e) { return false; } } }