Elemental Evil: Sessions 17

In the previous session the group entered the Temple of Howling Hatred, trying to stop the prophets of elemental evil from summoning their princes. This session started with a long discussion on the merits of retreating to safety, which I tried to hurry along to stop wasting time. After resting the group returned to the Temple, where I had put 8 fresh Kenku at the gate. On the first run, played as written, the Kenku were just making noises to scare the adventurers; this time they use the defense mechanism of the gate house, firing at the group through arrow slits. Although the Kenku were rather low level, that occupied the group for quite a while.

Finally they made it back to the step pyramid where they were before. Inside they found a bunch of cultists and stairs up. After killing the cultists they went up the stairs and met the prophetess of elemental evil air, Aerisi Kalinoth. Despite being a level 12 spellcaster, the prophetess was a pushover. She relied on concentration spells like Fly or Cloudkill, and that just doesn’t work. The group just ignored her henchmen and broke her concentration immediately after she cast anything, making her not very effective. I find the 5th edition D&D spell system rather boring, as it really encourages you to use only instant damage spells, because anything a bit more interesting is based on concentration and has little effect. For example Cloudkill used to be a very powerful spell in previous editions, but now it affected only the first character starting his turn in the cloud, who then broke the concentration of Aerisi and dispelled the cloud before it damaged anybody else.

The bard who was with Aerisi was even weaker, also due to concentration spells, and so the only serious monster in the encounter was an invisible stalker, who had more health than the other two together, was much harder to damage, and didn’t use concentration spells. Having finally killed that one as well, the group found a lot of monetary treasure and Windvane, the magical spear of Aerisi. However I had had to nerf that one on the request of the future DM of our group, in whose campaign the original wouldn’t have fit. So now it was just a +2 lance that opened the magical portals to the temples of elemental evil. At that point we stopped the session.

Hidden Pay2Win

This weekend I noticed a curious feature of Total War: Arena. If you play the game with a free account, you not only earn less xp, but silver is also tight, so you can only afford the necessary unit equipment. If you play with a premium account, you get a lot more silver (because the gross amount before expenses is increased, leaving a much larger net amount). And there is nothing in the game you can do with that silver, except for buying consumables.

Consumables, as the name suggests, only last for one battle. There are a couple of fun ones which give you a very short time effect, or allow you to build a trap or barricade. But there are also consumables which simply increase your stats for the whole duration of the battle. You can set those to auto-replenish, and get the effect in every battle, at the cost of silver. As a result, with a premium account you can play with consumables all the time and not have any money problems. With a free account you can only afford them once in a while. So in the end the premium players’ units have better stats than the free players’ units. But it is very discreet and not widely advertised. Hidden Pay2Win.

How abundance makes us poorer

Maybe it was to be expected with an offer that involves charity, but it turns out that for me the Humble Bundle Monthly is mostly an investment in a source for philosophical thoughts. When I initially bought the bundle in order to get Civ VI for cheap, I went for the three-month plan. So even if I since unsubscribed I just got my second months worth of games. And compared to the first month, there are even less games in there which I can see me playing. That is not to say that the offer is a bad one, or the games on offer are bad. Rather it reflects upon how my interests got narrower over time.

I am old enough to remember a time before video games. The first video game I played was Pong on a console that couldn’t play anything else, in black and white on a TV screen. When people got the first consoles with cartridges and computers, kids typically had just a handful of games, not necessarily chosen by themselves. If you only have 3 game cartridges, you will play the hell out of each of those games, whether those are your favorite games or not. Fast forward to 2017, where 7,672 games were released on Steam alone, again nearly doubling the number of Steam games available for a fourth year in a row.

Everybody has favorite games and favorite genres. If you are limited by the number of games available to you, you play what you got regardless of genre. If you have an abundance of choice, you get more and more picky and only play your favorite genres. The bottleneck becomes the amount of time available to play, so why should you play let’s say a platformer if you prefer role-playing games? Of course the consequence of that is that you end up with a much narrower experience. You only play a handful of favorite genres and don’t have the time for a bunch of other genres, which might offer a very different experience of gaming.

I see a parallel to the world of news and politics. Back in the day where your only source of news was one paper you and everybody in your street was subscribed to, you all got the same variety of news and opinions. Today there are so many sources of news and opinions that you can choose one which aligns well with your own opinions. If you are a fan of Trump, you watch Fox News and read Breitbart, if you are on the other side you watch CNN and read Huffington Post. But the result is that you end up in an echo chamber which doesn’t allow for a variety of opinions. This has gone so far that the echo chambers of today don’t even agree on the same set of facts. A news source that reports something uncomfortable to you is “fake news”, truth has become subservient to opinion.

The future is one in which we lead comfortable lives in which we play only our favorite games, see only our favorite genre of movies and TV shows, hear only news that please us. Until we have become so isolated from another group of people (which might well be our neighbors) that the two groups don’t consider each other of being of the same kind any more, and start killing each other off. The internet, which had a promise of offering us a much wider offer of everything from information to entertainment, ends up making us all poorer and more narrow-minded.

Learn ABC of “A/B testing”

What is A/B Testing?

A/B Testing is one of the best way to compare two or more versions of an application or a web page.It enables you to determine which one of them performs better and can generate better conversion rates. You compare two web pages or applications by showing the two variants (let’s call them A and B) to similar visitors at the same time. The one that gives a better conversion rate, wins!

All websites on the web have a goal – a reason for them to exist

  • eCommerce websites want visitors buying products
  • SaaS web apps(Software as a service) want visitors signing up for a trial and converting to paid visitors
  • News and media websites want readers to click on ads or sign up for paid subscriptions

Every business website wants visitors converting from just visitors to something else. The rate at which a website is able to do this is its “conversion rate”. Measuring the performance of a variation (A or B) means measuring the rate at which it converts visitors to goal achievers.

