/* * 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); } }