Home | History | Annotate | Download | only in voicemail
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.dialer.voicemail;
     18 
     19 import android.content.ContentUris;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.database.Cursor;
     23 import android.graphics.drawable.Drawable;
     24 import android.net.Uri;
     25 import android.os.AsyncTask;
     26 import android.os.Handler;
     27 import android.util.AttributeSet;
     28 import android.support.design.widget.Snackbar;
     29 import android.view.LayoutInflater;
     30 import android.view.View;
     31 import android.widget.ImageButton;
     32 import android.widget.LinearLayout;
     33 import android.widget.SeekBar;
     34 import android.widget.SeekBar.OnSeekBarChangeListener;
     35 import android.widget.Space;
     36 import android.widget.TextView;
     37 import android.widget.Toast;
     38 
     39 import com.android.common.io.MoreCloseables;
     40 import com.android.dialer.PhoneCallDetails;
     41 import com.android.dialer.R;
     42 import com.android.dialer.calllog.CallLogAsyncTaskUtil;
     43 
     44 import com.android.dialer.database.VoicemailArchiveContract;
     45 import com.android.dialer.database.VoicemailArchiveContract.VoicemailArchive;
     46 import com.android.dialer.util.AsyncTaskExecutor;
     47 import com.android.dialer.util.AsyncTaskExecutors;
     48 import com.android.dialerbind.ObjectFactory;
     49 import com.google.common.annotations.VisibleForTesting;
     50 
     51 import java.util.ArrayList;
     52 import java.util.HashMap;
     53 import java.util.Objects;
     54 import java.util.concurrent.TimeUnit;
     55 import java.util.concurrent.ScheduledFuture;
     56 import java.util.concurrent.ScheduledExecutorService;
     57 
     58 import javax.annotation.Nullable;
     59 import javax.annotation.concurrent.GuardedBy;
     60 import javax.annotation.concurrent.NotThreadSafe;
     61 import javax.annotation.concurrent.ThreadSafe;
     62 
     63 /**
     64  * Displays and plays a single voicemail. See {@link VoicemailPlaybackPresenter} for
     65  * details on the voicemail playback implementation.
     66  *
     67  * This class is not thread-safe, it is thread-confined. All calls to all public
     68  * methods on this class are expected to come from the main ui thread.
     69  */
     70 @NotThreadSafe
     71 public class VoicemailPlaybackLayout extends LinearLayout
     72         implements VoicemailPlaybackPresenter.PlaybackView,
     73         CallLogAsyncTaskUtil.CallLogAsyncTaskListener {
     74     private static final String TAG = VoicemailPlaybackLayout.class.getSimpleName();
     75     private static final int VOICEMAIL_DELETE_DELAY_MS = 3000;
     76     private static final int VOICEMAIL_ARCHIVE_DELAY_MS = 3000;
     77 
     78     /** The enumeration of {@link AsyncTask} objects we use in this class. */
     79     public enum Tasks {
     80         QUERY_ARCHIVED_STATUS
     81     }
     82 
     83     /**
     84      * Controls the animation of the playback slider.
     85      */
     86     @ThreadSafe
     87     private final class PositionUpdater implements Runnable {
     88 
     89         /** Update rate for the slider, 30fps. */
     90         private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
     91 
     92         private int mDurationMs;
     93         private final ScheduledExecutorService mExecutorService;
     94         private final Object mLock = new Object();
     95         @GuardedBy("mLock") private ScheduledFuture<?> mScheduledFuture;
     96 
     97         private Runnable mUpdateClipPositionRunnable = new Runnable() {
     98             @Override
     99             public void run() {
    100                 int currentPositionMs = 0;
    101                 synchronized (mLock) {
    102                     if (mScheduledFuture == null || mPresenter == null) {
    103                         // This task has been canceled. Just stop now.
    104                         return;
    105                     }
    106                     currentPositionMs = mPresenter.getMediaPlayerPosition();
    107                 }
    108                 setClipPosition(currentPositionMs, mDurationMs);
    109             }
    110         };
    111 
    112         public PositionUpdater(int durationMs, ScheduledExecutorService executorService) {
    113             mDurationMs = durationMs;
    114             mExecutorService = executorService;
    115         }
    116 
    117         @Override
    118         public void run() {
    119             post(mUpdateClipPositionRunnable);
    120         }
    121 
    122         public void startUpdating() {
    123             synchronized (mLock) {
    124                 cancelPendingRunnables();
    125                 mScheduledFuture = mExecutorService.scheduleAtFixedRate(
    126                         this, 0, SLIDER_UPDATE_PERIOD_MILLIS, TimeUnit.MILLISECONDS);
    127             }
    128         }
    129 
    130         public void stopUpdating() {
    131             synchronized (mLock) {
    132                 cancelPendingRunnables();
    133             }
    134         }
    135 
    136         private void cancelPendingRunnables() {
    137             if (mScheduledFuture != null) {
    138                 mScheduledFuture.cancel(true);
    139                 mScheduledFuture = null;
    140             }
    141             removeCallbacks(mUpdateClipPositionRunnable);
    142         }
    143     }
    144 
    145     /**
    146      * Handle state changes when the user manipulates the seek bar.
    147      */
    148     private final OnSeekBarChangeListener mSeekBarChangeListener = new OnSeekBarChangeListener() {
    149         @Override
    150         public void onStartTrackingTouch(SeekBar seekBar) {
    151             if (mPresenter != null) {
    152                 mPresenter.pausePlaybackForSeeking();
    153             }
    154         }
    155 
    156         @Override
    157         public void onStopTrackingTouch(SeekBar seekBar) {
    158             if (mPresenter != null) {
    159                 mPresenter.resumePlaybackAfterSeeking(seekBar.getProgress());
    160             }
    161         }
    162 
    163         @Override
    164         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    165             setClipPosition(progress, seekBar.getMax());
    166             // Update the seek position if user manually changed it. This makes sure position gets
    167             // updated when user use volume button to seek playback in talkback mode.
    168             if (fromUser) {
    169                 mPresenter.seek(progress);
    170             }
    171         }
    172     };
    173 
    174     /**
    175      * Click listener to toggle speakerphone.
    176      */
    177     private final View.OnClickListener mSpeakerphoneListener = new View.OnClickListener() {
    178         @Override
    179         public void onClick(View v) {
    180             if (mPresenter != null) {
    181                 mPresenter.toggleSpeakerphone();
    182             }
    183         }
    184     };
    185 
    186     /**
    187      * Click listener to play or pause voicemail playback.
    188      */
    189     private final View.OnClickListener mStartStopButtonListener = new View.OnClickListener() {
    190         @Override
    191         public void onClick(View view) {
    192             if (mPresenter == null) {
    193                 return;
    194             }
    195 
    196             if (mIsPlaying) {
    197                 mPresenter.pausePlayback();
    198             } else {
    199                 mPresenter.resumePlayback();
    200             }
    201         }
    202     };
    203 
    204     private final View.OnClickListener mDeleteButtonListener = new View.OnClickListener() {
    205         @Override
    206         public void onClick(View view ) {
    207             if (mPresenter == null) {
    208                 return;
    209             }
    210             mPresenter.pausePlayback();
    211             mPresenter.onVoicemailDeleted();
    212 
    213             final Uri deleteUri = mVoicemailUri;
    214             final Runnable deleteCallback = new Runnable() {
    215                 @Override
    216                 public void run() {
    217                     if (Objects.equals(deleteUri, mVoicemailUri)) {
    218                         CallLogAsyncTaskUtil.deleteVoicemail(mContext, deleteUri,
    219                                 VoicemailPlaybackLayout.this);
    220                     }
    221                 }
    222             };
    223 
    224             final Handler handler = new Handler();
    225             // Add a little buffer time in case the user clicked "undo" at the end of the delay
    226             // window.
    227             handler.postDelayed(deleteCallback, VOICEMAIL_DELETE_DELAY_MS + 50);
    228 
    229             Snackbar.make(VoicemailPlaybackLayout.this, R.string.snackbar_voicemail_deleted,
    230                             Snackbar.LENGTH_LONG)
    231                     .setDuration(VOICEMAIL_DELETE_DELAY_MS)
    232                     .setAction(R.string.snackbar_voicemail_deleted_undo,
    233                             new View.OnClickListener() {
    234                                 @Override
    235                                 public void onClick(View view) {
    236                                     mPresenter.onVoicemailDeleteUndo();
    237                                         handler.removeCallbacks(deleteCallback);
    238                                 }
    239                             })
    240                     .setActionTextColor(
    241                             mContext.getResources().getColor(
    242                                     R.color.dialer_snackbar_action_text_color))
    243                     .show();
    244         }
    245     };
    246 
    247     private final View.OnClickListener mArchiveButtonListener = new View.OnClickListener() {
    248         @Override
    249         public void onClick(View v) {
    250             if (mPresenter == null || isArchiving(mVoicemailUri)) {
    251                 return;
    252             }
    253             mIsArchiving.add(mVoicemailUri);
    254             mPresenter.pausePlayback();
    255             updateArchiveUI(mVoicemailUri);
    256             disableUiElements();
    257             mPresenter.archiveContent(mVoicemailUri, true);
    258         }
    259     };
    260 
    261     private final View.OnClickListener mShareButtonListener = new View.OnClickListener() {
    262         @Override
    263         public void onClick(View v) {
    264             if (mPresenter == null || isArchiving(mVoicemailUri)) {
    265                 return;
    266             }
    267             disableUiElements();
    268             mPresenter.archiveContent(mVoicemailUri, false);
    269         }
    270     };
    271 
    272     private Context mContext;
    273     private VoicemailPlaybackPresenter mPresenter;
    274     private Uri mVoicemailUri;
    275     private final AsyncTaskExecutor mAsyncTaskExecutor =
    276             AsyncTaskExecutors.createAsyncTaskExecutor();
    277     private boolean mIsPlaying = false;
    278     /**
    279      * Keeps track of which voicemails are currently being archived in order to update the voicemail
    280      * card UI every time a user opens a new card.
    281      */
    282     private static final ArrayList<Uri> mIsArchiving = new ArrayList<>();
    283 
    284     private SeekBar mPlaybackSeek;
    285     private ImageButton mStartStopButton;
    286     private ImageButton mPlaybackSpeakerphone;
    287     private ImageButton mDeleteButton;
    288     private ImageButton mArchiveButton;
    289     private ImageButton mShareButton;
    290 
    291     private Space mArchiveSpace;
    292     private Space mShareSpace;
    293 
    294     private TextView mStateText;
    295     private TextView mPositionText;
    296     private TextView mTotalDurationText;
    297 
    298     private PositionUpdater mPositionUpdater;
    299     private Drawable mVoicemailSeekHandleEnabled;
    300     private Drawable mVoicemailSeekHandleDisabled;
    301 
    302     public VoicemailPlaybackLayout(Context context) {
    303         this(context, null);
    304     }
    305 
    306     public VoicemailPlaybackLayout(Context context, AttributeSet attrs) {
    307         super(context, attrs);
    308         mContext = context;
    309         LayoutInflater inflater =
    310                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    311         inflater.inflate(R.layout.voicemail_playback_layout, this);
    312     }
    313 
    314     @Override
    315     public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) {
    316         mPresenter = presenter;
    317         mVoicemailUri = voicemailUri;
    318         if (ObjectFactory.isVoicemailArchiveEnabled(mContext)) {
    319             updateArchiveUI(mVoicemailUri);
    320             updateArchiveButton(mVoicemailUri);
    321         }
    322 
    323         if (ObjectFactory.isVoicemailShareEnabled(mContext)) {
    324             // Show share button and space before it
    325             mShareSpace.setVisibility(View.VISIBLE);
    326             mShareButton.setVisibility(View.VISIBLE);
    327         }
    328     }
    329 
    330     @Override
    331     protected void onFinishInflate() {
    332         super.onFinishInflate();
    333 
    334         mPlaybackSeek = (SeekBar) findViewById(R.id.playback_seek);
    335         mStartStopButton = (ImageButton) findViewById(R.id.playback_start_stop);
    336         mPlaybackSpeakerphone = (ImageButton) findViewById(R.id.playback_speakerphone);
    337         mDeleteButton = (ImageButton) findViewById(R.id.delete_voicemail);
    338         mArchiveButton =(ImageButton) findViewById(R.id.archive_voicemail);
    339         mShareButton = (ImageButton) findViewById(R.id.share_voicemail);
    340 
    341         mArchiveSpace = (Space) findViewById(R.id.space_before_archive_voicemail);
    342         mShareSpace = (Space) findViewById(R.id.space_before_share_voicemail);
    343 
    344         mStateText = (TextView) findViewById(R.id.playback_state_text);
    345         mPositionText = (TextView) findViewById(R.id.playback_position_text);
    346         mTotalDurationText = (TextView) findViewById(R.id.total_duration_text);
    347 
    348         mPlaybackSeek.setOnSeekBarChangeListener(mSeekBarChangeListener);
    349         mStartStopButton.setOnClickListener(mStartStopButtonListener);
    350         mPlaybackSpeakerphone.setOnClickListener(mSpeakerphoneListener);
    351         mDeleteButton.setOnClickListener(mDeleteButtonListener);
    352         mArchiveButton.setOnClickListener(mArchiveButtonListener);
    353         mShareButton.setOnClickListener(mShareButtonListener);
    354 
    355         mPositionText.setText(formatAsMinutesAndSeconds(0));
    356         mTotalDurationText.setText(formatAsMinutesAndSeconds(0));
    357 
    358         mVoicemailSeekHandleEnabled = getResources().getDrawable(
    359                 R.drawable.ic_voicemail_seek_handle, mContext.getTheme());
    360         mVoicemailSeekHandleDisabled = getResources().getDrawable(
    361                 R.drawable.ic_voicemail_seek_handle_disabled, mContext.getTheme());
    362     }
    363 
    364     @Override
    365     public void onPlaybackStarted(int duration, ScheduledExecutorService executorService) {
    366         mIsPlaying = true;
    367 
    368         mStartStopButton.setImageResource(R.drawable.ic_pause);
    369 
    370         if (mPositionUpdater != null) {
    371             mPositionUpdater.stopUpdating();
    372             mPositionUpdater = null;
    373         }
    374         mPositionUpdater = new PositionUpdater(duration, executorService);
    375         mPositionUpdater.startUpdating();
    376     }
    377 
    378     @Override
    379     public void onPlaybackStopped() {
    380         mIsPlaying = false;
    381 
    382         mStartStopButton.setImageResource(R.drawable.ic_play_arrow);
    383 
    384         if (mPositionUpdater != null) {
    385             mPositionUpdater.stopUpdating();
    386             mPositionUpdater = null;
    387         }
    388     }
    389 
    390     @Override
    391     public void onPlaybackError() {
    392         if (mPositionUpdater != null) {
    393             mPositionUpdater.stopUpdating();
    394         }
    395 
    396         disableUiElements();
    397         mStateText.setText(getString(R.string.voicemail_playback_error));
    398     }
    399 
    400     @Override
    401     public void onSpeakerphoneOn(boolean on) {
    402         if (on) {
    403             mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_up_24dp);
    404             // Speaker is now on, tapping button will turn it off.
    405             mPlaybackSpeakerphone.setContentDescription(
    406                     mContext.getString(R.string.voicemail_speaker_off));
    407         } else {
    408             mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_down_24dp);
    409             // Speaker is now off, tapping button will turn it on.
    410             mPlaybackSpeakerphone.setContentDescription(
    411                     mContext.getString(R.string.voicemail_speaker_on));
    412         }
    413     }
    414 
    415     @Override
    416     public void setClipPosition(int positionMs, int durationMs) {
    417         int seekBarPositionMs = Math.max(0, positionMs);
    418         int seekBarMax = Math.max(seekBarPositionMs, durationMs);
    419         if (mPlaybackSeek.getMax() != seekBarMax) {
    420             mPlaybackSeek.setMax(seekBarMax);
    421         }
    422 
    423         mPlaybackSeek.setProgress(seekBarPositionMs);
    424 
    425         mPositionText.setText(formatAsMinutesAndSeconds(seekBarPositionMs));
    426         mTotalDurationText.setText(formatAsMinutesAndSeconds(durationMs));
    427     }
    428 
    429     @Override
    430     public void setSuccess() {
    431         mStateText.setText(null);
    432     }
    433 
    434     @Override
    435     public void setIsFetchingContent() {
    436         disableUiElements();
    437         mStateText.setText(getString(R.string.voicemail_fetching_content));
    438     }
    439 
    440     @Override
    441     public void setFetchContentTimeout() {
    442         mStartStopButton.setEnabled(true);
    443         mStateText.setText(getString(R.string.voicemail_fetching_timout));
    444     }
    445 
    446     @Override
    447     public int getDesiredClipPosition() {
    448         return mPlaybackSeek.getProgress();
    449     }
    450 
    451     @Override
    452     public void disableUiElements() {
    453         mStartStopButton.setEnabled(false);
    454         resetSeekBar();
    455     }
    456 
    457     @Override
    458     public void enableUiElements() {
    459         mDeleteButton.setEnabled(true);
    460         mStartStopButton.setEnabled(true);
    461         mPlaybackSeek.setEnabled(true);
    462         mPlaybackSeek.setThumb(mVoicemailSeekHandleEnabled);
    463     }
    464 
    465     @Override
    466     public void resetSeekBar() {
    467         mPlaybackSeek.setProgress(0);
    468         mPlaybackSeek.setEnabled(false);
    469         mPlaybackSeek.setThumb(mVoicemailSeekHandleDisabled);
    470     }
    471 
    472     @Override
    473     public void onDeleteCall() {}
    474 
    475     @Override
    476     public void onDeleteVoicemail() {
    477         mPresenter.onVoicemailDeletedInDatabase();
    478     }
    479 
    480     @Override
    481     public void onGetCallDetails(PhoneCallDetails[] details) {}
    482 
    483     private String getString(int resId) {
    484         return mContext.getString(resId);
    485     }
    486 
    487     /**
    488      * Formats a number of milliseconds as something that looks like {@code 00:05}.
    489      * <p>
    490      * We always use four digits, two for minutes two for seconds.  In the very unlikely event
    491      * that the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
    492      */
    493     private String formatAsMinutesAndSeconds(int millis) {
    494         int seconds = millis / 1000;
    495         int minutes = seconds / 60;
    496         seconds -= minutes * 60;
    497         if (minutes > 99) {
    498             minutes = 99;
    499         }
    500         return String.format("%02d:%02d", minutes, seconds);
    501     }
    502 
    503     /**
    504      * Called when a voicemail archive succeeded. If the expanded voicemail was being
    505      * archived, update the card UI. Either way, display a snackbar linking user to archive.
    506      */
    507     @Override
    508     public void onVoicemailArchiveSucceded(Uri voicemailUri) {
    509         if (isArchiving(voicemailUri)) {
    510             mIsArchiving.remove(voicemailUri);
    511             if (Objects.equals(voicemailUri, mVoicemailUri)) {
    512                 onVoicemailArchiveResult();
    513                 hideArchiveButton();
    514             }
    515         }
    516 
    517         Snackbar.make(this, R.string.snackbar_voicemail_archived,
    518                 Snackbar.LENGTH_LONG)
    519                 .setDuration(VOICEMAIL_ARCHIVE_DELAY_MS)
    520                 .setAction(R.string.snackbar_voicemail_archived_goto,
    521                         new View.OnClickListener() {
    522                             @Override
    523                             public void onClick(View view) {
    524                                 Intent intent = new Intent(mContext,
    525                                         VoicemailArchiveActivity.class);
    526                                 mContext.startActivity(intent);
    527                             }
    528                         })
    529                 .setActionTextColor(
    530                         mContext.getResources().getColor(R.color.dialer_snackbar_action_text_color))
    531                 .show();
    532     }
    533 
    534     /**
    535      * If a voicemail archive failed, and the expanded card was being archived, update the card UI.
    536      * Either way, display a toast saying the voicemail archive failed.
    537      */
    538     @Override
    539     public void onVoicemailArchiveFailed(Uri voicemailUri) {
    540         if (isArchiving(voicemailUri)) {
    541             mIsArchiving.remove(voicemailUri);
    542             if (Objects.equals(voicemailUri, mVoicemailUri)) {
    543                 onVoicemailArchiveResult();
    544             }
    545         }
    546         String toastStr = mContext.getString(R.string.voicemail_archive_failed);
    547         Toast.makeText(mContext, toastStr, Toast.LENGTH_SHORT).show();
    548     }
    549 
    550     public void hideArchiveButton() {
    551         mArchiveSpace.setVisibility(View.GONE);
    552         mArchiveButton.setVisibility(View.GONE);
    553         mArchiveButton.setClickable(false);
    554         mArchiveButton.setEnabled(false);
    555     }
    556 
    557     /**
    558      * Whenever a voicemail archive succeeds or fails, clear the text displayed in the voicemail
    559      * card.
    560      */
    561     private void onVoicemailArchiveResult() {
    562         enableUiElements();
    563         mStateText.setText(null);
    564         mArchiveButton.setColorFilter(null);
    565     }
    566 
    567     /**
    568      * Whether or not the voicemail with the given uri is being archived.
    569      */
    570     private boolean isArchiving(@Nullable Uri uri) {
    571         return uri != null && mIsArchiving.contains(uri);
    572     }
    573 
    574     /**
    575      * Show the proper text and hide the archive button if the voicemail is still being archived.
    576      */
    577     private void updateArchiveUI(@Nullable Uri voicemailUri) {
    578         if (!Objects.equals(voicemailUri, mVoicemailUri)) {
    579             return;
    580         }
    581         if (isArchiving(voicemailUri)) {
    582             // If expanded card was in the middle of archiving, disable buttons and display message
    583             disableUiElements();
    584             mDeleteButton.setEnabled(false);
    585             mArchiveButton.setColorFilter(getResources().getColor(R.color.setting_disabled_color));
    586             mStateText.setText(getString(R.string.voicemail_archiving_content));
    587         } else {
    588             onVoicemailArchiveResult();
    589         }
    590     }
    591 
    592     /**
    593      * Hides the archive button if the voicemail has already been archived, shows otherwise.
    594      * @param voicemailUri the URI of the voicemail for which the archive button needs to be updated
    595      */
    596     private void updateArchiveButton(@Nullable final Uri voicemailUri) {
    597         if (voicemailUri == null ||
    598                 !Objects.equals(voicemailUri, mVoicemailUri) || isArchiving(voicemailUri) ||
    599                 Objects.equals(voicemailUri.getAuthority(),VoicemailArchiveContract.AUTHORITY)) {
    600             return;
    601         }
    602         mAsyncTaskExecutor.submit(Tasks.QUERY_ARCHIVED_STATUS,
    603                 new AsyncTask<Void, Void, Boolean>() {
    604             @Override
    605             public Boolean doInBackground(Void... params) {
    606                 Cursor cursor = mContext.getContentResolver().query(VoicemailArchive.CONTENT_URI,
    607                         null, VoicemailArchive.SERVER_ID + "=" + ContentUris.parseId(mVoicemailUri)
    608                         + " AND " + VoicemailArchive.ARCHIVED + "= 1", null, null);
    609                 boolean archived = cursor != null && cursor.getCount() > 0;
    610                 cursor.close();
    611                 return archived;
    612             }
    613 
    614             @Override
    615             public void onPostExecute(Boolean archived) {
    616                 if (!Objects.equals(voicemailUri, mVoicemailUri)) {
    617                     return;
    618                 }
    619 
    620                 if (archived) {
    621                     hideArchiveButton();
    622                 } else {
    623                     mArchiveSpace.setVisibility(View.VISIBLE);
    624                     mArchiveButton.setVisibility(View.VISIBLE);
    625                     mArchiveButton.setClickable(true);
    626                     mArchiveButton.setEnabled(true);
    627                 }
    628 
    629             }
    630         });
    631     }
    632 
    633     @VisibleForTesting
    634     public String getStateText() {
    635         return mStateText.getText().toString();
    636     }
    637 }
    638