Example

Let us assume that there is a web page and all the traffic is directed to this page. Now as a part of A/B Testing, you have made some minor changes like headlines, numbering, etc. on the same page and half of its traffic is directed to the modified version of this web page. Now you have version A and version B of the same web page and you can monitor the visitor’s actions using statistics and analysis to determine the version that yields a higher conversion rate.

A conversion rate is defined as the instance, when any visitor on your website performs a desired action. A/B Testing enables you to determine the best online marketing strategy for your business. Take a look at the following illustration. It shows that version A yields a conversion rate of 15% and version B yields a conversion rate of 22%.

Why Should You do A/B Test?

A/B testing allows you to make more out of your existing traffic. While the cost of acquiring paid traffic can be huge, the cost of increasing your conversions is minimal. To compare, a Small Business Plan  whose cost is equivalent to 5 to 10 Google Adwords clicks. The Return On Investment of A/B testing can be massive, as even small changes on a landing page or website can result in significant increases in leads generated, sales and revenue.

Traffic vs Conversion rate

What Can You Test?

Almost anything on your website that affects visitor behavior can be A/B tested.
webpage

  1. Headlines
  2. Sub headlines
  3. Paragraph Text
  4. Testimonials
  5. Call to Action text
  6. Call to Action Button
  7. Links
  8. Images
  9. Content near the fold
  10. Social proof
  11. Media mentions
  12. Awards and badges
Advanced tests can include pricing structures, sales promotions, free trial lengths, navigation and UX experiences, free or paid delivery, and more.

A/B Testing Process

The correct way to run an A/B testing experiment is to follow a scientific process. It includes the following steps:

Study your Website Data: Use a website analytics tool such as Google Analytics, and find the problem areas in your conversion funnel. For example, you can identify the pages with the highest bounce rate. Let’s say, your homepage has an unusually high bounce rate.

Observe User Behavior: Utilize visitor behavior analysis tools such as Heatmaps, Visitor Recordings, Form Analysis and On-page Surveys, and find what is stopping the visitors from converting. For example, “The CTA button is not prominent on the home page.”

Construct a Hypothesis: Per the insights from visitor behavior analysis tools, build a hypothesis aimed at increasing conversions. For example, “Increasing the size of the CTA button will make it more prominent and will increase conversions.”

Test your Hypothesis: Create a variation per your hypothesis, and A/B test it against the original page. For example, “A/B test your original home page against a version that has a larger CTA button.” Calculate the test duration with respect to the number of your monthly visitors, current conversion rate, and the expected change in the conversion rate.

Analyze Test Data and Draw Conclusions: Analyze the A/B test results, and see which variation delivered the highest conversions. If there is a clear winner among the variations, go ahead with its implementation. If the test remains inconclusive, go back to step number three and rework your hypothesis.

Report results to all concerned: Let others in Marketing, IT, and UI/UX know of the test results and the insights generated.

A/B Testing – Tools

There are various tools that can be used to generate hypothesis and to run the variations, these include:

  • Visual Website optimizer (VWO)
  • Google Content Experiments
  • Optimizely

All these tools are capable to run A/B Tests and to find the winner, but to perform post analysis these tools should be integrated with Google Analytics.

A/B Testing – Google Analytics

Google Analytics has two options for analyzing the data −

  • Universal Analytics
  • Classic Google Analytics

New Universal Analytics feature allow you to use 20 concurrent A/B Tests sending data to Google Analytics, however the Classic version allows only up to five.

Integrating Optimizely with Google Universal Analytics

To integrate Optimizely in to Universal Google Analytics, first select the ON button on its side panel. Then you must have an available Custom to populate with Optimizely experiment data. Then the tracking code must be placed at the bottom of the  head section of your pages. Google Analytics integration will not function properly unless the Optimizely snippet is above this Analytics snippet.

Configuration Steps

Optimizely uses Universal Google Analytics’ “Custom Dimensions” to tag your visitors with the experiments and variations to which they’ve been added. Configuring Optimizely to begin sending this information to Universal Analytics requires four steps −

Step 1

Add the following JavaScript code to your site wherever the Universal Analytics code exists after the ga(‘create’…) function fires and before the Universal Analytics ga(‘send’,’pageview’) function fires and the tracking call is made (see details in the next section) −

// Optimizely Universal Analytics Integration
window.optimizely = window.optimizely || [];
window.optimizely.push(“activateUniversalAnalytics”);

Step 2

In the Optimizely Editor, go to Options → Integrations then click on the Universal Analytics checkbox to enable the integration.

Step 3

Select the custom dimension you would like Optimizely to use. You have to ensure that the Custom Dimension should not be in use already by any other part of your site, or by another currently-running Optimizely experiment.

Step 4

Select a Custom Tracker if you are using a custom event tracker other than the default. This will change Optimizely’s integration call to use the custom tracker rather than the default.

Creating a Custom Report using Google Analytics

First step is to log into your Universal Analytics account and click the Customization tab at the top. You should see a Custom Reports list.

Next is to set up a Custom Report for each experiment that you have integrated Universal Analytics with.

  • Click on the New Custom Report → Enter the report title and add the metric groups you wish to view in the report.
  • To filter this report for only your Optimizely experiment, choose the Custom Dimension you set up previously as one of the Dimension Drilldowns.
  • Add this dimension in the Filters section and use a Regex match on the experiment ID for the experiment you want to filter.
  • Click on Save.

Want to learn Digital Marketing?

World of Warcraft today

I got a “gift” from Blizzard, 7 days of free subscription to WoW. Not that I would have needed it, I still have several tokens I could exchange for game time. But it did what it was supposed to do, prompt me to update the client and play World of Warcraft for an hour or so. Unfortunately for Blizzard that didn’t get me hooked again. Instead I got rather bored with running errands, aka quests, and logged out again.

