Home | History | Annotate | Download | only in sample
      1 /*
      2  * Copyright (C) 2016 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 package com.android.car.cluster.sample;
     17 
     18 import static com.android.car.cluster.sample.DebugUtil.DEBUG;
     19 
     20 import android.annotation.Nullable;
     21 import android.app.Presentation;
     22 import android.car.cluster.renderer.NavigationRenderer;
     23 import android.car.navigation.CarNavigationInstrumentCluster;
     24 import android.car.navigation.CarNavigationStatusManager;
     25 import android.content.ComponentName;
     26 import android.content.ContentResolver;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.content.ServiceConnection;
     30 import android.content.res.Resources;
     31 import android.graphics.Bitmap;
     32 import android.graphics.Color;
     33 import android.hardware.display.DisplayManager;
     34 import android.media.MediaDescription;
     35 import android.media.MediaMetadata;
     36 import android.media.session.PlaybackState;
     37 import android.os.Bundle;
     38 import android.os.Handler;
     39 import android.os.IBinder;
     40 import android.os.Looper;
     41 import android.os.SystemClock;
     42 import android.os.UserHandle;
     43 import android.provider.Settings;
     44 import android.telecom.Call;
     45 import android.telecom.GatewayInfo;
     46 import android.text.TextUtils;
     47 import android.util.Log;
     48 import android.util.SparseArray;
     49 import android.view.Display;
     50 
     51 import com.android.car.cluster.sample.MediaStateMonitor.MediaStateListener;
     52 import com.android.car.cluster.sample.cards.MediaCard;
     53 import com.android.car.cluster.sample.cards.NavCard;
     54 
     55 import java.text.DecimalFormatSymbols;
     56 import java.text.NumberFormat;
     57 import java.util.Locale;
     58 import java.util.Objects;
     59 import java.util.Timer;
     60 import java.util.TimerTask;
     61 
     62 /**
     63  * This class is responsible for subscribing to system events (such as call status, media status,
     64  * etc.) and updating accordingly UI component {@link ClusterView}.
     65  */
     66 /*package*/ class InstrumentClusterController {
     67 
     68     private final static String TAG = DebugUtil.getTag(InstrumentClusterController.class);
     69 
     70     private final Context mContext;
     71     private final NavigationRenderer mNavigationRenderer;
     72 
     73     private ClusterView mClusterView;
     74     private MediaStateMonitor mMediaStateMonitor;
     75     private MediaStateListenerImpl mMediaStateListener;
     76     private ClusterInCallService mInCallService;
     77     private MessagingNotificationHandler mNotificationHandler;
     78     private StatusBarNotificationListener mNotificationListener;
     79     private RetriableServiceBinder mInCallServiceRetriableBinder;
     80     private RetriableServiceBinder mNotificationServiceRetriableBinder;
     81 
     82     InstrumentClusterController(Context context) {
     83         mContext = context;
     84         mNavigationRenderer = new NavigationRendererImpl(this);
     85 
     86         init();
     87     }
     88 
     89     private void init() {
     90         grantNotificationListenerPermissionsIfNecessary(mContext);
     91 
     92         final Display display = getInstrumentClusterDisplay(mContext);
     93         if (DEBUG) {
     94             Log.d(TAG, "Instrument cluster display: " + display);
     95         }
     96         if (display == null) {
     97             return;
     98         }
     99 
    100         mClusterView = new ClusterView(mContext);
    101         Presentation presentation = new InstrumentClusterPresentation(mContext, display);
    102         presentation.setContentView(mClusterView);
    103 
    104         // To handle incoming messages
    105         mNotificationHandler = new MessagingNotificationHandler(mClusterView);
    106 
    107         mMediaStateListener = new MediaStateListenerImpl(this);
    108         mMediaStateMonitor = new MediaStateMonitor(mContext, mMediaStateListener);
    109 
    110         mInCallServiceRetriableBinder = new RetriableServiceBinder(
    111                 new Handler(Looper.getMainLooper()),
    112                 mContext,
    113                 ClusterInCallService.class,
    114                 ClusterInCallService.ACTION_LOCAL_BINDING,
    115                 mInCallServiceConnection);
    116         mInCallServiceRetriableBinder.attemptToBind();
    117 
    118         mNotificationServiceRetriableBinder = new RetriableServiceBinder(
    119                 new Handler(Looper.getMainLooper()),
    120                 mContext,
    121                 StatusBarNotificationListener.class,
    122                 StatusBarNotificationListener.ACTION_LOCAL_BINDING,
    123                 mNotificationListenerConnection);
    124         mNotificationServiceRetriableBinder.attemptToBind();
    125 
    126         // Show default card - weather
    127         mClusterView.enqueueCard(mClusterView.createWeatherCard());
    128 
    129         presentation.show();
    130     }
    131 
    132     NavigationRenderer getNavigationRenderer() {
    133         return mNavigationRenderer;
    134     }
    135 
    136     private final ServiceConnection mInCallServiceConnection = new ServiceConnection() {
    137         @Override
    138         public void onServiceConnected(ComponentName name, IBinder binder) {
    139             if (DEBUG) {
    140                 Log.d(TAG, "onServiceConnected, name: " + name + ", binder: " + binder);
    141             }
    142 
    143             mInCallService = ((ClusterInCallService.LocalBinder) binder).getService();
    144             mInCallService.registerListener(mCallServiceListener);
    145 
    146             // The InCallServiceImpl could be bound when we already have some active calls, let's
    147             // notify UI about these calls.
    148             for (Call call : mInCallService.getCalls()) {
    149                 mCallServiceListener.onStateChanged(call, call.getState());
    150             }
    151             mInCallServiceRetriableBinder = null;
    152         }
    153 
    154         @Override
    155         public void onServiceDisconnected(ComponentName name) {
    156             if (DEBUG) {
    157                 Log.d(TAG, "onServiceDisconnected, name: " + name);
    158             }
    159         }
    160     };
    161 
    162     private final ServiceConnection mNotificationListenerConnection = new ServiceConnection() {
    163         @Override
    164         public void onServiceConnected(ComponentName name, IBinder binder) {
    165             if (DEBUG) {
    166                 Log.d(TAG, "onServiceConnected, name: " + name + ", binder: " + binder);
    167             }
    168 
    169             mNotificationListener = ((StatusBarNotificationListener.LocalBinder) binder)
    170                     .getService();
    171             mNotificationListener.setHandler(mNotificationHandler);
    172 
    173             mNotificationServiceRetriableBinder = null;
    174         }
    175 
    176         @Override
    177         public void onServiceDisconnected(ComponentName name) {
    178             if (DEBUG) {
    179                 Log.d(TAG, "onServiceDisconnected, name: "+ name);
    180             }
    181         }
    182     };
    183 
    184     private final Call.Callback mCallServiceListener = new Call.Callback() {
    185         @Override
    186         public void onStateChanged(Call call, int state) {
    187             if (DEBUG) {
    188                 Log.d(TAG, "onCallStateChanged, call: " + call + ", state: " + state);
    189             }
    190 
    191             runOnMain(() -> InstrumentClusterController.this.onCallStateChanged(call, state));
    192         }
    193     };
    194 
    195     private String extractPhoneNumber(Call call) {
    196         String number = "";
    197         Call.Details details = call.getDetails();
    198         if (details != null) {
    199             GatewayInfo gatewayInfo = details.getGatewayInfo();
    200 
    201             if (gatewayInfo != null) {
    202                 number = gatewayInfo.getOriginalAddress().getSchemeSpecificPart();
    203             } else if (details.getHandle() != null) {
    204                 number = details.getHandle().getSchemeSpecificPart();
    205             }
    206         } else {
    207             number = mContext.getResources().getString(R.string.unknown);
    208         }
    209 
    210         return number;
    211     }
    212 
    213     private void onCallStateChanged(Call call, int state) {
    214         if (DEBUG) {
    215             Log.d(TAG, "onCallStateChanged, call: " + call + ", state: " + state);
    216         }
    217 
    218         switch (state) {
    219             case Call.STATE_ACTIVE: {
    220                 Call.Details details = call.getDetails();
    221                 if (details != null) {
    222                     long duration = System.currentTimeMillis() - details.getConnectTimeMillis();
    223                     mClusterView.handleCallConnected(SystemClock.elapsedRealtime() - duration);
    224                 }
    225             } break;
    226             case Call.STATE_CONNECTING: {
    227 
    228             } break;
    229             case Call.STATE_DISCONNECTING: {
    230                 mClusterView.handleCallDisconnected();
    231             } break;
    232             case Call.STATE_DIALING: {
    233                 String phoneNumber = extractPhoneNumber(call);
    234                 String displayName = TelecomUtils.getDisplayName(mContext, phoneNumber);
    235                 Bitmap image = TelecomUtils
    236                         .getContactPhotoFromNumber(mContext.getContentResolver(), phoneNumber);
    237                 mClusterView.handleDialingCall(image, displayName);
    238             } break;
    239             case Call.STATE_DISCONNECTED: {
    240                 mClusterView.handleCallDisconnected();
    241             } break;
    242             case Call.STATE_HOLDING:
    243                 break;
    244             case Call.STATE_NEW:
    245                 break;
    246             case Call.STATE_RINGING: {
    247                 String phoneNumber = extractPhoneNumber(call);
    248                 String displayName = TelecomUtils.getDisplayName(mContext, phoneNumber);
    249                 Bitmap image = TelecomUtils
    250                         .getContactPhotoFromNumber(mContext.getContentResolver(), phoneNumber);
    251                 if (image != null) {
    252                     if (DEBUG) {
    253                         Log.d(TAG, "Incoming call, contact image size: " + image.getWidth()
    254                                 + "x" + image.getHeight());
    255                     }
    256                 }
    257                 mClusterView.handleIncomingCall(image, displayName);
    258             } break;
    259             default:
    260                 Log.w(TAG, "Unexpected call state: " + state + ", call : " + call);
    261         }
    262     }
    263 
    264     private static void grantNotificationListenerPermissionsIfNecessary(Context context) {
    265         ComponentName componentName = new ComponentName(context,
    266                 StatusBarNotificationListener.class);
    267         String componentFlatten = componentName.flattenToString();
    268 
    269         ContentResolver resolver = context.getContentResolver();
    270         String grantedComponents = Settings.Secure.getString(resolver,
    271                 Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
    272 
    273         if (grantedComponents != null) {
    274             String[] allowed = grantedComponents.split(":");
    275             for (String s : allowed) {
    276                 if (s.equals(componentFlatten)) {
    277                     if (DEBUG) {
    278                         Log.d(TAG, "Notification listener permission granted.");
    279                     }
    280                     return;  // Permission already granted.
    281                 }
    282             }
    283         }
    284 
    285         if (DEBUG) {
    286             Log.d(TAG, "Granting notification listener permission.");
    287         }
    288         Settings.Secure.putString(resolver,
    289                 Settings.Secure.ENABLED_NOTIFICATION_LISTENERS,
    290                 grantedComponents + ":" + componentFlatten);
    291 
    292     }
    293 
    294     /* package */ void onDestroy() {
    295         if (mMediaStateMonitor != null) {
    296             mMediaStateMonitor.release();
    297             mMediaStateMonitor = null;
    298         }
    299         if (mMediaStateListener != null) {
    300             mMediaStateListener.release();
    301             mMediaStateListener = null;
    302         }
    303         if (mInCallService != null) {
    304             mContext.unbindService(mInCallServiceConnection);
    305             mInCallService = null;
    306         }
    307         if (mNotificationListener != null) {
    308             mContext.unbindService(mNotificationListenerConnection);
    309             mNotificationListener = null;
    310         }
    311         if (mInCallServiceRetriableBinder != null) {
    312             mInCallServiceRetriableBinder.release();
    313             mInCallServiceRetriableBinder = null;
    314         }
    315         if (mNotificationServiceRetriableBinder != null) {
    316             mNotificationServiceRetriableBinder.release();
    317             mNotificationServiceRetriableBinder = null;
    318         }
    319     }
    320 
    321     private static Display getInstrumentClusterDisplay(Context context) {
    322         DisplayManager displayManager = context.getSystemService(DisplayManager.class);
    323         Display[] displays = displayManager.getDisplays();
    324 
    325         if (DEBUG) {
    326             Log.d(TAG, "There are currently " + displays.length + " displays connected.");
    327             for (Display display : displays) {
    328                 Log.d(TAG, "  " + display);
    329             }
    330         }
    331 
    332         if (displays.length > 1) {
    333             // TODO: Put this into settings?
    334             return displays[displays.length - 1];
    335         }
    336         return null;
    337     }
    338 
    339     private static void runOnMain(Runnable runnable) {
    340         new Handler(Looper.getMainLooper()).post(runnable);
    341     }
    342 
    343     private static class MediaStateListenerImpl implements MediaStateListener {
    344         private final Timer mTimer = new Timer("ClusterMediaProgress");
    345         private final ClusterView mClusterView;
    346 
    347         private MediaData mCurrentMedia;
    348         private MediaAppInfo mMediaAppInfo;
    349         private MediaCard mCard;
    350         private PlaybackState mPlaybackState;
    351         private TimerTask mTimerTask;
    352 
    353         MediaStateListenerImpl(InstrumentClusterController renderer) {
    354             mClusterView = renderer.mClusterView;
    355         }
    356 
    357         void release() {
    358             if (mTimerTask != null) {
    359                 mTimerTask.cancel();
    360                 mTimerTask = null;
    361             }
    362         }
    363 
    364         @Override
    365         public void onPlaybackStateChanged(final PlaybackState playbackState) {
    366             if (DEBUG) {
    367                 Log.d(TAG, "onPlaybackStateChanged, playbackState: " + playbackState);
    368             }
    369 
    370             if (mTimerTask != null) {
    371                 mTimerTask.cancel();
    372                 mTimerTask = null;
    373             }
    374 
    375             if (playbackState != null) {
    376                 if ((playbackState.getState() == PlaybackState.STATE_PLAYING
    377                             || playbackState.getState() == PlaybackState.STATE_BUFFERING)) {
    378                     mPlaybackState = playbackState;
    379 
    380                     if (mCurrentMedia != null) {
    381                         showMediaCardIfNecessary(mCurrentMedia);
    382 
    383                         if (mCurrentMedia.duration > 0) {
    384                             startTrackProgressTimer();
    385                         }
    386                     }
    387                 } else if (playbackState.getState() == PlaybackState.STATE_STOPPED
    388                         || playbackState.getState() == PlaybackState.STATE_ERROR
    389                         || playbackState.getState() == PlaybackState.STATE_NONE) {
    390                     hideMediaCard();
    391                 }
    392             } else {
    393                 hideMediaCard();
    394             }
    395 
    396         }
    397 
    398         private void startTrackProgressTimer() {
    399             mTimerTask = new TimerTask() {
    400                 @Override
    401                 public void run() {
    402                     runOnMain(() -> {
    403                         if (mPlaybackState == null || mCard == null) {
    404                             return;
    405                         }
    406                         long trackStarted = mPlaybackState.getLastPositionUpdateTime()
    407                                 - mPlaybackState.getPosition();
    408                         long trackDuration = mCurrentMedia == null ? 0 : mCurrentMedia.duration;
    409 
    410                         long currentTime = SystemClock.elapsedRealtime();
    411                         long progressMs = (currentTime - trackStarted);
    412                         if (trackDuration > 0) {
    413                             mCard.setProgress((int)((progressMs * 100) / trackDuration));
    414                         }
    415                     });
    416                 }
    417             };
    418 
    419             mTimer.scheduleAtFixedRate(mTimerTask, 0, 1000);
    420         }
    421 
    422 
    423         @Override
    424         public void onMetadataChanged(MediaMetadata metadata) {
    425             if (DEBUG) {
    426                 Log.d(TAG, "onMetadataChanged: " + metadata);
    427             }
    428             MediaData data = MediaData.createFromMetadata(metadata);
    429             if (data == null) {
    430                 hideMediaCard();
    431             }
    432             mCurrentMedia = data;
    433         }
    434 
    435         private void hideMediaCard() {
    436             if (DEBUG) {
    437                 Log.d(TAG, "hideMediaCard");
    438             }
    439 
    440             if (mCard != null) {
    441                 mClusterView.removeCard(mCard);
    442                 mCard = null;
    443             }
    444 
    445             // Remove all existing media cards if any.
    446             MediaCard mediaCard;
    447             do {
    448                 mediaCard = mClusterView.getCardOrNull(MediaCard.class);
    449                 if (mediaCard != null) {
    450                     mClusterView.removeCard(mediaCard);
    451                 }
    452             } while (mediaCard != null);
    453         }
    454 
    455         private void showMediaCardIfNecessary(MediaData data) {
    456             if (!needToCreateMediaCard(data)) {
    457                 return;
    458             }
    459 
    460             int accentColor = mMediaAppInfo == null
    461                     ? Color.GRAY : mMediaAppInfo.getMediaClientAccentColor();
    462 
    463             mCard = mClusterView.createMediaCard(
    464                     data.albumCover, data.title, data.subtitle, accentColor);
    465             if (data.duration <= 0) {
    466                 mCard.setProgress(100); // unknown position
    467             } else {
    468                 mCard.setProgress(0);
    469             }
    470             mClusterView.enqueueCard(mCard);
    471         }
    472 
    473         private boolean needToCreateMediaCard(MediaData data) {
    474             return (mCard == null)
    475                     || !Objects.equals(mCard.getTitle(), data.title)
    476                     || !Objects.equals(mCard.getSubtitle(), data.subtitle);
    477         }
    478 
    479         @Override
    480         public void onMediaAppChanged(MediaAppInfo mediaAppInfo) {
    481             mMediaAppInfo = mediaAppInfo;
    482         }
    483 
    484         private static class MediaData {
    485             final Bitmap albumCover;
    486             final String subtitle;
    487             final String title;
    488             final long duration;
    489 
    490             private MediaData(MediaMetadata metadata) {
    491                 MediaDescription mediaDescription = metadata.getDescription();
    492                 title = charSequenceToString(mediaDescription.getTitle());
    493                 subtitle = charSequenceToString(mediaDescription.getSubtitle());
    494                 albumCover = mediaDescription.getIconBitmap();
    495                 duration = metadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
    496             }
    497 
    498             static MediaData createFromMetadata(MediaMetadata metadata) {
    499                 return  metadata == null ? null : new MediaData(metadata);
    500             }
    501 
    502             private static String charSequenceToString(@Nullable CharSequence cs) {
    503                 return cs == null ? null : String.valueOf(cs);
    504             }
    505 
    506             @Override
    507             public String toString() {
    508                 return "MediaData{" +
    509                         "albumCover=" + albumCover +
    510                         ", subtitle='" + subtitle + '\'' +
    511                         ", title='" + title + '\'' +
    512                         ", duration=" + duration +
    513                         '}';
    514             }
    515         }
    516     }
    517 
    518     private static class NavigationRendererImpl extends NavigationRenderer {
    519 
    520         private final InstrumentClusterController mController;
    521 
    522         private ClusterView mClusterView;
    523         private Resources mResources;
    524 
    525         private NavCard mNavCard;
    526 
    527         NavigationRendererImpl(InstrumentClusterController controller) {
    528             mController = controller;
    529         }
    530 
    531         @Override
    532         public CarNavigationInstrumentCluster getNavigationProperties() {
    533             if (DEBUG) {
    534                 Log.d(TAG, "getNavigationProperties");
    535             }
    536             return CarNavigationInstrumentCluster.createCustomImageCluster(
    537                     1000, /* 1 Hz*/
    538                     64,   /* image width */
    539                     64,   /* image height */
    540                     32);  /* color depth */
    541         }
    542 
    543         @Override
    544         public void onEvent(int eventType, Bundle bundle) {
    545             if (DEBUG) {
    546                 Log.d(TAG, "onEvent");
    547             }
    548             // Implement this.
    549         }
    550     }
    551 
    552     /**
    553      * Services might not be ready for binding. This class will retry binding after short interval
    554      * if previous binding failed.
    555      */
    556     private static class RetriableServiceBinder {
    557         private static final long RETRY_INTERVAL_MS = 500;
    558         private static final long MAX_RETRY = 30;
    559 
    560         private Handler mHandler;
    561         private final Context mContext;
    562         private final Intent mIntent;
    563         private final ServiceConnection mConnection;
    564 
    565         private long mAttemptsLeft = MAX_RETRY;
    566 
    567         private final Runnable mBindRunnable = () -> attemptToBind();
    568 
    569         RetriableServiceBinder(Handler handler, Context context, Class<?> cls, String action,
    570                 ServiceConnection connection) {
    571             mHandler = handler;
    572             mContext = context;
    573             mIntent = new Intent(mContext, cls);
    574             mIntent.setAction(action);
    575             mConnection = connection;
    576         }
    577 
    578         void release() {
    579             mHandler.removeCallbacks(mBindRunnable);
    580         }
    581 
    582         void attemptToBind() {
    583             boolean bound = mContext.bindServiceAsUser(mIntent,
    584                     mConnection, Context.BIND_AUTO_CREATE, UserHandle.CURRENT_OR_SELF);
    585 
    586             if (!bound && --mAttemptsLeft > 0) {
    587                 mHandler.postDelayed(mBindRunnable, RETRY_INTERVAL_MS);
    588             } else if (!bound) {
    589                 Log.e(TAG, "Gave up to bind to a service: " + mIntent.getComponent() + " after "
    590                         + MAX_RETRY + " retries.");
    591             }
    592         }
    593     }
    594 }
    595