blob: c714558394ec4fb5ebc03e92d9a0407ac4903bdb [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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.
*/
package com.android.tv.dialog;
import android.app.ActivityManager;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.media.tv.TvContentRating;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.TextView;
import android.widget.Toast;
import com.android.tv.R;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.dialog.picker.TvPinPicker;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.TvSettings;
import dagger.android.AndroidInjection;
import com.android.tv.common.flags.UiFlags;
import javax.inject.Inject;
public class PinDialogFragment extends SafeDismissDialogFragment {
private static final String TAG = "PinDialogFragment";
private static final boolean DEBUG = false;
/** PIN code dialog for unlock channel */
public static final int PIN_DIALOG_TYPE_UNLOCK_CHANNEL = 0;
/**
* PIN code dialog for unlock content. Only difference between {@code
* PIN_DIALOG_TYPE_UNLOCK_CHANNEL} is it's title.
*/
public static final int PIN_DIALOG_TYPE_UNLOCK_PROGRAM = 1;
/** PIN code dialog for change parental control settings */
public static final int PIN_DIALOG_TYPE_ENTER_PIN = 2;
/** PIN code dialog for set new PIN */
public static final int PIN_DIALOG_TYPE_NEW_PIN = 3;
// PIN code dialog for checking old PIN. Only used in this class.
private static final int PIN_DIALOG_TYPE_OLD_PIN = 4;
/** PIN code dialog for unlocking DVR playback */
public static final int PIN_DIALOG_TYPE_UNLOCK_DVR = 5;
private static final int MAX_WRONG_PIN_COUNT = 5;
private static final int DISABLE_PIN_DURATION_MILLIS = 60 * 1000; // 1 minute
private static final String TRACKER_LABEL = "Pin dialog";
private static final String ARGS_TYPE = "args_type";
private static final String ARGS_RATING = "args_rating";
public static final String DIALOG_TAG = PinDialogFragment.class.getName();
private int mType;
private int mRequestType;
private boolean mPinChecked;
private boolean mDismissSilently;
private TextView mWrongPinView;
private View mEnterPinView;
private TextView mTitleView;
private TvPinPicker mTvPinPicker;
private SharedPreferences mSharedPreferences;
private String mPrevPin;
private String mPin;
private String mRatingString;
private int mWrongPinCount;
private long mDisablePinUntil;
private final Handler mHandler = new Handler();
@Inject TvInputManagerHelper mTvInputManagerHelper;
@Inject UiFlags mUiFlags;
public static PinDialogFragment create(int type) {
return create(type, null);
}
public static PinDialogFragment create(int type, String rating) {
PinDialogFragment fragment = new PinDialogFragment();
Bundle args = new Bundle();
args.putInt(ARGS_TYPE, type);
args.putString(ARGS_RATING, rating);
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(Context context) {
AndroidInjection.inject(this);
super.onAttach(context);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mRequestType = getArguments().getInt(ARGS_TYPE, PIN_DIALOG_TYPE_ENTER_PIN);
mType = mRequestType;
mRatingString = getArguments().getString(ARGS_RATING);
setStyle(STYLE_NO_TITLE, 0);
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
mDisablePinUntil = TvSettings.getDisablePinUntil(getActivity());
if (ActivityManager.isUserAMonkey()) {
// Skip PIN dialog half the time for monkeys
if (Math.random() < 0.5) {
exit(true);
}
}
mPinChecked = false;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dlg = super.onCreateDialog(savedInstanceState);
dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation;
return dlg;
}
@Override
public String getTrackerLabel() {
return TRACKER_LABEL;
}
@Override
public void onStart() {
super.onStart();
// Dialog size is determined by its windows size, not inflated view size.
// So apply view size to window after the DialogFragment.onStart() where dialog is shown.
Dialog dlg = getDialog();
if (dlg != null) {
dlg.getWindow()
.setLayout(
getResources().getDimensionPixelSize(R.dimen.pin_dialog_width),
LayoutParams.WRAP_CONTENT);
}
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.pin_dialog, container, false);
mWrongPinView = (TextView) v.findViewById(R.id.wrong_pin);
mEnterPinView = v.findViewById(R.id.enter_pin);
mTitleView = (TextView) mEnterPinView.findViewById(R.id.title);
mTvPinPicker = v.findViewById(R.id.tv_pin_picker);
mTvPinPicker.setOnClickListener(
view -> {
String pin = getPinInput();
if (!TextUtils.isEmpty(pin)) {
done(pin);
}
});
if (TextUtils.isEmpty(getPin())) {
// If PIN isn't set, user should set a PIN.
// Successfully setting a new set is considered as entering correct PIN.
mType = PIN_DIALOG_TYPE_NEW_PIN;
}
switch (mType) {
case PIN_DIALOG_TYPE_UNLOCK_CHANNEL:
mTitleView.setText(R.string.pin_enter_unlock_channel);
break;
case PIN_DIALOG_TYPE_UNLOCK_PROGRAM:
mTitleView.setText(R.string.pin_enter_unlock_program);
break;
case PIN_DIALOG_TYPE_UNLOCK_DVR:
TvContentRating tvContentRating =
TvContentRating.unflattenFromString(mRatingString);
if (TvContentRating.UNRATED.equals(tvContentRating)) {
mTitleView.setText(getString(R.string.pin_enter_unlock_dvr_unrated));
} else {
mTitleView.setText(
getString(
R.string.pin_enter_unlock_dvr,
mTvInputManagerHelper
.getContentRatingsManager()
.getDisplayNameForRating(tvContentRating)));
}
break;
case PIN_DIALOG_TYPE_ENTER_PIN:
mTitleView.setText(R.string.pin_enter_pin);
break;
case PIN_DIALOG_TYPE_NEW_PIN:
if (TextUtils.isEmpty(getPin())) {
mTitleView.setText(R.string.pin_enter_create_pin);
} else {
mTitleView.setText(R.string.pin_enter_old_pin);
mType = PIN_DIALOG_TYPE_OLD_PIN;
}
}
if (mType != PIN_DIALOG_TYPE_NEW_PIN) {
updateWrongPin();
}
mTvPinPicker.requestFocus();
return v;
}
private void updateWrongPin() {
if (getActivity() == null) {
// The activity is already detached. No need to update.
mHandler.removeCallbacks(null);
return;
}
int remainingSeconds = (int) ((mDisablePinUntil - System.currentTimeMillis()) / 1000);
boolean enabled = remainingSeconds < 1;
if (enabled) {
mWrongPinView.setVisibility(View.INVISIBLE);
mEnterPinView.setVisibility(View.VISIBLE);
mWrongPinCount = 0;
} else {
mEnterPinView.setVisibility(View.INVISIBLE);
mWrongPinView.setVisibility(View.VISIBLE);
mWrongPinView.setText(
getResources()
.getQuantityString(
R.plurals.pin_enter_countdown,
remainingSeconds,
remainingSeconds));
mHandler.postDelayed(this::updateWrongPin, 1000);
}
}
private void exit(boolean pinChecked) {
mPinChecked = pinChecked;
dismiss();
}
/** Dismisses the pin dialog without calling activity listener. */
public void dismissSilently() {
mDismissSilently = true;
dismiss();
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (DEBUG) Log.d(TAG, "onDismiss: mPinChecked=" + mPinChecked);
SoftPreconditions.checkState(getActivity() instanceof OnPinCheckedListener);
if (!mDismissSilently && getActivity() instanceof OnPinCheckedListener) {
((OnPinCheckedListener) getActivity())
.onPinChecked(mPinChecked, mRequestType, mRatingString);
}
mDismissSilently = false;
}
private void handleWrongPin() {
if (++mWrongPinCount >= MAX_WRONG_PIN_COUNT) {
mDisablePinUntil = System.currentTimeMillis() + DISABLE_PIN_DURATION_MILLIS;
TvSettings.setDisablePinUntil(getActivity(), mDisablePinUntil);
updateWrongPin();
} else {
showToast(R.string.pin_toast_wrong);
}
}
private void showToast(int resId) {
Toast.makeText(getActivity(), resId, Toast.LENGTH_SHORT).show();
}
private void done(String pin) {
if (DEBUG) Log.d(TAG, "done: mType=" + mType + " pin=" + pin + " stored=" + getPin());
switch (mType) {
case PIN_DIALOG_TYPE_UNLOCK_CHANNEL:
case PIN_DIALOG_TYPE_UNLOCK_PROGRAM:
case PIN_DIALOG_TYPE_UNLOCK_DVR:
case PIN_DIALOG_TYPE_ENTER_PIN:
if (TextUtils.isEmpty(getPin()) || pin.equals(getPin())) {
exit(true);
} else {
resetPinInput();
handleWrongPin();
}
break;
case PIN_DIALOG_TYPE_NEW_PIN:
resetPinInput();
if (mPrevPin == null) {
mPrevPin = pin;
mTitleView.setText(R.string.pin_enter_again);
} else {
if (pin.equals(mPrevPin)) {
setPin(pin);
exit(true);
} else {
if (TextUtils.isEmpty(getPin())) {
mTitleView.setText(R.string.pin_enter_create_pin);
} else {
mTitleView.setText(R.string.pin_enter_new_pin);
}
mPrevPin = null;
showToast(R.string.pin_toast_not_match);
}
}
break;
case PIN_DIALOG_TYPE_OLD_PIN:
// Call resetPinInput() here because we'll get additional PIN input
// regardless of the result.
resetPinInput();
if (pin.equals(getPin())) {
mType = PIN_DIALOG_TYPE_NEW_PIN;
mTitleView.setText(R.string.pin_enter_new_pin);
} else {
handleWrongPin();
}
break;
}
}
public int getType() {
return mType;
}
private void setPin(String pin) {
if (DEBUG) Log.d(TAG, "setPin: " + pin);
mPin = pin;
mSharedPreferences.edit().putString(TvSettings.PREF_PIN, pin).apply();
}
private String getPin() {
if (mPin == null) {
mPin = mSharedPreferences.getString(TvSettings.PREF_PIN, "");
}
return mPin;
}
private String getPinInput() {
return mTvPinPicker.getPin();
}
private void resetPinInput() {
mTvPinPicker.resetPin();
}
/**
* A listener to the result of {@link PinDialogFragment}. Any activity requiring pin code
* checking should implement this listener to receive the result.
*/
public interface OnPinCheckedListener {
/**
* Called when {@link PinDialogFragment} is dismissed.
*
* @param checked {@code true} if the pin code entered is checked to be correct, otherwise
* {@code false}.
* @param type The dialog type regarding to what pin entering is for.
* @param rating The target rating to unblock for.
*/
void onPinChecked(boolean checked, int type, String rating);
}
}