One major difficulty I have with World of Warcraft is that the buttons I have for each character have changed so often over the life of this game. Which means that even on my main character which I have played literally for thousands of hours I can’t remember the optimum sequence of button presses after a year and a half of not playing the game. That doesn’t appear to matter for quests, I can do those with just randomly mashing buttons, but it is a serious barrier to re-entry if I wanted to play again.

The next thing that hit me was getting billions of artifact points thrown at me for doing not much. It basically made all the effort I had previously put into artifact weapons seem pointless. On the other hand, I had stopped playing with only part 1 of the achievement necessary for flying done, and it turns out that part 2 still needs weeks of grinding to get to. No thanks!

In summary, World of Warcraft has changed the details frequently (which makes it hard to remember how to play well), while not changing the basic structure of the game enough (which makes it hard to find a renewed interest in playing). I still don’t think I will buy the next expansion, Battle for Azeroth.

Simplified : Client_Server – Socket Programming in Java

Client_Server – Socket Programming in Java

java socket programming



Client -server communication

At a basic level, network-based systems consist of a server , client , and a media for communication. A computer running a program that makes a request for services is called client  machine. A computer running a program that offers requested services from one or more clients is called  server machine.

Client-Server Architcture


What are Sockets?

In Client-Server architecture, you have two processes (running client-Server programs) that want to communicate with each other. For that, they have to establish a communication link between themselves. There is a network available,they just need to connect to this network , for this they use sockets.

Sockets in client server architecture

A socket is one endpoint of a two-way communication link between two programs running on the network.

An endpoint is a combination of an IP address and a port number. Every TCP connection can be uniquely identified by its two endpoints. That way you can have multiple connections between your host and the server.
The java.net package in the Java platform provides a class, Socket, that implements one side of a two-way connection between your Java program and another program on the network. 

How do I Open a Socket ?

If you are programming a client, then you would open a socket like this:
 Socket s;
s
= new Socket("Machine name", PortNumber);
Where Machine name is the machine you are trying to open a connection to, and Port-Number is the port (a number) on which the server you are trying to connect to is running. When selecting a port number, you should note that port numbers between 0 and 1,023 are reserved for privileged users (that is, super user or root). These port numbers are reserved for standard services, such as email, FTP, and HTTP. When selecting a port number for your server, select one that is greater than 1,023!


It is a good practice to handle exceptions. The above can be written as:

    Socket s;
try {
s
= new Socket("Machine name", PortNumber);
}
catch (IOException e) {
System.out.println(e);
}
If you are programming a server, then this is how you open a socket:

    ServerSocket ss;
try {
ss
= new ServerSocket(PortNumber);
}
catch (IOException e) {
System.out.println(e);
}
When implementing a server you also need to create a socket object from the ServerSocket in order to listen for and accept connections from clients.

Socket clientSocket = null;
try {
serviceSocket
= ss.accept();
}
catch (IOException e) {
System.out.println(e);
}

How do I create Buffered Reader?

Java BufferedReader class is used to read the text from a character-based input stream. It can be used to read data line by line by readLine() method. It makes the performance fast. 
    try {
BufferedReader br = new BufferedReader(new InputStreamReader(ss.getInputStream()));

}
catch (IOException e) {
System.out.println(e);
}

How do I create an output stream?

On the client side, you can create an output stream to send information to the server socket using the class  of java.io:
    PrintStream output;
try {
OutputStream ostream = sock.getOutputStream();
DataOutputStream dos = new DataOutputStream(ostream);

}
catch (IOException e) {
System.out.println(e);
}
The class DataOutputStream allows you to write Java primitive data types; many of its methods write a single Java primitive type to the output stream. The method writeBytes is a useful one.

How do I Close a Socket ?

On the client side:

    try {
output
.close();
input
.close();
s
.close();
}
catch (IOException e) {
System.out.println(e);
}

On the server side:

    try {
output
.close();
input
.close();
ss
.close();
}
catch (IOException e) {
System.out.println(e);
}
Let’s see java code for one-way communication using socket programming:

Client Side Code:
import java.net.Socket; 
import java.io.OutputStream;
import java.io.DataOutputStream;

public class SCPTL
{
public static void main(String args[]) throws Exception
{
Socket sock = new Socket("localhost", 5000);
String message1 = "Learn-Intern-Certify from SCPTL";

OutputStream ostream = sock.getOutputStream();
DataOutputStream dos = new DataOutputStream(ostream);
dos.writeBytes(message1);
dos.close();
ostream.close();
sock.close();
}
}
Server Side Code:
import java.net.ServerSocket;            
import java.net.Socket;
import java.io.*;

public class SERVER
{
public static void main(String args[]) throws Exception
{
ServerSocket sersock = new ServerSocket(5000);
System.out.println("server is ready"); // message to know the server is running

Socket sock = sersock.accept();

BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream()));

String message2 = br.readLine();
System.out.println(message2);
sock.close(); sersock.close();
}
}

Output:

Client:

Client side output
Server:

Server side output

Want to learn more about java?

Using Street View and Geocoding in your Android app

Today, Google Maps is easily one of the world’s most popular mapping services, allowing millions of users to plan their journeys, discover new places to visit, and get a taste of what it’s really like to walk around places they may have never even visited.

We’ve already looked at how you can use the Google Maps API to retrieve and display the user’s location, but this powerful API isn’t limited to sticking a pin in a map!

In this article, we’ll be looking at some of the additional features that are included in the Google Maps API. By the end of this article, you’ll know how to:

  • Give your users the freedom to switch between all the different Google Maps styles: Normal, Satellite, Terrain and Hybrid.
  • Convert the device’s longitude and latitude coordinates into a more user-friendly street address, and display this information as part of your UI.
  • Display 360-degree, interactive panoramas of locations across the globe, by adding Street View support to your app.

Creating a basic Google Maps app

