I am using Unity IAP 4.1.4.
As you know there are 4 different test cards on purchase testing. My question is what should I do when payment is slow? If user quits the game before payment validated I can't approve the payment and purchase is pending. Is there any way to handle slow payments?
This is the script.
public class IAPManager : MonoBehaviour
{
private string coin250k = "//";
private string coin500k = "//";
public void OnPurchaseComplete(Product product)
{
var validator = new CrossPlatformValidator(GooglePlayTangle.Data(),AppleTangle.Data(), Application.identifier);
bool ValidPurchase = false;
try
{
// On Google Play, result has a single product ID.
// On Apple stores, receipts contain multiple products.
var result = validator.Validate(product.receipt);
// For informational purposes, we list the receipt(s)
Debug.Log("Receipt is valid. Contents:");
foreach (IPurchaseReceipt productReceipt in result)
{
Debug.Log(productReceipt.productID);
Debug.Log(productReceipt.purchaseDate);
Debug.Log(productReceipt.transactionID);
GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
if (null != google)
{
// This is Google's Order ID.
// Note that it is null when testing in the sandbox
// because Google's sandbox does not provide Order IDs.
ValidPurchase = true;
Debug.Log(" google transaction" + google.transactionID);
Debug.Log(" google transaction" + google.purchaseState);
Debug.Log(" google transaction" + google.purchaseToken);
}
AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
if (null != apple)
{
ValidPurchase = true;
Debug.Log(" apple transaction1" + apple.originalTransactionIdentifier);
Debug.Log(" apple transaction2" + apple.subscriptionExpirationDate);
Debug.Log(" apple transaction3" + apple.cancellationDate);
Debug.Log(" apple transaction4" + apple.quantity);
}
}
}
catch (IAPSecurityException)
{
Debug.Log("Invalid receipt, not unlocking content");
ValidPurchase = false;
}
//product.receipt;
if (ValidPurchase)
{
if (product.definition.id == coin250k)
{
//
}
else if (product.definition.id == coin500k)
{
//
}
}
}
public void OnPurchaseFailed(Product product, PurchaseFailureReason reason)
{
Debug.Log(product.definition.id + " failed because: "+ reason);
}
}
Related
I am using a non-consumable product to switch off ads for good, after purchasing.
When users buy pro everything works fine. Ads are switched off, also autorenewal in google store works fine after the user reinstalls the app.
My problem starts when the user wants a refund. After all, will be processed by Google and the customer will get his money. I don't know how where to subscribe to my function to turn off the pro version again on the users' app.
I am using IAPManager:
public class IAPManager : MonoBehaviour, IStoreListener
{
public List<Product> ProductList = new List<Product>();
public const string Pro = "pro";
private const string _androidPostFix = "_android";
[SerializeField] I18NText purchaseProBtnText;
public IAPManager Instance;
private void Awake()
{
if (Instance == null) Instance = this;
else Destroy(gameObject);
Init();
EventManager.PurchaseProduct += PurchaseProduct;
EventManager.GetProLocalizedPriceString += GetProLocalPriceString;
}
private void OnDestroy()
{
EventManager.PurchaseProduct -= PurchaseProduct;
EventManager.GetProLocalizedPriceString -= GetProLocalPriceString;
}
public List<Product> AllProducts { get { return m_Controller.products.all.ToList(); } }
private IStoreController m_Controller;
private ITransactionHistoryExtensions m_TransactionHistoryExtensions;
private IGooglePlayStoreExtensions m_GooglePlayStoreExtensions;
private bool m_IsGooglePlayStoreSelected;
private bool m_PurchaseInProgress;
#if RECEIPT_VALIDATION
private CrossPlatformValidator validator;
#endif
private string GetProLocalPriceString()
{
#if UNITY_ANDROID && !UNITY_EDITOR
foreach (Product product in ProductList)
{
if (product.metadata.localizedTitle == Pro)
{
return product.metadata.localizedPriceString;
}
}
return null;
#elif UNITY_EDITOR
foreach (Product product in ProductList)
{
if (product.metadata.localizedTitle == "Fake title for pro")
{
return product.metadata.localizedPriceString;
}
}
return null;
#endif
}
public void Init()
{
var module = StandardPurchasingModule.Instance();
module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
var builder = ConfigurationBuilder.Instance(module);
// Set this to true to enable the Microsoft IAP simulator for local testing.
builder.Configure<IMicrosoftConfiguration>().useMockBillingSystem = false;
m_IsGooglePlayStoreSelected = Application.platform == RuntimePlatform.Android && module.appStore == AppStore.GooglePlay;
#if AGGRESSIVE_INTERRUPT_RECOVERY_GOOGLEPLAY
// For GooglePlay, if we have access to a backend server to deduplicate purchases, query purchase history
// when attempting to recover from a network-interruption encountered during purchasing. Strongly recommend
// deduplicating transactions across app reinstallations because this relies upon the on-device, deletable
// TransactionLog database.
builder.Configure<IGooglePlayConfiguration>().aggressivelyRecoverLostPurchases = true;
// Use purchaseToken instead of orderId for all transactions to avoid non-unique transactionIDs for a
// single purchase; two ProcessPurchase calls for one purchase, differing only by which field of the receipt
// is used for the Product.transactionID. Automatically true if aggressivelyRecoverLostPurchases is enabled
// and this API is not called at all.
builder.Configure<IGooglePlayConfiguration>().UsePurchaseTokenForTransactionId(true);
#endif
//builder.AddProduct(Pro, ProductType.NonConsumable);
builder.AddProduct(Pro, ProductType.NonConsumable, new IDs
{
{Pro, GooglePlay.Name},
//{Pro+_iosPostFix, AppleAppStore.Name}
}
);
#if INTERCEPT_PROMOTIONAL_PURCHASES
// On iOS and tvOS we can intercept promotional purchases that come directly from the App Store.
// On other platforms this will have no effect; OnPromotionalPurchase will never be called.
builder.Configure<IAppleConfiguration>().SetApplePromotionalPurchaseInterceptorCallback(OnPromotionalPurchase);
Debug.Log("Setting Apple promotional purchase interceptor callback");
#endif
#if RECEIPT_VALIDATION
string appIdentifier;
#if UNITY_5_6_OR_NEWER
appIdentifier = Application.identifier;
#else
appIdentifier = Application.bundleIdentifier;
#endif
try
{
validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), appIdentifier);
}
catch (NotImplementedException exception)
{
Debug.Log("Cross Platform Validator Not Implemented: " + exception);
}
#endif
// Now we're ready to initialize Unity IAP.
UnityPurchasing.Initialize(this, builder);
}
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
m_Controller = controller;
m_TransactionHistoryExtensions = extensions.GetExtension<ITransactionHistoryExtensions>();
m_GooglePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();
#if SUBSCRIPTION_MANAGER
Dictionary<string, string> introductory_info_dict = m_AppleExtensions.GetIntroductoryPriceDictionary();
#endif
// Sample code for expose product sku details for apple store
//Dictionary<string, string> product_details = m_AppleExtensions.GetProductDetails();
Debug.Log("Available items:");
foreach (var item in controller.products.all)
{
if (item.availableToPurchase)
{
Debug.Log(string.Join(" - ",
new[]
{
item.metadata.localizedTitle,
item.metadata.localizedDescription,
item.metadata.isoCurrencyCode,
item.metadata.localizedPrice.ToString(),
item.metadata.localizedPriceString,
item.transactionID,
item.receipt
}));
ProductList.Add(item);
#if INTERCEPT_PROMOTIONAL_PURCHASES
// Set all these products to be visible in the user's App Store according to Apple's Promotional IAP feature
// https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/PromotingIn-AppPurchases/PromotingIn-AppPurchases.html
m_AppleExtensions.SetStorePromotionVisibility(item, AppleStorePromotionVisibility.Show);
#endif
#if SUBSCRIPTION_MANAGER
// this is the usage of SubscriptionManager class
if (item.receipt != null) {
if (item.definition.type == ProductType.Subscription) {
if (checkIfProductIsAvailableForSubscriptionManager(item.receipt)) {
string intro_json = (introductory_info_dict == null || !introductory_info_dict.ContainsKey(item.definition.storeSpecificId)) ? null : introductory_info_dict[item.definition.storeSpecificId];
SubscriptionManager p = new SubscriptionManager(item, intro_json);
SubscriptionInfo info = p.getSubscriptionInfo();
Debug.Log("product id is: " + info.getProductId());
Debug.Log("purchase date is: " + info.getPurchaseDate());
Debug.Log("subscription next billing date is: " + info.getExpireDate());
Debug.Log("is subscribed? " + info.isSubscribed().ToString());
Debug.Log("is expired? " + info.isExpired().ToString());
Debug.Log("is cancelled? " + info.isCancelled());
Debug.Log("product is in free trial peroid? " + info.isFreeTrial());
Debug.Log("product is auto renewing? " + info.isAutoRenewing());
Debug.Log("subscription remaining valid time until next billing date is: " + info.getRemainingTime());
Debug.Log("is this product in introductory price period? " + info.isIntroductoryPricePeriod());
Debug.Log("the product introductory localized price is: " + info.getIntroductoryPrice());
Debug.Log("the product introductory price period is: " + info.getIntroductoryPricePeriod());
Debug.Log("the number of product introductory price period cycles is: " + info.getIntroductoryPricePeriodCycles());
} else {
Debug.Log("This product is not available for SubscriptionManager class, only products that are purchase by 1.19+ SDK can use this class.");
}
} else {
Debug.Log("the product is not a subscription product");
}
} else {
Debug.Log("the product should have a valid receipt");
}
#endif
}
}
}
public void Restore()
{
m_GooglePlayStoreExtensions.RestoreTransactions(OnTransactionsRestored);
Debug.Log("Restore method");
}
private void OnTransactionsRestored(bool success)
{
Debug.Log("Transactions restored." + success);
}
public void OnPurchaseFailed(Product item, PurchaseFailureReason r)
{
Debug.Log("Purchase failed: " + item.definition.id);
Debug.Log(r);
// Detailed debugging information
Debug.Log("Store specific error code: " + m_TransactionHistoryExtensions.GetLastStoreSpecificPurchaseErrorCode());
if (m_TransactionHistoryExtensions.GetLastPurchaseFailureDescription() != null)
{
Debug.Log("Purchase failure description message: " +
m_TransactionHistoryExtensions.GetLastPurchaseFailureDescription().message);
}
m_PurchaseInProgress = false;
}
public void OnInitializeFailed(InitializationFailureReason error)
{
Debug.Log("Billing failed to initialize!");
switch (error)
{
case InitializationFailureReason.AppNotKnown:
Debug.LogError("Is your App correctly uploaded on the relevant publisher console?");
break;
case InitializationFailureReason.PurchasingUnavailable:
// Ask the user if billing is disabled in device settings.
Debug.Log("Billing disabled!");
break;
case InitializationFailureReason.NoProductsAvailable:
// Developer configuration error; check product metadata.
Debug.Log("No products available for purchase!");
break;
}
}
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
{
Debug.Log("Purchase OK: " + e.purchasedProduct.definition.id);
Debug.Log("Receipt: " + e.purchasedProduct.receipt);
m_PurchaseInProgress = false;
#if RECEIPT_VALIDATION // Local validation is available for GooglePlay, and Apple stores
if (m_IsGooglePlayStoreSelected ||
Application.platform == RuntimePlatform.IPhonePlayer ||
Application.platform == RuntimePlatform.OSXPlayer ||
Application.platform == RuntimePlatform.tvOS) {
try {
var result = validator.Validate(e.purchasedProduct.receipt);
Debug.Log("Receipt is valid. Contents:");
foreach (IPurchaseReceipt productReceipt in result) {
Debug.Log(productReceipt.productID);
Debug.Log(productReceipt.purchaseDate);
Debug.Log(productReceipt.transactionID);
GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
if (null != google) {
Debug.Log(google.purchaseState);
Debug.Log(google.purchaseToken);
}
AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
if (null != apple) {
Debug.Log(apple.originalTransactionIdentifier);
Debug.Log(apple.subscriptionExpirationDate);
Debug.Log(apple.cancellationDate);
Debug.Log(apple.quantity);
}
// For improved security, consider comparing the signed
// IPurchaseReceipt.productId, IPurchaseReceipt.transactionID, and other data
// embedded in the signed receipt objects to the data which the game is using
// to make this purchase.
}
}
catch (IAPSecurityException ex)
{
Debug.Log("Invalid receipt, not unlocking content. " + ex);
return PurchaseProcessingResult.Complete;
}
catch (NotImplementedException exception)
{
Debug.Log("Cross Platform Validator Not Implemented: " + exception);
}
}
#endif
// Unlock content from purchases here.
if (e.purchasedProduct.definition.id == Pro)
{
//PlayerPrefs.SetInt("pro", 1);
Debug.Log("PURCHASED PRODUCT!");
//AdsManager.Instance.ProActive = true;
AdsManager.Instance.HideAdsPro();
//EventBroker.CallOnProBought();
}
#if USE_PAYOUTS
if (e.purchasedProduct.definition.payouts != null) {
Debug.Log("Purchase complete, paying out based on defined payouts");
foreach (var payout in e.purchasedProduct.definition.payouts) {
Debug.Log(string.Format("Granting {0} {1} {2} {3}", payout.quantity, payout.typeString, payout.subtype, payout.data));
}
}
#endif
#if DELAY_CONFIRMATION
StartCoroutine(ConfirmPendingPurchaseAfterDelay(e.purchasedProduct));
return PurchaseProcessingResult.Pending;
#else
return PurchaseProcessingResult.Complete;
#endif
}
/// <summary>
/// Call this method to start purchase product.
/// </summary>
/// <param name="productID">Product ID from products on Google Dev Dashboard</param>
public void PurchaseProduct(string productID)
{
Debug.Log("PurchaseProduct");
if (m_PurchaseInProgress == true)
{
Debug.Log("Please wait, purchase in progress");
return;
}
if (m_Controller == null)
{
Debug.LogError("Purchasing is not initialized");
return;
}
if (m_Controller.products.WithID(productID) == null)
{
Debug.LogError("No product has id " + productID);
return;
}
m_PurchaseInProgress = true;
m_Controller.InitiatePurchase(m_Controller.products.WithID(productID), "developerPayload");
}
}
I found a solution myself.
I checked product.hasRecipt to get info if the product is bought.
So the receipt disappears after a successful revoke process in the google app store.
And every time the app is restarted this bool is sent to the app during IAP initialization.
I have an application in AppGallery available also for the older Huawei phones such as P8 or P9. I noticed that the old phones sometimes catch crash because of outdated HMS Core. I read that HMS Core can update itself automatically but I want do it immediately after I run the app.
It is possible to call it when you use map (I have it on the second screen) but I want inform user about outdated HMS Core as soon as possible.
Is there any sample how to manually call HMS Core version check?
To manually upgrade HMS Core (APK) on a Huawei phone, call the sample code provided below to check whether the version number is earlier than 5.0.0.300.
If so, the HMS Core download screen on AppGallery will appear.
Besides, remember to replace channelId in the URI.
Defined channelId as needed to identify channel data. channelId is the ID of a level-1 channel, that is, the channel from which the user comes.
public static void checkUpdate(Context context){
String version = "5.0.0.300";
String pkg = "com.huawei.hwid";
if (version.compareTo(getVersionCode(pkg, context)) > 0) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("hiapplink://com.huawei.appmarket?appId=C10132067&channelId=xxxxx&referrer=&detailType=0"));
context.startActivity(intent);
}
}
private static String getVersionCode(String packageName, Context context) {
PackageManager packageManager = context.getPackageManager();
try {
PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
return packageInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return "";
}
}
Yes, it is possible. Here is the provided solution. Everything is based on variable baseVersion. If it's value is higher than your HMS Core version code then you will get mentioned dialog.
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.util.Log;
import com.huawei.hms.adapter.AvailableAdapter;
import com.huawei.hms.adapter.internal.AvailableCode;
import com.huawei.hms.api.ConnectionResult;
/**
* Check for HMS Update
*/
public class HmsUpdateUtil {
private static final String TAG = "HmsUpdateUtil";
private static boolean isInitialized = false;
private static int versionCheckResult = 12;
/**
* Check if HMS needs update
*
* #param context context
* #return result,0 Available, 1 not Available
*/
public static int isHmsAvailable(Context context) {
if (versionCheckResult == ConnectionResult.SUCCESS ) {
return ConnectionResult.SUCCESS;
}
Log.d(TAG, "isInitialized is:" + isInitialized);
if (isInitialized) {
return 1;
}
// minimum HMS version, if less than this version, result will not be 0
int baseVersion = 50000300;
AvailableAdapter availableAdapter = new AvailableAdapter(baseVersion);
int result = availableAdapter.isHuaweiMobileServicesAvailable(context);
Log.i(TAG, "HMS update result is: " + result);
isInitialized = true;
if (result == ConnectionResult.SUCCESS) {
Log.i(TAG, "HMS is avaiable");
} else {
if (availableAdapter.isUserResolvableError(result)) {
resolution(availableAdapter, context);
} else {
Log.e(TAG, "HMS is not avaiable " + AvailableCode.ERROR_NO_ACTIVITY);
}
}
versionCheckResult = result;
return result;
}
private static void resolution(AvailableAdapter availableAdapter, Context context) {
Log.i(TAG, "HMS update start :");
Activity activity = findActivity(context);
if (activity == null) {
Log.e(TAG, "HMS is not available" + AvailableCode.ERROR_NO_ACTIVITY);
return;
}
// this method will be call upgrade dialog box.
availableAdapter.startResolution(activity, new AvailableAdapter.AvailableCallBack() {
#Override
public void onComplete(int result) {
if (result == AvailableCode.SUCCESS) {
versionCheckResult = result;
Log.i(TAG, "HMS update start success");
} else {
Log.e(TAG, "HMS update failed: " + result);
isInitialized = false;
}
}
});
}
/**
* Get Activity by Context
* #param context context
* #return Activity
*/
public static Activity findActivity(Context context) {
Activity activity = null;
if (context instanceof Activity) {
return (Activity) context;
}
if (context instanceof ContextWrapper) {
ContextWrapper wrapper = (ContextWrapper) context;
return findActivity(wrapper.getBaseContext());
} else {
return activity;
}
}
}
I am using Unity 5.4 with the latest Facebook API (7.8) for this.
I am trying to find out what my friends Facebook ID is after clicking a Game Request they sent me. I have looked into and have been using FB.GetAppLink but it doesnt appear to give me the friends Facebook ID, only my Facebook ID and the games ID.
I am needing this information so that after accepting their challenge I can send them back a gift if I was successful.
Thanks
Found the answer after umping through a lot of hoops...
public void GetDeepLink ()
{
FB.GetAppLink(DeepLinkCallBack);
}
public void DeepLinkCallBack(IAppLinkResult result)
{
if (string.IsNullOrEmpty(result.Error))
{
IDictionary<string, object> dict = result.ResultDictionary;
if (dict.ContainsKey("target_url"))
{
string url = dict["target_url"].ToString();
string keyword = "request_ids=";
int k = 0;
while (k < url.Length - keyword.Length && !url.Substring(k, keyword.Length).Equals(keyword))
k++;
k += keyword.Length;
int l = k;
while (url[l] != '&' && url[l] != '%')
l++;
string id = url.Substring(k, l - k);
FB.API("/" + id + "_" + AccessToken.CurrentAccessToken.UserId, HttpMethod.GET, DeepLinkCallBackCallBack);
}
else
{
Debug.Log("Applink Error :" + result.Error);
}
}
}
void DeepLinkCallBackCallBack(IGraphResult result)
{
Debug.Log("Request callback");
Debug.Log("========================");
if (string.IsNullOrEmpty(result.Error))
{
IDictionary<string, object> dict = result.ResultDictionary;
if (dict.ContainsKey("from"))
{
IDictionary<string,object> from = dict["from"] as Dictionary<string, object>;
if (from.ContainsKey("name"))
{
Debug.Log(from["name"]);
deepLinkUserName = from["name"].ToString();
}
if (from.ContainsKey("id"))
{
deepLinkUserName = from["name"].ToString();
FB.API("/" + dict["id"], HttpMethod.DELETE, null);
}
}
}
else
{
Debug.Log("Error in request:" + result.Error);
}
}
I notice that Parse Unity support still doesn't provide push notification for iOS.
Has anyone implemented a Unity plugin or another solution to support iOS Push Notifications via Parse?
(Cross posted on Unity Answers.)
It's actually possible now, using a ParseObject to mock up the ParseInstallation object.
Gist here: https://gist.github.com/gfosco/a3d092651c32ba3385e6
Explanation in the Parse Google Group: https://groups.google.com/d/msg/parse-developers/ku8-r91_o6s/6ioQ9T2TP7wJ
Attach this script to a GameObject, replace the important parts with your own:
using UnityEngine;
using System.Collections;
using Parse;
public class PushBehaviorScript : MonoBehaviour {
bool tokenSent = false;
public ParseObject currentInstallation = null;
void Start () {
if (PlayerPrefs.HasKey ("currentInstallation")) {
string objId = PlayerPrefs.GetString ("currentInstallation");
currentInstallation = ParseObject.CreateWithoutData ("_Installation", objId);
}
if (currentInstallation == null) {
#if UNITY_IPHONE && !UNITY_EDITOR
NotificationServices.RegisterForRemoteNotificationTypes (RemoteNotificationType.Alert | RemoteNotificationType.Badge | RemoteNotificationType.Sound);
#endif
}
}
void FixedUpdate () {
if (!tokenSent && currentInstallation == null) {
#if UNITY_IPHONE && !UNITY_EDITOR
byte[] token = NotificationServices.deviceToken;
if(token != null) {
tokenSent = true;
string tokenString = System.BitConverter.ToString(token).Replace("-", "").ToLower();
Debug.Log ("OnTokenReived");
Debug.Log (tokenString);
ParseObject obj = new ParseObject("_Installation");
obj["deviceToken"] = tokenString;
obj["appIdentifier"] = "com.parse.unitypush";
obj["deviceType"] = "ios";
obj["timeZone"] = "UTC";
obj["appName"] = "UnityPushTest";
obj["appVersion"] = "1.0.0";
obj["parseVersion"] = "1.3.0";
obj.SaveAsync().ContinueWith(t =>
{
if (obj.ObjectId != null) {
PlayerPrefs.SetString ("currentInstallation", obj.ObjectId);
}
});
}
#endif
}
}
}
To implement iOS push with parse.com You need to get the token from apple first. Then save Current instalation
Unity does have this functionality build in now.
//push notification
bool tokenSent = false;
void RegisterForPush()
{
Debug.Log("Register for push");
tokenSent = false;
#if UNITY_IOS
UnityEngine.iOS.NotificationServices.RegisterForNotifications(NotificationType.Alert |
NotificationType.Badge |
NotificationType.Sound, true);
#endif
}
void Update () {
if (!tokenSent) {
byte[] token = UnityEngine.iOS.NotificationServices.deviceToken;
if (token != null) {
// send token to a provider
tokenSent = true;
string hexToken = "%" + System.BitConverter.ToString(token).Replace('-', '%');
#if UNITY_IOS
ParseManager.instance.RegisterForPush(hexToken);
#endif
}
}
}
And inside ParseManager (or whatever class you use to manage parse>client communication)
public void RegisterForPush(string token) {
Debug.Log("Parse updating instalation");
ParseInstallation instalation = ParseInstallation.CurrentInstallation;
instalation["deviceToken"] = token;
instalation["user"] = ParseUser.CurrentUser.ObjectId;
instalation["timeZoneOffset"] = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now);
instalation.SaveAsync();
}
Tested on Unity 5.1.2 and iOS 8.4
Can someone tell me if it is possible to silently print using google cloud print from an android device?
The goal is that my app grabs a file from a URL or from the SD card and then sends it to a specific printer - all without interaction from anyone looking at the screen or touching anything. It will actually be triggered by a barcode scan on a blue tooth connected device.
Thanks
Well, it is possible but I don't know why there's not too much information about it in the documentation...
The tricky part is connecting to the google cloud print API using only the android device (with no third party servers as the documentation explains here: https://developers.google.com/cloud-print/docs/appDevGuide ), so that's what I'm going to explain.
First, you have to include in your app the Google sign-in API, I recommend firebase API https://firebase.google.com/docs/auth/android/google-signin
Then you have to go to your Google API console: https://console.developers.google.com in the menu, go to Credentials scroll to OAuth 2.0 client IDs select Web client (auto created by Google Service) and save into your project the Client ID and Client secret keys... In my project, I saved them as "gg_client_web_id" and "gg_client_web_secret" as you will see below in the code.
Next, I'm going to paste all the code and then I'll explain it:
public class MainActivity extends AppCompatActivity
implements GoogleApiClient.OnConnectionFailedListener {
private GoogleApiClient mGoogleApiClient;
private FirebaseAuth mAuth;
private FirebaseAuth.AuthStateListener mAuthListener;
private static final int REQUEST_SINGIN = 1;
private TextView txt;
public static final String TAG = "mysupertag";
public static final String URLBASE = "https://www.google.com/cloudprint/";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
txt = (TextView) findViewById(R.id.txt);
mAuth = FirebaseAuth.getInstance();
// Configure Google Sign In
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.gg_client_web_id))
.requestEmail()
.requestServerAuthCode(getString(R.string.gg_client_web_id))
.requestScopes(new Scope("https://www.googleapis.com/auth/cloudprint"))
.build();
mGoogleApiClient = new GoogleApiClient.Builder(this)
.enableAutoManage(this /* FragmentActivity */, this /* OnConnectionFailedListener */)
.addApi(Auth.GOOGLE_SIGN_IN_API, gso)
.build();
findViewById(R.id.sign_in_button).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
signIn();
}
});
mAuthListener = new FirebaseAuth.AuthStateListener() {
#Override
public void onAuthStateChanged(#NonNull FirebaseAuth firebaseAuth) {
FirebaseUser user = firebaseAuth.getCurrentUser();
if (user != null) {
// User is signed in
Log.d(TAG, "onAuthStateChanged:signed_in:" + user.getUid());
} else {
// User is signed out
Log.d(TAG, "onAuthStateChanged:signed_out");
}
// ...
}
};
}
#Override
public void onConnectionFailed(#NonNull ConnectionResult connectionResult) {
Log.d(TAG, "error connecting: " + connectionResult.getErrorMessage());
Toast.makeText(this, "error CONN", Toast.LENGTH_LONG).show();
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...);
if (requestCode == REQUEST_SINGIN) {
GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
if (result.isSuccess()) {
// Google Sign In was successful, authenticate with Firebase
GoogleSignInAccount account = result.getSignInAccount();
firebaseAuthWithGoogle(account);
} else {
// Google Sign In failed, update UI appropriately
// ...
Toast.makeText(this, "error ", Toast.LENGTH_LONG).show();
}
}
}
private void signIn() {
Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient);
startActivityForResult(signInIntent, REQUEST_SINGIN);
}
#Override
public void onStart() {
super.onStart();
mAuth.addAuthStateListener(mAuthListener);
}
#Override
public void onStop() {
super.onStop();
if (mAuthListener != null) {
mAuth.removeAuthStateListener(mAuthListener);
}
}
private void firebaseAuthWithGoogle(final GoogleSignInAccount acct) {
Log.d(TAG, "firebaseAuthWithGoogle:" + acct.getId());
AuthCredential credential = GoogleAuthProvider.getCredential(acct.getIdToken(), null);
mAuth.signInWithCredential(credential)
.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
#Override
public void onComplete(#NonNull Task<AuthResult> task) {
Log.d(TAG, "signInWithCredential:onComplete:" + task.isSuccessful());
// If sign in fails, display a message to the user. If sign in succeeds
// the auth state listener will be notified and logic to handle the
// signed in user can be handled in the listener.
FirebaseUser user = task.getResult().getUser();
txt.setText(user.getDisplayName() + "\n" + user.getEmail());//todo
if (!task.isSuccessful()) {
Log.w(TAG, "signInWithCredential", task.getException());
Toast.makeText(MainActivity.this, "Authentication failed.",
Toast.LENGTH_SHORT).show();
}
getAccess(acct.getServerAuthCode());
}
});
}
private void getPrinters(String token) {
Log.d(TAG, "TOKEN: " + token);
String url = URLBASE + "search";
Ion.with(this)
.load("GET", url)
.addHeader("Authorization", "Bearer " + token)
.asString()
.withResponse()
.setCallback(new FutureCallback<Response<String>>() {
#Override
public void onCompleted(Exception e, Response<String> result) {
Log.d(TAG, "finished " + result.getHeaders().code() + ": " +
result.getResult());
if (e == null) {
Log.d(TAG, "nice");
} else {
Log.d(TAG, "error");
}
}
});
}
private void getAccess(String code) {
String url = "https://www.googleapis.com/oauth2/v4/token";
Ion.with(this)
.load("POST", url)
.setBodyParameter("client_id", getString(R.string.gg_client_web_id))
.setBodyParameter("client_secret", getString(R.string.gg_client_web_secret))
.setBodyParameter("code", code)
.setBodyParameter("grant_type", "authorization_code")
.asString()
.withResponse()
.setCallback(new FutureCallback<Response<String>>() {
#Override
public void onCompleted(Exception e, Response<String> result) {
Log.d(TAG, "result: " + result.getResult());
if (e == null) {
try {
JSONObject json = new JSONObject(result.getResult());
getPrinters(json.getString("access_token"));
} catch (JSONException e1) {
e1.printStackTrace();
}
} else {
Log.d(TAG, "error");
}
}
});
}}
As you can see, in the onCreate the important part is creating the GoogleSignInOptions WITH the google cloud print scope AND calling the requestIdToken/requestServerAuthCode methods.
Then in the firebaseAuthWithGoogle method call the getAccess method in order to get the OAuth access token, for making all requests I'm using Ion library: https://github.com/koush/ion
Next with the access_token you can now do requests to the google cloud print API, in this case I call the getPrinters method, in this method I call the "search" method (from google cloud print API) to get all the printers associated to the google account that has signed in.. (to associate a printer to a google account visit this: https://support.google.com/cloudprint/answer/1686197?hl=en&p=mgmt_classic ) Note the .addHeader("Authorization", "Bearer " + token), this is the important part of the request, the "token" var is the access_token, you NEED add this Authorization header in order to use the API and don't forget to refresh when it expires, as explained here : https://developers.google.com/identity/protocols/OAuth2ForDevices in the "Using a refresh token" part.
And that's it, you can now print something sending a POST request to the "submit" method of the google cloud print API, I recommend to go here: https://developers.google.com/cloud-print/docs/appInterfaces and see all the methods available and how to use them (wich parameters send to them, etc). Of course in that link explains the "submit" method too.
EDIT:
EXAMPLE OF HOW TO SEND A REQUEST TO "/submit" FOR PRINTING USING ION LIBRARY AND MJSON LIBRARY (https://bolerio.github.io/mjson/) THE MJSON IS FOR CREATING A JSON OBJECT, YOU CAN CREATE IT THE WAY YOU PREFER
private void printPdf(String pdfPath, String printerId) {
String url = URLBASE + "submit";
Ion.with(this)
.load("POST", url)
.addHeader("Authorization", "Bearer " + YOUR_ACCESS_TOKEN)
.setMultipartParameter("printerid", printerId)
.setMultipartParameter("title", "print test")
.setMultipartParameter("ticket", getTicket())
.setMultipartFile("content", "application/pdf", new File(pdfPath))
.asString()
.withResponse()
.setCallback(new FutureCallback<Response<String>>() {
#Override
public void onCompleted(Exception e, Response<String> result) {
if (e == null) {
Log.d(TAG, "PRINTTT CODE: " + result.getHeaders().code() +
", RESPONSE: " + result.getResult());
Json j = Json.read(result.getResult());
if (j.at("success").asBoolean()) {
Toast.makeText(MainActivity.this, "Success", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(MainActivity.this, "ERROR", Toast.LENGTH_LONG).show();
}
} else {
Toast.makeText(MainActivity.this, "ERROR", Toast.LENGTH_LONG).show();
Log.d(TAG, e.toString());
}
}
});
}
private String getTicket() {
Json ticket = Json.object();
Json print = Json.object();
ticket.set("version", "1.0");
print.set("vendor_ticket_item", Json.array());
print.set("color", Json.object("type", "STANDARD_MONOCHROME"));
print.set("copies", Json.object("copies", 1));
ticket.set("print", print);
return ticket.toString();
}
Yes, You can achieve silent print using this REST API(https://www.google.com/cloudprint/submit) ,I have done it using WCF Service.
you need to download contents from url as base64 content, then add
contentType=dataUrl
in the request.
Here is the code..
postData = "printerid=" + PrinterId;
postData += "&title=" + JobTitle;
postData += "&ticket=" + ticket;
postData += "&content=data:" + documentContent.ContentType + ";base64," + documentContent.Base64Content;
postData += "&contentType=dataUrl";
postData += "&tag=test";
Then , please make a request to submit REST API in this way.
var request = (HttpWebRequest)WebRequest.Create("https://www.google.com/cloudprint/submit");
var data = Encoding.ASCII.GetBytes(postData);
request.Headers.Add("Authorization: Bearer " + Token);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.ContentLength = data.Length;
request.UseDefaultCredentials = true;
using (var stream = request.GetRequestStream())
{
stream.Write(data, 0, data.Length);
}
var response = (HttpWebResponse)request.GetResponse();
string responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
JavaScriptSerializer json_serializer = new JavaScriptSerializer();
PrintJobResponse printInfo = json_serializer.Deserialize<PrintJobResponse>(responseString);
return printInfo;
Thanks.
For anybody reading this now, after a lot of searching around I have found it is a lot easier and faster to set up to just use Zapier to catch a hook and print to google cloud print (from cordova at least, i can't speak for native apps)