com.first.akashshrivastava.showernow.ShowerActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.first.akashshrivastava.showernow.ShowerActivity.java

Source

package com.first.akashshrivastava.showernow;

import android.app.Activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.facebook.CallbackManager;
import com.facebook.FacebookSdk;
import com.facebook.appevents.AppEventsLogger;
import com.facebook.share.model.ShareLinkContent;
import com.facebook.share.widget.ShareDialog;
import com.github.clans.fab.FloatingActionButton;
import com.github.clans.fab.FloatingActionMenu;
import com.google.android.gms.ads.AdRequest;
import com.google.android.gms.ads.AdView;
import com.google.android.gms.ads.MobileAds;
import com.google.android.gms.fitness.data.DataPoint;
import com.google.android.gms.fitness.data.Field;
import com.google.android.gms.fitness.data.Value;
import com.google.android.gms.fitness.request.OnDataPointListener;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;

import java.util.Calendar;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import cn.fanrunqi.waveprogress.WaveProgressView;

import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;

/**
 * Created by akashshrivastava on 31/07/16.
 */
public class ShowerActivity extends Activity implements OnDataPointListener, SensorEventListener {

    private static final String TAG = "ShowerActivity";

    //Facebook initialization
    ShareLinkContent linkContent;
    ShareDialog shareDialog;

    private Sensor sensorType;
    private SensorManager mSensorManager;//steps manager
    private float steps;
    private boolean gotSteps = false;

    WaveProgressView waveProgressbar;
    private TextView guyText;
    private TextView topText;

    private double timeDiff;
    private float stepsDiff;

    private PendingIntent pendingIntent;//for alarm manager

    int age;
    String fluffiness = null;
    String gender;

    private long oldTime;
    private float oldSteps;

    private boolean newUser = false;

    private DatabaseReference mDatabaseReference;
    private FirebaseAuth mFirebaseAuth;

    int flag = 1;

    int extraAge; //shared preferanses
    String extraFluffiness;
    String extraGender;
    long extraOldTime;
    float extraSteps;

    boolean waveUpdated = false;