Before we can implement any of these features, we need to create a project that displays a basic Google Maps fragment.

To get this setup out of the way as quickly as possible, I’ll be using Android Studio’s ‘Google Maps Activity’ template and generating a debug API key, which is required if your project is going to display any Google Maps content. Just be aware that debug API keys aren’t particularly secure, so before publishing an application you must always generate a new API key based on your project’s release certificate.

  • Create a new project using the ‘Google Maps Activity’ template.
  • Open your project’s res/values/google_maps_api.xml file. This file contains a URL with all the information the Google API Console needs to generate an API key. Find this URL, and copy/paste it into your web browser.
  • Make sure ‘Create a project’ is selected in the Console’s dropdown menu, and then click ‘Continue.’
  • Click ‘Create API key.’
  • The API Console will prompt you to restrict the API key. A restricted API will only work on a platform that supports that type of application, which tends to make your key more secure. Unless you have a specific reason not to, you should select ‘Restrict key.’
  • Under ‘Key restriction,’ make sure ‘Android apps’ is selected, and then click ‘Save.’
  • Copy your API key, and then switch back to Android Studio.
  • Open your project’s google_maps_api.xml file and paste your API key into the YOUR_KEY section:

<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">YOUR_KEY</string>
  • Open your module-level build.gradle file and add the Google Maps dependencies:

dependencies {
compile 'com.google.android.gms:play-services-maps:11.6.2'
compile 'com.google.android.gms:play-services-location:11.6.2'

If your project refuses to compile, then make sure your development environment is up to date, by opening the Android SDK Manager and installing any available updates – in particular make sure you have the latest versions of Google Play Services and Google Repository.

This is the bare minimum required to display Google Maps content, so at this point you may want to take this project for a spin by installing it on your physical smartphone or tablet, or an AVD (Android Virtual Device). If you’re testing this project on an AVD, then you’ll need to use a system image that includes the Google APIs.

Currently this project displays a map with a marker permanently set to Sydney, Australia. This isn’t exactly going to wow your users, so let’s look at a few different ways of making this project more interesting.

Displaying the user’s address with reverse geocoding

When you include Google Maps content in your application, you typically display the user’s current location via a marker, but there’s plenty of scenarios where it’s more useful to display location as a street address. For example, if you’re booking a taxi the old-fashioned way (i.e by calling the taxi company) or arranging to meet a friend, then knowing the street you’re currently on is going to be pretty useful!

While your users could work this out for themselves by zooming in on their location marker and looking at the surrounding labels, you can provide a much better experience by presenting this information to them. This process of converting a set of longitude and latitude values into a street address, is known as reverse geocoding.

In this section, we’re going to add a button to our application that, when tapped, retrieves the device’s longitude and latitude, reverse geocodes these coordinates into an approximate street address, and then presents this information to the user.

Update your layout

Let’s start with the easy stuff, and update our user interface. When you create a project using the Google Maps Activity template, the activity_maps.xml file contains a SupportMapFragment that fills the entire screen.

I’m going to expand on this layout to include a ‘Get My Location’ button that, when tapped, updates a TextView with the reverse geocoded data.


<RelativeLayout xmlns:android="http://ift.tt/nIICcg"
xmlns:tools="http://ift.tt/LrGmb4"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.jessicathornsby.google_maps.MapsActivity" >

<fragment
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:scrollbars="vertical"
class="com.google.android.gms.maps.SupportMapFragment"/>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="horizontal"
android:padding="10dp"
android:layout_alignParentBottom="true"
android:background="#ffffff" >

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/get_location"
android:gravity="left" />

<TextView
android:id="@+id/textview"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="right"
android:layout_weight="1"/>

</LinearLayout>
</RelativeLayout>

Create your strings

Next, define the string resources that we’ll be using throughout this project:


//Create the button label//

<string name="get_location">Get my location</string>
<string name="address_text">"Address: %1$s"</string>

The second string resource is a placeholder that contains the following:

  • %1. A placeholder for a value. This value will either be a formatted address or a message that an error has occurred.
  • $s. The format of the placeholder value, i.e a String.

You convert latitude and longitude values into a physical address using the getFromLocation() method, which returns a list of Address objects.

The level of detail returned by getFromLocation() will vary depending on the location. Sometimes reverse geocoding may return a full address, right down to the house number; sometimes it’ll return the name of the nearest building – and occasionally it may return no information at all.

While the latter is unlikely, your application shouldn’t crash if it does encounter this scenario. Here, I’m creating a String just in case this app can’t match the coordinates to any known address:


<string name="no_address">Cannot retrieve address at this time</string>

On devices running Android 6.0 (API level 23) and higher, applications need to request permissions at runtime, and the user can then accept or deny each request, on a permission-by-permission basis.

If the user denies a permission request, then you need to communicate the impact this will have on your application. In this project, I’m going to display the following text as part of a toast:


<string name="location_permission_denied">Location permission denied. Current location unavailable.</string>

When working on your own Android projects, you may also want to disable or remove parts of your application that rely on the denied permission, for example removing items from menus, or “greying out” certain UI controls.

Add the Internet permission

Reverse geocoding requires an Internet connection, so open your project’s Manifest and add the Internet permission:


<uses-permission android:name="android.permission.INTERNET" />

Create an AyncTask

Since reverse geocoding uses the network, it has the potential to block Android’s main thread. To avoid Application Not Responding (ANR) errors and application crashes, you must perform the reverse geocoding operation off the main thread. There’s various ways of creating background threads, but I’m going to use an AsyncTask.

Create a new Java class (I’m naming mine ReverseGeo) and implement the AsyncTask:


import android.location.Address;
import java.util.ArrayList;
import android.os.AsyncTask;
import android.content.Context;
import android.location.Location;
import android.location.Geocoder;
import java.util.List;
import java.util.Locale;
import java.io.IOException;
import android.text.TextUtils;

/**
* Created by jessicathornsby on 06/12/2017.
*/

class ReverseGeo extends AsyncTask<Location, Void, String> {

private Context mContext;

//Add a parameter for the onTaskComplete interface that we’ll be creating shortly//

private OnTaskComplete mListener;

ReverseGeo(Context applicationContext, OnTaskComplete listener) {
mListener = listener;
mContext = applicationContext;

}

//Publish the results of our AsyncTask; in this instance that’s the returned address//

@Override

//Override the onPostExecute() method//

protected void onPostExecute(String address) {

//Once the AsyncTask has finished,
//call onTaskComplete and update your UI with the returned address//

mListener.onTaskComplete(address);
super.onPostExecute(address);
}

//Implement AsyncTask’s doInBackground() method,
//where we’ll convert the Location object into an address//

@Override
protected String doInBackground(Location... params) {

//Create a Geocoder object, which is a class that can perform geocoding operations//

Geocoder mGeocoder = new Geocoder(mContext,

//Localize the address//

Locale.getDefault());

//Obtain a Location object//

Location location = params[0];

//Create an empty List of Address objects, which will eventually contain the returned address//

List<Address> addresses = null;

//Create a String to hold the formatted address//

String printAddress = "";

//Obtain the list of addresses for the current location, using getFromLocation//

try {
addresses = mGeocoder.getFromLocation(
location.getLatitude(),
location.getLongitude(),

//Specify the maximum number of addresses that the TextView should display//

1);

//Catch any exceptions, for example if the network is unavailable//

} catch (IOException ioException) {
printAddress = mContext.getString(R.string.no_address);

}

//If the geocoder can't match the coordinates to an address, then return an empty list//

if (addresses.size() == 0) {
if (printAddress.isEmpty()) {

//If the address list is empty, then display the no_address string//

printAddress = mContext.getString(R.string.no_address);

}
} else {

//If the list isn’t empty, then create an ArrayList of strings//

Address address = addresses.get(0);
ArrayList<String> addressList = new ArrayList<>();

//Fetch the address lines, using getMaxAddressLineIndex,
//and then and combine them into a String//

for (int i = 0; i <= address.getMaxAddressLineIndex(); i++) {
addressList.add(address.getAddressLine(i));
}

printAddress = TextUtils.join(
",",
addressList);

}

//Return the printAddress object//

return printAddress;
}

//Create the OnTaskComplete interface, which takes a String as an argument//

interface OnTaskComplete {
void onTaskComplete(String result);
}
}

Implement ReverseGeo in MapsActivity

Next, we need to implement ReverseGeo in our project’s automatically-generated MapsActivity class, and then override the onTaskComplete() method. I’m also implementing the onClickListener so our application can respond to the user tapping the ‘Get My Location’ button.


import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.Manifest;
import android.content.pm.PackageManager;
import android.widget.TextView;
import android.widget.Toast;
import android.view.View;

public class MapsActivity extends AppCompatActivity implements
ReverseGeo.OnTaskComplete {

private static final int MY_PERMISSIONS_REQUEST_LOCATION = 1;
private Button button;
private TextView textview;
private boolean addressRequest;

//Create a member variable of the FusedLocationProviderClient type//

private FusedLocationProviderClient mFusedLocationClient;
private LocationCallback mLocationCallback;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_maps);

button = findViewById(R.id.button);
textview = findViewById(R.id.textview);

//Initialize mFusedLocationClient//

mFusedLocationClient = LocationServices.getFusedLocationProviderClient(
this);

//Create the onClickListener//

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

//Call getAddress, in response to onClick events//

if (!addressRequest) {
getAddress();

}
}
});

