Java sample: app submission with game options and trailers
This article provides Java code examples that demonstrate how to use the Microsoft Store submission API for these tasks:
- Obtain an Azure AD access token to use with the Microsoft Store submission API.
- Create an app submission
- Configure Store listing data for the app submission, including the gaming and trailers advanced listing options.
- Upload the ZIP file containing the packages, listing images, and trailer files for the app submission.
- Commit the app submission.
Create an app submission
The CreateAndSubmitSubmissionExample
class implements a main
program that calls other example methods to use the Microsoft Store submission API to create and commit an app submission that contains game options and a trailer. To adapt this code for your own use:
- Assign the
tenantId
variable to the tenant ID for your app, and assign theclientId
andclientSecret
variables to the client ID and key for your app. For more information, see How to associate an Azure AD application with your Partner Center account - Assign the
applicationId
variable to the Store ID of the app for which you want to create a submission.
import java.io.IOException;
import java.text.MessageFormat;
import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import org.apache.http.client.ClientProtocolException;
public class CreateAndSubmitSubmissionExample {
public static void main(String[] args) {
// Add your tenant ID, client ID, and client secret here.
String tenantId = "";
String clientId = "";
String clientSecret = "";
DevCenterAccessTokenClient accessTokenClient = new DevCenterAccessTokenClient(tenantId, clientId, clientSecret);
String accessToken = accessTokenClient.getAccessToken("https://manage.devcenter.microsoft.com");
// The application ID is taken from your app dashboard page's URI in Dev Center,
// e.g. https://developer.microsoft.com/en-us/dashboard/apps/{application_id}/
String applicationId = "";
DevCenterClient devCenter = new DevCenterClient("https://manage.devcenter.microsoft.com", accessToken);
try {
// Get the application object, and cancel any in progress submissions.
JsonObject app = devCenter.getApplicationJsonObject(applicationId);
JsonObject inProgressSubmission = app.getJsonObject("pendingApplicationSubmission");
if (inProgressSubmission != null) {
String inProgressSubmissionId = inProgressSubmission.getString("id");
devCenter.cancelInProgressSubmission(applicationId, inProgressSubmissionId);
}
// Create a new submission, based on the last published submission.
JsonObject submission = devCenter.createSubmission(applicationId);
String submissionId = submission.getString("id");
// JsonObjects are immutable, so we'll build up our changes to the submission and
// then merge it with the submission object.
JsonObjectBuilder submissionChanges = Json.createObjectBuilder();
// The following fields are required:
submissionChanges.add("applicationCategory", "Games_Fighting");
submissionChanges.add("listings", getListingsObject());
submissionChanges.add("pricing", getPricingObject());
submissionChanges.add("packages", Json.createArrayBuilder().add(getPackageObject()));
submissionChanges.add("allowTargetFutureDeviceFamilies", getDeviceFamiliesObject());
// Add new Gaming Options to the submission.
JsonObject gamingOptions = getGamingOptionsJsonObject();
submissionChanges.add("gamingOptions", Json.createArrayBuilder().add(gamingOptions));
// Add new Trailers to the submission.
JsonObject trailer = getTrailerObject();
submissionChanges.add("trailers", Json.createArrayBuilder().add(trailer));
// Continue updating the submission_json object with additional options as needed.
// After you've finished, call the Update API with the code below to save it:
JsonObject submissionToUpdate = mergeJsonObjects(submission, submissionChanges.build());
JsonObject updatedSubmission = devCenter.updateSubmission(applicationId, submissionId, submissionToUpdate);
// All images and packages should be located in a single ZIP file. In the submission JSON,
// the file names for all objects requiring them (icons, packages, etc.) must exactly
// match the file names from the ZIP file.
String zipFilePath = "";
devCenter.uploadZipFileForSubmission(applicationId, submissionId, zipFilePath);
// Committing the submission will start the submission process for it. Once committed,
// the submission can no longer be changed.
devCenter.commitSubmission(applicationId, submissionId);
// After committing, you can poll the commit API for the status of the submission's process using
// the following code.
boolean waitingForCommitToStart = true;
while (waitingForCommitToStart) {
String status = devCenter.getSubmissionStatus(applicationId, submissionId);
System.out.println(MessageFormat.format("Submission status: {0}", status));
waitingForCommitToStart = status.equals("CommitStarted");
if (waitingForCommitToStart) {
try {
Thread.sleep(60000); // Wait one minute to check Dev Center again.
} catch (InterruptedException iex) {
System.out.println("The sleep was interrupted. Checking Dev Center now.");
}
}
}
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private static JsonObject mergeJsonObjects(JsonObject baseObject, JsonObject mergeObject) {
JsonObjectBuilder builder = Json.createObjectBuilder();
for (String baseKey : baseObject.keySet()) {
if (mergeObject.containsKey(baseKey)) {
builder.add(baseKey, mergeObject.get(baseKey));
} else {
builder.add(baseKey, baseObject.get(baseKey));
}
}
for (String mergeKey : mergeObject.keySet()) {
if (!baseObject.containsKey(mergeKey)) {
builder.add(mergeKey, mergeObject.get(mergeKey));
}
}
return builder.build();
}
private static JsonObject getListingsObject() {
// This structure holds basic information to display in the store.
JsonObjectBuilder baseListing = Json.createObjectBuilder();
baseListing.add("copyrightAndTrademarkInfo", "(C) 2017 Microsoft");
baseListing.add("licenseTerms", "http://example.com/licenseTerms.aspx");
baseListing.add("privacyPolicy", "http://example.com/privacyPolicy.aspx");
baseListing.add("supportContact", "support@example.com");
baseListing.add("websiteUrl", "http://example.com");
baseListing.add("description", "A sample game showing off gameplay options code.");
baseListing.add("releaseNotes", "Initial release");
// The title of the app must match a reserved name for the app in Dev Center.
// If it doesn't, attempting to update the submission will fail.
baseListing.add("title", "Super Dev Center API Simulator 2017");
// Up to 7 keywords may be provided in a listing.
JsonArrayBuilder keywords = Json.createArrayBuilder();
keywords.add("SampleApp").add("SampleFightingGame").add("GameOptions");
baseListing.add("keywords", keywords);
JsonArrayBuilder features = Json.createArrayBuilder();
features.add("Doesn't crash");
features.add("Likes to eat chips");
baseListing.add("features", features);
// If your app works better with specific hardware (or needs it), you can
// add or update values here.
JsonArrayBuilder hardwarePreferences = Json.createArrayBuilder();
hardwarePreferences.add("Keyboard");
hardwarePreferences.add("Mouse");
baseListing.add("hardwarePreferences", hardwarePreferences);
JsonArrayBuilder images = Json.createArrayBuilder();
// There are several types of images available; at least one screenshot
// is required.
JsonObjectBuilder image = Json.createObjectBuilder();
image.add("fileName", "tile.png");
image.add("description", "The tile as it appears in the store.");
image.add("imageType", "Icon");
images.add(image);
baseListing.add("images", images);
JsonObjectBuilder listing = Json.createObjectBuilder();
listing.add("baseListing", baseListing);
// If there are any specific overrides to above information for Windows 8,
// Windows 8.1, Windows Phone 7.1, 8.0, or 8.1, you can add information here.
listing.add("platformOverrides", Json.createObjectBuilder());
JsonObjectBuilder listings = Json.createObjectBuilder();
// Each listing is targeted at a specific language-locale code, e.g. EN-US.
listings.add("en-us", listing);
return listings.build();
}
private static JsonObject getPackageObject() {
JsonObjectBuilder pkg = Json.createObjectBuilder();
// The file name is relative to the root of the uploaded ZIP file.
pkg.add("fileName", "bin/super_dev_ctr_api_sim.appxupload");
// If you haven't begun to upload the file yet, set this value to "PendingUpload".
pkg.add("fileStatus", "PendingUpload");
return pkg.build();
}
private static JsonObject getPricingObject() {
JsonObjectBuilder pricing = Json.createObjectBuilder();
// How long the trial period is, if one is allowed. Valid values are NoFreeTrial,
// OneDay, SevenDays, FifteenDays, ThirtyDays, or TrialNeverExpires.
pricing.add("trialPeriod", "NoFreeTrial");
// Maps to the default price for the app.
pricing.add("priceId", "Free");
// If you'd like to offer your app in different markets at different prices, you
// can provide priceId values per language/locale code.
pricing.add("marketSpecificPricing", Json.createObjectBuilder().build());
return pricing.build();
}
private static JsonObject getDeviceFamiliesObject() {
JsonObjectBuilder futureDeviceFamilies = Json.createObjectBuilder();
// Supported values are Desktop, Mobile, Xbox, and Holographic. To make
// the app available on that specific platform, set the value to True.
futureDeviceFamilies.add("Desktop", true);
futureDeviceFamilies.add("Mobile", false);
futureDeviceFamilies.add("Xbox", true);
futureDeviceFamilies.add("Holographic", false);
return futureDeviceFamilies.build();
}
private static JsonObject getGamingOptionsJsonObject() {
JsonObjectBuilder gamingOptions = Json.createObjectBuilder();
// The genres of your game.
JsonArrayBuilder genres = Json.createArrayBuilder();
genres.add("Games_Fighting");
gamingOptions.add("genres", genres);
// Set this to true if your game supports local multiplayer. This field
// is required.
gamingOptions.add("isLocalMultiplayer", true);
// If local multiplayer is supported, you must provide the minimum and
// maximum players supported. Valid values are between 2 and 1000 inclusive.
gamingOptions.add("localMultiplayerMinPlayers", 2);
gamingOptions.add("localMultiplayerMaxPlayers", 4);
// Set this to True if your game supports local co-op play. This field is required.
gamingOptions.add("isLocalCooperative", true);
// If local co-op is supported, you must provide the minimum and maximum players
// supported. Valid values are between 2 and 1000 inclusive.
gamingOptions.add("localCooperativeMinPlayers", 2);
gamingOptions.add("localCooperativeMaxPlayers", 4);
// Set this to True if your game supports online multiplayer. This field is required.
gamingOptions.add("isOnlineMultiplayer", true);
// If online multiplayer is supported, you must provide the minimum and maximum players
// supported. Valid values are between 2 and 1000 inclusive.
gamingOptions.add("onlineMultiplayerMinPlayers", 2);
gamingOptions.add("onlineMultiplayerMaxPlayers", 4);
// Set this to true if your game supports online co-op play. This field is required.
gamingOptions.add("isOnlineCooperative", true);
// If online co-op is supported, you must provide the minimum and maximum players
// supported. Valid values are between 2 and 1000 inclusive.
gamingOptions.add("onlineCooperativeMinPlayers", 2);
gamingOptions.add("onlineCooperativeMaxPlayers", 4);
// If your game supports broadcasting a stream to other players, set this field to True.
// This field is required.
gamingOptions.add("isBroadcastingPrivilegeGranted", true);
// If your game supports cross-device play (e.g. a player can play on an Xbox One with
// their friend who's playing on a PC), set this field to True. This field is required.
gamingOptions.add("isCrossPlayEnabled", true);
// If your game supports Kinect usage, set this field to "Enabled", otherwise, set it to
// "Disabled". This field is required.
gamingOptions.add("kinectDataForExternal", "Disabled");
// Free text about any other peripherals that your game supports. This field is optional.
gamingOptions.add("otherPeripherals", "Supports the usage of all fighting joysticks.");
return gamingOptions.build();
}
private static JsonObject getTrailerObject() {
JsonObjectBuilder trailer = Json.createObjectBuilder();
// This is the filename of the trailer. The file name is a relative path to the
// root of the ZIP file to be uploaded to the API.
trailer.add("VideoFileName", "trailers/main/my_awesome_trailer.mpeg");
// Aside from the video itself, a trailer can have metadata assets including a title and images
// such as screenshots or alternate images. These are keyed by market code (see the end of this
// method for an example.
JsonObjectBuilder trailerAssetsByCountry = Json.createObjectBuilder();
JsonObjectBuilder trailerAssetSet = Json.createObjectBuilder();
// The title of the trailer to display in the store.
trailerAssetSet.add("Title", "Main Trailer");
// The list of images provided with the trailer that are shown when the trailer isn't playing.
JsonArrayBuilder trailerImageAssets = Json.createArrayBuilder();
JsonObjectBuilder mainTrailerImage = Json.createObjectBuilder();
// The file name of the image. The file name is a relative path to the root of the ZIP
// file to be uploaded to the API.
mainTrailerImage.add("FileName", "trailers/main/thumbnail.png");
// A plaintext description of what the image represents.
mainTrailerImage.add("Description", "The thumbnail for the trailer shown before the user clicks play");
trailerImageAssets.add(mainTrailerImage);
// Add a second image.
JsonObjectBuilder altImage = Json.createObjectBuilder();
altImage.add("FileName", "trailers/main/alt-img.png");
altImage.add("Description", "The image to show after the trailer plays");
trailerImageAssets.add(altImage);
trailerAssetSet.add("ImageList", trailerImageAssets);
// This line creates the trailer asset for en-us.
trailerAssetsByCountry.add("en-us", trailerAssetSet);
trailer.add("TrailerAssets", trailerAssetsByCountry);
return trailer.build();
}
}
Obtain an Azure AD access token
The DevCenterAccessTokenClient
class defines a helper method that uses the your tenantId
, clientId
and clientSecret
values to create an Azure AD access token to use with the Microsoft Store submission API.
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.text.MessageFormat;
import javax.json.Json;
import javax.json.JsonReader;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
/**
* A client for getting access tokens to the Dev Center API.
* @author Microsoft
*/
public final class DevCenterAccessTokenClient {
private String tenantId;
private String clientId;
private String clientSecret;
/**
* Creates a new access token client for Dev Center.
* @param tenantId Your tenant ID for the app.
* @param clientId Your client ID for the app.
* @param clientSecret Your client secret string for the app.
*/
public DevCenterAccessTokenClient(String tenantId, String clientId, String clientSecret) {
this.tenantId = tenantId;
this.clientId = clientId;
this.clientSecret = clientSecret;
}
/**
* Gets an access token for the specific resource.
* @param resource The full URI to the resource to be accessed.
* @return An access token for that resource, good for one hour.
*/
public String getAccessToken(String resource) {
// Generate access token. Access token is valid for 1 hour.
String tokenEndpoint = "https://login.microsoftonline.com/{0}/oauth2/token";
HttpPost tokenRequest = new HttpPost(MessageFormat.format(tokenEndpoint, this.tenantId));
String tokenRequestBody = MessageFormat.format("grant_type=client_credentials&client_id={0}&client_secret={1}&resource={2}", this.clientId, this.clientSecret, resource);
tokenRequest.setEntity(new StringEntity(tokenRequestBody, "utf-8"));
tokenRequest.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
CloseableHttpClient httpclient = HttpClients.createDefault();
ResponseHandler<String> responseHandler = new BasicResponseHandler();
try {
String tokenResponse = httpclient.execute(tokenRequest, responseHandler);
JsonReader reader = Json.createReader(new ByteArrayInputStream(tokenResponse.getBytes("UTF-8")));
String accessToken = reader.readObject().getString("access_token");
return accessToken;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
Helper methods to invoke the submission API and upload submission files
The DevCenterClient
class defines helper methods that invoke a variety of methods in the Microsoft Store submission API and upload the ZIP file containing the packages, listing images, and trailer files for the app submission.
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.text.MessageFormat;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
/**
* A client for accessing commands in the Dev Center API.
* @author Microsoft
*/
public final class DevCenterClient implements ResponseHandler<JsonObject> {
private String baseUri;
private String accessToken;
/**
* Creates a new dev center client instance.
* @param baseUri The base URI for the Dev Center API.
* @param accessToken The access token for web call authentication.
*/
public DevCenterClient(String baseUri, String accessToken) {
this.baseUri = baseUri;
this.accessToken = accessToken;
}
/**
* Returns the application JSON object from the Dev Center API.
* @param applicationId The application ID.
* @return A JSON object from Dev Center.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public JsonObject getApplicationJsonObject(String applicationId) throws ClientProtocolException, IOException {
String path = MessageFormat.format("/v1.0/my/applications/{0}", applicationId);
return get(path);
}
/**
* Cancels and deletes the in-progress submission from the application.
* @param applicationId The application ID.
* @param submissionId The submission ID.
* @return A JSON object from Dev Center.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public JsonObject cancelInProgressSubmission(String applicationId, String submissionId) throws ClientProtocolException, IOException {
String path = MessageFormat.format("/v1.0/my/applications/{0}/submissions/{1}", applicationId, submissionId);
return delete(path);
}
/**
* Creates a new submission in Dev Center.
* @param applicationId The application ID.
* @return The submission JSON object from Dev Center.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public JsonObject createSubmission(String applicationId) throws ClientProtocolException, IOException {
String path = MessageFormat.format("/v1.0/my/applications/{0}/submissions", applicationId);
return post(path);
}
/**
* Updates the submission in Dev Center.
* @param applicationId The application ID.
* @param submissionId The submission ID.
* @param submission The submission JSON object.
* @return The updated submission JSON object from Dev Center.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public JsonObject updateSubmission(String applicationId, String submissionId, JsonObject submission) throws ClientProtocolException, IOException {
String path = MessageFormat.format("/v1.0/my/applications/{0}/submissions/{1}", applicationId, submissionId);
return put(path, submission);
}
/**
* Gets the submission in Dev Center.
* @param applicationId The application ID.
* @param submissionId The submission ID.
* @return The submission JSON object from Dev Center.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public JsonObject getSubmission(String applicationId, String submissionId) throws ClientProtocolException, IOException {
String path = MessageFormat.format("/v1.0/my/applications/{0}/submissions/{1}", applicationId, submissionId);
return get(path);
}
/**
* Commits the submission to Dev Center.
* @param applicationId The application ID.
* @param submissionId The submission ID.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public void commitSubmission(String applicationId, String submissionId) throws ClientProtocolException, IOException {
String path = MessageFormat.format("/v1.0/my/applications/{0}/submissions/{1}/commit", applicationId, submissionId);
post(path);
}
/**
* Returns the submission status of this submission.
* @param applicationId The application ID.
* @param submissionId The submission ID.
* @return The status of the submission in Dev Center.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public String getSubmissionStatus(String applicationId, String submissionId) throws ClientProtocolException, IOException {
JsonObject response = getSubmission(applicationId, submissionId);
String status = response.getString("status");
if (status == null || status.isEmpty())
{
return "Unknown";
}
return status;
}
/**
* Uploads a ZIP archive containing the binaries, image assets, trailers, and other components to the submission.
* @param applicationId The application ID.
* @param submissionId The submission ID.
* @param zipFilePath The file path to the ZIP file.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public void uploadZipFileForSubmission(String applicationId, String submissionId, String zipFilePath) throws ClientProtocolException, IOException {
JsonObject submission = getSubmission(applicationId, submissionId);
String fileUploadUrl = submission.getString("fileUploadUri");
CloseableHttpClient httpclient = HttpClients.createDefault();
File uploadFile = new File(zipFilePath);
HttpPut uploadFileRequest = new HttpPut(fileUploadUrl.replace("+", "%2B")); // Encode '+', otherwise it will be decoded as ' '
uploadFileRequest.addHeader("x-ms-blob-type", "BlockBlob");
uploadFileRequest.setEntity(new FileEntity(uploadFile));
httpclient.execute(uploadFileRequest);
}
private JsonObject get(String path) throws ClientProtocolException, IOException {
HttpGet request = new HttpGet(this.baseUri + path);
return invoke(request);
}
private JsonObject put(String path, JsonObject obj) throws ClientProtocolException, IOException {
HttpPut request = new HttpPut(this.baseUri + path);
request.addHeader("Content-Type", "application/json; charset=utf-8");
request.setEntity(new StringEntity(SerializeJsonObject(obj)));
return invoke(request);
}
private JsonObject post(String path) throws ClientProtocolException, IOException {
return post(path, null);
}
private JsonObject post(String path, JsonObject obj) throws ClientProtocolException, IOException {
HttpPost request = new HttpPost(this.baseUri + path);
request.addHeader("Content-Type", "application/json; charset=utf-8");
if (obj != null)
{
request.setEntity(new StringEntity(SerializeJsonObject(obj)));
}
return invoke(request);
}
private JsonObject delete(String path) throws ClientProtocolException, IOException {
HttpDelete request = new HttpDelete(this.baseUri + path);
return invoke(request);
}
private JsonObject invoke(HttpUriRequest request) throws ClientProtocolException, IOException {
CloseableHttpClient client = HttpClients.createDefault();
request.addHeader("Authorization", "Bearer " + accessToken);
request.addHeader("User-Agent", "Java");
JsonObject response = client.execute(request, this);
return response;
}
public JsonObject handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
StatusLine status = response.getStatusLine();
int statusCode = status.getStatusCode();
String reasonPhrase = status.getReasonPhrase();
HttpEntity entity = response.getEntity();
JsonObject returnValue = null;
if(entity != null && entity.getContentLength() != 0) {
JsonReader reader = Json.createReader(entity.getContent());
returnValue = reader.readObject();
}
if (statusCode < 200 || statusCode > 299) {
if (returnValue != null) {
throw new HttpResponseException(statusCode, returnValue.toString());
}
throw new HttpResponseException(statusCode, reasonPhrase);
}
return returnValue;
}
private static String SerializeJsonObject(JsonObject obj) {
StringWriter writer = new StringWriter();
Json.createWriter(writer).writeObject(obj);
String body = writer.toString();
return body;
}
}