svpino.com
Hi! My name is Santiago. Software developer, regular guy, English apprentice, and writer enthusiast.
Jul 30, 2012
Still waiting for Google+ comments on Blogger
More Google+ integration into Blogger, but still not integrated comments. I've been requesting, this, forever, but is still not here. Obviously is going to happen at some point because it's a natural transition for a product like Blogger. Question is when...
Jul 27, 2012
Design and test. Go back and start the same process over and over again
Yes, this is the Nth time I play with the look 'n feel of the blog, but I really like to explore new things. Unfortunately I'm not a designer, so it takes me more time than normal to settle with something that I really like.
Today I can go to bed feeling that the site looks better than ever (it doesn't mean I'm 100% happy though). A huge accomplishment was adding Prettify to format the source code. Neat, but I'd have to go through every single post in my archive and add some styles to get it working. Not gonna happen today... but it will.
The next goal will be to fix some navigation issues and add links to the RSS of the blog, Twitter, and Google+ (middle finger to Facebook). Hope to get it done by this coming week, will see.
If you have something to say about my extra-minimalist design, go ahead.
Today I can go to bed feeling that the site looks better than ever (it doesn't mean I'm 100% happy though). A huge accomplishment was adding Prettify to format the source code. Neat, but I'd have to go through every single post in my archive and add some styles to get it working. Not gonna happen today... but it will.
The next goal will be to fix some navigation issues and add links to the RSS of the blog, Twitter, and Google+ (middle finger to Facebook). Hope to get it done by this coming week, will see.
If you have something to say about my extra-minimalist design, go ahead.
Jul 16, 2012
Connectivity and Battery Life in Android. Making them play nicely.
Last week I announced Stocktile, a stock market application to make my life easier and enjoy checking on my tickers even more. At the end of that post, I wrote the biggest lesson I learnt while I was developing the app:
So connectivity and battery life are the main two players. They need to play seamlessly and safe. Unfortunately is difficult to get it right, so a lot of developers don't pay attention to these details. That's mainly why tons of Android apps are battery and data-plans killers. They just don't care.
On the Android developers site there are two awesome articles explaining the ins and outs of developing and application that properly takes care of connectivity and battery life. I'm not going to reproduce here everything said, but I strongly recommend any developer to read those training sessions. I want to focus instead in the code that powers Stocktile, and how I combined different techniques to make the application a good citizen in Android-land.
Stocktile downloads the market information for every stock ticker added to your dashboard. You can have 3 tickers, or hundreds of them. Not matter how many, you surely want them to display the latest market information available all the time. That's the point, and that was my goal. However, because I'm feeding my app using the Yahoo! Finance API, there's going to be a minimum delay of 15 minutes between market updates. That's the first constraint I have to play with: no updates will be requested if the latest one was less than 15 minutes ago.
But what happens if the user is not using the application? Do we want to keep updating it? Remember that every update will impact the user's data-plan and the battery life of the device, so the answer is no. If the users don't care about our app, why bothering them with updates they are not going to see? So that's the second rule: the application will be updated only if users are actively using it.
However, downloading information from the network takes time, and we don't want our users waiting from fresh data to come when they open our app. So we should rethink our updates policies: what about updating automatically even if the user is not actively using the app but only when the device is plugged to a power source and connected to a WiFi network? That sounds better!
There are two more use cases that I had to take care of: when the user adds a new ticker to the dashboard, we want to update it immediately. And, if during an update an error occurs, we need to re-schedule the next update as soon as possible. However, it might be the case that something is wrong with the server so a continuous update will further damage the battery life of the device. To solve this, we need to employ a back-off pattern as explained on the Android training classes.
So let's put everything together:
After the update is scheduled, another important thing is done in the code: a BroadcastReceiver for connectivity updates is enabled. Why? Because we need to be notified as soon as the user is not longer connected to the WiFi network. Since connectivity updates are very frequent, we don't want to keep the BroadcastReceiver enabled all the time, so we enable and disable it as needed.
Now, what happens if the battery is not plugged in? Well, we need to hold off in any new updates, so we go ahead and cancel any pending schedule. Also, if we are not actively waiting for the device to go back online, we can also disable the connectivity BroadcastReceiver until we get back plugged in to the wall. Pay special attention to the
The other bit of code to pay attention is the flag
Read closely. Try for yourself, and ask if you get lost.
Main challenge: The web is always online. Mobile applications are not. It's an entire layer of complexity added to our applications.Indeed. Not having a reliable and fast Internet connection all the time is something traditional developers are not used to. Mobile imposes this restriction, and we have to deal with it if we want to make our applications solid options for consumers.
But it's not just about connectivity
There's another challenge though that we don't need to worry about outside the mobile ecosystem: battery life. In such small devices, there's a very small battery and dozen of applications competing to use a pice of it. Unfortunately, technology is still not where we can stop worrying about how much power our application sucks, so we need to use it carefully or face the anger of our users.So connectivity and battery life are the main two players. They need to play seamlessly and safe. Unfortunately is difficult to get it right, so a lot of developers don't pay attention to these details. That's mainly why tons of Android apps are battery and data-plans killers. They just don't care.
On the Android developers site there are two awesome articles explaining the ins and outs of developing and application that properly takes care of connectivity and battery life. I'm not going to reproduce here everything said, but I strongly recommend any developer to read those training sessions. I want to focus instead in the code that powers Stocktile, and how I combined different techniques to make the application a good citizen in Android-land.
Setting our rules
First of all, take into account that every application is different and requires a different level of thinking. What works for me, might not work for a news application or any other category. However, the approach is very similar, so I hope to help more than one out there.Stocktile downloads the market information for every stock ticker added to your dashboard. You can have 3 tickers, or hundreds of them. Not matter how many, you surely want them to display the latest market information available all the time. That's the point, and that was my goal. However, because I'm feeding my app using the Yahoo! Finance API, there's going to be a minimum delay of 15 minutes between market updates. That's the first constraint I have to play with: no updates will be requested if the latest one was less than 15 minutes ago.
But what happens if the user is not using the application? Do we want to keep updating it? Remember that every update will impact the user's data-plan and the battery life of the device, so the answer is no. If the users don't care about our app, why bothering them with updates they are not going to see? So that's the second rule: the application will be updated only if users are actively using it.
However, downloading information from the network takes time, and we don't want our users waiting from fresh data to come when they open our app. So we should rethink our updates policies: what about updating automatically even if the user is not actively using the app but only when the device is plugged to a power source and connected to a WiFi network? That sounds better!
There are two more use cases that I had to take care of: when the user adds a new ticker to the dashboard, we want to update it immediately. And, if during an update an error occurs, we need to re-schedule the next update as soon as possible. However, it might be the case that something is wrong with the server so a continuous update will further damage the battery life of the device. To solve this, we need to employ a back-off pattern as explained on the Android training classes.
So let's put everything together:
- The minimun time between updates will be 15 minutes.
- If the user is not using the application, new information is going to be downloaded only when the device is plugged to a power source and running on a WiFi network.
- If the user adds a new ticker, an update will be performed immediately only for that ticker.
- If an error occurs while updating, a new back-off update will be scheduled as soon as possible.
The BatteryBroadcastReceiver
Let's take a look to all the code pieces that make up these guidelines. Let's start with our second rule above. For that one, we need to create two different BroadcastReceivers, one for monitoring the battery and the other to monitoring connectivity changes. Look at the following lines in our manifest file:<receiver
android:name=".BatteryBroadcastReceiver"
android:exported="false" >
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
<action android:name="android.intent.action.ACTION_POWER_DISCONNECTED" />
</intent-filter>
</receiver>
Then our Java class:
public class BatteryBroadcastReceiver extends BroadcastReceiver {
private final static String LOG_TAG = BatteryBroadcastReceiver.class.getName();
@Override
public void onReceive(Context context, Intent intent) {
if (isTheBatteryPluggedIn(context)) {
if (areWeUsingWiFi(context)) {
Log.d(LOG_TAG, "On WiFi and charging, let's update");
Intent intent = new Intent(context, MarketCollectionReceiver.class);
intent.setAction(Constants.SCHEDULE_BACKGROUND);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
((AlarmManager) context.getSystemService(Context.ALARM_SERVICE))
.setInexactRepeating(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis(),
AlarmManager.INTERVAL_HALF_HOUR,
pendingIntent);
}
ComponentName componentName = new ComponentName(
context,
ConnectivityBroadcastReceiver.class);
context.getPackageManager()
.setComponentEnabledSetting(componentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
}
else {
Log.d(LOG_TAG, "Not charging anymore. Hold off in any new updates");
Intent intent = new Intent(context, MarketCollectionReceiver.class);
intent.setAction(Constants.SCHEDULE_BACKGROUND);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
((AlarmManager) context.getSystemService(Context.ALARM_SERVICE))
.cancel(pendingIntent);
SharedPreferences pref = context.getSharedPreferences(
Constants.PREFERENCES, Context.MODE_PRIVATE);
boolean areWeWaitingForConnectivity =
pref.getBoolean(Constants.PREFERENCE_STATUS_WAITING_FOR_CONNECTIVITY, false);
if (!areWeWaitingForConnectivity) {
ComponentName componentName = new ComponentName(context,
ConnectivityBroadcastReceiver.class);
context.getPackageManager().setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
}
}
}
Looks more complicated than what really is. Let's go step by step. The very first line asks whether the battery is plugged in or not. Here is that code:
public boolean isTheBatteryPluggedIn(Context context) {
Intent batteryIntent = context.getApplicationContext()
.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
int plugged = batteryIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
return plugged == BatteryManager.BATTERY_PLUGGED_AC
|| plugged == BatteryManager.BATTERY_PLUGGED_USB;
}
Then we ask whether we are using WiFi:
public static boolean areWeUsingWiFi(Context context) {
ConnectivityManager connectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
boolean isConnected = networkInfo != null ? networkInfo.isConnected() : false;
boolean isWiFi = isConnected
? networkInfo.getType() == ConnectivityManager.TYPE_WIFI : false;
return isWiFi;
}
In case both conditions are true updates are safe, so we can schedule a regular update using the AlarmManager service. Note how these updates will be performed every 30 minutes since the user is not necessarily using the application.After the update is scheduled, another important thing is done in the code: a BroadcastReceiver for connectivity updates is enabled. Why? Because we need to be notified as soon as the user is not longer connected to the WiFi network. Since connectivity updates are very frequent, we don't want to keep the BroadcastReceiver enabled all the time, so we enable and disable it as needed.
Now, what happens if the battery is not plugged in? Well, we need to hold off in any new updates, so we go ahead and cancel any pending schedule. Also, if we are not actively waiting for the device to go back online, we can also disable the connectivity BroadcastReceiver until we get back plugged in to the wall. Pay special attention to the
areWeWaitingForConnectivity
flag. This is going to be true in case an error occurred while updating and we are waiting for the connectivity to come back to re-run the update.The ConnectivityBroadcastReceiver
First, we need to declare our receiver in the manifest file. Note how we want it to be disabled by default. Our code will take care of enabling it only when necessary: <receiver
android:name=".receivers.ConnectivityBroadcastReceiver"
android:enabled="false"
android:exported="false" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
Then the Java class:
public class ConnectivityBroadcastReceiver extends BroadcastReceiver {
private final static String LOG_TAG = ConnectivityBroadcastReceiver.class.getName();
@Override
public void onReceive(Context context, Intent intent) {
if (areWeOnline(context)) {
SharedPreferences pref = context.getSharedPreferences(Constants.PREFERENCES,
Context.MODE_PRIVATE);
boolean wereWeWaitingForConnectivity = sharedPreferences.getBoolean(
Constants.PREFERENCE_STATUS_WAITING_FOR_CONNECTIVITY, false);
if (wereWeWaitingForConnectivity) {
Log.d(LOG_TAG, "Connectivity was just re-established, let's update.");
DataProvider.startStockQuoteCollectorService(context, null);
ComponentName componentName = new ComponentName(
context, ConnectivityBroadcastReceiver.class);
context.getPackageManager().setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
else {
if (areWeUsingWiFi(context)) {
if (isTheBatteryPluggedIn(context)) {
Log.d(LOG_TAG, "We are on WiFi and charging, so let's update");
Intent intent = new Intent(context, MarketCollectionReceiver.class);
intent.setAction(Constants.SCHEDULE_BACKGROUND);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT);
((AlarmManager) context.getSystemService(Context.ALARM_SERVICE))
.setInexactRepeating(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis(),
AlarmManager.INTERVAL_HALF_HOUR,
pendingIntent);
}
}
else {
Log.d(LOG_TAG, "We aren't using WiFi, so don't update");
Intent intent = new Intent(context, MarketCollectionReceiver.class);
intent.setAction(Constants.SCHEDULE_BACKGROUND);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT);
((AlarmManager) context.getSystemService(Context.ALARM_SERVICE))
.cancel(pendingIntent);
}
}
}
else {
Log.d(LOG_TAG, "We aren't online so don't update.");
Intent intent = new Intent(context, MarketCollectionReceiver.class);
intent.setAction(Constants.SCHEDULE_BACKGROUND);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
((AlarmManager) context.getSystemService(Context.ALARM_SERVICE))
.cancel(pendingIntent);
}
}
}
Very similar to the BatteryBroadcastReceiver, but in this case we start asking if we are online (not only on a WiFi, but with any Internet access):
public boolean areWeOnline(Context context) {
ConnectivityManager connectivityManager = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return networkInfo != null ? networkInfo.isConnected() : false;
}
If we are, and we were waiting for the connectivity to come back (an error occurred while updating), then we run an update immediately and disable the BroadcastReceiver. If we don't need an immediate update, then we ask whether the battery is plugged in, and we are on a WiFi connection. From there, everything is pretty much the same to the BatteryBroadcastReceiver.Receiving alarms and firing updates
These two BroadcastReceivers work pretty well in conjunction to schedule our background updates, but there's another piece they need to fire up the updates: another BroadcastReceiver that's going to be kicked off by the scheduled alarms. Round of applauses for our MarketCollectionReceiver:public class MarketCollectionReceiver extends BroadcastReceiver {
private final static String LOG_TAG = MarketCollectionReceiver.class.getName();
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Constants.SCHEDULE_RETRY)) {
SharedPreferences pref = context.getSharedPreferences(
Constants.PREFERENCES, Context.MODE_PRIVATE);
boolean retrying = pref.getBoolean(
Constants.PREFERENCE_COLLECTOR_RETRYING, false);
if (retrying) {
Log.d(LOG_TAG, "Retrying update...");
DataProvider.startStockQuoteCollectorService(context, null);
}
else {
Log.d(LOG_TAG, "Update was already successfully performed");
}
}
else if (intent.getAction().equals(Constants.SCHEDULE_BACKGROUND)) {
if (isTheBatteryPluggedIn(context) && areWeUsingWiFi(context)) {
Log.d(LOG_TAG, "Performing background update...");
DataProvider.startStockQuoteCollectorService(context, null);
}
else {
Log.d(LOG_TAG, "We are not longer able to perform background updates");
Intent intent = new Intent(context, MarketCollectionReceiver.class);
intent.setAction(Constants.SCHEDULE_BACKGROUND);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT);
((AlarmManager) context.getSystemService(Context.ALARM_SERVICE))
.cancel(pendingIntent);
}
}
else if (intent.getAction().equals(Constants.SCHEDULE_AUTOMATIC)) {
Log.d(LOG_TAG, "Performing automatic update");
DataProvider.startStockQuoteCollectorService(context, null);
}
}
}
If you take a closer look to the code above, you'll notice that this BroadcastReceiver is using the action of the passed Intent to determine what kind of update was scheduled. In our Battery and Connectivity broadcast receivers we only use "BACKGROUND" updates, but the application uses three different types: BACKGROUND, RETRY, and AUTOMATIC. When the device is plugged in, and is on WiFi, we fire a BACKGROUND update. If an error occurs while updating, we fire a RETRY update. If the user is actively using the application or a new ticker is added, we fire an AUTOMATIC update. The rest of the code, should be self-explanatory.Now, what about our back-off updates?
That happens when the update occurs. I'm not going to post the entire Service that takes care of the updates, but just the relevant sections:...
int retries = sharedPreferences.getInt(Constants.PREFERENCE_COLLECTOR_RETRIES, 0);
...
if (retrying || wereWeWaitingForConnectivity || areWeUpdatingOnlyOneTicker
|| (!areWeUpdatingOnlyOneTicker
&& currentTime - lastUpdate > Constants.COLLECTOR_MIN_REFRESH_INTERVAL)) {
try {
update(...);
Log.d(LOG_TAG, "Update was successfully completed");
...
}
catch (Exception e) {
Log.e(LOG_TAG, "Update failed.", e);
if (areWeOnline(this)) {
Log.d(LOG_TAG, "Scheduling an alarm for retrying the update...");
retries++;
Editor editor = sharedPreferences.edit();
editor.putBoolean(Constants.PREFERENCE_COLLECTOR_RETRYING, true);
editor.putInt(Constants.PREFERENCE_COLLECTOR_RETRIES, retries);
editor.commit();
long interval = Constants.COLLECTOR_MIN_RETRY_INTERVAL * retries;
if (interval > Constants.COLLECTOR_MAX_REFRESH_INTERVAL) {
interval = Constants.COLLECTOR_MAX_REFRESH_INTERVAL;
}
Intent intent = new Intent(context, MarketCollectionReceiver.class);
intent.setAction(Constants.SCHEDULE_RETRY);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT);
((AlarmManager) getSystemService(Context.ALARM_SERVICE))
.set(AlarmManager.RTC,
System.currentTimeMillis() + interval,
pendingIntent);
}
else {
Log.d(LOG_TAG, "We are not online.");
Editor editor = sharedPreferences.edit();
editor.putBoolean(
Constants.PREFERENCE_STATUS_WAITING_FOR_CONNECTIVITY, true);
editor.commit();
ComponentName componentName =
new ComponentName(this, ConnectivityBroadcastReceiver.class);
getPackageManager().setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
}
}
}
First, note the retries
variable. Every time an error occurs, we increment a counter and save it in our preferences. The next update will use this value to delay every update and avoid retries when the server is continuously failing. Note how in this case we set up the alarm with a RETRY action. In case we are online when the error occurs, then we set the flag WAITING_FOR_CONNECTIVITY
and enable our ConnectivityBroadcastReceiver to get notified as soon as we are back online.
The other bit of code to pay attention is the flag
areWeUpdatingOnlyOneTicker
. Although I didn't post the section where this flag gets initialized, it means than the user added a new ticker to the dashboard and we need to run an update just for it.It's a lot, I know
Yes, it gets tricky, verbose, and it's easy to loose track of every component. By reading back what I just wrote I'm realizing how painful is for developers to take care of so many details. Some day this will get done for us under the hood, or we won't need to worry anymore when technology gets to a point where battery and connectivity are not longer a concern. Unfortunately we are not there yet, and this topic is really important if we want to develop an application that doesn't kill our devices.Read closely. Try for yourself, and ask if you get lost.
Jul 12, 2012
Stocktile sees the light
Finally! Stocktile is out!
After 4 weeks of long nights in front of my computer, Stocktile is live on Google Play in two different versions: Lite and HD. Mostly, I'm glad because now I get to use an application that does exactly what I want... nothing less, nothing more. If you want to see some screenshots, follow any of the links above.
What I've learnt has been huge. Lots and lots of things to share about the Android platform. Let's see what can I put together when I get my strength back from this past month.
Before going to bed, I do want to share the main lesson from these days. Copied from a tweet I posted 4 days ago:
After 4 weeks of long nights in front of my computer, Stocktile is live on Google Play in two different versions: Lite and HD. Mostly, I'm glad because now I get to use an application that does exactly what I want... nothing less, nothing more. If you want to see some screenshots, follow any of the links above.
What I've learnt has been huge. Lots and lots of things to share about the Android platform. Let's see what can I put together when I get my strength back from this past month.
Before going to bed, I do want to share the main lesson from these days. Copied from a tweet I posted 4 days ago:
Main challenge: The web is always online. Mobile applications are not. It's an entire layer of complexity added to our applications.Think about it.
Jul 3, 2012
Playing a little bit with functional programming
You should start reading "Functional Programming - Why should we care?" and take a look to the example C# code at the end of the article.
Then, if you are more of a Java fan, move onto "Functional Programming - Rewriting the example in Java" to see how to do the same thing without delegates and Lambda expressions.
Yes, the second article is mine (not the first one, though). I will be posting from time to time in my company's blog and linking the articles here.
Then, if you are more of a Java fan, move onto "Functional Programming - Rewriting the example in Java" to see how to do the same thing without delegates and Lambda expressions.
Yes, the second article is mine (not the first one, though). I will be posting from time to time in my company's blog and linking the articles here.
Jan 12, 2012
Blogger, I appreciate your changes but that's not what I asked for
What a coincidence: I was talking about it yesterday and a few hours later Google posted "Engage with your readers through threaded commenting" on their Blogger blog.
Thanks Google for moving forward, but I think you are using the wrong road.
Where is Google+ in all this? Seems confusing to me that you are throwing G+ everywhere in your products, and going forward with a "custom-made" commenting system for Blogger. Something doesn't add up here.
I hope you have strong reasons for doing this. It took you 5+ years to revamp comments on Blogger, so I basically lost all my hopes in seeing G+ taking over here.
But again, thank you for doing something.
Thanks Google for moving forward, but I think you are using the wrong road.
Where is Google+ in all this? Seems confusing to me that you are throwing G+ everywhere in your products, and going forward with a "custom-made" commenting system for Blogger. Something doesn't add up here.
I hope you have strong reasons for doing this. It took you 5+ years to revamp comments on Blogger, so I basically lost all my hopes in seeing G+ taking over here.
But again, thank you for doing something.
Jan 11, 2012
Microsoft and Nokia's Plans for Marketing Windows Phone in 2012
Microsoft and Nokia's Plans for Marketing Windows Phone in 2012
This is pathetic from Microsoft:
This is the kind of stuff that should be scrutinized by the Justice Department. This is really going to hurt consumers. And careful... this potentially can hurt the stores as well: who is going to trust those sellers anymore when everybody knows they are getting money by handing you a Windows Phone when you ask for "the best device"?
Microsoft, I guess my sentiments toward you are far from changing anytime soon.
This is pathetic from Microsoft:
Included in the plan are sales incentives for retail workers, aimed at getting them to finally start recommending Windows Phone as an alternative to Android and iPhone. The amount of payments are $10 to $15 per handset sold, depending on the number sold, for some handset models.If they can't get love from consumers the traditional way (by designing a compelling product), they are going to buy that love paying commissions to sell reps.
This is the kind of stuff that should be scrutinized by the Justice Department. This is really going to hurt consumers. And careful... this potentially can hurt the stores as well: who is going to trust those sellers anymore when everybody knows they are getting money by handing you a Windows Phone when you ask for "the best device"?
Microsoft, I guess my sentiments toward you are far from changing anytime soon.
Subscribe to:
Posts (Atom)