//Create a LocationCallback object//

mLocationCallback = new LocationCallback() {

@Override

//Override the onLocationResult() method,
//which is where this app receives its location updates//

public void onLocationResult(LocationResult locationResult) {
if (addressRequest) {

//Execute ReverseGeo in response to addressRequest//

new ReverseGeo(MapsActivity.this, MapsActivity.this)

//Obtain the device's last known location from the FusedLocationProviderClient//

.execute(locationResult.getLastLocation());
}
}
};
}

//Implement getAddress//

private void getAddress() {
if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]
{Manifest.permission.ACCESS_FINE_LOCATION},
MY_PERMISSIONS_REQUEST_LOCATION);
} else {
addressRequest = true;

//Request location updates//

mFusedLocationClient.requestLocationUpdates
(getLocationRequest(),
mLocationCallback,
null);

//If the geocoder retrieves an address, then display this address in the TextView//

textview.setText(getString(R.string.address_text));

}
}

//Specify the requirements for your application's location requests//

private LocationRequest getLocationRequest() {
LocationRequest locationRequest = new LocationRequest();

//Specify how often the app should receive location updates, in milliseconds//

locationRequest.setInterval(10000);
return locationRequest;
}

@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {

switch (requestCode) {
case MY_PERMISSIONS_REQUEST_LOCATION:
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {

//If the permission request has been granted, then call getAddress//

getAddress();
} else {
Toast.makeText(this,
R.string.location_permission_denied,
Toast.LENGTH_SHORT).show();
}
break;
}
}

@Override
public void onTaskComplete(String result) {
if (addressRequest) {

//Update the TextView with the reverse geocoded address//

textview.setText(getString(R.string.address_text,
result));
}
}
}

Testing your reverse geocoding application

Let’s put this application to the test:

  • Install the updated project on your Android device.
  • Make sure you’re connected to the Internet.
  • Tap the ‘Get My Location’ button.
  • Grant the ACCESS_FINE_LOCATION request; the TextView should update to display an estimated street address.