    CallbackManager callbackManager;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_shower_activity);

        /*Launching BootReceiver to test
        Intent playIntent = new Intent(getApplicationContext(), BootReceiver.class);
        startActivity(playIntent);
        */

        //Mobile ads initialization....The long number is the AdID, can be found on AdMob -  ca-app-pub-8782530512283806/2988799979
        MobileAds.initialize(getApplicationContext(), "ca-app-pub-8782530512283806/2988799979");
        AdView mAdView = (AdView) findViewById(R.id.adView);
        AdRequest adRequest = new AdRequest.Builder().build();
        mAdView.loadAd(adRequest);

        //Facebook SDK initialization...
        FacebookSdk.sdkInitialize(getApplicationContext());
        AppEventsLogger.activateApp(this);

        shareDialog = new ShareDialog(this);

        mDatabaseReference = FirebaseDatabase.getInstance().getReference();
        mFirebaseAuth = FirebaseAuth.getInstance();

        final ImageView genderImage = (ImageView) findViewById(R.id.imageGender);

        guyText = (TextView) findViewById(R.id.guyText);

        topText = (TextView) findViewById(R.id.textView2);

        SharedPreferences prefs = getSharedPreferences("myPrefs", Context.MODE_PRIVATE);

        extraAge = prefs.getInt("age", 0);
        extraFluffiness = prefs.getString("fluffiness", "");
        extraGender = prefs.getString("gender", "");
        extraOldTime = prefs.getLong("time", 0);
        extraSteps = prefs.getFloat("stepsBoot", 0);

        switch (extraGender) {
        case "male":
            genderImage.setImageResource(R.drawable.male_white_outline);
            break;
        case "female":
            genderImage.setImageResource(R.drawable.female_white_outline);
            break;
        case "other":
            genderImage.setImageResource(R.drawable.other_white_outline);
            break;
        }

        genderImage.setOnTouchListener(new View.OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) { //set alarm
                //Swith ccase
                switch (event.getAction()) {

                case MotionEvent.ACTION_UP:
                    // PRESSED ..PRESSED

                    if (genderImage.getDrawable().getConstantState() == getResources()
                            .getDrawable(R.drawable.female_white_outline_pressed).getConstantState()) {
                        genderImage.setImageResource(R.drawable.female_white_outline);
                    } else if (genderImage.getDrawable().getConstantState() == getResources()
                            .getDrawable(R.drawable.male_white_outline_pressed).getConstantState()) {
                        genderImage.setImageResource(R.drawable.male_white_outline);
                    } else if (genderImage.getDrawable().getConstantState() == getResources()
                            .getDrawable(R.drawable.other_white_outline_pressed).getConstantState()) {
                        genderImage.setImageResource(R.drawable.other_white_outline);
                    }

                    //Resets the wave after shower..this is not getting called for some reason....
                    waveProgressbar.setCurrent(0, "");
                    guyText.setText("0 %");
                    waveProgressbar.setVisibility(View.INVISIBLE);

                    topText.setText("You have showered! \n When the wave hits 100% its time for your next shower ");

                    if (fluffiness != null && gotSteps) {
                        Calendar cal = Calendar.getInstance();
                        Intent activate = new Intent(ShowerActivity.this, AlarmReceiver.class);
                        activate.putExtra("age", age);
                        activate.putExtra("fluffiness", fluffiness);
                        activate.putExtra("gender", gender);
                        activate.putExtra("steps", steps);
                        activate.putExtra("time", System.currentTimeMillis());

                        AlarmManager alarms;
                        PendingIntent alarmIntent = PendingIntent.getBroadcast(ShowerActivity.this, 0, activate,
                                FLAG_CANCEL_CURRENT);
                        alarms = (AlarmManager) getSystemService(ALARM_SERVICE);
                        alarms.cancel(alarmIntent);

                        alarms.setRepeating(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis() + 5000, 1000 * 60,
                                alarmIntent);//sets the alarm

                        mDatabaseReference.child("User").child(mFirebaseAuth.getCurrentUser().getUid())
                                .child("Steps").setValue(steps);//sets old steps
                        oldSteps = steps;
                        mDatabaseReference.child("User").child(mFirebaseAuth.getCurrentUser().getUid())
                                .child("Time").setValue(System.currentTimeMillis());
                        oldTime = System.currentTimeMillis();
                        newUser = false;

                        SharedPreferences sharedPref = getSharedPreferences("myPrefs", Context.MODE_PRIVATE);
                        SharedPreferences.Editor editor = sharedPref.edit();
                        editor.putInt("age", age);
                        editor.putString("fluffiness", fluffiness);
                        editor.putString("gender", gender);
                        editor.putFloat("steps", steps);
                        editor.putLong("time", System.currentTimeMillis());
                        editor.putFloat("stepsBoot", 0);
                        editor.putBoolean("bootStart", true);
                        editor.apply();

                    } else if (!gotSteps) {
                        Toast.makeText(getApplicationContext(), "Waiting for steps", Toast.LENGTH_SHORT).show();
                    }
                    return true; // if you want to handle the touch event

                case MotionEvent.ACTION_DOWN:
                    // RELEASED..RELEASED..

                    if (genderImage.getDrawable().getConstantState() == getResources()
                            .getDrawable(R.drawable.female_white_outline).getConstantState()) {
                        genderImage.setImageResource(R.drawable.female_white_outline_pressed);

                    } else if (genderImage.getDrawable().getConstantState() == getResources()
                            .getDrawable(R.drawable.male_white_outline).getConstantState()) {
                        genderImage.setImageResource(R.drawable.male_white_outline_pressed);

                    } else if (genderImage.getDrawable().getConstantState() == getResources()
                            .getDrawable(R.drawable.other_white_outline).getConstantState()) {
                        genderImage.setImageResource(R.drawable.other_white_outline_pressed);

                    }

                    return true; // if you want to handle the touch event
                }
                //Switch case end bracket

                return false;
            }

        });

        createWave();
        setMenuColor();
        setupStepcount();
        setWaveHeight();

        FloatingActionButton editDetails = (FloatingActionButton) findViewById(R.id.menu_item4); //edit user information. Goes to main activity
        editDetails.setOnClickListener(new View.OnClickListener() {

            public void onClick(View v) {

                /*
                Fragment fragment = new Fragment();
                FragmentTransaction transaction = manager.beginTransaction();
                transaction.add(ShowerActivity.java);
                transaction.addToBackStack(ShowerActivity.java);
                transaction.commit();
                    
                */
                //Fragment B at pos 2 should open when edit details is pressed..
                Intent i = new Intent(ShowerActivity.this, MainActivity.class);
                i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(i);
            }
        });

        FloatingActionButton fabmenuDeleteAccount = (FloatingActionButton) findViewById(R.id.delete_Account);
        fabmenuDeleteAccount.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                dialog();
            }
        });

        FloatingActionButton fabMenuItem1 = (FloatingActionButton) findViewById(R.id.menu_item);
        fabMenuItem1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                /*ComponentName receiver = new ComponentName(ShowerActivity.this, AlarmReceiver.class); // alarms.cancel(alarmIntent);??
                PackageManager pm = ShowerActivity.this.getPackageManager();
                pm.setComponentEnabledSetting(receiver,
                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                    PackageManager.DONT_KILL_APP);
                    
                stopService(new Intent(StepCountService.STEP_COUNT_SERVICE));*/

                //mFirebaseAuth.getCurrentUser().getUid() =null;

                SharedPreferences sharedPref = getSharedPreferences("myPrefs", Context.MODE_PRIVATE);
                SharedPreferences.Editor editor = sharedPref.edit();
                editor.putInt("age", 0);
                editor.putString("fluffiness", "");
                editor.putString("gender", "");
                editor.putFloat("steps", 0);
                editor.putLong("time", 0);
                editor.putFloat("stepsBoot", 0);
                editor.putBoolean("bootStart", false);
                editor.apply();

                Intent activate = new Intent(ShowerActivity.this, AlarmReceiver.class);
                PendingIntent alarmIntent = PendingIntent.getBroadcast(ShowerActivity.this, 0, activate,
                        FLAG_CANCEL_CURRENT);

                AlarmManager alarms = (AlarmManager) getSystemService(ALARM_SERVICE);
                alarms.cancel(alarmIntent);

                stopService(new Intent(ShowerActivity.this, StepCountService.class));

                mFirebaseAuth.getInstance().signOut();
                Intent i = new Intent(ShowerActivity.this, LoginActivity.class);
                startActivity(i);
            }
        });

        final FloatingActionButton shareButton = (FloatingActionButton) findViewById(R.id.shareButton);
        shareButton.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View view) {

                Intent shareIntent = new Intent(Intent.ACTION_SEND);
                shareIntent.setType("text/plain");
                String shareBody = "Check out this showering app at: https://play.google.com/store/apps/details?id=com.first.akashshrivastava.showernow \n";
                String shareSubString = "An app that tells you when you should shower and apparently keeps you clean";
                shareIntent.putExtra(Intent.EXTRA_SUBJECT, shareSubString);
                shareIntent.putExtra(Intent.EXTRA_TEXT, shareBody);
                startActivity(Intent.createChooser(shareIntent, " Share using the following"));

            }
        });

        mDatabaseReference.addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot snap) {

                try {
                    if ((!(mFirebaseAuth.getCurrentUser().getUid().isEmpty()))
                            && snap.child("User").child(mFirebaseAuth.getCurrentUser().getUid()).child("gender")
                                    .getValue().toString().equalsIgnoreCase("female")) {
                        genderImage.setImageResource(R.drawable.female_white_outline);
                    } else if (snap.child("User").child(mFirebaseAuth.getCurrentUser().getUid()).child("gender")
                            .getValue().toString().equalsIgnoreCase("male")) {
                        genderImage.setImageResource(R.drawable.male_white_outline);
                    } else if (snap.child("User").child(mFirebaseAuth.getCurrentUser().getUid()).child("gender")
                            .getValue().toString().equalsIgnoreCase("other")) {
                        genderImage.setImageResource(R.drawable.other_white_outline);
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
                age = Integer.parseInt(snap.child("User").child(mFirebaseAuth.getCurrentUser().getUid())
                        .child("Age").getValue().toString());//gotta get int

                fluffiness = snap.child("User").child(mFirebaseAuth.getCurrentUser().getUid()).child("gender")
                        .getValue().toString();
                gender = snap.child("User").child(mFirebaseAuth.getCurrentUser().getUid()).child("gender")
                        .getValue().toString();

                if (snap.child("User").child(mFirebaseAuth.getCurrentUser().getUid()).child("Time").exists()) {
                    oldTime = snap.child("User").child(mFirebaseAuth.getCurrentUser().getUid()).child("Time")
                            .getValue(Long.class);
                    oldSteps = snap.child("User").child(mFirebaseAuth.getCurrentUser().getUid()).child("Steps")
                            .getValue(float.class);

                } else {
                    newUser = true;
                }
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {

            }
        });
    }

    //Dialog box that is called when someone hits DELETE account
    public void dialog() {

        new AlertDialog.Builder(ShowerActivity.this).setTitle("Really why? :( ")
                .setMessage("Are you sure you want to delete your account?")
                .setNegativeButton("No, go back", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                        dialog.dismiss();

                    }
                }).setPositiveButton("Yes, delete!", new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                        FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();

                        assert user != null;
                        user.delete().addOnCompleteListener(new OnCompleteListener<Void>() {
                            @Override
                            public void onComplete(@NonNull Task<Void> task) {
                                if (task.isSuccessful()) {
                                    Toast.makeText(getApplicationContext(), "User deleted", Toast.LENGTH_SHORT)
                                            .show();

                                    SharedPreferences sharedPref = getSharedPreferences("myPrefs",
                                            Context.MODE_PRIVATE);
                                    SharedPreferences.Editor editor = sharedPref.edit();
                                    editor.putInt("age", 0);
                                    editor.putString("fluffiness", "");
                                    editor.putString("gender", "");
                                    editor.putFloat("steps", 0);
                                    editor.putLong("time", 0);
                                    editor.putFloat("stepsBoot", 0);
                                    editor.putBoolean("bootStart", false);
                                    editor.apply();
                                }
                            }
                        });

                        Log.d(TAG, "User account deleted.");
                        Intent i = new Intent(ShowerActivity.this, SignupActivity.class);
                        startActivity(i);
                        finish();
                        //yes code
                    }
                }).create().show();

    }

    @Override
    public void onBackPressed() { //if back is pressed the application closes
        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.addCategory(Intent.CATEGORY_HOME);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        startActivity(intent);
    }

    void setWaveHeight() {//refreshes height of the wave and wave text
        //if(!waveUpdated) {
        if (extraAge == 0) {
            waveProgressbar.setCurrent(0, "");//refreshes wave
            guyText.setText("Press me!");

        } else {
            if (gotSteps) {
                stepsDiff = steps - oldSteps;
            } else {
                stepsDiff = extraSteps;
            }

            double progress = ((double) TimeUnit.MILLISECONDS.toHours(System.currentTimeMillis() - extraOldTime)
                    / 24) / Calculation.daysBetweenShowers(stepsDiff, extraAge, extraGender, extraFluffiness); // to minutes / 60
            int percent = (int) Math.floor(progress * 100);

            if (percent >= 100) {
                waveProgressbar.setWaveColor("#4590ef"); //"#5b9ef4"
                waveProgressbar.setCurrent(100, "");//refreshes wave
                guyText.setText("Shower now!");
            } else if (percent > 70 && percent < 95) {
                waveProgressbar.setWaveColor("#ff003b"); //"#5b9ef4..color in Hex in Quotes"
                waveProgressbar.setCurrent(percent, "");//refreshes wave
                guyText.setText(percent + " %");
            } else {
                waveProgressbar.setCurrent(percent, "");//refreshes wave
                guyText.setText(percent + " %");

            }
        }
        // }
        // waveUpdated = true;
    }

    void setupStepcount() {
        /*mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
            
        // have a default sensor configured
        //int sensorType = Sensor.TYPE_LIGHT;
        int sensorType = Sensor.TYPE_STEP_COUNTER;
            
        // we need the light sensor
        Sensor sensor = mSensorManager.getDefaultSensor(sensorType);
            
        // TODO we could have the sensor reading delay configurable also though that won't do much
        // in this use case since we work with the alarm manager
        mSensorManager.registerListener(this, sensor,
            SensorManager.SENSOR_DELAY_NORMAL);
        */

        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        sensorType = mSensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);

        if (mSensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) != null) {
            mSensorManager.registerListener(this, sensorType, SensorManager.SENSOR_DELAY_NORMAL);
        } else {
            Toast.makeText(getApplicationContext(), "Pedometer doesn't exist", Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    public final void onSensorChanged(SensorEvent sensorEvent) {
        steps = sensorEvent.values[0];
        gotSteps = true;
        setWaveHeight();
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int i) {
        //do nothing here
    }

    private void createWave() {//adds the wave effect
        waveProgressbar = (WaveProgressView) findViewById(R.id.waveProgressbar);
        waveProgressbar.setCurrent(0, ""); // 77, "788M/1024M"
        waveProgressbar.setMaxProgress(100);
        waveProgressbar.setText("#FFFFFF", 41);//"#FFFF00", 41
        waveProgressbar.setWaveColor("#80ddfc"); //"#5b9ef4"

        waveProgressbar.setWave(50, 250);//float mWaveHight,float mWaveWidth
        waveProgressbar.setmWaveSpeed(10);//The larger the value, the slower the vibration

        guyText.setText("Loading..");
    }

    private void setMenuColor() {//sets the right color(seems like it cant be done through xml)
        FloatingActionMenu fabMenu = (FloatingActionMenu) findViewById(R.id.menu);
        fabMenu.setMenuButtonColorNormal(getResources().getColor(R.color.colorAccent));//colorAccent
        fabMenu.setMenuButtonColorPressed(getResources().getColor(R.color.colorAccentPressed));
    }

    //Auto Gen methods with Fitness API
    //Following methods implemented when using FItness API under OnDataPointListener, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener

    @Override
    protected void onStart() {
        super.onStart();
    }

    int totalSteps = 0;

    //This function is called everytime OnDataPoint is called, this is where it all computes...
    public void updateSecondsLeftForNextShower() {

        //Create instance of calculation and calculate seconds left for next shower
        //Update value for seconds left for next shower

        mDatabaseReference.addListenerForSingleValueEvent(new ValueEventListener() {
            @Override

            public void onDataChange(DataSnapshot dataSnapshot) {

                String currentGender = dataSnapshot.child("User").child(mFirebaseAuth.getCurrentUser().getUid())
                        .child("gender").getValue().toString();
                String currentFluffiness = dataSnapshot.child("User").child(mFirebaseAuth.getCurrentUser().getUid())
                        .child("Fluffiness").getValue().toString();
                int currentWeight = Integer.parseInt(dataSnapshot.child("User")
                        .child(mFirebaseAuth.getCurrentUser().getUid()).child("Weight").getValue().toString());
                int currentAge = Integer.parseInt(dataSnapshot.child("User")
                        .child(mFirebaseAuth.getCurrentUser().getUid()).child("Age").getValue().toString());
                int currentHeight = Integer.parseInt(dataSnapshot.child("User")
                        .child(mFirebaseAuth.getCurrentUser().getUid()).child("Height").getValue().toString());

                //Call the method in Calculation to find the time..
                //TODO update secondsLeftForNextShower variable at the end...

                //double timeRemaining = calculation.daysBetweenShowers(totalSteps,currentAge,currentGender,currentFluffiness);

                // TODO Update timeRemaining to the wave here
                //TODO UPDATE secondsLeftForNextShower variable
                //TODO UPDATE STEPS, either by equating to zero or finding difference between trigger points

                Log.e("Gender", currentGender + " " + currentFluffiness);
                //Log.e("steps", "Total steps" + totalSteps);
                Log.e("weight", "weight" + currentWeight);
                Log.e("age", "age" + currentAge);
                Log.e("height", "height" + currentHeight);

            }

            @Override
            public void onCancelled(DatabaseError databaseError) {

            }
        });

    }

    @Override
    public void onDataPoint(DataPoint dataPoint) {
        for (final Field field : dataPoint.getDataType().getFields()) {
            final Value value = dataPoint.getValue(field);

            totalSteps = value.asInt();

            //Check for time - Updates wave
            updateSecondsLeftForNextShower();

            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Log.e("onDataPoint", "OnDataPoint is being called.....");
                    Toast.makeText(getApplicationContext(), "Field: " + field.getName() + " Value: " + value,
                            Toast.LENGTH_SHORT).show();

                }
            });
        }
    }

    //Releasing the API when done, saves battery....
    @Override
    protected void onStop() {
        super.onStop();
    }

    //Facebook API funciton calls!!!
    //Function is called when Sharebutton on FAB menu is clicked...
    /*
    public void postPicture() {
    try{
        linkContent = new ShareLinkContent.Builder()
                .setContentTitle("Showers now")
                .setContentDescription("Showers now, saving the planet and keeping clean")
                .setContentUrl(Uri.parse("https://play.google.com/store/apps/details?id=com.first.akashshrivastava.showernow"))
                .build();
        shareDialog.show(linkContent);
        
    }catch(Exception e) {
        Toast.makeText(getApplicationContext(), "Facebook not installed", Toast.LENGTH_SHORT).show();
    }
        
        
        
        
    } */

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
    }

}

/*
Copyright 2017 Akash Shrivastava & Martin Strm
    
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
    
http://www.apache.org/licenses/LICENSE-2.0
    
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 */