Since we’re requesting the ACCESS_FINE_LOCATION permission at runtime, we need to test how our application handles rejection:

  • Launch your device’s ‘Settings’ application.
  • Tap ‘Apps.’
  • Select the maps application from the list.
  • Select ‘Permissions.’
  • Push the ‘Location’ slider into the ‘Off’ position.
  • Launch your maps application.
  • Tap the ‘Get My Location’ button.
  • When prompted, deny the ACCESS_FINE_LOCATION request; the application should respond by displaying a toast.

You should also test how your application functions when it has access to your location, but can’t match the coordinates to any known address. If you’re using a physical Android device, then you can test this scenario using a third party app:

  • Download an application that can spoof your location, such as the free ‘Fake GPS’ app.
  • Use this application to trick your device into believing you’re somewhere that doesn’t have a street address – the middle of the ocean is usually a safe bet!
  • Switch back to your maps application, and tap ‘Get My Location.’ The TextView should display the no_address string.

If you’re testing this project on an AVD, then you can change the device’s coordinates using the strip of buttons that appear alongside the emulator:

  • Click the three-dotted menu icon (where the cursor is positioned in the following screenshot).

  • Select ‘Location’ from the left-hand menu.
  • Enter a new set of longitude/longitude values, and click ‘Send.’
  • Press the application’s ‘Get My Location’ button; the TextView should update to display the no_address string.

Adding different map types

Any Google Maps content you include in your app will use the “normal” map style by default – but “normal” isn’t the only option!

The Google Maps API supports a few different map styles:

  • MAP_TYPE_SATELLITE. A Google Earth satellite photograph, without road or feature labels.
  • MAP_TYPE_HYBRID. A satellite photograph with road and feature labels.
  • MAP_TYPE_TERRAIN. A topographic map featuring contour lines, labels and perspective shading, with some labels.

To display anything other than a “normal” map, you’ll need to use the setMapType method:


mMap = googleMap;
mMap.setMapType(GoogleMap.MAP_TYPE_TERRAIN);

Alternatively, why not give your users the freedom to switch between map styles?

In this section, we’re going to add a dropdown menu that allows your users to move between the normal, hybrid, terrain and satellite map styles, with ease.

Start by creating a menu resource:

  • Control-click your project’s ‘res’ directory and select ‘New > Android Resource File.’
  • Give this resource a name; I’m using ‘maps_menu.’
  • Open the ‘Resource type’ dropdown and select ‘Menu.’
  • Click ‘OK.’
  • Copy/paste the following code into this file:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://ift.tt/nIICcg"
xmlns:app="http://ift.tt/GEGVYd">
<item android:id="@+id/normal"
android:title="@string/normal"
app:showAsAction="never"/>
<item android:id="@+id/hybrid"
android:title="@string/hybrid"
app:showAsAction="never"/>
<item android:id="@+id/satellite"
android:title="@string/satellite"
app:showAsAction="never"/>
<item android:id="@+id/terrain"
android:title="@string/terrain"
app:showAsAction="never"/>
</menu>

Open your project’s strings.xml file and define all the menu labels:


<string name="normal">Normal map</string>
<string name="terrain">Terrain map</string>
<string name="hybrid">Hybrid map</string>
<string name="satellite">Satellite map</string>

Next, you’ll need to implement the menu in your MapsActivity. To make this process clearer, I’ve removed all geocoding-specific code from this Activity.


import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.maps.GoogleMap;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;

public class MapsActivity extends AppCompatActivity implements
OnMapReadyCallback, GoogleApiClient.ConnectionCallbacks {

private GoogleMap mMap;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_maps);

//Obtain the SupportMapFragment//

SupportMapFragment mapFragment = SupportMapFragment.newInstance();
getSupportFragmentManager().beginTransaction()
.add(R.id.map, mapFragment).commit();
mapFragment.getMapAsync(this);
}

//Override the onCreateOptionsMenu() method//

@Override
public boolean onCreateOptionsMenu(Menu menu) {

//Inflate the maps_menu resource//

MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.maps_menu, menu);
return true;
}

//Override the onOptionsItemSelected() method//

@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.normal:

//Use setMapType to change the map style based on the user’s selection//

mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
return true;
case R.id.hybrid:
mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
return true;
case R.id.terrain:
mMap.setMapType(GoogleMap.MAP_TYPE_TERRAIN);
return true;
case R.id.satellite:
mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
return true;
default:
return super.onOptionsItemSelected(item);
}
}

@Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;

if (ContextCompat.checkSelfPermission(this,
android.Manifest.permission.ACCESS_COARSE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
mMap.setMyLocationEnabled(true);
}
}

public void onConnected(Bundle bundle) {
//To do//
}

@Override
public void onConnectionSuspended(int i) {

}
}

Install the updated application on your physical Android device or AVD, open the menu, and test all the different map styles.

Adding Street View to your project

Even examining the same location across multiple map styles can’t quite compare to the experience of exploring that location from a first-person perspective – which is where Street View comes in.

In this final section, I’ll show you how to provide a tangible sense of what a location is really like, by integrating Street View into our application.

Let’s start by updating our layout:


<RelativeLayout xmlns:android="http://ift.tt/nIICcg"
xmlns:tools="http://ift.tt/LrGmb4"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.jessicathornsby.google_maps.MapsActivity" >

<fragment
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:scrollbars="vertical"
class="com.google.android.gms.maps.SupportMapFragment"/>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="horizontal"
android:padding="10dp"
android:layout_alignParentBottom="true"
android:background="#ffffff" >

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Street View"
android:onClick="launchStreetView"
android:gravity="left" />

<TextView
android:id="@+id/textview"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="right"
android:layout_weight="1"/>

</LinearLayout>
</RelativeLayout>

Next, I’m going to create a StreetViewActivity, where I’ll implement the Street View service. When you include a Street View panorama in your application, all the standard Street View actions are included by default, which is why the following code doesn’t contain any manual implementations of panning and zooming gestures, or navigating to adjacent panoramas, as you already get all this functionality for free!

Since I’m displaying the Street View panorama inside an Android View, I’m using StreetViewPanoramaView, which is a subclass of the View class. To display a panorama inside a Fragment, you’d use StreetViewPanoramaFragment instead.


import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.ViewGroup.LayoutParams;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.StreetViewPanoramaOptions;
import com.google.android.gms.maps.StreetViewPanoramaView;

public class StreetViewActivity extends AppCompatActivity {

//Define the LatLng value we’ll be using for the paranorma’s initial camera position//

private static final LatLng LONDON = new LatLng(51.503324, -0.119543);
private StreetViewPanoramaView mStreetViewPanoramaView;
private static final String STREETVIEW_BUNDLE_KEY = "StreetViewBundleKey";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

//Configure the panorama by passing in a StreetViewPanoramaOptions object//

StreetViewPanoramaOptions options = new StreetViewPanoramaOptions();
if (savedInstanceState == null) {

//Set the panorama’s location//

options.position(LONDON);
}

mStreetViewPanoramaView = new StreetViewPanoramaView(this, options);
addContentView(mStreetViewPanoramaView,
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));

Bundle mStreetViewBundle = null;
if (savedInstanceState != null) {
mStreetViewBundle = savedInstanceState.getBundle(STREETVIEW_BUNDLE_KEY);
}
mStreetViewPanoramaView.onCreate(mStreetViewBundle);
}

}

Don’t forget to add the StreetViewActivity to your Manifest:


<activity
android:name=".MapsActivity"
android:label="@string/title_activity_maps">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".StreetViewActivity"></activity>
</application>

Finally, we need to implement launchStreetView in our MapsActivity, so that android:onClick=”launchStreetView” triggers the StreetViewActivity class:


import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.maps.GoogleMap;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import android.content.Intent;
import android.view.View;

public class MapsActivity extends AppCompatActivity implements
OnMapReadyCallback, GoogleApiClient.ConnectionCallbacks {

private GoogleMap mMap;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_maps);

SupportMapFragment mapFragment = SupportMapFragment.newInstance();
getSupportFragmentManager().beginTransaction()
.add(R.id.map, mapFragment).commit();
mapFragment.getMapAsync(this);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {

MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.maps_menu, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.normal:
mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
return true;
case R.id.hybrid:
mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
return true;
case R.id.terrain:
mMap.setMapType(GoogleMap.MAP_TYPE_TERRAIN);
return true;
case R.id.satellite:
mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
return true;
default:
return super.onOptionsItemSelected(item);
}
}

@Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;

if (ContextCompat.checkSelfPermission(this,
android.Manifest.permission.ACCESS_COARSE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
mMap.setMyLocationEnabled(true);
}
}

public void onConnected(Bundle bundle) {
//To do//
}

@Override
public void onConnectionSuspended(int i) {

}

public void launchStreetView(View view) {
Intent intent = new Intent(MapsActivity.this, StreetViewActivity.class);
startActivity(intent);
}
}

Install this project on your Android device, and give the ‘Street View’ button a tap. Your application should respond by launching a new Activity displaying a 360 degree panorama of the London Eye.

Wrapping Up

In this article, we explored a few ways of enhancing your app’s Google Maps content, by adding support for Street View, multiple map styles, and reverse geocoding – but these are still just a few of the features that the Google Maps API has to offer.

What Google Maps features have you used in your own projects? Let us know in the comments below!

Masters and Servants

If you watch a film or TV series like Downton Abbey, you can learn about how the class structure of society worked a century ago. Many of those concepts of hereditary masters and servants are now completely outdated. But while class borders have become a lot more flexible today, classes still do exist. In today’s economy there are still masters, who are the customers paying for a service, and servants, who then get money for providing those services. Of course the guy who is a servant all day, for example an Uber driver, can come home and become the master by ordering a pizza delivered. But the rich are more likely to receive services, and the poor are more likely to provide those services; we aren’t really much more equal than back in the days of Downton Abbey.

This class divide has also reached games. If you can afford to buy $60 games or spend money in Free2Play games, you get services provided to you. If you play those Free2Play games for free, you end up being the content for other players. It is as if you were paid for providing a service as opponent for another player, only that you don’t get paid in cash but in access to the game.

I don’t like being a servant to a game company. Game companies, like most other companies, treat their customers like royalty, and their employees like garbage. So I don’t want to work for the game company, be the content, provide a service as a cheap replacement of an artificial intelligence. In particular I hate games where even if you pay money, you never can escape from that role as servant, because you always are content for other players.

I just can’t play the new Magic Arena, because it only has a PvP mode. Not only don’t I like serving as content for other players. I also don’t like the content that other players provide to me: Playing against random humans means total unpredictability, you can end up against a complete pushover or the guy who spent hundreds of dollars and hours on the game and is a complete pro. On the one side I feel bad if I play against a human and have to quit early because real life intervenes (which makes the game rather unsuitable for mobile platforms), but on the other side I hate it when my opponent quits early. I much prefer playing against an AI, where there is no social contract, and my opponent plays in a more predictable manner. Previous electronic versions of Magic the Gathering have proven that an AI can be created that plays the game reasonably well. So making a version of Magic without AI to me feels like simple exploitation of players as content, and I’m not willing to be exploited like that.

Former CIA Official Suggests Trump Campaign Team May Have ‘Welcomed’ Russian Election Interference

No one bothered to report an obviously relevant Trump Tower meeting.

Former CIA and U.S. Defense Department official Jeremy Bash told MSNBC host Nicolle Wallace that President Donald Trump’s actions after being cautioned by the FBI about Russia raise serious red flags.

Bash specifically noted that the Trump team’s disinterest in informing the FBI about a Trump Tower meeting with Russians following the bureau’s alert is suspicious.

“I think the fact that the campaign, the candidate was warned and that the candidate and the campaign did not then go back to the FBI after the Trump Tower meeting is a huge red flag that, not only were they unconcerned with this Russian overture,  but they welcomed it and in some ways want to conceal it,” he said. 

“To have warned the FBI would have been to expose their own conduct, the campaign’s own conduct, and that is something that Bob Mueller will be very, very interested in.”

Watch the segment below.

 

Related Stories

  • If Trump Fires Mueller, Is a Watergate Rerun Coming?
  • Robert Mueller Has the Trump Team Panicked, No Matter What the President Says
  • Intelligence Analyst Malcolm Nance Compares Fox News Rhetoric to ‘Psychological Warfare’

Corporate Media Allowed Net Neutrality to Die in Silence

The flagship morning news shows on broadcast and cable news covered net neutrality for less than four minutes combined.

The Federal Communications Commission (FCC) today voted to repeal net neutrality rules, which will allow internet service providers to block or slow down service and access to websites, or charge fees for faster service.

If you weren’t aware of this potentially monumental change that will significantly impact your internet access, that’s because the major news networks mostly haven’t been doing their jobs.

Hours before today’s FCC repeal vote, the flagship morning news shows on the six major broadcast and cable news networks devoted an embarrassingly small amount of time to covering net neutrality. Relative silence from the major news networks on net neutrality is unfortunately nothing new, as Media Matters has previously documented.

This morning, most of the morning news programs either completely ignored the impending move or cursorily mentioned it for a few seconds at a time. Among the cable news networks, Fox News’ Fox & Friends spent just 52 seconds on net neutrality. MSNBC’s Morning Joe and CNN’s New Day did not cover the story at all. (It was covered for about half a minute on MSNBC’s early morning show, First Look, and roughly one minute on CNN’s early morning show, Early Start. After the conclusion of Morning Joe, MSNBC has been covering net neutrality in detail on MSNBC Live.)

The broadcast networks also spent scant time on the issue: ABC’s Good Morning America devoted just 14 seconds to net neutrality and NBC’s Today didn’t mention it at all. CBS This Morning led the pack with two and a half minutes of coverage this morning, and was the only one of the flagship morning programs to run a full segment on the topic.

Since November 28, cable news networks have mostly given net neutrality minimal coverage: approximately five minutes each on CNN and Fox News and almost 17 minutes on MSNBC, which has consistently devoted the most coverage to net neutrality in recent weeks. Broadcast networks have been mostly crickets, too. Since November 28, NBC has devoted about eight minutes to covering net neutrality while CBS has spent close to five minutes, and ABC has devoted just 14 seconds to the topic — the brief mention on Good Morning America this morning.

Since November 20, when news first broke about the planned repeal, the six networks have devoted a combined nearly one hour and 53 minutes to the story; although, MSNBC alone has accounted for more than one hour and three minutes of that total coverage time. The vast majority of the coverage occurred before November 28.

Under Trump, the Republican-led FCC has already done significant damage to the local news landscape and paved the way for major corporate consolidation in media — but repealing net neutrality seems to be its most unpopular action yet. A new survey found that 83 percent of Americans don’t approve of the FCC’s repeal proposal — including 3 out of 4 Republicans. Even the FCC’s own chief technology officer warned against the move. And 18 attorneys general had called for a delay in the vote due to widespread fraudulent comments during the public comment period.

That’s probably why chairman Ajit Pai’s media tour in the days before the net neutrality repeal has largely targeted conservative and far-right media that may provide a (marginally) more friendly audience. Since November 21, Pai has given four cable news interviews: two with Fox & Friends, one with Fox News’ Tucker Carlson, and a fourth with conservative talk radio host Hugh Hewitt at MSNBC. He did not give an interview to any of the three major broadcast networks. (Pai also seemingly promoted the repeal by appearing in an embarrassing video at The Daily Caller along with renowned plagiarist Benny Johnson and a Pizzagate conspiracy theorist.)

It’s also why major news networks’ relative silence on such a deeply unpopular and hugely consequential action like the FCC’s repeal vote is a net benefit to the commission and to major corporations — and keeps an informed public from fighting back.

Methodology

Media Matters searched the Snapstream database of television video transcripts for any mentions of “net neutrality,” “Federal Communications Commission,” or “FCC” from November 20 through December 14, 2017 on ABC’s Good Morning AmericaWorld News Tonight with David Muir, and This Week with George Stephanopoulos; CBS’s This Morning, Evening News, and Face the Nation with John Dickerson; NBC’s Today, Nightly News with Lester Holt, and Meet the Press with Chuck Todd; and all-day programming (through 9am on December 14) on the three major cable news networks — CNN, Fox News Channel, and MSNBC. We also searched the Nexis transcript database and the iQ Media transcript database for the same terms. Since November 23 was Thanksgiving, some networks altered their regularly scheduled programming on that day.

We included any segment about FCC chair Ajit Pai’s proposal or the FCC vote scheduled for December 14 following Politico’s November 20 report on the proposal. We timed all such segments from start to finish, and excluded any breaks to other news or to commercials. We also included portions of multi-topic segments when two or more speakers discussed the FCC chair’s proposal or the scheduled vote on the proposal with one another. In those instances, we only timed the relevant discussion and not the entire segment. We excluded passing mentions of the proposal or its vote, and we excluded teasers of upcoming segments about the proposal or its vote.

Note: This post has been updated to reflect that the FCC officially moved to repeal net neutrality rules in a 3-2 vote on December 14. 

Rob Savillo contributed research to this report. Charts by Sarah Wasko.

 

Related Stories

  • 12 Most Insane Rules From the Biggest Neo-Nazi Website on the Internet
  • Late-Night Hosts Agree: After Roy Moore’s Defeat, ‘The Real Loser Here Is Donald Trump’
  • Blame the Media? The NY Times Coverage of Clinton v. Trump Was Lacking